自從上次參加 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 | import requests |
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 | g0 = "DHLIYJEG" # 最終要變成的字串 |
Flag:
1 | AIS3{G0D_D4MN_4N9R_15_5UP3R_P0W3RFU1!!!} |
Crypto
babyRSA
在 encrypt()
裡面,是使用字元的 ASCII 一一加密,而不是整個字串轉成一個大數後再加密。
因為是用 ASCII 一個字元一個字元加密,加上我們有密文。所以只要窮舉 ASCII 的數字假裝是明文,然後公鑰加密後跟實際密文比對,如果相符代表就是那個 ASCII 數字。整個過程中不需要找出真正的私鑰。
Script:
1 | encryped = [] # 題目給的密文串列 |
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 | payload = padding + rbp + pop_rdi + param1 + win1 |
程式執行流程是這樣:
rip
返回位置被pop_rdi
的 gadget 覆蓋goodbye()
結束後,跳轉至0x402540
,也就是執行pop rdi; ret
的位置- 此時 stack 頂端是
param1
,所以rdi
就存了param1
- 接著
pop_rdi
又 return,此時跳轉到win1()
,並且帶有參數,成功進入取得 flag 的流程。
Script:
1 | from pwn import * |
執行 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。