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