As part of an Intro to Security course I’m taking, my professor gave us a crackme style exercise to practice reading x86 assembly and basic reverse engineering.
The program is pretty simple. It accepts a password as an argument and we’re told that if the password is correct, “ok” is printed.
$ ./crackme usage: ./crackme <secret> $ ./crackme test $
As usual, I start by running
file on the binary, which shows that it’s a
standard x64 ELF binary.
file also says that the binary is “not stripped”, which means
that it includes symbols. All I really know about symbols are that they can
include debugging information about a binary like function and variable names
and some symbols aren’t really necessary; they can be stripped out to reduce
the binary’s size and make reverse engineering more challenging. Maybe I’ll
do a more in depth post on this in the future.
$ file crackme crackme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0x3fcf895b7865cb6be6b934640d1519a1e6bd6d39, not stripped
Next, I run
strings, hoping to get lucky and find the password amongst the
strings in the binary. Strings looks for series of printable characters followed
by a NULL, but unfortunately nothing here works as the password.
$ strings crackme /lib64/ld-linux-x86-64.so.2 exd4 libc.so.6 puts printf memcmp __libc_start_main __gmon_start__ GLIBC_2.2.5 fffff. AWAVA AUATL A\A]A^A_ usage: %s <secret> ;*3$"
Since that didn’t work, we’re forced to disassemble the binary and
actually try to reverse engineer it.
We’ll start with
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Let’s break this down a little.
1 2 3
Starting at the beginning, we see the stack pointer decremented as part of the function prologue. The prologue is a set of setup steps involving saving the old frame’s base pointer on the stack, reassigning the base pointer to the current stack pointer, then subtracting the stack pointer a certain amount to make room on the stack for local variables, etc. We don’t see the former two steps because this is the main function so it doesn’t really have a function calling it, so saving/setting the base pointer isn’t necessary.
edi register is
compared to 1 and if it is less than or equal, we jump to offset 39.
1 2 3 4 5 6 7 8
Here at offset 39, we print something then jump to offset 34 where we repair the stack (undo the sub instruction from the prologue) and return (ending execution).
This is likely how the program checks the arguments and prints the usage message if no arguments are supplied (which would cause argc/edi to be 1).
However if we supply an argument,
edi is 0x2 and we move past the
Here we can see the
verify_secret function being called with a parameter
rdi. This is most likely the argument we passed into the program. We can
confirm this with gdb (I’m using it with peda here).
gdb-peda$ tele $rsi 0000| 0x7fffffffeb48 --> 0x7fffffffed6e ("/home/vagrant/crackme/crackme") 0008| 0x7fffffffeb50 --> 0x7fffffffed8c --> 0x4548530074736574 ('test') 0016| 0x7fffffffeb58 --> 0x0
rsi points to the first element of
argv, so incrementing that by 8 bytes
(because 64 bit) points to
argv, which is our input.
If we look after the
verify_secret call we can see the program checks
eax is 0 and if it is, jumps to offset 34, ending the program. However, if
eax is not zero, we’ll hit a
puts call before exiting, which will presumably
print out the “ok” message we want.
1 2 3 4 5 6 7
Now lets disassemble
verify_secret to see how the input validation is performed,
and to see how we can make it return non-zero.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
I won’t walk through this one in detail because understanding each line
isn’t necessary to crack this. Let’s skip to
the memcmp call. If memcmp returns 0,
eax is set to 1 and the function
returns. This is exactly what we want. From the man page,
memcmp takes three
parameters, two buffers to compare and their lengths, and returns 0 if the
buffers are identical.
1 2 3 4
Here’s the setup to the
memcmp call. We can see the third parameter for length
is the immediate 0x18 meaning the buffers will be 24 bytes in length. If we
examine address 0x600a80, we find this 24 byte string:
gdb-peda$ hexd 0x600a80 /2 0x00600a80 : 91 bf a4 85 85 c3 ba b9 9f a6 b6 b1 93 b9 83 8f ................ 0x00600a90 : ae b1 ae c1 bc 80 ca ca 00 00 00 00 00 00 00 00 ................
Since this is a direct address to some memory, we can be fairly certain that
we’ve found some sort of secret value! Based on the
movzx eax,BYTE PTR [rdi]
instruction (offset 7)
which moves a byte from the input string into eax, the
xor eax, 0xfffffff7
instruction (offset 36), and
add rdi, 0x1 instruction (offset 54) which increments the char*
pointer to our input string, we can reasonably guess
that this function is xor'ing each character of our input with 0xf7 and writing
the result into a buffer which begins at
rsp (also pointed to by
we now know the secret (
\x91\xbf\xa4\x85...) and the xor key (
pretty easy to extract the password we need by xor'ing each byte of the secret
with the xor key.
Here’s a way to do this with python.
str = '\x91\xbf\xa4\x85\x85\xc3\xba\xb9\x9f\xa6\xb6\xb1\x93\xb9\x83\x8f\xae\xb1\xae\xc1\xbc\x80\xca\xca' ba = bytearray(str) for i, byte in enumerate(ba): ba[i] ^= 0xf7 print ba
Which results in this:
$ python crack.py fHSrr4MNhQAFdNtxYFY6Kw== $ ./crackme fHSrr4MNhQAFdNtxYFY6Kw== ok