Skip to content
Reversing ELF

Reversing ELF

December 7, 2020

Reversing ELF cover: an ELF hexdump under a magnifying glass

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.

⚡
TL;DR — Eight ELF crackmes, eight techniques: run it, 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.

One trick per crackme: run, strings, base64, gdb, LD_PRELOAD, cmp bytes, r2, set $eax

Setup

Tools I used:

  • radare2 (r2)
  • gdb
  • the usual suspects: file, strings, objdump, base64

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_cmd

Every 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 stripped

It’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
.comment

super_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
.comment

Crack

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_7h1ng5

Result

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 OK

Crack

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)
🎯
“Hidden” strings are only hidden on disk. Break right before the comparison and the plaintext is sitting in a register, waiting for you.

Result

$ ./crackme4 my_m0r3_secur3_pwd
password OK

crackme5: 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 stripped

strings 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  _fini

The 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 deeper

Result

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 stripped

The 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  _fini

Crack

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,%al

Convert the hex values with an ASCII table: 31 33 33 37 5f 70 77 64 → 1337_pwd.

Result

$ ./crackme6 1337_pwd
password OK

crackme7: 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 stripped

Nothing 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_scanf

Crack

We want sym.giveFlag at 0x080486a6 to run. Who calls it?

[0x080483c0]> axt sym.giveFlag
main 0x804867c [CALL] call sym.giveFlag
[0x080483c0]> pdf @ main

radare2 disassembly of main in crackme7, showing cmp eax, 0x7a69 before the giveFlag call

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

radare2 disassembly of main in crackme8, showing cmp eax, 0xcafef00d

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
c
🔢
3405705229 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, hook strncmp with LD_PRELOAD, or read the cmp operands directly.
  • You don’t always need the right input. Changing a register in gdb skips the check entirely.
  • afl plus axt in radare2 is a fast way to go from “where’s the flag function?” to “what guards it?”.

h4ppy r3v3r5e! ;)

Last updated on