<<

solving the first four ioli crackmes

1686818677

this post was originally released on october 10th, 2021. i managed to recover it from a flash drive and thought it'd be cool to re-release it. i hope the insight into a layman's mind does something for you

most of the code sections were originally contained in screenshots. i used apple's ocr to copy/paste said code into actual snippets. this process might have produced some errors and/or inconsistencies that i may have overlooked when manually trying to fix the scan, so take any assembly output you read with a grain of salt. look at this more like a museum piece than an actual technical showcase. if the latter is what you came here for, i'm sorry to disappoint but at the same time happy to point you in the direction of someone who actually cares about tech and exclusively publishes technical articles

recently i've been spending more time on crackmes.one, casually browsing through the easy challenges, hoping to eventually stumble upon one i can actually solve without looking up hints in the submitted solutions. some of the easy ones can be solved by setting a break on the strcmp function call and examining the memory addresses stored in %rdi and %rsi. once algorithms come into play, i quickly start hitting walls

after careful consideration of the risks associated with a cocaine habit, both in terms of health and finance, i have decided against purchasing an ida pro license and will instead be using gdb to solve the first four IOLI crackmes. these are typically considered to be baby level intro binaries which is ideal for my skill level

0x00

since these crackmes are not considered to be malicious, let's execute the first one outside of a sandbox and see what happens. i would also recommend doing this at your workplace if you want to go down as the hero striking fear into the hearts of APTs all around the globe with your unshakeable bravery

$ ./crackme0x00
IOLI Crackme Level 0x00
Password: 1234
Invalid Password!

unfortunately my solid guess did not end up being the correct password. let's use strings next:

PTRh
IOLI Crackme Level 0×00
Password:
250382
Invalid Password!
Password OK :)
GCC: (GNU) 3.4.6 (Gentoo 3.4.6-12, ssp-3.4.6-1.0, pie-8.7.10

i have this hunch that 250382 could be the correct password. after re-running the program and entering the magic number, we get a Password OK :). first one done

0x01

1234 doesn't seem to be the solution to the second one either. strings also does not give any interesting information. guess we'll have to look at some assembly. let's run the program using our trusty gdb and disassemble the main function

0x0804841b <+55>:    mov    %eax,0x4(%esp)
0x0804841f <+59>:   movl   $0x804854c, (%esp)
0x08048426 <+66>:    call   0×804830c <scanf@plt>
0x0804842b <+71>:    cmpl   $0×149a,-0×4(%ebp)
0x08048432 <+78>:    je     0x8048442 <main+94>
0x08048434 <+80>:    movl   $0x8048541, (%esp)
0x0804843b <+87>:    call   0×804831c <printf@plt>
0x08048440 <+92>:    jmp    0×804844 <main+106>
0x08048442 <+94>:    movl   $0x8048562, (%esp)
0x08048449 <+101>:   call   0×804831c <printf@plt>
0x0804844e <+106>:   mov    $0x0,%eax

the interesting part here is the cmpl (compare long) instruction. it seems to compare the constant 0x149a to whatever is in %ebp-0x4, probably our guess of 1234. setting a break at that instruction and running the program gives us a chance to inspect the stack

(gdb) × $ebp+0x8
Oxffffd0f0:    0x000004d2
(gdb) × $ebp+0xc
Oxffffd0f4:    0x00052b24

04d2 in decimal indeed turns out to be 1234, which is being compared to 0x149a or 5274 in decimal. if the value we passed to scanf would have been equal, execution would jump to a nice printf call with whatever string is hiding at memory address $0x8048562 as the argument. it's reasonable to assume that the values need to be equal in a program that checks for a password. re-running the program and entering 5274 solves the crackme

0x02

from this point onward, i'll be skipping the strings usage since it probably won't get us very far anymore. disassembling main reveals that there is another cmp instruction waiting for a breakpoint. this time it's comparing whatever is in %eax and whatever the memory address at %ebp-0x4 holds. instead of just reading the correct password which is stored at %ebx-0xc once we hit the breakpoint, let's spice it up and try to understand how the correct password actually comes to be. to do that, we need to consider the instructions that are being executed after the scanf call since our input is stored at that point in time

0×0804842b <+71>:    movl   $0x5a,-0x8 (%ebp)
0x08048432 <+78>:    movl   $Ox1ec,-Oxc (%ebp)
0×08048439 <+85>:    mov    -Oxc(%ebp),%edx
0x0804843c <+88>:    lea    -0x8(%ebp),%eax
0x0804843f <+91>:    add    %edx,(%eax)
0x08048441 <+93>:    mov    -0x8(%ebp),%eax
0x08048444 <+96>:    imul   -0x8(%ebp),%eax
0x08048448 <+100>:   mov    %eax,-Oxc(%ebp)
0x0804844b <+103>:   mov    -0x4 (%ebp),%eax
0x0804844e <+106>:   cmp    -Oxc (%ebp),%eax
0x08048451 <+109>:   jne    0×8048461 <main+125>
0x08048453 <+111>:   movl   $0x804856f,(%esp)
0x0804845a <+118>:   call   0x804831c <printf@plt>
0x0804845f <+123>:   jmp    0x804846d <main+137>
0x08048461 <+125>:   movl   $0x804857f,(%esp)
0x08048468 <+132>:   call   0x804831c <printf@plt>

first the cpu moves two integers, namely 0x5a (90 in decimal) and 0x1ec (492 in decimal) into memory, located at %ebp-0x8 and %ebp-0xc respectively. afterwards, the value located at %ebp-0xc i.e. 492 is being moved into the %edx register. the following lea instruction loads the address at offset %ebp-0x8 into the %eax register. using the add instruction, the value 492 is then added to whatever value the freshly loaded address in %eax holds, in this case the aforementioned 90. the result of 582 is then stored in the second operand aka the destination of the add instruction, which is the memory address %eax contains. in other words, 582 is now stored at the offset 90 was previously stored at (%ebp-0x8)

next, the raw value of 582 is being moved into the %eax register before a signed multiplication is performed on both the offset %ebp-0x8 (which holds 582) and the 582 in %eax, storing the result of what is essentially a square (582*582 = 582^2 = 338724) in %eax. the result is then copied from %eax into the offset %ebp-0xc, overwriting the 492 that was previously stored there

ultimately, the computer then moves the password i entered (1234, stored at %ebp-0x4) into %eax and compares it to the value in %ebp-0xc (338724). if it's equal, the challenge is solved. running the program and entering 338724 as the password solves the challenge

0x03

the disassembly of main tells me that this time we have a dedicated function for checking whether the password is correct, aptly called "test" and located at memory address 0x804846e. let's set a breakpoint there, run the program and disassemble on arrival. i'll use 1234 as input again

0x0804846e <+0>:    push    %ebp
0x0804846f <+1>:    mov     %esp,%ebp
0x08048471 <+3>:    sub     $0x8,%esp
0x08048444 <+6>:    mov     0x8(%ebp),%eax
0×08048477 <+9>:    cmp     0xc(%ebp),%eax
0x0804847a <+12>:   je      0x804848a <test+28>
0x0804847c <+14>:   movl    $0x80485ec,(%esp)
0x08048483 <+21>:   call    0x8048414 <shift>
0×08048488 <+26>:   jmp     0x8048896 <test+40>
0×0804848a <+28>:   movl    $0x80485fe,(%esp)
0×08048491 <+35>:   call    0x8048414 <shift>
0x08048496 <+40>:   leave
0x08048497 <+41>:   ret

once again, we have a comparison followed by a jump. if the operands for the cmp instruction are equal, then we jump to 0x0804848a which moves a string to the top of the stack (indicated by the stack pointer being referenced, stack pointer always points to top of stack) as an argument to a mysterious shift function call. let's see what string is being passed to said function

0x0804848a <+28>:    movl    $0x80485fee,(%esp)
0x08048491 <+35>:    call    0x8048414 <shift>
0x08048496 <+40>:    leave
0x08048497 <+41>:    ret
End of assembler dump.
(gdb) x/s  0×80485fe
0x80485fe:    "SdvVZTug#RN$$$非=

interesting. "Sdvvzrug" looks a lot like what "Password" would look like after being run through some sort of rotation cipher. let's disassemble the shift function and try to make sense of it all

(gab) disas shift
Dump of assembler code for function shift:
  0x08048414 <+0>:    push    %ebp
  0×08048415 <+1>:    mov     %esp,%ebp
  0x08048417 <+3>:    sub     $0x98,%esp
  0×0804841d <+9>:    movl    $0x0,-0x7c(%ebp)
  0×08048424 <+16>:   mov     0x8(%ebp),%eax
  0×08048427 <+19>:   mov     %eax,(%esp)
  0×0804842a <+22>:   call    0x8048340 <strlen@plt>
  0x0804842f <+27>:  cmp     %eax,-0x7c(%ebp)
  0x08048432 <+30>:   jae     0x8048450 <shift+60>
  0×08048434 <+32>:   lea     -0x78(%ebp),%eax
  0x08048437 <+35>:   mov     %eax,%edx
  0x08048439 <+37>:   add     -0x7c(%ebp),%edx
  0x0804843c <+40>:   mov     -0x7c(%ebp),%eax
  0x08048431 <+43>:   add     0x8(%ebp),%eax
  0x08048442 <+46>:   movzbl  (%eax),%eax
  0x08048445 <+49>:   sub     $0x3,%al
  0x08048447 <+51>:   mov     %al,(%edx)
  0x08048449 <+53>:   lea     -0x7c(%ebp),%eax
  0×0804844с <+56>:   incl    (%eax)
  0x0804844e <+58>:   jmp     0x8048424 <shift+16>
  0x08048450 <+60>:   lea     -0x78(%ebp),%eax
  0x08048453 <+63>:   add     -0x7c(%ebp),%eax
  0x08048456 <+66>:   movb    $0x0,(%eax)
  0x08048459 <+69>:   lea     -0x78(%ebp),%eax
  0x0804845c <+72>:   mov     %eax,0×4(%esp)
  0x08048460 <+76>:   movl    $0×804858,(%esp)
  0x08048467 <+83>:   call    0x8048350 <printf@plt>
  0x0804846c <+88>:   leave
  0×08048460 <+89>:   ret

a few things come to mind here. i've previously stumbled upon a crackme that uses some kind of loop and the disassembly looks pretty similar to that. take the 0 being moved into an offset, the strlen function call and the incl instruction that increments a value for example. the primitive code could look something like this

for (int i = 0; i < strlen(whatever); i++) {
  rotate(whatever[i]);
}

regardless, i assume the shift function is a red herring, trying to lead you astray, since it ultimately ends in a printf call. the strings "password ok" and "invalid password" are stored in a rotated form and then unrotated using the shift function. let's instead focus on the previously mentioned cmp instruction before the jump happens and set a breakpoint there

the cmp instruction compares two values, one being stored at memory address %ebp+0x8 and the other at %ebp+0xc. examining these memory addresses nets us the following result

0x08048474 <+6>:     mov    0x8(%ebp),%eax
0x08048477 <+9>:     cmp    0xc(%ebp),%eax
0x0804847a <+12>:    je     0x804848a <test+28>
0x0804847c <+14>:    movl   $0x80485ec,(%esp)
0x08048483 <+21>:   call   0x8048414 <shift>
0X08048488 <+26>:    jmp    0x8048496 <test+40>
0x0804848а <+28>:    movl   0x80485fe,(%esp)
0x08048491 <+35>:    call   0x8048414 <shift>
0X08048496 <+40>:    leave
0x08048497 <+41>:    ret
End of assembler dump.
(gdb) × $ebp+0×8
Oxffffd0f0:    0x000004d2
(edb) × $ebp+Oxc oxiffidor:
Oxffffd0f4:    0x00052b24

0x4d2 is once again our "1234" input and 0x52b24 is probably our solution. the latter converts to 338724 which appears to be the solution for the previous challenge. sly devils, these crackme authors. entering that at the password prompt will solve the challenge. one thing that i have completely glossed over is the fact that the main function after the scanf call looks exactly the same as the previous one. i suppose paying more attention would have saved me a lot of time here

the difficulty increases from here on. i have already tried 0x04 and couldn't solve it without help. i should probably read more theory first before attempting to dive into harder stuff

bonus

in addition to the ioli, here's a random crackme, scoring a 1 on a scale of 1 to 6 in terms of difficulty. pretty easy then. the gist of it is to avoid jumps to the failure function call

skipping the function prologue and boring intro shit, we end up with this first section:

0x00000000000011b0 <+27>:   callq  0x1304 <_Z9readInputv>
0x00000000000011b5 <+32>:   lea    0x30e4(%rip),%rax        #### 0x42a0 <password>
0x00000000000011bc <+39>:   mov    %rax,-0x8(%rbp)
0x00000000000011c0 <+43>:   mov    -0x8(%rbp),%rax
0x00000000000011c4 <+47>:   movzbl (%rax),%eax
0x00000000000011c7 <+50>:   cmp    $0x48,%al
0x00000000000011c9 <+52>:   je     0x11d0 <main+59>
0x00000000000011cb <+54>:   callq  0x12c5 <_Z6failedv>

input is being read, the memory address containing our password at offset +0x30e4 from %rip is being loaded into the accumulator register %rax and then copied to offset %rbp-0x8. so far so good. the movzbl instruction then retrieves our password from memory by dereferencing the pointer stored in %rax and places it in the %eax register.

before i continue, let's talk about endiannness. x86 platforms are little endian. this means that if we have a string with a length of 4 bytes and store it in a register, e.g. %eax, the most significant byte i.e. the first character of the string will be stored in the lower region of %eax which can then be accessed using %al. here's a little visualization:

string is "1234" which translates to 0x34333231 in hex

  4    3    2    1
-------------------
0x34 0x33 0x32 0x31 eax (4 bytes) = "4321"
          0x32 0x31  ax (2 bytes) = "21"
          0x32       ah (1 byte)  = "2"
               0x31  al (1 byte)  = "1"

for some reason i don't understand yet, %rax, %eax and %al all only contain 0x31, the first character of my input. i suppose since %rax points at a memory address containing 4 bytes of string, dereferencing this pointer returns the first byte in the string, which is 0x31, instead of the whole string

either way, the value in %al (0x31 or "1" in ascii) is compared to 0x48. looking at an ascii table, 0x48 turns out to be the character "H". since "1" is not equal to "H", we immediately jump to the failure function, printing the failure string and exiting the program. the first character therefore will be "H". but what about the rest of the string?

0x00000000000011d0 <+59>:   addq   $0x1,-0x8(%rbp)
0x00000000000011d5 <+64>:   mov    -0x8(%rbp),%rax
0x00000000000011d9 <+68>:   movzbl (%rax),%eax
0x00000000000011dc <+71>:   cmp    $0x31,%al
0x00000000000011de <+73>:   je     0x11e5 <main+80>
0x00000000000011e0 <+75>:   callq  0x12c5 <_Z6failedv>

in the above block, the value 1 is added to the memory address at offset %rbp-0x8 (where our password is stored). that basically means "move one character in the input string forward" since each character is 1 byte wide and %rbp-0x8 points at the first character, %rbp-0x7, which is the result of the addq operation, consequently points at the second character in the string. it is once again compared to an ascii character, namely 0x31 which translates to "1" as we already know

the rest of the code is pretty trivial and only requires looking at an ascii table. the password turns out to be "H1DD3N". this input will succeed all cmp checks, causing the cpu to eventually jump to the success function, solving the challenge

$ ./crack1_by_D4RK_FL0W
Crackme1 By D4RK_FL0W

Password: H1DD3N

Well Done, password is H1DD3N

i'm gonna go watch the joker movie now and it's probably going to suck

I’m a negative of a person. All I want is blackness, blackness and silence - Sylvia Plath