日記

日本語の勉強のためのブログ

SECCON Beginners CTF 2024 へなちょこwriteup

2024/06/15, 16に開催されたSECCON Beginners CTF 2024に1人チームで参加し、8問解いて173位*1だった。

図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.engeLLVMの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

参考:

以上の中間言語から、だいたい以下のような処理を行っていることがわかる。

// コンパイルしてないので動かなかったらごめんなさい
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チーム