Forknife is still a thing right?
nc ctf.umbccd.io 4100
Category: pwn
Points: 350
Author: trashcanna
Challenge Binary: rop
We were given a 32-bit executable with canaries and PIE disabled. The main functionality of the binary includes only 3 small functions:
Figure 1: Disassembly of challenge binary
The vulnerability is highlighted in red. tryme stores up to 25 bytes into an 8-byte buffer leading to a classical stack-buffer overflow. Unfortunately 25 bytes allow only very limited exploitation, in fact if we look at the stackframe of tryme the overflow allows us to place a maximum of only 2 ROP-gadgets.
| Offset | Content | Size |
| -0x4 | Return pointer | 4 |
| -0x8 | Saved ebp | 4 |
| -0xc | Saved ebx | 4 |
| -0x14 | input | 8 |
Table 1: Stackframe of tryme
Looking at the symbol table we can see that the binary contains a lot of functions that don't get called:
Figure 2: Symbol table excerpt
The first 7 functions set the shellcode up, which was mmaped earlier in welcome. Each of these functions appends 4 specific bytes to the shellcode. The last function win takes the shellcode and executes it. After puzzling a bit we can get the following shellcode:
0: 31 c0 xor eax, eax
2: 50 push eax
3: 68 2f 2f 73 68 push 0x68732f2f
8: 68 2f 62 69 6e push 0x6e69622f
d: 89 e3 mov ebx, esp
f: 89 c1 mov ecx, eax
11: 89 c2 mov edx, eax
13: b0 0b mov al, 0xb
15: cd 80 int 0x80
17: 31 c0 xor eax, eax
19: 40 inc eax
1a: cd 80 int 0x80
with the following order:
So our task is to create a ROP-chain that calls these 7 functions in exactly that order followed by a call to win to get a shell. But we can place only 2 ROP-gadgets so how do we execute 8?
If we take a look at the disassembly of main:
Figure 3: Disassembly of main
We can see that a CALL tryme follows an AND ESP, 0xfffffff0. If we take the stackframe of tryme from above and add some stack addresses we get the following stack layout (where x stands for a random byte):
| Address | Offset | Content | Size |
| xxxxxxx0 | -0x0 | Aligned by main | |
| xxxxxxxC | -0x4 | Return pointer | 4 |
| xxxxxxx8 | -0x8 | Saved ebp | 4 |
| xxxxxxx4 | -0xc | Saved ebx | 4 |
| xxxxxxx0 | -0x10 | input + 4 | 4 |
| xxxxxxxC | -0x14 | input + 0 | 4 |
Table 2: Stackframe with addresses
If we now overwrite the return pointer of tryme with the address of main (0x08049714) we can cause a second alignment, this time at address xxxxxxxC moving the stack pointer all the way down to input + 4 preserving everything else in tryme's stackframe. After that main calls tryme again overwriting input + 0 with the return address for tryme. We then have again the possibility to do a buffer-overflow. If we repeat this we can allocate multiple stackframes that are directly adjacent to each other. This is the setup for our ROP-chain.
The exploit strategy is to create the following stack layout by repeatedly calling main through the buffer-overflows:
| Offset | Stackframe Layout | Size | ROP-gadget | Nesting Level |
| -0x0 | Aligned by main | 4 | win() | main |
| -0x4 | Return pointer | 4 | main() | 1. tryme |
| -0x8 | Saved ebp | 4 | garbage | 1. tryme |
| -0xC | Saved ebx | 4 | pop ebx; pop ebp | 1. tryme |
| -0x10 | input + 4 | 4 | garbage | 1. tryme |
| -0x14 | input + 0 / Return Pointer | 4 | main() | 1./2. tryme |
| -0x18 | Saved ebp | 4 | pop ebx; pop ebp | 2. tryme |
| -0x1C | Saved ebx | 4 | dusty_depot() | 2. tryme |
| -0x20 | input + 4 | 4 | garbage | 2. tryme |
| -0x24 | input + 0 / Return Pointer | 4 | main() | 2./3. tryme |
| -0x28 | Saved ebp | 4 | pop ebx; pop ebp | 3. tryme |
| -0x2C | Saved ebx | 4 | lonely_lodge() | 3. tryme |
| -0x30 | input + 4 | 4 | garbage | 3. tryme |
| -0x34 | input + 0 / Return Pointer | 4 | main() | 3./4. tryme |
| ... | ... | ... | ... | ... |
| -0x74 | input + 0 / Return Pointer | 4 | pop ebx | 7./8. tryme |
| -0x78 | Saved ebp | 4 | garbage | 8. tryme |
| -0x7C | Saved ebx | 4 | garbage | 8. tryme |
| -0x84 | input | 8 | garbage | 8. tryme |
Table 3: Final ROP-chain
Note how here the last function before win is dusty_depot and not loot_lake because if you leave out the last 4 bytes of the shellcode you still get a valid execve() and this makes the ROP-chain a bit shorter.
exploit.py executes the exploit as described above and we get
DawgCTF{f0rtni9ht_xD}