Easy Elf (reversing.kr)
Goal: find the password that makes the binary print Correct!
Difficulty: first taste of stripped analysis
$ file easy_elfELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked,interpreter /lib/ld-linux.so.2, ... strippedwe have three facts right away:
- ELF32 - i386, little endian
- stripped - no symbol table, so no
mainto jump to. - dynaimcally linked - uses
libc, so only 2 real imports matter:scanfandwrite
$ strings easy_elf
...__isoc99_scanfwrite[^_]Correct!Reversing.Kr Easy ELFWrongscanf + “Correct!”/“Wrong” + a compare = a classic input-check crackme.
2. Finding main in the stripped binary
There’s no main symbol, so we follow the standard entry-point chain:
- Entry point from ELF header:
0x8048380(that’s_start) _startcalls__libc_start_main(main, argc, argv, init, fini, ...)-mainis pushed on the stack first:
804838b: push 0x80485f0 ; __libc_csu_fini8048390: push 0x8048580 ; __libc_csu_init8048395: push ecx8048396: push esi8048397: push 0x804851b ; <-- main804839c: call __libc_start_mainso main = 0x804851b. This trick (push main; call __libc_start_main) is how you locate main in any stripped ELF.
3. main at 0x804851b
804851b: push ebp804851e: and esp, 0xfffffff08048524: mov [esp+8], 0x17 ; count = 23804852c: mov [esp+4], 0x804865d ; "Reversing.Kr Easy ELF\n\n"8048534: mov [esp], 0x1 ; fd = stdout804853b: call write ; print the banner
8048540: call 0x8048434 ; --> scanf("%s", &0x804a020)8048545: call 0x8048451 ; --> the check(); returns 0/1804854a: cmp eax, 0x1804854d: jne 0x804855b804854f: call 0x80484f7 ; write "Correct!\n"804855b: ... ; else write "Wrong\n"Two helpers to look at: the input wrapper 0x8048434 and the check 0x8048451.
Input wrapper (0x8048434):
804843a: mov eax, 0x8048650 ; "%s"804843f: mov [esp+4], 0x804a020804844a: call scanf ; scanf("%s", 0x804a020)Input lands at 0x804a020 (a BSS global). So 0x804a020 = input[0], 0x804a021 = input[1], etc.
4: Reading it byte by byte (0x8048451)
8048454: movzx eax, [0x804a021] ; input[1]804845b: cmp al, 0x31 ; == '1'804845d: je +0x0a ; else return 0→ constraint 1: input[1] == '1'
8048469: movzx eax, [0x804a020] ; input[0]8048470: xor eax, 0x348048473: mov [0x804a020], al ; input[0] ^= 0x34 (stored back!)
8048478: movzx eax, [0x804a022] ; input[2]804847f: xor eax, 0x328048482: mov [0x804a022], al ; input[2] ^= 0x32
8048487: movzx eax, [0x804a023] ; input[3]804848e: xor eax, 0x888048491: mov [0x804a023], al ; input[3] ^= 0x88→ the check XORs three bytes in place with 0x34, 0x32, 0x88.
8048496: movzx eax, [0x804a024] ; input[4]804849d: cmp al, 0x58 ; == 'X'→ constraint 2: input[4] == 'X'
80484a8: movzx eax, [0x804a025] ; input[5]80484af: test al, al ; must be 0→ constraint 3: input[5] == 0 — the string must be exactly 5 chars (NUL-terminated).
Now it compares the already-XORed values:
80484ba: movzx eax, [0x804a022] ; input[2] (after ^0x32)80484c1: cmp al, 0x7c ; == '|'
80484cc: movzx eax, [0x804a020] ; input[0] (after ^0x34)80484d3: cmp al, 0x78 ; == 'x'
80484de: movzx eax, [0x804a023] ; input[3] (after ^0x88)80484e5: cmp al, 0xdd---## 5. Reconstructing the pseudocode
char input[6]; // at 0x804a020scanf("%s", input);
if (input[1] != '1') return 0;input[0] ^= 0x34;input[2] ^= 0x32;input[3] ^= 0x88;if (input[4] != 'X') return 0;if (input[5] != 0) return 0; // must be exactly 5 charsif (input[2] != '|') return 0;if (input[0] != 'x') return 0;if (input[3] != 0xdd) return 0;return 1; // -> "Correct!"6. Solving for the key
Because XOR is symmetric (a ^ b == c ⟺ a == c ^ b), each byte inverts cleanly:
| byte | constraint | solve |
|---|---|---|
| input[0] | input[0] ^ 0x34 == 0x78 | 0x78 ^ 0x34 = 0x4C = L |
| input[1] | == '1' | 1 |
| input[2] | input[2] ^ 0x32 == 0x7c | 0x7c ^ 0x32 = 0x4E = N |
| input[3] | input[3] ^ 0x88 == 0xdd | 0xdd ^ 0x88 = 0x55 = U |
| input[4] | == 'X' | X |
| input[5] | == 0 | NUL (implied) |
print(''.join(chr(c) for c in [ 0x78 ^ 0x34, # L 0x31, # 1 0x7c ^ 0x32, # N 0xdd ^ 0x88, # U 0x58, # X]))# L1NUXFlag: L1NUX
7. What this challenge teaches
- Locating
mainin a stripped ELF via the__libc_start_maincall chain. - Reading
.rodata/.data(objdump -s -j .rodata) to resolve string references. - XOR is its own inverse — a XOR-obfuscated compare is trivial to invert once you see the constant.
- The gotcha: the binary mutates the input buffer in place before comparing, so you must compare ag>
8. Tools used
file · strings · readelf · objdump -d -M intel · radare2 (r2) · python3
note:
This is an ELF32 binary. On Arch Linux without lib32-glibc it won’t execute. The check can be reproduced in Python or verified by installing multilib:
sudo pacman -S lib32-glibc
Next reads
View all →9 Sept
Mini-Readelf: Gluing It All Together
The capstone. Four parts of pieces, headers, sections, symbols, relocations, joined into one tool that reads any ELF. The only new mechanic is the sh_link chain: offset into a table that holds offsets into a table that holds strings.
5 Sept
Relocations: How PIE Binaries Fix Their Addresses
A PIE binary can't write final addresses because ASLR moves it. The linker leaves placeholders and the loader patches them after mapping. That's a relocation: R_X86_64_RELATIVE, GLOB_DAT and JUMP_SLOT.
4 Sept
Symbol Tables: What Function Names Actually Are
Function names in a binary are just entries in a table. Two tables actually: .symtab and .dynsym. Here's what each is for, how the struct works, and how to resolve a name from an address.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.