今年的 AIS3 前測又輪到我出逆向毒害大家,這次每個題種其實都有一題考古題,整體難度也有下修,意思就是解法很多種,沒有全部擋起來,是個主辦那邊希望大家都能學到東西的概念,所以這篇 writeup 的字數可能也會很多,希望沒解開的、或是完全不知道要幹嘛的人都能看懂。
然後 source code 在這裡 github
總之這裡是我預設的解法, gogo:
🍍 TsaiBro (考古題)
題目敘述
很好….你很腦殘嗎….敢這樣講刀劍神域…….我死也不會放過你 我..要..殺死…你..
題解
這題之前在 BambooFox 邀講的社課上也有提到過,是個可以不用逆向也能夠解開的題目,跟去年前測是同一題,連名字也沒換 誒嘿(傳送門),差別只有使用的 table 不同,題目實作了自己的 Tap code,透過 8*8 的 table 來 encode 輸入:
$1$ | $2$ | $3$ | $4$ | $5$ | $6$ | $7$ | $8$ | |
---|---|---|---|---|---|---|---|---|
$1$ | 5 | 6 | 7 | 8 | 9 | { | } | _ |
$2$ | W | X | Y | 0 | y | z | A | B |
$3$ | a | b | c | d | m | n | o | p |
$4$ | S | T | U | V | G | H | I | J |
$5$ | K | L | M | N | u | v | w | x |
$6$ | e | f | g | h | q | r | s | t |
$7$ | i | j | k | l | O | P | Q | R |
$8$ | C | D | E | F | 1 | 2 | 3 | 4 |
舉例,W
這個字會被轉換成 . ..
,以此類推。
這個 table 可以透過黑箱測試推理、strings
、或是逆向找到,這裡用 radare2 看一下可以找到 table 在 0x201020
:
寫個 script 把 TsaiBroSaid
解回來就好了。
#!/usr/bin/env python3 |
Flag:
AIS3{y3s_y0u_h4ve_s4w_7h1s_ch4ll3ng3_bef0r3_bu7_its_m0r3_looooooooooooooooooong_7h1s_t1m3}
吐槽一下 more long 是什麼爛英文
🎹 Fallen Beat
題目敘述
CTF player,
我要挑戰你, I’m gonna challenge you!!
ZR
這是我的室友兼 lab 同學,
他已經考過金框暴龍天,他是個旋鈕人,
不像你是個敲鍵盤的,
所以我要測測你的程度到哪裡,
就用 ZR 的大一 project 來決勝負吧!!
得到 Full Combo 來讓我刮目相看!!
來自室友大一的 final project,音樂音量沒有 normalize 導致我已經耳聾,題目說只要 full combo 就會給你 flag,避免有人妄想用玩的,所以跟普通音遊規則不同,調整成只有 perfect 才會 combo,如果你還玩出來了,麻煩下面留言,我要跪你。
題解
java 是直譯式語言, compile 只是 compile 成 Java bytecode,幾乎所有的東西都還留著,包括變數名稱,decompile 後可以拿到幾乎跟原始碼一樣的 java code,直接用線上工具來 decompile,這題難的地方大概只有 code 滿多的,可以直接找一下變數名稱有沒有 flag:
在 Visual/PanelEnding.java
79 行附近可以找到一個變數叫做 flag ,內容看起來像是被加密過。
79 | this.lWest = new JLabel(); |
繼續往下看,在 setValue
中看得出來當 t == mc
的時候,cache
會拿來跟 flag
decrypt 然後印出來:
173 | if (t == mc) { |
然後追一下 t
、mc
、cache
分別是什麼,在 Control/GameControl.java
中可以找到 t
就是這個譜面總共有多少的音符, mc
就是遊戲結束時的 combo 數:
291 | this.pe.setValue(this.total, this.critical, this.early, this.late, this.miss, this.comboMax, this.info, this.cache); |
繼續往下追可以找到 cache 其實就是讀進來的譜面檔案
143 | while (br.ready()) { |
看到這裡基本上可以想到幾個解法:
- 改
if(t == mc)
的 bytecode 讓他直接通過 - 找到
cache
,模仿 code 自己 decrypt - 改 source code 然後重新編譯
- 遊戲修改器
- …
因為沒做什麼奇怪的事情,所以上面的解法應該都是做得到的,不過還是最推薦第二個做法,因為我 Mac 也會跑版,所以還是寫腳本解實在,當然你要直接抽 code 出來寫一個 java 也行:
#!/usr/bin/env python3 |
Flag:
AIS3{Wow_how_m4ny_h4nds_do_you_h4ve}
🧠 Stand up!Brain
題目敘述
又到了 Brain tell 咪 ㄜ joke 的時間了
這次輪到你說個笑話來聽聽了
題解
題目給的是一個用 C 寫的 Brainfuck interpreter,老梗,但不知道也沒關係,還是可以透過逆向看懂。
這裡用 radare2 disassemble,0x7e2
這裡可以看到輸入是 6 個字,推測輸入正確的 6 個字就會拿到 flag:
lea rax, [var_eh] |
可以嘗試暴搜正確的 6 個字是什麼,但滿久的,繼續往下看,0x853
跟 0x81c
這裡是一個 for 迴圈,每個迴圈做的事情是把 input 拿出來放到 0x00201380
的 array 上並且再補上一個元素 1
,所以 array 0x00201380
最後會變成 [input[0], 1, input[1], 1, input[2], 1, input[3], 1, input[4], 1, input[5], 1]
:
[0x81c] |
再往下看,會看到引用一串 data,這一串就是 Brainfuck 程式碼:
-------------------------------------------------------------------[>[-]<[-]]>[>--------------------------------------------------------[>[-]<[-]]>[>-------------------------------------------------------[>[-]<[-]]>[>------------------------------------------------------[>[-]<[-]]>[>---------------------------------------------------[>[-]<[-]]>[>---------------------------------[>[-]<[-]]>[>>----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++.[-]]]]]]] |
如果不知道 Brainfuck 還是能透過逆向知道這個語言的大概,程式在 0x986
的地方進入一個很大的 for loop,0x869
這裏開始是 switch 的範圍檢查,然後往下一點 0x87f
這裡是 switch 決定去哪個 case,這裡的 radare2 下的註解是錯的,因為最小的 case +(0x2b)
到最大的 case ](0x5d)
閉區間長度是 51,但其實只有 7 個 case:
0x87f [oi] |
這幾個 case 是 +
、-
、<
、>
、[
、]
、.
,看一下就可以知道每個符號的意義了,打下去太多,略一波wiki。
所以現在可以回頭看那一串 Brainfuck 在幹嘛了,基本上 -------------------------------------------------------------------[>[-]<[-]]>[..略..]
其實就是 if
,所以全部的 code 就是巢狀的 if
,拿個簡略的狀況說明,下面這段 code 的邏輯是 if(var0==1){..略..}
:
# 假設記憶體長這樣 memory[4,1] |
一開始 --
會把記憶體上的 memory[0]
扣 2 變成 2,執行到[
時如果當前指標指到的記憶體位置不是0
就會進入[>[-]<[-]]
,[>[-]<[-]]
做的事情是把下一個位置(memory[1]
)扣成 0 然後再把指標移回來memory[0]
,清空 memory[0]
,所以當執行到 >[...略...]
時,第一個>
移動指標到memory[1]
,是 0就不會去執行 [...略...]
,那如果重新來過,記憶體上的 4 一開始是 2 的話:
# 假設記憶體長這樣 memory[2,1] |
一開始 --
會把記憶體上的 memory[0]
扣 2 變成 0,執行到[
時因為現在指向的記憶體數值是 0 所以不會進入[>[-]<[-]]
,跳到後面執行>[...略...]
,第一個>
移動指標到memory[1]
,是 1,就會執行[...略...]
。
到這裡就很清楚了,輸入的 ascii code 要等於前面有幾個-
,這樣才會通過判定,可以知道正確的輸入是[67, 56, 55, 54, 51, 33]
就是C8763!
,把邏輯換成 C 會像是這樣:
1 | if(input[0] == 'C'){ |
當然也可以直接跳過所有的if
,只執行最後印 flag 的 code 就好了,可以用線上工具。
>>----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++. |
Flag:
AIS3{Th1s_1s_br4iNFUCK_bu7_m0r3_ez}
🍹 Long Island Iced Tea
題目敘述
長·島·冰·茶
我·的·最·愛
長·島·冰·茶
超·爽·口·感
咚咚咚ㄎㄧㄤ
咚咚咚ㄎㄧㄤ
咚咚咚ㄎㄧㄤ
我真的好ㄎㄧㄤ
附這個連結害我寫這篇的時候一直重播
題解
這裡就用 decompile 之後的 code 來講解,沒有 IDA 可以用 Ghidra,科普一下,Ghidra 唸作Gee-druh,或是你可以學我念 Costco。
基本上 main 做的事情就是 Zero padding
1 | puts("My encryptor 0.1"); |
沒什麼特別的,可以繼續往下看:
1 | __int64 __fastcall sub_83A(unsigned int *a1, __int64 a2) |
看起來是某種加密或是 hash 算法,如果夠敏感的話應該會注意到這個算法裡面有 magic number 存在,0x61C88647
,但要注意,這是 IDA 照 variable type 修正產出的 pseudo code,回去看 assembly,會發現其實是:
mov [rbp+var_4], 9E3779B9h |
本來是 v6 += 0x9E3779B9
,但 IDA 覺得 v6 -= 0x61C88647
比較好,總之只是 2’s complement,其實不管哪一個都可以找關鍵算法 TEA,仔細看一下會發現其實有點不太一樣,但應該足夠模仿寫出 decrypt 的程式了,就反轉操作而已,如果有耐心爬一下 wiki 會發現題目實作的算法是 XTEA,直接用 wiki 上的 decrypt 就行了,要注意的是 main 裡面其實只有加密第一塊 block,所以第一塊被加密了 9 次,只要解密第一塊就好了:
1 | puts("My encryptor 0.1"); |
因為只有第一塊有加密:
#My encryptor 0.1 |
所以這裡就有第二個解法是爆搜 3 byte 的文字(8 - len('AIS3{')
),賽中 @bronson113 跟我說他有非預期解,只要暴搜 2 byte,我還以為又寫爛 code …,雖然是真的 code 寫爛才敢放這題,不然好像有點太難,不知道要查 magic number 的人應該解不出來,本來是想出那種加密寫錯的沒錯,但怎麼出好像都有點太難,之後才知道原來大家暴搜都有猜字 = =,好,恩,總之你猜到那個字是 girl 就可以只暴搜 2 byte,聖結石保險起見買三件,保險起見我都搜 3 byte。
1 | from subprocess import * |
不過最快的解法還是直接寫 decrypt:
1 |
|
Flag:
AIS3{A!girl_g1v3_m3_Ur_IG_4nd_th1s_1s_m1ne_terryterry__}
補:看完 writeup,大概是一半的人用暴搜,一半的人有認出來是 XTEA,出的不錯?
打比賽的時候有遇過 rc4 位移故意寫錯要修,或土炮加密寫錯之類的題目,大概有個兩三題,但覺得出在這裡好像不太好
最後產了一題大便題🤔️
🌹 La vie en rose
題目敘述
題解
稍微逆向看一下會發現 binary 前面一直使用 _MEIPASS2
這個字串,會被拿去 call GetEnvironmentVariableW
之類的 API,查一下就可以發現這個是 PyInstaller 使用的一個環境變數,PyInstaller 是一個可以把 python 包成執行檔的工具(exe以外也可以),原理是把 python compile 成 bytecode 之後,執行檔執行時會把 bytecode 抓出來執行,不難想到應該會有工具可以把東西拉出來,PyInstaller Extractor。
所有的東西會被抽出來放進 La vie en rose.exe_extracted
資料夾,pyinstallerextractor.py 會告訴你 entry point 是啥:
pyinstxtractor.py:86: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses |
但如果它給錯,也可以用 grep 找到,因為 python bytecode 的字串是 raw data:
grep outland * |
所以我們接下來要做的事情是 decompile rose.pyc,這裡可以用 uncompyle6 來做,但是會有錯誤訊息,原因是 pyc 少了版本 magic number,很好理解,因為 PyInstaller 知道自己在這個 binary 用了什麼版本,所以他可以把 magic number 拔掉來節省空間:
uncompyle6 rose.pyc |
但是其實 python import 東西的時候是會檢查 magic number 的,所以 extract 出來的 library 應該都還留著 magic number:
xxd importlib.pyc | head |
所以其實也不需要知道版本, 可以直接拿到 magic number 550d 0d0a
。
假設不知道這件事情的話,那現在要先知道版本是什麼,要確定版本有幾個方法,執行 binary 去看他載了什麼 dll:
或是像剛剛一樣直接 grep,不然 ls 也行:
grep python * |
知道版本後,可以在 cpython github 找到 magic number:
261 | # Python 3.8a1 3400 (move frame block handling to compiler #17611) |
總之這裡可以直接把 importlib.pyc 550d 0d0a
補到 rose.pyc 前面就好了,但還要另外補上 12 byte,這樣 marshal 偏移到 0x10 才會對,其中包括 timestamp 跟 source size,不是很重要所以就隨便抓一抓也丟進去就好了:
head -c 16 PYZ-00.pyz_extracted/importlib.pyc | cat - rose.pyc > rose_ok.pyc |
然後試試看 decompile 會遇到 bytecode 錯誤:
uncompyle6 rose_ok.pyc |
問題出在 222 JUMP_FORWARD
想要跳去 256 JUMP_BACK 182 'to 182'
但 uncompyle6 出了點問題,我們可以嘗試把 JUMP_FORWARD 256
直接替換成 JUMP_BACK 182
:
L. 35 220 JUMP_BACK 182 'to 182' |
要修改他我們要先找到JUMP_FORWARD 的 opcode 是什麼
import dis |
marshal opcode 開始的地方在 0x2e,所以精準要修改的位置是 $0x2e + 222 = 0x10c$, 222 的旁邊 220 JUMP_BACK 182 'to 182'
正好是我們要的操作,可以直接複製貼上:
00000100: 6400 1900 640b 6b02 72e0 71b6 6e20 6500 d...d.k.r.q.n e. |
然後就可以 decompile 了:
uncompyle6 rose_ok.pyc > rose.py |
直接用註解來解釋在幹嘛:
1 | # uncompyle6 version 3.7.1 |
看到這裡不難發現其實就是在解聯立方程式,舉例來說,constraint[0]
是 notes[0]+notes[1]
跟 constraint[120]
是 notes[0]-notes[1]
的值,有唯一解,而且我們知道樂譜應該有 121 個 note,因為 len(constraint)=240
:
1 | #!/usr/bin/env python3 |
Flag:
AIS3{th1s_fl4g_red_lik3_ros3s_f1lls_ta1wan}
以防萬一你想知道這琴譜是那首:
🐉 Uroboros
題目敘述
你好啊愛德華大哥哥,真的沒想到你可以到第二十五層來呢!
不過就到這裡為止了,
接下來就由我 反重力三頭鎖鏈康妮·解放·緋紅 來做你的對手!
出這題才發現,這個 emoji 也太帥了ㄅ,元氣彈???
然後寫 blog 才發現,我 cout 字串的地方迴圈寫錯了🤔️,少印一個拉,哈哈,不過剛好沒輸入踩到那個點,笑到不行。
題解
有使用 structure 的 C++ 逆向題,仔細看可以發現,一開始做了一個 circular double linked list,整個環的長度是 314,原本的 code 看起來像是這樣:
15 | node* ring = new node; |
畫出來長這樣:
stateDiagram |
decompile 後 C++ 長得比較難看懂一點,大致是這樣:
// cout << "Only state alchemists are entitled to know the forbidden secret, show me that you are" << endl; |
學院派解法 (?)
所以 input 的每個字會按照順序被標記在環上,其實就是取模的概念,假設輸入A
,就會在環上面走 ord('A')*7 = 65*7 = 455
步,但是因為只有 314 個點(0~313)所以我們會走到 141 號 node:
又因為 7 跟 314 互質,所以我們可以直接做逆運算得到每一個位置是什麼,例如剛剛的A
我們可以從 141 逆推:
推出原本的字是chr(65)='A'
,照這樣就可以寫腳本了:
1 | target = "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 511847092 0 0 0 0 0 0 48 0 0 0 0 0 0 0 40984114273074 0 0 0 0 0 280 0 0 0 0 0 0 26022 26 0 0 0 0 0 0 2035 0 0 0 0 0 0 15 0 0 0 0 0 0 53 0 0 0 0 0 0 47 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 42350 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 29201 0 0 0 0 0 0 805 0 0 0 0 0 1 939 0 0 0 0 0 0 13 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7739889 0 0 0 0 0 0 1251 0 0 0 0 0 0 45 0 0 0 0 0 0 2950300 0 0 0 0 0 2 23 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 54 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 55 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0".split(' ') |
Flag:
AIS3{4ll_humonculus_h4v3_a_ur0b0r0s_m4rk_0n_the1r_b0dy}
但也可以把每個 printable 都先 *7%314
就可以知道對應的值了拉
野獸流解法 (?)
本來沒有印使用者輸入所產生的環長怎樣,但覺得這樣 debug 難度好像有點躍升,所以最後還是印出來了,所以這裡就有另外一個解法(我好像沒看到有人這樣解),就是可以先黑箱測試知道輸入是怎麼樣呈現在環上面的,之後就可以進行 byte by byte 爆破,拿來跟正解比比看就知道當前這一 byte 對不對了。