こんにちは。ぽちです。
この度、SECCON Beginners CTF 2025にチーム「THE Students」として参加しましたので、忘備録としてこのWriteupを残しておきます。
主にreversingを担当しました。初心者なので稚拙な部分もあるかと思いますが、一つの解法として参考にしていただければ幸いです。
去年のSECCON for Beginners 2024に続いての参加でしたが、今年はメンバーが変わり、少人数での挑戦となりました。
人数も少ない分、去年より順位も下がりましたが、今回も非常に楽しく学びの多いCTFでした。
Welcome (100pt / 865 solves)
問題
SECCON Beginners CTF 2025へようこそ Flagは ctf4b{W3lc0m3_2_SECCON_Beginners_CTF_2025} です
回答
flag
ctf4b{W3lc0m3_2_SECCON_Beginners_CTF_2025}
seesaw (100pt / 612 solves)
問題
RSA初心者です!pとqはこれでいいよね…?
配布ファイル: seesaw.zip(chall.py, output.txt)
chall.py
import os
from Crypto.Util.number import getPrime
FLAG = os.getenv("FLAG", "ctf4b{dummy_flag}").encode()
m = int.from_bytes(FLAG, 'big')
p = getPrime(512)
q = getPrime(16)
n = p * q
e = 65537
c = pow(m, e, n)
print(f"{n = }")
print(f"{c = }")
output.txt
n = 362433315617467211669633373003829486226172411166482563442958886158019905839570405964630640284863309204026062750823707471292828663974783556794504696138513859209
c = 104442881094680864129296583260490252400922571545171796349604339308085282733910615781378379107333719109188819881987696111496081779901973854697078360545565962079
解説・解法
この問題はq
が16bit
ということがポイントです。
つまり、n = p * q
のq
を総当たりで求めることができます。
与えられた情報
e = 65537
n = 362433315617467211669633373003829486226172411166482563442958886158019905839570405964630640284863309204026062750823707471292828663974783556794504696138513859209
c = 104442881094680864129296583260490252400922571545171796349604339308085282733910615781378379107333719109188819881987696111496081779901973854697078360545565962079
・q
は16bit → 最大でも2^16(65536)通りなので全探索。
・n % q == 0
を用いてq
を求める。
・p = n // q
からφ(n)
を計算。
・d = inverser(e, φ(n))
で秘密鍵を計算。
・m = pow(c, d, n)
で復号。
・FLAG = long_to_bytes(m)
でflagを獲得。
回答
solver.py
from Crypto.Util.number import long_to_bytes, inverse
n = 362433315617467211669633373003829486226172411166482563442958886158019905839570405964630640284863309204026062750823707471292828663974783556794504696138513859209
c = 104442881094680864129296583260490252400922571545171796349604339308085282733910615781378379107333719109188819881987696111496081779901973854697078360545565962079
e = 65537
for q in range(2, 1 16):
if n % q == 0:
p = n // q
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
try:
flag = long_to_bytes(m)
if b'ctf4b' in flag:
print(f"FLAG = {flag.decode()}")
break
except Exception:
continue
flag
ctf4b{unb4l4nc3d_pr1m35_4r3_b4d}
CrazyLazyProgram1 (100pt / 654 solves)
問題
改行が面倒だったのでワンライナーにしてみました。
配布ファイル: CrazyLazyProgram1.zip(CLP1.cs)
CLP1.cs
using System;class Program {static void Main() {int len=0x23;Console.Write("INPUT > ");string flag=Console.ReadLine();if((flag.Length)!=len){Console.WriteLine("WRONG!");}else{if(flag[0]==0x63&&flag[1]==0x74&&flag[2]==0x66&&flag[3]==0x34&&flag[4]==0x62&&flag[5]==0x7b&&flag[6]==0x31&&flag[7]==0x5f&&flag[8]==0x31&&flag[9]==0x69&&flag[10]==0x6e&&flag[11]==0x33&&flag[12]==0x72&&flag[13]==0x35&&flag[14]==0x5f&&flag[15]==0x6d&&flag[16]==0x61&&flag[17]==0x6b&&flag[18]==0x33&&flag[19]==0x5f&&flag[20]==0x50&&flag[21]==0x47&&flag[22]==0x5f&&flag[23]==0x68&&flag[24]==0x61&&flag[25]==0x72&&flag[26]==0x64&&flag[27]==0x5f&&flag[28]==0x32&&flag[29]==0x5f&&flag[30]==0x72&&flag[31]==0x33&&flag[32]==0x61&&flag[33]==0x64&&flag[34]==0x7d){Console.WriteLine("YES!!!\nThis is Flag :)");}else{Console.WriteLine("WRONG!");}}}}
これらのC#コードを可読性を高めるために改行してみました。
CLP1.cs
using System;
class Program
{
static void Main()
{
int len = 0x23;
Console.Write("INPUT > ");
string flag = Console.ReadLine();
if ((flag.Length) != len)
{
Console.WriteLine("WRONG!");
}
else
{
if (flag[0] == 0x63 &&
flag[1] == 0x74 &&
flag[2] == 0x66 &&
flag[3] == 0x34 &&
flag[4] == 0x62 &&
flag[5] == 0x7b &&
flag[6] == 0x31 &&
flag[7] == 0x5f &&
flag[8] == 0x31 &&
flag[9] == 0x69 &&
flag[10] == 0x6e &&
flag[11] == 0x33 &&
flag[12] == 0x72 &&
flag[13] == 0x35 &&
flag[14] == 0x5f &&
flag[15] == 0x6d &&
flag[16] == 0x61 &&
flag[17] == 0x6b &&
flag[18] == 0x33 &&
flag[19] == 0x5f &&
flag[20] == 0x50 &&
flag[21] == 0x47 &&
flag[22] == 0x5f &&
flag[23] == 0x68 &&
flag[24] == 0x61 &&
flag[25] == 0x72 &&
flag[26] == 0x64 &&
flag[27] == 0x5f &&
flag[28] == 0x32 &&
flag[29] == 0x5f &&
flag[30] == 0x72 &&
flag[31] == 0x33 &&
flag[32] == 0x61 &&
flag[33] == 0x64 &&
flag[34] == 0x7d)
{
Console.WriteLine("YES!!!\nThis is Flag :)");
}
else
{
Console.WriteLine("WRONG!");
}
}
}
}
解法
CyberChefで配列flag
の中身を変換します。
flag[]
0x63, 0x74, 0x66, 0x34, 0x62, 0x7b, 0x31, 0x5f, 0x31, 0x69, 0x6e, 0x33, 0x72, 0x35, 0x5f, 0x6d, 0x61, 0x6b, 0x33, 0x5f, 0x50, 0x47, 0x5f, 0x68, 0x61, 0x72, 0x64, 0x5f, 0x32, 0x5f, 0x72, 0x33, 0x61, 0x64, 0x7d
すると、flagが出てきます。
回答
flag
ctf4b{1_1in3r5_mak3_PG_hard_2_r3ad}
CrazyLazyProgram2 (100pt / 468 solves)
問題
コーディングが面倒だったので機械語で作ってみました。
配布ファイル: CrazyLazyProgram2.zip(CLP2.o)
zipファイルを展開すると、CLP2.o
というファイルが入っていました。
この.oファイル(オブジェクトファイル)をGhidraに投げて、デコンパイルしたmain関数
の中身を見てみます。
main()
void main(void){
char local_38;
...(略)
undefined4 local_c;
printf("Enter the flag: ");
__isoc99_scanf(&DAT_001003c6,&local_38);
local_c = 0;
if(local_38 == 'c' &&
(local_c = 1, cStack_37 == 't') &&
(local_c = 2, cStack_36 == 'f') &&
(local_c = 3, cStack_35 == '4') &&
(local_c = 4, cStack_34 == 'b') &&
(local_c = 5, cStack_33 == '{') &&
(local_c = 6, cStack_32 == 'G') &&
(local_c = 7, cStack_31 == 'O') &&
(local_c = 8, cStack_30 == 'T') &&
(local_c = 9, cStack_2f == 'O') &&
(local_c = 10, cStack_2e == '_') &&
(local_c = 0xb, cStack_2d == 'G') &&
(local_c = 0xc, cStack_2c == '0') &&
(local_c = 0xd, cStack_2b == 'T') &&
(local_c = 0xe, cStack_2a == '0') &&
(local_c = 0xf, cStack_29 == '_') &&
(local_c = 0x10, cStack_28 == '9') &&
(local_c = 0x11, cStack_27 == '0') &&
(local_c = 0x12, cStack_26 == 't') &&
(local_c = 0x13, cStack_25 == '0') &&
(local_c = 0x14, cStack_24 == '_') &&
(local_c = 0x15, cStack_23 == 'N') &&
(local_c = 0x16, cStack_22 == '0') &&
(local_c = 0x17, cStack_21 == 'm') &&
(local_c = 0x18, cStack_20 == '0') &&
(local_c = 0x19, cStack_1f == 'r') &&
(local_c = 0x1a, cStack_1e == '3') &&
(local_c = 0x1b, cStack_1d == '_') &&
(local_c = 0x1c, cStack_1c == '9') &&
(local_c = 0x1d, cStack_1b == '0') &&
(local_c = 0x1e, cStack_1a == 't') &&
(local_c = 0x1f, cStack_19 == '0') &&
(local_c = 0x20, cStack_18 == '}'))
{
puts("Flag is correct!");
}
return;
上記のコードは、&&
(AND)で繋がれた大量の比較処理をしているif文です。すべての条件が真(true)でないと、puts("Flag is correct!");
は実行されません。
したがって、local_38
が1文字目、cStack_37
が2文字目…というように、各変数と比較されている文字を順番に繋ぎ合わせることでflagが復元できます。
回答
flag
ctf4b{GOTO_G0T0_90t0_N0m0r3_90t0}
D-compile (100pt / 335 solves)
問題
C言語の次はこれ!
※一部環境ではlibgphobos5
が必要となります。また必要に応じてecho -n
をご利用ください。
配布ファイル: d-compile.zip
解法
-
ファイル形式の確認
まず、d-compile
のファイル種類をfile
コマンドを使用し確認します。
$ file ./d-compile
./d-compile: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=73b95b2e5aea9420c85741c0582cb0945a3a3c69, for GNU/Linux 3.2.0, not stripped
・上記のELF 64-bit LSB executable
からLinuxで動作する64ビットの実行ファイルであることが分かります。
・not stripped
: とてもありがたい情報です。これは、シンボル情報(関数名や変数名など)がバイナリ内に削除されず残っていることを意味します。これにより、逆アセンブルした際に関数名から処理内容を推測しやすくなり、解析が楽になります。
2. 埋め込まれた文字列の調査
次にstrings
コマンドを実行してバイナリ内にハードコードされている文字列がないか探します。
$ strings ./d-compile
...(略)
input flag>
way to go! this is the flag :)
this is wrong
...(略)
challnege
challnege.d
...(略)
_Dmain
...(略)
出力が長いので一部割愛しますが、気になる文字列が見つかりました。
プログラムの挙動に関する文字列
・input flag>
・way to go! this is the flag :)
・this is wrong
これらの文字列から、このプログラムがユーザーにフラグの入力を促し、その正誤を判定するフラグチェッカーであると推測できます。
解析の突破口となりうる文字列
・challnege.d
: .d
という拡張子から、このプログラムがD言語
で書かれている可能性が高いとわかります。
・_Dmain
: not stripped
だったおかげでD言語のmain
関数にあたるシンボル名がそのまま残っているのを発見できました。恐らくプログラムのメインロジックはここから始まると考えられます。
-
逆アセンブル(
_Dmain
関数の解析)
ターミナルでobjdump
を使用して解析していきます。
objdump -M intel -d ./d-compile
実行するとえげつない量の出力が出てくるので、頑張って_Dmain
の部分を探してください。
000000000000335a _Dmain>:
335a: 55 push rbp
335b: 48 89 e5 mov rbp,rsp
335e: 41 57 push r15
3360: 41 56 push r14
3362: 41 55 push r13
3364: 41 54 push r12
3366: 53 push rbx
3367: 48 83 ec 58 sub rsp,0x58
336b: 48 c7 45 90 20 00 00 mov QWORD PTR [rbp-0x70],0x20
3372: 00
3373: be 20 00 00 00 mov esi,0x20
3378: 48 8b 05 51 4c 00 00 mov rax,QWORD PTR [rip+0x4c51] # 7fd0 _D11TypeInfo_Aa6__initZ@Base>
337f: 48 89 c7 mov rdi,rax
3382: e8 e9 fd ff ff call 3170 _d_arrayliteralTX@plt>
3387: c6 45 b0 63 mov BYTE PTR [rbp-0x50],0x63
338b: c6 45 b1 74 mov BYTE PTR [rbp-0x4f],0x74
338f: c6 45 b2 66 mov BYTE PTR [rbp-0x4e],0x66
3393: c6 45 b3 34 mov BYTE PTR [rbp-0x4d],0x34
3397: c6 45 b4 62 mov BYTE PTR [rbp-0x4c],0x62
339b: c6 45 b5 7b mov BYTE PTR [rbp-0x4b],0x7b
339f: c6 45 b6 4e mov BYTE PTR [rbp-0x4a],0x4e
33a3: c6 45 b7 33 mov BYTE PTR [rbp-0x49],0x33
33a7: c6 45 b8 78 mov BYTE PTR [rbp-0x48],0x78
33ab: c6 45 b9 74 mov BYTE PTR [rbp-0x47],0x74
33af: c6 45 ba 5f mov BYTE PTR [rbp-0x46],0x5f
33b3: c6 45 bb 54 mov BYTE PTR [rbp-0x45],0x54
33b7: c6 45 bc 72 mov BYTE PTR [rbp-0x44],0x72
33bb: c6 45 bd 33 mov BYTE PTR [rbp-0x43],0x33
33bf: c6 45 be 6e mov BYTE PTR [rbp-0x42],0x6e
33c3: c6 45 bf 64 mov BYTE PTR [rbp-0x41],0x64
33c7: c6 45 c0 5f mov BYTE PTR [rbp-0x40],0x5f
33cb: c6 45 c1 44 mov BYTE PTR [rbp-0x3f],0x44
33cf: c6 45 c2 5f mov BYTE PTR [rbp-0x3e],0x5f
33d3: c6 45 c3 31 mov BYTE PTR [rbp-0x3d],0x31
33d7: c6 45 c4 61 mov BYTE PTR [rbp-0x3c],0x61
33db: c6 45 c5 6e mov BYTE PTR [rbp-0x3b],0x6e
33df: c6 45 c6 39 mov BYTE PTR [rbp-0x3a],0x39
33e3: c6 45 c7 75 mov BYTE PTR [rbp-0x39],0x75
33e7: c6 45 c8 61 mov BYTE PTR [rbp-0x38],0x61
33eb: c6 45 c9 67 mov BYTE PTR [rbp-0x37],0x67
33ef: c6 45 ca 33 mov BYTE PTR [rbp-0x36],0x33
33f3: c6 45 cb 5f mov BYTE PTR [rbp-0x35],0x5f
33f7: c6 45 cc 31 mov BYTE PTR [rbp-0x34],0x31
33fb: c6 45 cd 30 mov BYTE PTR [rbp-0x33],0x30
33ff: c6 45 ce 31 mov BYTE PTR [rbp-0x32],0x31
3403: c6 45 cf 7d mov BYTE PTR [rbp-0x31],0x7d
3407: 48 8d 55 b0 lea rdx,[rbp-0x50]
340b: 48 8b 0a mov rcx,QWORD PTR [rdx]
340e: 48 8b 5a 08 mov rbx,QWORD PTR [rdx+0x8]
3412: 48 89 08 mov QWORD PTR [rax],rcx
3415: 48 89 58 08 mov QWORD PTR [rax+0x8],rbx
3419: 48 8b 4a 10 mov rcx,QWORD PTR [rdx+0x10]
341d: 48 8b 5a 18 mov rbx,QWORD PTR [rdx+0x18]
3421: 48 89 48 10 mov QWORD PTR [rax+0x10],rcx
3425: 48 89 58 18 mov QWORD PTR [rax+0x18],rbx
3429: 48 89 45 98 mov QWORD PTR [rbp-0x68],rax
342d: 48 c7 45 80 0b 00 00 mov QWORD PTR [rbp-0x80],0xb
3434: 00
3435: 48 8d 05 cc 2b 00 00 lea rax,[rip+0x2bcc] # 6008 _IO_stdin_used+0x8>
343c: 48 89 45 88 mov QWORD PTR [rbp-0x78],rax
3440: 48 8b 5d 80 mov rbx,QWORD PTR [rbp-0x80]
3444: 48 8b 75 88 mov rsi,QWORD PTR [rbp-0x78]
3448: 48 89 da mov rdx,rbx
344b: 48 89 f0 mov rax,rsi
344e: 48 89 d7 mov rdi,rdx
3451: 48 89 c6 mov rsi,rax
3454: e8 83 00 00 00 call 34dc _D3std5stdio__T7writelnTAyaZQnFNfQjZv>
3459: bf 0a 00 00 00 mov edi,0xa
345e: e8 01 02 00 00 call 3664 _D3std5stdio__T6readlnTAyaZQmFwZQj>
3463: 48 89 45 a0 mov QWORD PTR [rbp-0x60],rax
3467: 48 89 55 a8 mov QWORD PTR [rbp-0x58],rdx
346b: 48 8b 45 90 mov rax,QWORD PTR [rbp-0x70]
346f: 48 8b 55 98 mov rdx,QWORD PTR [rbp-0x68]
3473: 48 8b 7d a0 mov rdi,QWORD PTR [rbp-0x60]
3477: 48 8b 75 a8 mov rsi,QWORD PTR [rbp-0x58]
347b: 48 89 d1 mov rcx,rdx
347e: 48 89 c2 mov rdx,rax
3481: e8 00 02 00 00 call 3686 _D4core8internal5array8equality__T8__equalsTaTaZQoFNaNbNiNeMxAaMxQeZb>
3486: 84 c0 test al,al
3488: 74 20 je 34aa _Dmain+0x150>
348a: 41 be 1e 00 00 00 mov r14d,0x1e
3490: 4c 8d 3d 81 2b 00 00 lea r15,[rip+0x2b81] # 6018 _IO_stdin_used+0x18>
3497: 4c 89 f2 mov rdx,r14
349a: 4c 89 f8 mov rax,r15
349d: 48 89 d7 mov rdi,rdx
34a0: 48 89 c6 mov rsi,rax
34a3: e8 34 00 00 00 call 34dc _D3std5stdio__T7writelnTAyaZQnFNfQjZv>
34a8: eb 1e jmp 34c8 _Dmain+0x16e>
34aa: 41 bc 0d 00 00 00 mov r12d,0xd
34b0: 4c 8d 2d 80 2b 00 00 lea r13,[rip+0x2b80] # 6037 _IO_stdin_used+0x37>
34b7: 4c 89 e2 mov rdx,r12
34ba: 4c 89 e8 mov rax,r13
34bd: 48 89 d7 mov rdi,rdx
34c0: 48 89 c6 mov rsi,rax
34c3: e8 14 00 00 00 call 34dc _D3std5stdio__T7writelnTAyaZQnFNfQjZv>
34c8: b8 00 00 00 00 mov eax,0x0
34cd: 48 83 c4 58 add rsp,0x58
34d1: 5b pop rbx
34d2: 41 5c pop r12
34d4: 41 5d pop r13
34d6: 41 5e pop r14
34d8: 41 5f pop r15
34da: 5d pop rbp
34db: c3 ret
中身を要約すると、以下の3ステップで構成されています。
- 正解フラグの文字列をメモリ上に構築する。
- ユーザーからの入力を受け取る。
- 1と2を比較し、結果に応じてメッセージを表示する。
1.正解フラグの構築
_Dmain
の前半、0x3387
から0x3403
のアドレスにかけて、以下のような命令が大量に並んでいます。
3387: c6 45 b0 63 mov BYTE PTR [rbp-0x50],0x63
338b: c6 45 b1 74 mov BYTE PTR [rbp-0x4f],0x74
338f: c6 45 b2 66 mov BYTE PTR [rbp-0x4e],0x66
3393: c6 45 b3 34 mov BYTE PTR [rbp-0x4d],0x34
...(略)
3403: c6 45 cf 7d mov BYTE PTR [rbp-0x31],0x7d
これらの処理は、スタック上のメモリ領域([rdp-0x50]
から始まる場所)に1バイトずつ即値を書き込んでいる処理です。この即値をASCII文字として解釈すると、正解となるflagが浮かび上がってきます。
なので、これらのASCII文字をCyberChefにかけて復号すると、flagとなります。
回答
flag
ctf4b{N3xt_Tr3nd_D_1an9uag3_101}
wasm_S_exp (100pt / 330 solves)
問題
フラグをチェックしてくれるプログラム
配布ファイル: wasm_S_exp.zip(check_flag.wat)
zipを展開するとcheck_flag.wat
というWAT(Wasm Text Format)形式のファイルが入っていました。
(module
(memory (export "memory") 1 )
(func (export "check_flag") (result i32)
i32.const 0x7b
i32.const 38
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x67
i32.const 20
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x5f
i32.const 46
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x21
i32.const 3
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x63
i32.const 18
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x6e
i32.const 119
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x5f
i32.const 51
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x79
i32.const 59
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x34
i32.const 9
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x57
i32.const 4
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x35
i32.const 37
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x33
i32.const 12
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x62
i32.const 111
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x63
i32.const 45
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x7d
i32.const 97
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x30
i32.const 54
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x74
i32.const 112
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x31
i32.const 106
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x66
i32.const 43
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x34
i32.const 17
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x34
i32.const 98
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x54
i32.const 120
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x5f
i32.const 25
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x6c
i32.const 127
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 0x41
i32.const 26
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
i32.const 1
return
)
(func $stir (param $x i32) (result i32)
i32.const 1024
i32.const 23
i32.const 37
local.get $x
i32.const 0x5a5a
i32.xor
i32.mul
i32.add
i32.const 101
i32.rem_u
i32.add
return
)
)
解説・解法
このWasmモジュールには2つの主要な関数があります。
・check_flag
: flagの検証を行うメインの関数
・stir
: flagの、どの位置の文字をチェックするかを計算するための関数。一種の難読化。
1.check_flag関数の解析
この関数は同じような構造のブロックが何度も繰り返されています。
i32.const 0x7b
i32.const 38
call $stir
i32.load8_u
i32.ne
if
i32.const 0
return
end
このロジックから、stir
関数が計算したメモリアドレスにある文字が、指定された文字と一致するかを調べていることがわかります。全てのチェックを通過すれば、最終的に1
(成功)を返します。
2.stir関数の解析
次に、メモリアドレスを計算しているstir
関数を解読します。
(func $stir (param $x i32) (result i32)
i32.const 1024
i32.const 23
i32.const 37
local.get $x
i32.const 0x5a5a
i32.xor
i32.mul
i32.add
i32.const 101
i32.rem_u
i32.add
return
)
これはスタックマシンなので、下から順に計算します。数式にすると以下のようになります。address = 1024 + ((37 *(x ^ 0x5a5a) + 23) % 101)
ここで1024
はflagが格納されるメモリのベースアドレス、x
はcheck_flag
から渡されるパラメータに対応します。このx
を用いて((37 *(x ^ 0x5a5a) + 23) % 101)
を計算した結果が、検証対象となる文字のインデックス(何文字目か)となります。
3.flagの復元
あとはcheck_flag
にある全てのブロックについて、x
の値からインデックスを計算し、対応する文字を当てはめていくだけです。
回答
solver.py
def stir(x):
index = ((37 * (x ^ 0x5a5a) + 23) % 101)
return index
def solve():
checks = [
(0x7b, 38), (0x67, 20), (0x5f, 46), (0x21, 3), (0x63, 18),
(0x6e, 119), (0x5f, 51), (0x79, 59), (0x34, 9), (0x57, 4),
(0x35, 37), (0x33, 12), (0x62, 111), (0x63, 45), (0x7d, 97),
(0x30, 54), (0x74, 112), (0x31, 106), (0x66, 43), (0x34, 17),
(0x34, 98), (0x54, 120), (0x5f, 25), (0x6c, 127), (0x41, 26),
]
flag_chars = []
for char_code, x_val in checks:
index = stir(x_val)
character = chr(char_code)
flag_chars.append((index, character))
flag_chars.sort()
final_flag = "".join([char for index, char in flag_chars])
return final_flag
if __name__ == "__main__":
flag = solve()
print(f"Flag: {flag}")
flag
ctf4b{WAT_4n_345y_l0g1c!}
MAFC (339pt / 144 solves)
問題
flagが欲しいかい?ならこのマルウェアを解析してみな。
配布ファイル: MAFC.zip(file.encrypted, MalwareAnalysis-FirstChallenge.exe)
zipを展開すると、flag.encryptedとMalwareAnalysis-FirstChallange.exeが入ってました。
最初からGhidraに投げずに、初期調査を行います。
実行結果(長いです)
$ file MalwareAnalysis-FirstChallenge.exe
MalwareAnalysis-FirstChallenge.exe: PE32+ executable (console) x86-64, for MS Windows, 6 sections
$ file flag.encrypted
flag.encrypted: data
$ strings MalwareAnalysis-FirstChallenge.exe | head -20
!This program cannot be run in DOS mode.
.text
.rdata
.data
.pdata
.rsrc
.reloc
$ hexdump -C flag.encrypted | head -10
00000000 02 69 6f b6 fb 1f 26 4f 97 db 92 09 66 cc b8 a7 |.io...&O....f...|
00000010 d4 a1 e5 0e 9b e8 57 4d fe 96 11 83 af 06 1f d8 |......WM........|
00000020 c2 22 05 80 d5 69 a5 4e 3b 78 f5 e0 bd 73 02 6c |."...i.N;x...s.l|
00000030 7b 62 aa 9e 6c bf ad 2c 96 04 87 31 95 da b1 c2 |{b..l..,...1....|
00000040
$ ls -la
total 28
drwxr-xr-x 2 kali kali 4096 Jul 25 11:33 .
drwxrwxr-x 3 kali kali 4096 Jul 26 03:58 ..
-rw-r--r-- 1 kali kali 64 Jul 25 11:33 flag.encrypted
-rw-r--r-- 1 kali kali 15872 Jul 25 11:33 MalwareAnalysis-FirstChallenge.exe
$ readpe MalwareAnalysis-FirstChallenge.exe
DOS Header
Magic number: 0x5a4d (MZ)
Bytes in last page: 144
Pages in file: 3
Relocations: 0
Size of header in paragraphs: 4
Minimum extra paragraphs: 0
Maximum extra paragraphs: 65535
Initial (relative) SS value: 0
Initial SP value: 0xb8
Initial IP value: 0
Initial (relative) CS value: 0
Address of relocation table: 0x40
Overlay number: 0
OEM identifier: 0
OEM information: 0
PE header offset: 0xf8
PE header
Signature: 0x00004550 (PE)
COFF/File header
Machine: 0x8664 IMAGE_FILE_MACHINE_AMD64
Number of sections: 6
Date/time stamp: 1749952047 (Sun, 15 Jun 2025 01:47:27 UTC)
Symbol Table offset: 0
Number of symbols: 0
Size of optional header: 0xf0
Characteristics: 0x22
Characteristics names
IMAGE_FILE_EXECUTABLE_IMAGE
IMAGE_FILE_LARGE_ADDRESS_AWARE
Optional/Image header
Magic number: 0x20b (PE32+)
Linker major version: 14
Linker minor version: 44
Size of .text section: 0x1600
Size of .data section: 0x2a00
Size of .bss section: 0
Entrypoint: 0x1970
Address of .text section: 0x1000
ImageBase: 0x140000000
Alignment of sections: 0x1000
Alignment factor: 0x200
Major version of required OS: 6
Minor version of required OS: 0
Major version of image: 0
Minor version of image: 0
Major version of subsystem: 6
Minor version of subsystem: 0
Win32 version value: 0
Overwrite OS major version: (default)
Overwrite OS minor version: (default)
Overwrite OS build number: (default)
Overwrite OS platform id: (default)
Size of image: 0x9000
Size of headers: 0x400
Checksum: 0
Subsystem required: 0x3 (IMAGE_SUBSYSTEM_WINDOWS_CUI)
DLL characteristics: 0x8160
DLL characteristics names
IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
IMAGE_DLLCHARACTERISTICS_NX_COMPAT
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
Size of stack to reserve: 0x100000
Size of stack to commit: 0x1000
Size of heap space to reserve: 0x100000
Size of heap space to commit: 0x1000
Loader Flags: 0
Loader Flags names
Data directories
Directory
IMAGE_DIRECTORY_ENTRY_IMPORT: 0x40ac (180 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_RESOURCE: 0x7000 (480 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_EXCEPTION: 0x6000 (540 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_BASERELOC: 0x8000 (88 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_DEBUG: 0x37a0 (112 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG: 0x3660 (320 bytes)
Directory
IMAGE_DIRECTORY_ENTRY_IAT: 0x3000 (592 bytes)
Imported functions
Library
Name: KERNEL32.dll
Functions
Function
Hint: 630
Name: GetFileSize
Function
Hint: 156
Name: CloseHandle
Function
Hint: 306
Name: DeleteFileA
Function
Hint: 221
Name: CreateFileA
Function
Hint: 1631
Name: WriteFile
Function
Hint: 1192
Name: ReadFile
Function
Hint: 1296
Name: RtlLookupFunctionEntry
Function
Hint: 1303
Name: RtlVirtualUnwind
Function
Hint: 1530
Name: UnhandledExceptionFilter
Function
Hint: 676
Name: GetModuleHandleW
Function
Hint: 1463
Name: SetUnhandledExceptionFilter
Function
Hint: 576
Name: GetCurrentProcess
Function
Hint: 1288
Name: RtlCaptureContext
Function
Hint: 1495
Name: TerminateProcess
Function
Hint: 951
Name: IsProcessorFeaturePresent
Function
Hint: 1152
Name: QueryPerformanceCounter
Function
Hint: 577
Name: GetCurrentProcessId
Function
Hint: 581
Name: GetCurrentThreadId
Function
Hint: 793
Name: GetSystemTimeAsFileTime
Function
Hint: 943
Name: IsDebuggerPresent
Function
Hint: 921
Name: InitializeSListHead
Library
Name: ADVAPI32.dll
Functions
Function
Hint: 222
Name: CryptSetKeyParam
Function
Hint: 200
Name: CryptDestroyKey
Function
Hint: 194
Name: CryptAcquireContextW
Function
Hint: 203
Name: CryptEncrypt
Function
Hint: 196
Name: CryptCreateHash
Function
Hint: 198
Name: CryptDeriveKey
Function
Hint: 217
Name: CryptHashData
Function
Hint: 220
Name: CryptReleaseContext
Function
Hint: 199
Name: CryptDestroyHash
Library
Name: VCRUNTIME140.dll
Functions
Function
Hint: 1
Name: _CxxThrowException
Function
Hint: 28
Name: __current_exception_context
Function
Hint: 33
Name: __std_exception_copy
Function
Hint: 34
Name: __std_exception_destroy
Function
Hint: 8
Name: __C_specific_handler
Function
Hint: 27
Name: __current_exception
Function
Hint: 60
Name: memcpy
Function
Hint: 62
Name: memset
Library
Name: api-ms-win-crt-runtime-l1-1-0.dll
Functions
Function
Hint: 60
Name: _register_onexit_function
Function
Hint: 30
Name: _crt_atexit
Function
Hint: 21
Name: _c_exit
Function
Hint: 22
Name: _cexit
Function
Hint: 5
Name: __p___argv
Function
Hint: 55
Name: _initterm_e
Function
Hint: 52
Name: _initialize_onexit_table
Function
Hint: 35
Name: _exit
Function
Hint: 85
Name: exit
Function
Hint: 54
Name: _initterm
Function
Hint: 40
Name: _get_initial_narrow_environment
Function
Hint: 51
Name: _initialize_narrow_environment
Function
Hint: 24
Name: _configure_narrow_argv
Function
Hint: 61
Name: _register_thread_local_exe_atexit_callback
Function
Hint: 66
Name: _set_app_type
Function
Hint: 64
Name: _seh_filter_exe
Function
Hint: 4
Name: __p___argc
Function
Hint: 103
Name: terminate
Function
Hint: 58
Name: _invoke_watson
Library
Name: api-ms-win-crt-stdio-l1-1-0.dll
Functions
Function
Hint: 84
Name: _set_fmode
Function
Hint: 1
Name: __p__commode
Function
Hint: 147
Name: puts
Library
Name: api-ms-win-crt-heap-l1-1-0.dll
Functions
Function
Hint: 22
Name: _set_new_mode
Function
Hint: 24
Name: free
Function
Hint: 25
Name: malloc
Function
Hint: 8
Name: _callnewh
Library
Name: api-ms-win-crt-math-l1-1-0.dll
Functions
Function
Hint: 9
Name: __setusermatherr
Library
Name: api-ms-win-crt-locale-l1-1-0.dll
Functions
Function
Hint: 8
Name: _configthreadlocale
Exported functions
Sections
Section
Name: .text
Virtual Size: 0x15bc (5564 bytes)
Virtual Address: 0x1000
Size Of Raw Data: 0x1600 (5632 bytes)
Pointer To Raw Data: 0x400
Number Of Relocations: 0
Characteristics: 0x60000020
Characteristic Names
IMAGE_SCN_CNT_CODE
IMAGE_SCN_MEM_EXECUTE
IMAGE_SCN_MEM_READ
Section
Name: .rdata
Virtual Size: 0x196e (6510 bytes)
Virtual Address: 0x3000
Size Of Raw Data: 0x1a00 (6656 bytes)
Pointer To Raw Data: 0x1a00
Number Of Relocations: 0
Characteristics: 0x40000040
Characteristic Names
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
Section
Name: .data
Virtual Size: 0x720 (1824 bytes)
Virtual Address: 0x5000
Size Of Raw Data: 0x200 (512 bytes)
Pointer To Raw Data: 0x3400
Number Of Relocations: 0
Characteristics: 0xc0000040
Characteristic Names
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
IMAGE_SCN_MEM_WRITE
Section
Name: .pdata
Virtual Size: 0x21c (540 bytes)
Virtual Address: 0x6000
Size Of Raw Data: 0x400 (1024 bytes)
Pointer To Raw Data: 0x3600
Number Of Relocations: 0
Characteristics: 0x40000040
Characteristic Names
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
Section
Name: .rsrc
Virtual Size: 0x1e0 (480 bytes)
Virtual Address: 0x7000
Size Of Raw Data: 0x200 (512 bytes)
Pointer To Raw Data: 0x3a00
Number Of Relocations: 0
Characteristics: 0x40000040
Characteristic Names
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
Section
Name: .reloc
Virtual Size: 0x58 (88 bytes)
Virtual Address: 0x8000
Size Of Raw Data: 0x200 (512 bytes)
Pointer To Raw Data: 0x3c00
Number Of Relocations: 0
Characteristics: 0x42000040
Characteristic Names
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_DISCARDABLE
IMAGE_SCN_MEM_READ
$ objdump -f MalwareAnalysis-FirstChallenge.exe | grep start
start address 0x0000000140001970
$ strings -a MalwareAnalysis-FirstChallenge.exe | grep -i -E "(key|pass|secret|flag|aes|rc4|crypt|encrypt|decrypt)"
flag.txt
Failed to handle flag.txt
flag.encrypted
Failed to handle flag.encrypted
CryptAcquireContext() Error
CryptCreateHash() Error
ThisIsTheEncryptKey
CryptHashData() Error
CryptDeriveKey() Error
CryptSeKeyParam() Error
CryptSeKeyParam() with IV Error
CryptSetKeyParam() with set MODE Error
CryptEncrypt() Error
CryptDestroyKey() error
CryptDestroyHash() error
CryptReleaseContext() error
CryptReleaseContext
CryptSetKeyParam
CryptDestroyHash
CryptHashData
CryptDeriveKey
CryptCreateHash
CryptEncrypt
CryptAcquireContextW
CryptDestroyKey
$ strings -a MalwareAnalysis-FirstChallenge.exe | grep -E "^[a-zA-Z0-9]{8,16}$"
ReadFile
WriteFile
CreateFileA
DeleteFileA
CloseHandle
GetFileSize
CryptSetKeyParam
CryptDestroyHash
CryptHashData
CryptDeriveKey
CryptCreateHash
CryptEncrypt
CryptDestroyKey
terminate
RtlVirtualUnwind
TerminateProcess
GetModuleHandleW
$ strings -a MalwareAnalysis-FirstChallenge.exe | grep -i "ctf"
上記の結果から以下のことがわかりました。
-
Windows CryptoAPIの使用: インポートテーブルに
CryptCreateHash
やCryptDeriveKey
といったWindows CryptoAPIの関数群が含まれており、Windows標準の暗号化機能を使用している -
暗号鍵文字列:
strings
の出力の中に、暗号鍵らしき文字列ThisIsTheEncryptKey
がある
次に、実行ファイル内のコードをGhidraで解析します。
Symbol Treeで使用されている関数名を確認してみると、main
関数が確認できませんね。
これはコンパイル時にシンボル情報が削除されていることを意味します。
ソースコードに記述されているmain
やWinMain
といった関数名は、人間がコードを読みやすくするためのシンボル(目印) です。
関数名はどうなるの?
関数名は以下の手順で新たな関数名になります。
- ソースコード段階:
int main(){...}
のように、main
という名前がはっきりと存在します。 - コンパイル段階: ソースコードをコンピュータが実行できる機械語に変換する際、ファイルサイズを小さくしたり、解析を少しでも難しくしたりするために、これらのシンボル情報は通常取り除かれます。
- 実行ファイル段階: 最終的に出来上がったexeファイルには、
main
という名前は残っておらず、「プログラムのこの部分(例:アドレス0x1400011a0
)から処理を始めなさい」という情報だけが記録されます。
Ghidraは、この名前が削除された実行ファイルを解析します。そのため、関数の場所はわかっていても元の名前はわからないので、FUN_ + アドレス
の形の名前が自動的に与えられます。
解析に戻ります。
Symbol Treeを確認するとentry()
という関数があります。中身を確認してみましょう。
entry();
void entry(void)
{
FUN_140001d8c();
FUN_1400017f4();
return;
}
・これがプログラムの開始点です。
・関数FUN_14001d8c()
とFUN_140017f4
を順番に呼び出しています。
ではこれら2つの関数の中身も追っていきましょう。
FUN_14001d8c();
void FUN_140001d8c(void)
{
DWORD DVar1;
_FILETIME local_res8;
LARGE_INTEGER local_res10;
_FILETIME local_18 [2];
if (DAT_140005000 == 0x2b992ddfa232) {
local_res8.dwLowDateTime = 0;
local_res8.dwHighDateTime = 0;
GetSystemTimeAsFileTime(&local_res8);
local_18[0] = local_res8;
DVar1 = GetCurrentThreadId();
local_18[0] = (_FILETIME)((ulonglong)local_18[0] ^ (ulonglong)DVar1);
DVar1 = GetCurrentProcessId();
local_18[0] = (_FILETIME)((ulonglong)local_18[0] ^ (ulonglong)DVar1);
QueryPerformanceCounter(&local_res10);
DAT_140005000 =
((ulonglong)local_res10.s.LowPart 0x20 ^
CONCAT44(local_res10.s.HighPart,local_res10.s.LowPart) ^ (ulonglong)local_18[0] ^
(ulonglong)local_18) & 0xffffffffffff;
if (DAT_140005000 == 0x2b992ddfa232) {
DAT_140005000 = 0x2b992ddfa233;
}
}
DAT_140005040 = ~DAT_140005000;
return;
}
これはプログラムのセキュリティ機能(セキュリティクッキーなど)を初期化する、コンパイラが自動で生成するコードです。
・GetSystemTimeAsFileTime
やQueryPerformanceCounter
などを使って予測されにくい「乱数」のようなものを生成しています。
無視して問題ありません。
FUN_1400017f4();
int FUN_1400017f4(void)
{
undefined8 uVar1;
bool bVar2;
char cVar3;
undefined1 uVar4;
int iVar5;
longlong *plVar6;
undefined8 uVar7;
undefined8 *puVar8;
undefined4 *puVar9;
undefined8 unaff_RBX;
undefined8 in_R9;
undefined1 uVar10;
iVar5 = (int)unaff_RBX;
cVar3 = FUN_140001b88(1);
if (cVar3 == '\0') {
FUN_140001ebc(7);
}
else {
bVar2 = false;
uVar10 = 0;
uVar4 = __scrt_acquire_startup_lock();
iVar5 = (int)CONCAT71((int7)((ulonglong)unaff_RBX >> 8),uVar4);
if (DAT_140005690 != 1) {
if (DAT_140005690 == 0) {
DAT_140005690 = 1;
iVar5 = _initterm_e(&DAT_1400032a0,&DAT_1400032b8);
if (iVar5 != 0) {
return 0xff;
}
_initterm(&DAT_140003288,&DAT_140003298);
DAT_140005690 = 2;
}
else {
bVar2 = true;
uVar10 = 1;
}
__scrt_release_startup_lock(uVar4);
plVar6 = (longlong *)FUN_140001ea0();
if ((*plVar6 != 0) && (cVar3 = FUN_140001c50(plVar6), cVar3 != '\0')) {
(*(code *)*plVar6)(0,2,0,in_R9,uVar10);
}
plVar6 = (longlong *)FUN_140001ea8();
if ((*plVar6 != 0) && (cVar3 = FUN_140001c50(plVar6), cVar3 != '\0')) {
_register_thread_local_exe_atexit_callback(*plVar6);
}
uVar7 = _get_initial_narrow_environment();
puVar8 = (undefined8 *)__p___argv();
uVar1 = *puVar8;
puVar9 = (undefined4 *)__p___argc();
iVar5 = FUN_1400011a0(*puVar9,uVar1,uVar7);
cVar3 = FUN_140002010();
if (cVar3 != '\0') {
if (!bVar2) {
_cexit();
}
__scrt_uninitialize_crt(1,0);
return iVar5;
}
goto LAB_140001960;
}
}
FUN_140001ebc(7);
LAB_140001960:
exit(iVar5);
}
これはC言語(C++)のプログラムがmain
関数を呼び出す前に必ず行われるランタイム初期化処理です。
「じゃあこれ関係なくない?」って思いますよね。
いいえ。あるんです。
プログラムをよく読んでみて下さい。
・_initterm_e
や__p___argc
といった名前から、これがmain
関数を呼び出すための準備をしていることがわかります。
最も重要なポイント⚠
54行目(下から17行目)を見てください。
FUN_1400017f4();
iVar5 = FUN_1400011a0(*puVar9,uVar1,uVar7);
この関数(FUN_1400017f4)内部で、FUN_140011a0
という関数が呼び出されています。
これこそが、プログラムの本体(main
関数に相当する部分)です。
では、この関数を解析しましょう。
FUN_140011a0();
void FUN_1400011a0(void)
{
uint uVar1;
code *pcVar2;
longlong lVar3;
BOOL BVar4;
DWORD nNumberOfBytesToRead;
HANDLE hFile;
HANDLE hFile_00;
void *pvVar5;
longlong lVar6;
BYTE *pbData;
BYTE *_Memory;
ulonglong _Size;
longlong lVar7;
BYTE *pBVar8;
undefined1 auStackY_b8 [32];
HCRYPTKEY local_78;
HCRYPTPROV local_70;
BYTE local_68 [8];
DWORD local_60;
DWORD local_5c;
HCRYPTHASH local_58;
BYTE local_50 [24];
ulonglong local_38;
local_38 = DAT_140005000 ^ (ulonglong)auStackY_b8;
pbData = (BYTE *)0x0;
hFile = CreateFileA("flag.txt",0x80000000,1,(LPSECURITY_ATTRIBUTES)0x0,3,0x80,(HANDLE)0x0);
if (hFile == (HANDLE)0xffffffffffffffff) {
puts("Failed to handle flag.txt\n");
}
else {
hFile_00 = CreateFileA("flag.encrypted",0x40000000,0,(LPSECURITY_ATTRIBUTES)0x0,2,0x80,
(HANDLE)0x0);
if (hFile_00 == (HANDLE)0xffffffffffffffff) {
puts("Failed to handle flag.encrypted\n");
goto LAB_140001637;
}
BVar4 = CryptAcquireContextW
(&local_70,(LPCWSTR)0x0,
L"Microsoft Enhanced RSA and AES Cryptographic Provider",0x18,0);
if ((BVar4 == 0) &&
(BVar4 = CryptAcquireContextW
(&local_70,(LPCWSTR)0x0,
L"Microsoft Enhanced RSA and AES Cryptographic Provider",0x18,8),
BVar4 == 0)) {
puts("CryptAcquireContext() Error\n");
goto LAB_140001637;
}
BVar4 = CryptCreateHash(local_70,0x800c,0,0,&local_58);
if (BVar4 == 0) {
puts("CryptCreateHash() Error\n");
goto LAB_140001637;
}
builtin_memcpy(local_50 + 0x10,"Key",4);
builtin_memcpy(local_50,"ThisIsTheEncrypt",0x10);
lVar6 = -1;
do {
lVar7 = lVar6 + 1;
lVar3 = lVar6 + 1;
lVar6 = lVar7;
} while (local_50[lVar3] != '\0');
BVar4 = CryptHashData(local_58,local_50,(DWORD)lVar7,0);
if (BVar4 == 0) {
puts("CryptHashData() Error\n");
goto LAB_140001637;
}
BVar4 = CryptDeriveKey(local_70,0x6610,local_58,0x1000000,&local_78);
if (BVar4 == 0) {
puts("CryptDeriveKey() Error\n");
goto LAB_140001637;
}
local_68[0] = '\x01';
local_68[1] = '\0';
local_68[2] = '\0';
local_68[3] = '\0';
BVar4 = CryptSetKeyParam(local_78,3,local_68,0);
if (BVar4 == 0) {
puts("CryptSeKeyParam() Error\n");
goto LAB_140001637;
}
BVar4 = CryptSetKeyParam(local_78,1,(BYTE *)L"IVCanObfuscation",0);
if (BVar4 == 0) {
puts("CryptSeKeyParam() with IV Error\n");
goto LAB_140001637;
}
local_68[4] = '\x01';
local_68[5] = '\0';
local_68[6] = '\0';
local_68[7] = '\0';
BVar4 = CryptSetKeyParam(local_78,4,local_68 + 4,0);
if (BVar4 == 0) {
puts("CryptSetKeyParam() with set MODE Error\n");
goto LAB_140001637;
}
nNumberOfBytesToRead = GetFileSize(hFile,(LPDWORD)0x0);
uVar1 = nNumberOfBytesToRead + 0x10;
_Size = (ulonglong)uVar1;
pBVar8 = pbData;
if (uVar1 != 0) {
if (_Size 0x1000) {
pbData = (BYTE *)operator_new(_Size);
}
else {
if ((ulonglong)uVar1 + 0x27 _Size) {
FUN_140001100();
pcVar2 = (code *)swi(3);
(*pcVar2)();
return;
}
pvVar5 = operator_new((ulonglong)uVar1 + 0x27);
if (pvVar5 == (void *)0x0) goto LAB_140001653;
pbData = (BYTE *)((longlong)pvVar5 + 0x27U & 0xffffffffffffffe0);
*(void **)(pbData + -8) = pvVar5;
}
pBVar8 = pbData + _Size;
memset(pbData,0,_Size);
}
local_60 = 0;
BVar4 = ReadFile(hFile,pbData,nNumberOfBytesToRead,&local_60,(LPOVERLAPPED)0x0);
if (BVar4 == 0) {
puts("ReadFile() Error\n");
}
else {
lVar6 = -1;
do {
lVar6 = lVar6 + 1;
} while (pbData[lVar6] != '\0');
local_5c = (int)lVar6 + 1;
BVar4 = CryptEncrypt(local_78,0,1,0,pbData,&local_5c,0x40);
if (BVar4 == 0) {
puts("CryptEncrypt() Error\n");
}
else {
BVar4 = WriteFile(hFile_00,pbData,0x40,(LPDWORD)0x0,(LPOVERLAPPED)0x0);
if (BVar4 == 0) {
puts("WriteFile() error\n");
}
else {
CloseHandle(hFile);
CloseHandle(hFile_00);
BVar4 = DeleteFileA("flag.txt");
if (BVar4 == 0) {
puts("DeleteFileA() error\n");
}
else {
BVar4 = CryptDestroyKey(local_78);
if (BVar4 == 0) {
puts("CryptDestroyKey() error\n");
}
else {
BVar4 = CryptDestroyHash(local_58);
if (BVar4 == 0) {
puts("CryptDestroyHash() error\n");
}
else {
BVar4 = CryptReleaseContext(local_70,0);
if (BVar4 == 0) {
puts("CryptReleaseContext() error\n");
}
}
}
}
}
}
}
if (pbData != (BYTE *)0x0) {
_Memory = pbData;
if ((0xfff (ulonglong)((longlong)pBVar8 - (longlong)pbData)) &&
(_Memory = *(BYTE **)(pbData + -8), (BYTE *)0x1f pbData + (-8 - (longlong)_Memory))) {
LAB_140001653:
_invoke_watson((wchar_t *)0x0,(wchar_t *)0x0,(wchar_t *)0x0,0,0);
}
free(_Memory);
}
}
LAB_140001637:
FUN_140001680(local_38 ^ (ulonglong)auStackY_b8);
return;
}
長いですね。めんどくさいですがもう少しです。CryptCreateHash
やCryptDeriveKey
を呼び出している箇所を確認し、それらのAPIに渡されている引数を特定します。
こちらを参照してください。
上記を踏まえて解析を行ったところ、以下全ての暗号化パラメータが判明しました。
-
ハッシュアルゴリズム: SHA-256
・CryptCreateHash(local_70,0x800c,0,0,&local_58)
・0x800c
はCALG_SHA_256
の定数 -
暗号化アルゴリズム: AES-256
・CryptDeriveKey(local_70,0x6610,local_58,0x1000000,&local_78);
・0x6610
はCALG_AES_256
の定数であり、キー長は32バイト -
モード: CBC
・CryptSetKeyParam(local_78,4,local_68 + 4,0)
・第二引数の4(KP_MODE)
、第三引数のデータに値1(CRYPT_MODE_CBC)
がセットされている -
パスワード: “ThisIsEncryptKey”
・builtin_memcpy(local_50,"ThisIsTheEncrypt",0x10);
とbuiltin_memcpy(local_50 + 0x10,"Key",4);
上記は、メモリ上で"ThisIsEncrypt"
と"Key"
を連結して、結果として"ThisIsEncryptKey"
という文字列を生成しています。 -
IV(初期化ベクトル)
・CryptSetKeyParam(local_78, 1, (BYTE *)L"IVCanObfuscation", 0)
・IVとして、L"IVObfuscation"
というワイド文字列(1文字2バイト) が指定されている
AESのIVは16バイトなので、プログラムはこの文字列のメモリ上での表現(UTF-16LE)の先頭16バイトをIVとして使用する
・バイト列:b'I\x00V\x00C\x00a\x00n\x00O\x00b\x00f\x00'
これらを踏まえてsolverを書きましょう。
回答
solver1(失敗)
solver1.py
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
ENCRYPTED_FILE = "flag.encrypted"
PASSWORD = b"ThisIsTheEncryptKey"
IV = b"IVCanObfuscation"
if __name__ == "__main__":
try:
with open(ENCRYPTED_FILE, "rb") as f:
encrypted_data = f.read()
except FileNotFoundError:
print(f"Error: {ENCRYPTED_FILE} Not Found")
exit()
h_sha256 = hashlib.sha256()
h_sha256.update(PASSWORD)
key = h_sha256.digest()
print("-" * 80)
print(f"[*] Password: {PASSWORD.decode()}")
print(f"[*] IV: {IV.decode()}")
print(f"[*] Key: {key.hex()}")
print("-" * 80)
try:
cipher = AES.new(key, AES.MODE_CBC, IV)
decrypted_data = cipher.decrypt(encrypted_data)
unpadded_data = unpad(decrypted_data, AES.block_size)
print("\nDecrypted.")
print(f"\nFLAG: {unpadded_data.decode('utf-8')}\n")
except Exception as e:
print(f"\nError: {e}")
$ python3 solver.py
--------------------------------------------------------------------------------
[*] Password: ThisIsTheEncryptKey
[*] IV: IVCanObfuscation
[*] Key: adafd798c69ffaef2b2bbb44364f0952b988cdd37bb66bb2cb19b5827a8a2465
--------------------------------------------------------------------------------
Decrypted.
FLAG: c"sUO4tb,>/Y(1y0u_suc3553d_2_ana1yz3_Ma1war3!!!}
solver2.py
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
ENCRYPTED_FILE = "flag.encrypted"
PASSWORD = b"ThisIsTheEncryptKey"
IV = b'I\x00V\x00C\x00a\x00n\x00O\x00b\x00f\x00'
if __name__ == "__main__":
try:
with open(ENCRYPTED_FILE, "rb") as f:
encrypted_data = f.read()
except FileNotFoundError:
print(f"Error: {ENCRYPTED_FILE} Not Found.")
exit()
h_sha256 = hashlib.sha256()
h_sha256.update(PASSWORD)
key = h_sha256.digest()
print("-" * 80)
print(f"[*] Password: {PASSWORD.decode()}")
print(f"[*] IV (raw): {IV.hex()}")
print(f"[*] Key: {key.hex()}")
print("-" * 80)
try:
cipher = AES.new(key, AES.MODE_CBC, IV)
decrypted_data = cipher.decrypt(encrypted_data)
unpadded_data = unpad(decrypted_data, AES.block_size)
print("\nDecrypted.")
flag_content = unpadded_data.decode('utf-8')
print(f"\nF: {flag_content}")
except Exception as e:
print(f"\nError: {e}")
$ python3 solver2.py
--------------------------------------------------------------------------------
[*] Password: ThisIsTheEncryptKey
[*] IV (raw): 49005600430061006e004f0062006600
[*] Key: adafd798c69ffaef2b2bbb44364f0952b988cdd37bb66bb2cb19b5827a8a2465
--------------------------------------------------------------------------------
Decrypted.
FLAG: ctf4b{way_2_90!_y0u_suc3553d_2_ana1yz3_Ma1war3!!!}
flag
ctf4b{way_2_90!_y0u_suc3553d_2_ana1yz3_Ma1war3!!!}
今回、2回目のCTF参加で初めてReversingに挑戦しましたが、非常に面白かったです。バイナリを読んでいるときは頭がおかしくなりそうでしたが、Rev問は6問中5問解くことができ、大きな達成感がありました!(最後の問題でしょうもないミスをして時間を溶かしてしまったのは内緒です…)
Cryptoなど他の分野にも挑戦したかったのですが、こちらはさっぱりでした。もっと幅広い知識を身につけていきたいです。
また、今回は「セキュリティ・キャンプ全国大会 2025」で出会った仲間とチームを組むことができ、非常に有意義な時間を過ごせました。この貴重な経験と繋がりを、今後の学習の糧にしていきたいと思います。
最後までお読みいただきありがとうございました。
Views: 0