DawgCTF: Where we roppin boys?

16 apr. 2020

Forknife is still a thing right?

nc ctf.umbccd.io 4100


Category: pwn
Points: 350
Author: trashcanna
Challenge Binary: rop

Overview

We were given a 32-bit executable with canaries and PIE disabled. The main functionality of the binary includes only 3 small functions:


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.

OffsetContentSize
-0x4Return pointer4
-0x8Saved ebp4
-0xcSaved ebx4
-0x14input8

Table 1: Stackframe of tryme

Hidden Functionalities

Looking at the symbol table we can see that the binary contains a lot of functions that don't get called:


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:


  1. tilted_towers
  2. junk_junction
  3. snobby_shores
  4. greasy_grove
  5. lonely_lodge
  6. dusty_depot
  7. loot_lake

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?

Expanding our possibilities

If we take a look at the 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):

AddressOffsetContentSize
xxxxxxx0-0x0Aligned by main
xxxxxxxC-0x4Return pointer4
xxxxxxx8-0x8Saved ebp4
xxxxxxx4-0xcSaved ebx4
xxxxxxx0-0x10input + 44
xxxxxxxC-0x14input + 04

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.

Crafting an exploit

The exploit strategy is to create the following stack layout by repeatedly calling main through the buffer-overflows:

OffsetStackframe LayoutSizeROP-gadgetNesting Level
-0x0Aligned by main4win()main
-0x4Return pointer4main()1. tryme
-0x8Saved ebp4garbage1. tryme
-0xCSaved ebx4pop ebx; pop ebp1. tryme
-0x10input + 44garbage1. tryme
-0x14input + 0 / Return Pointer4main()1./2. tryme
-0x18Saved ebp4pop ebx; pop ebp2. tryme
-0x1CSaved ebx4dusty_depot()2. tryme
-0x20input + 44garbage2. tryme
-0x24input + 0 / Return Pointer4main()2./3. tryme
-0x28Saved ebp4pop ebx; pop ebp3. tryme
-0x2CSaved ebx4lonely_lodge()3. tryme
-0x30input + 44garbage3. tryme
-0x34input + 0 / Return Pointer4main()3./4. tryme
...............
-0x74input + 0 / Return Pointer4pop ebx7./8. tryme
-0x78Saved ebp4garbage8. tryme
-0x7CSaved ebx4garbage8. tryme
-0x84input8garbage8. 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.

Profit

exploit.py executes the exploit as described above and we get

DawgCTF{f0rtni9ht_xD}