Reversing ELF
Reverse engineering looks scary from the outside, but the first steps are surprisingly friendly. TryHackMe’s Reversing ELF room hands you eight small Linux crackmes, and each one teaches a new trick. Here’s how I solved all of them.
strings, base64, a gdb breakpoint on strcmp, an LD_PRELOAD hook, reading cmp bytes, spotting a magic number in radare2, and patching $eax at runtime.Setup
Tools I used:
My working directory after finishing all the challenges looked like this:
$ tree
.
├── crackme_1
│ ├── crackme1
│ └── flag.txt
├── crackme_2
│ ├── crackme2
│ └── flag.txt
├── crackme_3
│ ├── crackme3
│ └── flag.txt
├── crackme_4
│ ├── crackme4
│ ├── flag.txt
│ └── gdb_cmd
├── crackme_5
│ ├── crackme5
│ ├── flag.txt
│ ├── preload.c
│ └── preload.so
├── crackme_6
│ ├── crackme6
│ └── flag.txt
├── crackme_7
│ ├── crackme7
│ └── flag.txt
└── crackme_8
├── crackme8
├── flag.txt
└── gdb_cmdEvery crackme below follows the same rhythm: Recon (what is it?), Crack (the trick), Result (the flag or password).
crackme1: just run it
Recon
$ file crackme1
crackme1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=672f525a7ad3c33f190c060c09b11e9ffd007f34, not strippedIt’s an ELF, the standard Linux executable format. So let’s run it.
Result
$ ./crackme1
flag{not_that_kind_of_elf}That’s it. Running the binary is enough. A gentle warm-up. :)
crackme2: strings
Recon
$ file crackme2
crackme2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b799eb348f3df15f6b08b3c37f8feb269a60aba7, not stripped
$ ./crackme2
Usage: ./crackme2 password
$ ./crackme2 aaa
Access denied.Crack
The password has to live somewhere in the binary. strings dumps every printable sequence:
$ strings crackme2
/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
puts
printf
memset
strcmp
...
Usage: %s password
super_secret_password
...
.got.plt
.data
.bss
.commentsuper_secret_password sits right next to the usage message. Suspicious.
Result
$ ./crackme2 super_secret_password
Access granted.
flag{if_i_submit_this_flag_then_i_will_get_points}crackme3: strings + base64
Recon
$ file crackme3
crackme3: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4cf7250afb50109f0f1a01cc543fbf5ba6204a73, stripped
$ strings crackme3
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
puts
strlen
malloc
...
Usage: %s PASSWORD
malloc failed
ZjByX3kwdXJfNWVjMG5kX2xlNTVvbl91bmJhc2U2NF80bGxfN2gzXzdoMW5nNQ==
Correct password!
Come on, even my aunt Mildred got this one!
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
...
.got.plt
.data
.bss
.commentCrack
Two hints in one: a string ending in == and the full base64 alphabet right below it. That’s base64.
$ echo "ZjByX3kwdXJfNWVjMG5kX2xlNTVvbl91bmJhc2U2NF80bGxfN2gzXzdoMW5nNQ==" | base64 -d
f0r_y0ur_5ec0nd_le55on_unbase64_4ll_7h3_7h1ng5Result
A couple of seconds of work and the third flag is ours: f0r_y0ur_5ec0nd_le55on_unbase64_4ll_7h3_7h1ng5.
crackme4: gdb breakpoint on strcmp
Recon
$ file crackme4
crackme4: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=862ee37793af334043b423ba50ec91cfa132260a, not stripped
$ strings crackme4
/lib64/ld-linux-x86-64.so.2
libc.so.6
puts
...
password OK
password "%s" not OK
Usage : %s password
This time the string is hidden and we used strcmp
...No password in the strings this time, but the binary kindly tells us it uses strcmp.
$ ./crackme4
Usage : ./crackme4 password
This time the string is hidden and we used strcmp
$ ./crackme4 AAA
password "AAA" not OKCrack
If the hidden password is built at runtime, it must be in memory by the time strcmp is called. So let’s find that call:
$ objdump -D crackme4 | grep call | grep strcmp
4006d5: e8 46 fe ff ff callq 400520 <strcmp@plt>Then write a small gdb_cmd script: break on the call, run with a dummy password, and print the string in $rax.
break * 0x4006D5
run aaa
x/s $rax$ gdb -x gdb_cmd ./crackme4
GNU gdb (GDB) 10.1
...
Undefined command: "import". Try "help".
Reading symbols from ./crackme4...
(No debugging symbols found in ./crackme4)
Breakpoint 1 at 0x4006d5
Breakpoint 1, 0x00000000004006d5 in compare_pwd ()
0x7fffffffe440: "my_m0r3_secur3_pwd"
(gdb)Result
$ ./crackme4 my_m0r3_secur3_pwd
password OKcrackme5: LD_PRELOAD hook
Recon
$ file crackme5
crackme5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a426dcf8ed3de8cb02f3ee4f38ee36b4ed568519, not strippedstrings shows nothing interesting this time, so I’ll skip the output. Let’s list the functions instead:
gdb: info functions
$ gdb crackme5
GNU gdb (GDB) 10.1
...
Reading symbols from crackme5...
(No debugging symbols found in crackme5)
(gdb) info functions
All defined functions:
Non-debugging symbols:
0x0000000000400528 _init
0x0000000000400560 strncmp@plt
0x0000000000400570 puts@plt
0x0000000000400580 strlen@plt
0x0000000000400590 __stack_chk_fail@plt
0x00000000004005a0 __libc_start_main@plt
0x00000000004005b0 atoi@plt
0x00000000004005c0 __isoc99_scanf@plt
0x00000000004005d0 __gmon_start__@plt
0x00000000004005e0 _start
0x0000000000400610 deregister_tm_clones
0x0000000000400650 register_tm_clones
0x0000000000400690 __do_global_dtors_aux
0x00000000004006b0 frame_dummy
0x00000000004006d6 strcmp_
0x0000000000400773 main
0x000000000040086e check
0x00000000004008d0 __libc_csu_init
0x0000000000400940 __libc_csu_fini
0x0000000000400944 _finiThe interesting import is strncmp@plt. It’s probably what compares our input with the password.
Crack
Because strncmp comes from a shared library, we can override it with our own version and print both arguments. Here’s preload.c:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
int strncmp(const char *s1, const char *s2, size_t n) {
printf("S1: [%s], S2: [%s]\n", s1, s2);
int (*original_strcmp)(const char*, const char*, size_t);
original_strcmp = dlsym(RTLD_NEXT, "strcmp");
return (*original_strcmp)(s1, s2, n);
}LD_PRELOAD tells the dynamic linker to load your library first, so your strncmp wins over the one in libc. dlsym(RTLD_NEXT, ...) then fetches the “real” function so the program keeps working.Compile it and run the crackme with the library preloaded:
$ gcc -Wall -fPIC -shared -o preload.so preload.c -ldl
$ LD_PRELOAD=./preload.so ./crackme5
Enter your input:
AAA
S1: [AAA], S2: [OfdlDSA|3tXb32~X3tX@sX`4tXtz]
Always dig deeperResult
The second argument is the expected input: OfdlDSA|3tXb32~X3tX@sX`4tXtz. Another flag down.
crackme6: reading cmp bytes
Recon
$ file crackme6
crackme6: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=022f1a8e479cab9f7263af75bcdbb328bda7f291, not strippedThe function list has a very promising name: my_secure_test.
gdb: info functions
$ gdb ./crackme6
GNU gdb (GDB) 10.1
...
Reading symbols from ./crackme6...
(No debugging symbols found in ./crackme6)
(gdb) info functions
All defined functions:
Non-debugging symbols:
0x0000000000400418 _init
0x0000000000400450 puts@plt
0x0000000000400460 printf@plt
0x0000000000400470 __libc_start_main@plt
0x0000000000400480 __gmon_start__@plt
0x0000000000400490 _start
0x00000000004004c0 deregister_tm_clones
0x00000000004004f0 register_tm_clones
0x0000000000400530 __do_global_dtors_aux
0x0000000000400550 frame_dummy
0x000000000040057d my_secure_test
0x00000000004006d1 compare_pwd
0x0000000000400711 main
0x0000000000400760 __libc_csu_init
0x00000000004007d0 __libc_csu_fini
0x00000000004007d4 _finiCrack
my_secure_test starts at 0x40057d and ends at 0x4006d0. A character-by-character check usually means one cmp per character, so let’s grab every cmp in that range:
$ objdump -D crackme6 | egrep -e "4005|4006" | grep "cmp "
400597: 3c 31 cmp $0x31,%al
4005bf: 3c 33 cmp $0x33,%al
4005e7: 3c 33 cmp $0x33,%al
40060f: 3c 37 cmp $0x37,%al
400637: 3c 5f cmp $0x5f,%al
40065f: 3c 70 cmp $0x70,%al
400684: 3c 77 cmp $0x77,%al
4006a9: 3c 64 cmp $0x64,%alConvert the hex values with an ASCII table: 31 33 33 37 5f 70 77 64 → 1337_pwd.
Result
$ ./crackme6 1337_pwd
password OKcrackme7: magic number in radare2
Recon
$ file crackme7
crackme7: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7ee4206d91718e7b0bef16a7c03f8fa49c4a39e7, not strippedNothing useful in the strings again, so on to the functions, this time with radare2:
$ r2 crackme7
-- Mess with the best, Die like the rest
[0x080483c0]> aaa
...
[0x080483c0]> afl
0x080483c0 1 33 entry0
0x08048380 1 6 sym.imp.__libc_start_main
0x08048400 4 43 sym.deregister_tm_clones
0x08048430 4 53 sym.register_tm_clones
0x08048470 3 30 sym.__do_global_dtors_aux
0x08048490 4 43 -> 40 entry.init0
0x080486a6 4 149 sym.giveFlag
0x080487a0 1 2 sym.__libc_csu_fini
0x080483f0 1 4 sym.__x86.get_pc_thunk.bx
0x080487a4 1 20 sym._fini
0x08048740 4 93 sym.__libc_csu_init
0x080484bb 20 491 main
0x08048324 3 35 sym._init
0x08048360 1 6 sym.imp.printf
0x08048370 1 6 sym.imp.puts
0x08048390 1 6 sym.imp.memset
0x080483a0 1 6 sym.imp.__isoc99_scanfCrack
We want sym.giveFlag at 0x080486a6 to run. Who calls it?
[0x080483c0]> axt sym.giveFlag
main 0x804867c [CALL] call sym.giveFlag
[0x080483c0]> pdf @ main
Just before the call there’s cmp eax, 0x7a69. Convert 0x7a69 to decimal and you get 31337. Very leet.
Result
Type it in as the menu choice:
$ ./crackme7
Menu:
[1] Say hello
[2] Add numbers
[3] Quit
[>] 31337
Wow such h4x0r!
flag{much_reversing_very_ida_wow}crackme8: patching $eax in gdb
Recon
$ file crackme8
crackme8: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=fef76e38b5ff92ed0d08870ac523f9f3f8925a40, not stripped
$ ./crackme8
Usage: ./crackme8 password
$ ./crackme8 abcdef
Access denied.Same routine as before: list functions and find who calls giveFlag.
$ r2 crackme8
-- 256 colors ought to be enough for anybody
[0x080483a0]> aaa
...
[0x080483a0]> afl
0x080483a0 1 33 entry0
0x08048360 1 6 sym.imp.__libc_start_main
0x080483e0 4 43 sym.deregister_tm_clones
0x08048410 4 53 sym.register_tm_clones
0x08048450 3 30 sym.__do_global_dtors_aux
0x08048470 4 43 -> 40 entry.init0
0x08048524 4 149 sym.giveFlag
0x08048620 1 2 sym.__libc_csu_fini
0x080483d0 1 4 sym.__x86.get_pc_thunk.bx
0x08048624 1 20 sym._fini
0x080485c0 4 93 sym.__libc_csu_init
0x0804849b 6 137 main
0x08048300 3 35 sym._init
0x08048340 1 6 sym.imp.printf
0x08048350 1 6 sym.imp.puts
0x08048370 1 6 sym.imp.memset
0x08048380 1 6 sym.imp.atoi
[0x080483a0]> axt sym.giveFlag
main 0x8048512 [CALL] call sym.giveFlag
[0x080483a0]> pdf @ main
Crack
At 0x080484e4 the program runs cmp eax, 0xcafef00d. Instead of working out what input produces that value, I just put it into $eax myself before the comparison. Here’s the gdb_cmd file:
break * 0x080484E4
r 1
set $eax=3405705229
c3405705229 is simply 0xcafef00d in decimal.$ gdb -x gdb_cmd ./crackme8
GNU gdb (GDB) 10.1
...
Undefined command: "import". Try "help".
Reading symbols from ./crackme8...
(No debugging symbols found in ./crackme8)
Breakpoint 1 at 0x80484e4
Breakpoint 1, 0x080484e4 in main ()
Access granted.
flag{at_least_this_cafe_wont_leak_your_credit_card_numbers}
[Inferior 1 (process 1537622) exited normally]
(gdb)Result
flag{at_least_this_cafe_wont_leak_your_credit_card_numbers}. That’s the last one.
Wrap-up
- Always start cheap:
file, run it,strings. Three of the eight fell to that alone. - Comparisons are the weak spot. Break on
strcmp, hookstrncmpwithLD_PRELOAD, or read thecmpoperands directly. - You don’t always need the right input. Changing a register in gdb skips the check entirely.
aflplusaxtin radare2 is a fast way to go from “where’s the flag function?” to “what guards it?”.
h4ppy r3v3r5e! ;)