reversing.kr writeup === [TOC] # 個人資訊 * ID: zeze * Country: Taiwan * 名次: 143 * 分數: 3530 # 動機 1. 網路沒看到台灣人的完整 writeup 2. 把自己對所有題目的思路記錄下來 3. 順便記一下觀念和工具的使用時機與方法 # 工具 我在 Linux 上常用的 hexeditor: `sudo apt install ncurses-hexedit` # 題目 ## Easy Crack 執行後長這樣,隨便輸入會跳 `Incorrect Password` ![](https://i.imgur.com/bZMkwtH.png) 先用 [exeinfope](https://exeinfo-pe.en.uptodown.com/windows),看起來沒有殼 ![](https://i.imgur.com/XBmVfIj.png) 用 ida32 打開後反編譯 WinMain ![](https://i.imgur.com/V9zKQYH.png) 只有呼叫一個函式 [DialogBoxParamA](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-dialogboxparama),其中第四個參數 `DialogFunc` 就是指向 dialogbox 的 procedure 指標。 因此點入 `DialogFunc`,這是一個 stdcall ![](https://i.imgur.com/V9DoUus.png) 再點進 `sub_401080` ![](https://i.imgur.com/XFw0eCG.png) 基本上就是用 `GetDlgItemTextA` 取出我們的輸入放到 變數 `String`,接著做一些判斷後再決定要跳出 `Incorrect password` 還是 `Congratulations`。 照 stack 的順序,在 `String` 是 `E`,`v3` 是 `a`,`v4` 是 `a5y`,`v5` 是 `R3versing` 所以整串 flag 就是 `Ea5yR3versing` ## Easy Keygen 執行後長這樣,要我們輸入 Name 和 Serial,根據 Readme,要找到 Serial 為 `5B134977135E7D13` 對應的 Name ![](https://i.imgur.com/mCZ5mSg.png) 沒加殼 ![](https://i.imgur.com/p4ZjG6k.png) 打開 ida 看 `main`,有兩個 scanf,分別是 `Name` 和 `Serial`。 ![](https://i.imgur.com/Osrmxf6.png) 重點是迴圈中的部分,它對我們的 `Name` 做了一些事。[sprintf](http://tw.gitbook.net/c_standard_library/c_function_sprintf.html) 第一個參數是目標字串位址,第二個是 format,之後的就是 format 中的代入值。 所以 `Name` 的每一個字元都會變成 hex 的形式,最後合成一個字串跟 `Serial` 做比對,寫個 script 跑出 flag `K3yg3nm3` ```::python serial = '5B134977135E7D13' v13 = [int(serial[i] + serial[i+1], 16) for i in range(0, len(serial), 2)] v3 = 0 v6 = [0x10, 0x20, 0x30] i = 0 name = '' while v3 < len(v13): if i >= 3: i = 0 name += chr(v13[v3] ^ v6[i]) v3 += 1 i += 1 print(name) ``` ## Easy Unpack 執行後一片空白,這題 ReadMe 說要 Find the OEP,也就是說很可能有殼 ![](https://i.imgur.com/8PLdI7V.png) 看來是個不知名的殼,可能是出題者自己做的,有關殼的科普看[這裡](https://ithelp.ithome.com.tw/articles/10188209) ![](https://i.imgur.com/OTZdv3g.png) 由於脫殼後通常會有一個很大的 jmp,所以就直接用 ida 解掉,得知 OEP 為 `401150` ![](https://i.imgur.com/DceWm5f.png) 另一個方法是用 debugger,慢慢執行到有很大的跳轉的地方為止 ## Music Player 執行起來長這樣,丟給它一個 mp3 檔案就會開始播放,只播一分鐘就會停掉並且跳出一個訊息框(msgbox)。Readme 說要讓它播放超過一分鐘 ![](https://i.imgur.com/ZOUKbn1.png) 沒殼 ![](https://i.imgur.com/CzVndj5.png) 這題給了一個 exe 和 dll,先用 ida 看 exe,似乎看不出什麼特別的 ![](https://i.imgur.com/SB5l1gi.png) 再用 ida 看 dll 的部分,太大了,要先想辦法找地方下手 ![](https://i.imgur.com/gkfiah6.png) 首先先想辦法找到它跳出訊息框的函式,再來看它是根據什麼來決定是否顯示訊息框的。 用 x32dbg 動態分析,一直 f9 往下跑,直到音樂跑了一分鐘跳出訊息框為止。這時查看呼叫堆疊(call stack),看看這個訊息框是哪個函式 ![](https://i.imgur.com/1GUHQML.png) 得知訊息框的函式位址為 `72A1D132`,而 call 它的位址在 `4045DE`,於是點兩下後會跳到 `4045DE` 的 CPU 畫面 ![](https://i.imgur.com/x84T8lH.png) 最後就是要看它判斷是否呼叫訊息框的地方,往上滑可以看到 ``` 00404563 | cmp eax,EA60 | 00404568 | mov dword ptr ss:[ebp-18],eax | 0040456B | jl music_player.4045FE | ``` 其中 0xEA60 = 60000,也就是一分鐘,把 `jl` 改成 `jmp`,結果跳出錯誤 ![](https://i.imgur.com/8EJ8AtE.png) 這次把斷點下在 `4045FE` 也就是之前改的 `jl` 的目的地位址。在 59 秒時下斷點後會停在這,接著按 F8 一步步執行,看在那裡會出錯。 ![](https://i.imgur.com/iNFEbcS.png) 在到 60 秒之前,這個 `004046AB | jge music_player.4046BF |` 都會跳轉,只有在 60 秒到達時才會繼續往下走後引發 exception。所以把這邊的 `jge` 也改成 `jmp` 試試看。執行完就拿到 flag `L1stenCare` ![](https://i.imgur.com/t4a2xuL.png) ## Replace 沒殼,32-bit 的 C++ program ![](https://i.imgur.com/DIsGbtO.png) 執行後隨便輸入後就關掉了 ![](https://i.imgur.com/Mx9Mb83.png) 開 ida 分析後發現在 `DialogFunc()` 有一行紅字,也許是程式中寫入的,sub_40066F 也沒有辦法分析 ![](https://i.imgur.com/hKcK4b9.png) 只好動態分析,開 x32dbg,搜尋字串引用後發現 `Correct` 跟進去下斷點,在 `40466F` 也下一個斷點 ![](https://i.imgur.com/ejeJJR0.png) 執行後按 F7 跟進 `40466F`,基本上這段做的就是將我們的輸入加 2,再加 0x601605C7,最後再 +1 +1。之後跳去 0x406090,其中會把剛剛算出來的數字的當成位址,並把那位址和那位址+1 填成 0x90(nop) 看到這邊目標就很明確了,由於 `jmp replace.404690` 的後面跟著 `jmp replace.401084`,所以只要把 `jmp replace.401084` 填成兩個 nop,就可以輸出 `Correct` 算個數學就可以拿到 flag `2687109798` ``` x + 2 + 0x601605C7 + 1 + 1 = 0x401071 x = -1607857498 由於溢位,因為是吃 int,所以要加 2 ^ 32 x + 2^32 = 2687109798 ``` ## ImagePrc 沒殼,C++ 寫的 ![](https://i.imgur.com/ycfLusd.png) 執行之後跳出一個視窗,中間空白可以畫畫,下面有個 Check 按鈕,隨便亂畫按下按鈕會出現 Wrong ![](https://i.imgur.com/Zs5vuV9.png) 開 ida 靜態分析,先看 WinMain,其中程式註冊了一個 class,其中 lpfnWndProc 是 `sub_401130` ![](https://i.imgur.com/ju7lB4i.png) 點進 `sub_401130` 觀察一下後發現有一行 call 了 MessageBoxA,其中 Text 是 `Wrong` ![](https://i.imgur.com/wH0QlN0.png) 於是目前目標就是讓程式不要執行到這一行。在這一行之前有個 while 迴圈,只要成功執行 90000 次,就會直接 return。 而 while 的比較條件為 `*bitmap_index == bitmap_index[v14]`,這部分可以用動態分析去看實際的內容是什麼。 用 x32dbg 打開後,在 while 迴圈前的位址下斷點(找到位址的方法可以用 ida,把游標點在 while 迴圈的前一行後按下 tab,查看底下顯示的位址)。執行後先什麼都不用畫,直接按 Check,應該會停在剛設的斷點 ![](https://i.imgur.com/OK5Dh1b.png) 下面有個 `cmp dl, bl` 就是 while 迴圈的比較條件,而 dl 來自 [ecx],bl 來自 [ecx + eax],也就是說剛剛 ida 看到的 `bitmap_index` 應該就是這兩個其中一個 先看看 [ecx],在下面資料視窗中按下 `ctrl+g`,輸入 ecx 現在的值 `42F0048`,可以看到連續 90000 個 `FF`,可以猜到這應該是我們的輸入,因為我們剛剛什麼都沒畫就直接交出去 ![](https://i.imgur.com/Rf3DT4N.png) 再來看看 [ecx + eax],先算出 ``` ecx + eax = 0x42F0048 + 0xFC18E018 = 0x10047E060 因為溢位,所以其實是 0x47E060 ``` 也是一堆 `FF`,不過有好幾個是 `00`,看來這就是我們要的,所以現在把這一段 dump 下來,在下面命令中輸入 `savedata dump.mem, 47E060 , 15F90`,`savedata` 是用來 dump 記憶體的指令,第一個參數是檔名,第二個參數是開始的位址,第三個參數是大小,參數分別以逗點隔開 dump 完後,由於這是 bitmap(一種圖片格式),不過少了文件頭,需要自己填上 我是用 HxD 打開 dump.mem,可以在前面插入文件頭,先參考 [bitmap 文件頭格式](https://crazycat1130.pixnet.net/blog/post/1345538)。先到網路上隨便下載一個 bmp 檔案並用 HxD 打開,因為文件頭有 54 bytes,所以複製前 54 bytes 到 dump.mem 的開頭 ![](https://i.imgur.com/P3YWlg8.png) 接著要修改幾個欄位,首先是 filesize(2~6 bytes),這是整個檔案大小,也就是原本的 90000 再加上 54 bytes 文件頭 `90000+54 = 0x15fc6`。再來是寬度(0x12~0x16),從 ida 的 `CreateCompatibleBitmap` 可以看到是 `200 = 0xc8`。最後是高度(0x16~0x1A),`150 = 0x96`。其他不用動,改完後大概長這樣 ![](https://i.imgur.com/2Jk4a2V.png) 最後把副檔名改成 bmp,就可以打開拿到 flag `GOT` ![](https://i.imgur.com/WVP28V7.png) ## Position 沒殼,C++ 寫的 ![](https://i.imgur.com/EgEJbvr.png) ReadMe 說要找到對應到 `76876-77776` 的 name,這個 name 共四個字,最後一個字是 `p` 執行起來長這樣,看起來目標很明確,就是要讓中間的字不是 Wrong ![](https://i.imgur.com/DWHv7ey.png) 用 ida 靜態分析,打開 wWinMain 會發現不知道下一步不知道去哪了,只有一個 `AfxWinMain()`,可以看[這個](https://www.itdaan.com/tw/738439)了解,基本上就是會呼叫一個 virtual function,這部分 ida 不知道在哪。但是打開 `sub_401CD0` 可以看到關鍵判斷式 ![](https://i.imgur.com/Lh7RZUD.png) 其中呼叫了 `sub_401740`,點進去後看到很長一串程式,裡面也能看到兩個 `GetWindowTextW()` 分別取出我們輸入的 name 和 serial ![](https://i.imgur.com/7vIuojt.png) 基本上後面的部分就是在對我們的 name 做一些操作後跟 serial 做比較,這部分可以用 z3 寫一個 script,就可以拿到 flag `bump` ``` from z3 import * serial = [ord(i) for i in '76876-77776'] v = [BitVec('v{}'.format(i), 32) for i in range(4)] s = Solver() s.add(v[3] == ord('p')) for i in range(4): s.add(v[i] >= ord('a')) s.add(v[i] <= ord('z')) v6 = v[0] v7 = (v6 & 1) + 5 v48 = ((v6 >> 4) & 1) + 5 v42 = ((v6 >> 1) & 1) + 5 v44 = ((v6 >> 2) & 1) + 5 v46 = ((v6 >> 3) & 1) + 5 v8 = v[1] v34 = (v8 & 1) + 1 v40 = ((v8 >> 4) & 1) + 1 v36 = ((v8 >> 1) & 1) + 1 v9 = ((v8 >> 2) & 1) + 1 v38 = ((v8 >> 3) & 1) + 1 s.add(serial[0] == 0x30 + v7 + v9) s.add(serial[1] == 0x30 + v46 + v38) s.add(serial[2] == 0x30 + v42 + v40) s.add(serial[3] == 0x30 + v44 + v34) s.add(serial[4] == 0x30 + v48 + v36) v20 = v[2] v21 = (v20 & 1) + 5 v49 = ((v20 >> 4) & 1) + 5 v43 = ((v20 >> 1) & 1) + 5 v45 = ((v20 >> 2) & 1) + 5 v47 = ((v20 >> 3) & 1) + 5 v22 = v[3] v35 = (v22 & 1) + 1 v41 = ((v22 >> 4) & 1) + 1 v37 = ((v22 >> 1) & 1) + 1 v23 = ((v22 >> 2) & 1) + 1 v39 = ((v22 >> 3) & 1) + 1 s.add(serial[6] == 0x30 + v21 + v23) s.add(serial[7] == 0x30 + v47 + v39) s.add(serial[8] == 0x30 + v43 + v41) s.add(serial[9] == 0x30 + v45 + v35) s.add(serial[10] == 0x30 + v49 + v37) if s.check() == sat: flag = '' for i in range(4): flag += chr(int(str(s.model()[v[i]]))) print(flag) ``` ## Direct3D FPS 沒殼,C++ 寫的 ![](https://i.imgur.com/l8hEqru.png) 執行後長得跟小時候玩的 CSO 很像,基本操作是控制人物的移動和發射子彈,每隻怪物要打好幾槍才死掉 ![](https://i.imgur.com/RQ273eO.jpg) 用 ida 打開後看 WinMain,裡面初始化很多遊戲的設定,一開始最讓我感興趣的是有一行 `MessageBoxA(hWnd, "Game Over! You are dead", "ReversingKr - FPS Game", 0x40u);`,只要更改 `407020` 位址的值,就可以更改血量,不過讓自己變成無敵也不能怎樣 ![](https://i.imgur.com/2PtrHvK.png) 第二個讓我感興趣的是 `sub_4039C0`,點進去可以看到 `MessageBoxA(hWnd, byte_407028, "Game Clear!", 0x40u);`,裡面用 while 迴圈持續判斷 `result` 位址的值是否為 `1`,如果是的話就加 `132 * 4`(因為是 int),直到 `result >= 0x40F8B4` ![](https://i.imgur.com/oDcki11.png) 所以目標是讓所有的 `result + 132 * 4 * n` 位址的值都變成 1。打開 x32dbg 跟著,會發現每打死一隻怪物,就會有其中一個 `result` 變成 0。 ![](https://i.imgur.com/oz53dXE.png) 於是用 x32dbg,在那個函數下一個斷點,把每個怪物的記憶體的那個 byte 都改為 0,會跳出 Game Clear!,但是沒有 flag,只有一串密文,很可能是在殺死怪物的過程中有做類似解密的動作 ![](https://i.imgur.com/F43OEqO.png) 其實從剛剛在 `sub_4039C0` 的 `MessageBoxA(hWnd, byte_407028, "Game Clear!", 0x40u);` 就知道了,那串 `byte_407028` 就是一串密文,游標指著它按下 x 做查詢,可以看到另一個函數有引用它 ![](https://i.imgur.com/TquMJDm.png) 裡面有做 xor 解密,基本上就是每殺死一隻怪物,就會把相對應的字元做解密。所以只要把這個密文跟 byte_409184[v1 * 4] 做 xor。 動態分析可以查看 byte_409184 位址的值,就是 0, 4, 8, 12, ...... 等差數列。ida 和 x32dbg 的位址只有 offset 一樣,所以要自己看一下實際位址為何 ![](https://i.imgur.com/1jb7e6k.png) 最後逆回來就可以拿到 flag `Thr3EDPr0m` ``` cipher = open('FPS.exe').read()[0x4e28:0x4e28+64] flag = '' for i in range(len(cipher)): flag += chr((i*4) ^ ord(cipher[i])) print(flag) ``` ## Ransomware 有 upx 殼,可以下載[脫殼工具](https://zh-tw.osdn.net/projects/sfnet_upx/downloads/upx/3.08/upx308w.zip/)解 ![](https://i.imgur.com/1MqKLK5.png) ReadMe.txt 說 `Decrypt File (EXE)`,可能代表它給的 file 是一個 exe,只是被加密了 執行後先輸出一段亂碼,再讓我們輸入 key,之後又噴出一些亂碼 ![](https://i.imgur.com/wuDrTwf.png) 脫殼完用 ida 打開看 _main,要反編譯時卻說檔案太大 `4135E0: too big function` ![](https://i.imgur.com/IFOL2eq.png) 開 x32dbg 直接一步一步跟,直到要進入 _main(0x4135E0) ![](https://i.imgur.com/XAQzQN3.png) 進去 0x4135E0,看到很多垃圾,先 push 再 pop,結果跟原本一樣的那種。原來檔案太大就是因為這些垃圾,所以可以把它們全部填成 nop(不能刪掉不然會出錯) ![](https://i.imgur.com/xlEZbb4.png) 下面這幾行可以幫我們 patch 成 nop ```::python f = open('run_pack.exe', 'rb').read() f = f.replace(b'\x60\x61\x90\x50\x58\x53\x5B', b'\x90' * 7) open('run_pack_patch.exe', 'wb').write(f) ``` 不過在垃圾前面的 function prologue(`55 8B EC 83 EC 24 53 56 57`) 需要搬到一堆 nop 的尾端當作 function 的開頭,不然一樣會因為 function 太大而反編譯不了。 ![](https://i.imgur.com/naEdoG0.png) 修補完後就可以用 ida 反編譯分析,在做的事情就是把 file 的每一個字元跟我們輸入的 key 做 xor 後再 not,即 `file[i] = ~(file[i] ^ key[i % len(key)])` ![](https://i.imgur.com/FtKL8Hj.png) 所以要拿到 key 的方法就是把 file 取 not 後跟正常的 exe 做 xor 就可以拿到 key `letsplaychess` ```::python f = open('file', 'rb').read() e = open('FPS.exe', 'rb').read() nf = b'' for i in range(100): nf += bytes([(e[i] ^ (~f[i])) % 256]) print(nf[:100]) ``` 不過這還不是 flag,因為還沒將 file 進行解密 ```::python cipher = open('file', 'rb').read() key = b'letsplaychess' plain = b'' i = 0 for c in cipher: plain += bytes([(c ^ (~key[i])) % 256]) i = (i + 1) % len(key) open('plain.exe', 'wb').write(plain) ``` 用差不多的方法將 file 解密後會拿到一個 exe,執行後就拿到 flag `Colle System` ## Twist1 有私有殼,ReadMe 說要用 x86 來跑 ![](https://i.imgur.com/544c5ut.png) 執行後可以輸入,但是輸入後就直接卡在那了 ![](https://i.imgur.com/5ttFIPF.png) IDA 打開後基本上沒有頭緒,也看不到大的跳轉,不過很明顯就是有殼 ![](https://i.imgur.com/a8HaOGT.png) 拿去 x32dbg 動態分析,會發現如果直接按 F9 繼續執行,程式會有 Exception,可能中間有 antidebugger,所以單步執行來看發生什麼事。 首先,讓程式產生 Exception 的是 `40107D` 的 call,最終程式是死在 `402052`,所以就按 F7 跟進去 ![](https://i.imgur.com/w3OBvS2.png) 之後會有一個迴圈(`407063~40706D`)在修改 .text 段的值,這部分就是在殼在做事了,這部分就直接 F4 到迴圈結束的下一行就好 ![](https://i.imgur.com/9Jngkme.png) 繼續執行,不管遇到什麼都繼續執行,現在主要是要找關鍵程式碼。執行到 `4070EB` 跟進去就直接 call `402050` Exception 了。推測是在`4070EB` 的 call 之前要滿足什麼條件才不會跑到這行,所以這邊就先改 ZF 後跳過去 ![](https://i.imgur.com/91qhMRv.png) 接著又是很多個 self-modifying 步驟,直到後面有個迴圈(`407139~40715B`),它檢查某一段 memory 的值是否為 `ABABABAB` 或 `EEFEEEFE`,如果是的話就會跟上一個一樣跳到 `402050` 產生 Exception,所以這邊在 `407155` 把 ecx 改成 `1F4` 後跳過去就好 ![](https://i.imgur.com/nfrN54j.png) 然後又接著各種 self-modifying 步驟,self-modifying 程式好玩的地方就在於,很多時候以為接下來要執行的程式看起來怪怪的要死掉時,在最後一刻就又會被變成可以執行的程式。 在一個大 jmp 後,來到 `40157C`,明顯就是 OEP,這時終於可以用 Scylla 來 dump 出脫殼後的程式。打開 Scylla 後,在 OEP 輸入 `4015C7`,按下 IAT Autosearch,最後按 Dump,就可以拿到一個脫殼後的執行檔 ![](https://i.imgur.com/xTp4E0M.png) 拿到 ida 看,可以看到函數已經被正常解析了,雖然 main 不能 decompile,不過也不長,就直接看就好。 基本上就是先印出 `Reversing.kr CrackMe` 和 `Input` 後,接著 call `401240`,最後判斷要輸出 `Correct` 還是 `Wrong` ![](https://i.imgur.com/xOZi16A.png) 其中 `401240` 應該就是把輸入做一些操作,這個函數可以 decompile ![](https://i.imgur.com/xyKUAOY.png) 可以看到它最後 JUMPOUT 到 `40720D`,而這個位址在 ida 上靜態看是不能跑的,這部分是因為 Scylla 沒有把 `40720D` 的值正確 dump 下來 ![](https://i.imgur.com/oTsfMnf.png) 所以一種方法是直接再動態分析原本的 exe,只是每次都要再重過一次前面的關;另一種方法是直接把那部分的程式 patch 好。因為我也不太確定要 patch 的範圍,所以就用第一種方法。 所以拿原始檔案跑到 OEP 後繼續執行,直到 `40162B` call `401270`,也就是 main,按 F7 跟進去 ![](https://i.imgur.com/gSiHShQ.png) 跑到 `40129B` 會發現程式死掉了,這是因為它存取到了不可存取的 address。這邊就直接把它 patch 成 nop,或是調整 EIP 跳過去就好。 ![](https://i.imgur.com/k0nxQAX.png) 然後就是剛剛 ida 看到的那三個步驟,輸入完後就可以繼續執行(建議輸入 `abcdefghij`)。跑到 `4012C6` 後就 call 了 `401240`,這邊也是 ida 剛看到的,按 F7 跟進去 ![](https://i.imgur.com/Cz7VBJ2.png) 到目前為止都是 ida 看得到的,直到 `40720D` 後就要純看組語了。到這裡可以看到我們的輸入被放在 `40B970` 執行到 `40726B` 時,會檢查某兩 Byte 是不是 `B8` 和 `BA`,如果不是的話就無窮迴圈,所以這邊就把那兩個 byte 改好繼續執行 ![](https://i.imgur.com/xCAls5C.png) 接著繼續執行到 `407294` 的地方,跟進去又會發現程式死掉了,這邊直接 patch 成 ret,我猜是出題者故意放的陷阱 ![](https://i.imgur.com/q3NXEz7.png) 再來又執行到 `4072C9`,這邊要是執行下去的話程式又要死了。而在這之前 `4072C4` 有做 `cmp` 判斷是否要執行,所以這邊把 `eax` 改成 `C0000353` 就好 ![](https://i.imgur.com/Pfq2lCs.png) 到了 `407437`,call 了 `407305` 會死掉,所以看是要改 `al` 還是 `ZF` 都可以。`407463` 同理 `407405` 把第一個字元和第七個字元放到 `40B990` 和 `40B991` ![](https://i.imgur.com/IEucOvs.png) `407488` 把放在 `40B991` 的值給了 bl,也就是我們輸入的第七個字元,然後 xor 0x36 ![](https://i.imgur.com/nWWQ6oe.png) 接下來做了很多不需要理會的操作後,`407600` 把剛剛的第七個字元 xor 0x36 的值拿出來跟 0x36 比必須相等,否則程式會死掉,也就是說第七個字元是 \x00 ![](https://i.imgur.com/9Ne5moQ.png) 在 `40760F` 取出放在 `40B990` 的輸入的第一個字元後, `407614` 要跟進去,會先把它 `ror al, 6` 存到 `40B000`,再 rol 4 存到 `40B001`,最後 xor 34 存到 `40B002` ![](https://i.imgur.com/3WrN6qc.png) `407700` 把 `40B000` 的字元取出後存到 cl,在 `4076D0` 終於做了 `cmp cl, 49`,就是說第一個字元 = rol(0x49, 6) ![](https://i.imgur.com/YuQSOf3.png) ![](https://i.imgur.com/oho29iQ.png) 繼續執行到 `4076E9`,可以看到它把我們的輸入一個一個拿出來放到暫存器,在 `40774F` 又存到 memory ``` 1: 40CCE8 2: 40CCEC 3: 40CCE0 4: 40CCF4 5: 40CCF0 6: 40CCE4 ``` 第三個字元在 `407783` 取出,xor 77 後,在 `4077A3` 做 cmp 要等於 0x35 ![](https://i.imgur.com/6By54lB.png) ![](https://i.imgur.com/ngcShQU.png) 第二個字元在 `407800` 取出,在 `4077AC` 做 xor,最後在 `4077C5` 做 cmp 要等於 0x69 ![](https://i.imgur.com/a8g6qSy.png) ![](https://i.imgur.com/3Ww9bpF.png) ![](https://i.imgur.com/ftl2wUo.png) 接著又是第一個字元,但是 `407817` 這裡不同的是,如果相等的話反而程式會結束掉,可能是出題者在防範我們這些解題者XD ![](https://i.imgur.com/82fYmXe.png) 第四個字元在 `40790E` 取出,xor 21 後要等於 0x64 ![](https://i.imgur.com/b0tZyBx.png) 第五和第六個字元以此類推,最後可以寫一個 python script,算出 flag `RIBENA` ```::python rightRotate = lambda n, d: (n >> (d%8)) | (n << ((8 - d) % 8)) & 0xFF leftRotate = lambda n, d: ((n << d) | (n >> (8 - d))) & 0xFF print(chr(leftRotate(0x49, 6))) #40CCE8 R print(chr(0x20 ^ 0x69))#40CCEC print(chr(0x77 ^ 0x35))#40CCE0 print(chr(0x21 ^ 0x64)) #40CCF4 print(chr(0x46 ^ 0x8))#40CCF0 print(chr(rightRotate(0x14, 4))) #40CCE4 ``` ## Easy ELF 這次是給個 ELF,先拿去 ida 靜態分析 ![](https://i.imgur.com/Ote1MtN.png) 其中檢查我們的輸入,如果不正確就會 `return 0` ![](https://i.imgur.com/vPQ39Sf.png) 所以就是把輸入照著它要求的算出來就可以拿到 flag `L1NUX` ```::python chr(0x34 ^ 120) + chr(49) + chr(124 ^ 0x32) + chr(0x88 ^ (256-35)) + chr(88) ``` ## WindowsKernel 沒殼 ![](https://i.imgur.com/hlYnzlh.png) 這題要在 x86 執行,並且要給權限 ![](https://i.imgur.com/P6THUC8.png) 用 ida 打開後,最關鍵的部分在 `401110`,其中有按下 Enable 或 Check 按鈕後執行的操作 ![](https://i.imgur.com/072aiYc.png) 這個判斷式的重點在它 call `401280` 來判斷結果為何。於是看看 `401280` 做了什麼。其中它 call 了 [`DeviceIoControl`](https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol),將第二個參數 `dwIoControlCode` 傳入 ![](https://i.imgur.com/BXSM0fX.png) 這部分就是在跟 driver 互動,因此現在得分析另一個檔案 WinKer.sys 打開後首先先看看 `DriverEntry`,其中 call 了兩個函數,一個是用來 debug 的 `14005`,另一個才是我們關心的程式操作。除非對 Windows 的資料結構很了解,不然直接看這部分會有點辛苦。主要是第 37 行有設了 [DRIVER_OBJECT](https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_driver_object) 的 Dispatch routine;還有第 44 行的 [`KeInitializeDpc`](https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-keinitializedpc),簡單來說就是會開一個 thread 去跑目標函數 ![](https://i.imgur.com/6lDqzeX.png) 先看 `11288`,裡面處理個剛剛我們看到的 `dwIoControlCode` 0x1000 與 0x2000,我們主要要關注的是 0x2000 的部分,因為這才是判斷輸出 `Correct!` 還是 `Wrong` 的地方,其中指是把 `13030` 設為 0 和設定 Irp ![](https://i.imgur.com/ZgpBWtQ.png) 再來是 `11266`,裡面很簡單,就是監聽在 0x60 port,也就是 keyboard,然後 call `111DC`,參數為監聽到的值 ![](https://i.imgur.com/5J1yd24.png) `111DC` 中就是在判斷輸入的值是什麼,而依照[鍵盤的 scan code 對照表](https://wiki.osdev.org/PS/2_Keyboard),就可以得出應該要輸入的值,例如 `case 1` 就是 `-91`,換成 unsigned char 是 0x92,就是對應到鍵盤的 `K` 可能會遇到的坑 1. `11156` 和 `110D0` 裡面都有 xor,`11156` 要 xor 0x12,而 `110D0` 要 xor 0x12 和 0x5,因為每次按下鍵盤都會從 `111DC` 2. 為什麼 case 偶數都沒東西呢? 仔細看上面的對照表連結,可以發現按下去跟拿起來都會送一個 scan code 3. 對照表給大寫,但是 flag 要改小寫 最後就可以拿到 flag `keybdinthook` PS. 之後查了一下 Driver 要怎麼做動態分析,沒想到步驟挺繁雜的,之後要找時間研究,附個 [MicroSoft 的 Manual](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/getting-started-with-windbg--kernel-mode-) ## AutoHotkey1 有 UPX 殼 ![](https://i.imgur.com/Tb6hCiH.png) 執行後可以輸入東西,按下 OK 後視窗就關閉了 ![](https://i.imgur.com/XojjsEV.png) 但是脫殼完後執行卻會出錯,看起來不像是解壞了,因為如果是解壞了,應該會像是 Ransomware 那題解壞一樣,沒有辦法執行 ![](https://i.imgur.com/kB9fAIh.png) 用 ida 靜態分析,發現有一大堆函數,先看看 `WinMain()` 也不知道在做什麼 ![](https://i.imgur.com/2a3nc8O.png) 所以先按 shift + F12 查查字串引用,找到 `EXE corrupted`,有兩個地方引用它,稍微逆向一下這部分 ![](https://i.imgur.com/BRyvu2y.png) 發現前面有個 if 判斷式,其中呼叫了 `sub_4508C7`,程式裡面對檔案本身做了一些檢查,要看懂這段程式得要對 [fread](http://tw.gitbook.net/c_standard_library/c_function_fread.html)、[fseek](http://tw.gitbook.net/c_standard_library/c_function_fseek.html) 有一定的認識 1. 檢查檔案後 4 bytes 2. 設倒數第 8 到倒數第 4 bytes 為一 int x,檢查第 x 到第 x + 15 bytes 3. 檢查第 x + 16 byte 4. 設第 x + 17 到第 x + 20 bytes 為一 int y,從第 x + 21 byte 開始讀入 y ^ 0xFAC1 bytes,設這個讀入的東西為 z 最後這個 z 會被丟入 `sub_450ABA` 做一堆運算,但是結果沒拿來做什麼,有點可疑。 ![](https://i.imgur.com/K7kyR4O.png) 所以用 x32dbg 下去跟,繞過前面檢查的方法就是只要把`GetModuleFileNameA()` 的檔名改掉,變成原本的檔案就好 ![](https://i.imgur.com/4GGDMgv.png) 跟進去後發現剛剛 ida 中那個 `sub_450ABA` 的第二個參數就是 0x20,也就是 MD5 的輸出長度。所以直接跟到這個 function 結束前,看看運算結果儲存的地方的值(a1)就好。可以發現有一串 MD5 在記憶體中 ` 220226394582d7117410e3c021748c2a`,再拿去[線上工具](https://pmd5.com/)解得到半個 flag `isolated` ![](https://i.imgur.com/XeY877j.png) 另一半的 flag 可以猜到就是跟我們的輸入有關 一樣得先找到關鍵程式碼,點選 ida 中的 import,可以看到引用的函式庫,把用到 GetWindowText 或 GetMessageA 的位址都下斷點,看看按下 OK 後會停在哪,最後鎖定了 `DialogFunc` ![](https://i.imgur.com/NeYX67r.png) 動態分析找 `GetWindowTextA()` 後輸入所在的位址,向上拉可以發現一串 MD5 `54593f6b9413fc4ff2b4dec2da337806`,解出來是 `pawn` ![](https://i.imgur.com/Z5IDstm.png) 其實如果繼續跟也是可以,在輸入所在的位址下硬體讀取的斷點,[教學在這](https://zhuanlan.zhihu.com/p/148919626)。在 0x457A00 這個位址中,程式比較了我們的輸入是否等於另一個字串,而那個字串就是剛剛我們往上翻找到的那一串 MD5 ![](https://i.imgur.com/JKB0bbV.png) 兩半就可以組成 flag `isolated pawn` ## CSHOP 執行後長這樣,空白視窗 ![](https://i.imgur.com/jsBTNDU.png) 看來是 C# 寫的,雖然看題目名稱也知道 ![](https://i.imgur.com/RGZU1wd.png) 用 dnspy 丟進去可以 decompile,它直接把 `W54RE6MIPSP6S` 寫在程式中了,不過順序可能不對 ![](https://i.imgur.com/lG6xqD3.png) 所以根據變數名稱,並對應著按鈕位置,可以找到 flag `P4W6RP6SES` ![](https://i.imgur.com/cP0VeC7.png) 不過後來發現較快的兩個解法 1. 程式把按鈕 size 設 0,所以我們點不到,因此只要把 size 改掉後重新編譯就可以點到了 2. 雖然按鈕 size 設 0,但是因為 TabIndex 為 0,所以空白鍵就等於按按鈕 ## PEPassword 拿到兩個 exe,分別為 Original.exe 和 Packed.exe。 Original.exe 沒有殼,執行後直接跳出視窗說 `Congratulation! Password is ??????????` ![](https://i.imgur.com/QSVKBP5.png) ![](https://i.imgur.com/hosSNSF.png) Packed.exe 有不知名的殼,執行後可以輸入東西,但是隨意輸入後沒有反應 ![](https://i.imgur.com/VxiRlSw.png) ![](https://i.imgur.com/qLCZkIn.png) 既然 Orginal.exe 沒有殼,就先逆它看看。用 ida 看一看,發現就只是把兩個字串做 xor,最後輸出剛好都是 `?`。目前不知道這個 exe 是做什麼用的,但是可以想到 Packed.exe 脫殼後應該就會做跟 Original.exe 差不多的事 ![](https://i.imgur.com/xZ0uN1g.png) 接著是 Packed.exe,先用 ida 看看有沒有可疑的東西,結果看了一看沒什麼想法。用 x32dbg 動態分析,按 F9 後停在了 EntryPoint,這邊有一個 call,如果直接 F8,整個程式就這樣跳出輸入視窗了,所以跟進去看看裡面做了什麼 ![](https://i.imgur.com/JNiJkLl.png) 查找 String Reference 沒發現可疑的東西。瘋狂的 F8 後,會發現最後有個地方執行完之後終於跳出輸入視窗 ![](https://i.imgur.com/GKYYV1c.png) 跟進去看看發現原來這是 [`GetMessageA`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagea),而下面兩個 call 分別是 [`TranslateMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-translatemessage) 和 [`DispatchMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-dispatchmessage)。中間有兩個 `je`,第一個是檢查我們輸入的東西是不是 `WM_QUIT`,所以這不用理它。第二個則是檢查 `[ebp+402A3E]` 是不是 0,所以有可能 `GetMessageA` 的回調函數中會做一些判斷改這個值。 這邊可以用 ida 的交叉引用功能,先在那個位址游標指著 `byte_402A3E[ebx]` 後按下 `x`,可以看哪裡有用到它,這邊能看到除了 `4090A8` 外,還有 `4091A8` 也使用了它 ![](https://i.imgur.com/v5Jc9BJ.png) 所以用 x32dbg 看看 `4091A8` 的 asm,可以看到的確這個地方幫 `byte_402A3E[ebx]` 做 inc ![](https://i.imgur.com/GeGzWIz.png) 而判斷要不要 inc 的是上面的 `cmp eax, 0xE98F842A`,而這邊的 eax 又是上面 `cmp eax, 0xE98F842A` 的回傳值,於是我們在這邊下一個斷點後,在輸入的視窗輸入東西就會停在這邊 ![](https://i.imgur.com/JSWVw0P.png) 在這個 call 按 `F7` 跟進去,在這裡對我們的輸入做了很多操作。不難看懂這段在做什麼,就是對我們輸入的每一個字元做 xor, add, ror, xor 重複 0x10000 次。但是這邊很難爆出來,因為假設正確的輸入有 n 個,並且都是英文數字([A-Za-z0-9]),要爆的數量也有 74^n,基本上 n 如果大於 4,就有點困難了,何況我們不知道 n 等於多少。所以這先放在備案,先繼續逆 ![](https://i.imgur.com/7R7VuBD.png) 在 `4090A8`,把 `byte_402A3E[ebp]` 這個值改成 1,之後就可以跟下去 ![](https://i.imgur.com/pgEbZ3E.png) 往下跟幾步後在 `4090CB` 有個 call,看了一下會發現它後面會對 0x401000 做一些操作,可能這裡就是解密的部分。而前面有兩個 `call 4091DA` 這邊又是拿我們的輸入去做跟剛剛很難爆不出來的那邊一樣的事情 ![](https://i.imgur.com/cIW2Na3.png) 前面兩個 call 完的結果會分別存在 eax 和 ebx,接著就是針對 0x401000 去解密,而解密的結果應該就會跟 Original.exe 的 0x401000 一樣。所以要怎麼知道 eax 和 ebx 分別是多少? 首先 eax 可以知道,因為那一句 `xor dword ptr ds:[edi], eax`,可以知道 eax 就是 Original.exe 和 Packed.exe 的 0x401000(一次 4 byte) 做 xor 那 ebx 呢? 因為 ebx 是 32 bit,也就是最多只要試 0xFFFFFFFF 次就可以爆完。而且我們也知道下一輪的 eax 會是 Original.exe 和 Packed.exe 的 0x401004 做 xor 因此只要把正確的 ebx 爆出來理論上就可以讓程式解密正確 ```::python eax = 0x014CEC81 ^ 0xB6E62E17 new_eax = 0x0D0C7E05 ^ 0x57560000 rightRotate = lambda n, d: (n >> (d%32)) | (n << ((32 - d) % 32)) & 0xFFFFFFFF def op(ebx): a = eax ^ ebx a = rightRotate(a, (ebx & 0xff00) >> 8) return a for i in range(0xFFFFFFFF): if i % 0x100000 == 0: print(hex(i)) try_eax = op(i) if try_eax == new_eax: print('answer is {}'.format(hex(i))) break ``` 在 x32dbg 塞回去就可以讓程式正常解密並執行,最後爆出 ebx 是 `0xb2f098e8`,但是這個 ebx 是已經執行過 `rol ebx, cl` 的 ebx,所以 ebx 要在執行過 `409226` 之後才塞回去。而 eax 是最一開始的 eax,所以 eax 要在 `40921F` 執行前塞回去 ![](https://i.imgur.com/gmKiJ2j.png) ![](https://i.imgur.com/rCW8dnG.png) 塞回去後繼續執行就噴出 flag `From_GHL2_!!` ![](https://i.imgur.com/nIvvPk0.png) ## HateIntel 這次給的不是 exe,先用 file 指令看看 ``` $ file hateintel hateintel: Mach-O arm executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL> ``` 丟入 IDA 靜態分析,在 sub_2224 發現接收輸入的程式碼 ![](https://i.imgur.com/8yaAap8.png) 簡單來說就是將我們的輸入拿去做一些處理,最後跟 byte_3004 去做對比,所以就是要找到對英的輸入,所以將那段逆回來就可以拿到 flag `Do_u_like_ARM_instructi0n?:)` ```::python table = {} for i in range(256): now = i for j in range(4): now *= 2 if now & 0x100: now |= 1 table[now % 256] = i cipher = open('HateIntel', 'rb').read()[0x2004:0x2004+28] flag = '' for c in cipher: flag += chr(table[c]) print(flag) ``` ## SimpleVM 這題是個 Linux 題,用 `file SimpleVM` 得到 `SimpleVM: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, stripped` 用 ida 查看發現有噴錯 `Illegal program entry point (C023DC)`,不過這不影響執行。如果要修正的話,就用 hexeditor 查看一下 SimpleVM 這個檔案,會發現它總共的長度是到 `13EA`。然而若輸入 `readelf -a SimpleVM`,fileSiz, MemSiz 卻都是 `13c7` ``` Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x00c01000 0x00c01000 0x013c7 0x013c7 RWE 0x1000 LOAD 0x00019c 0x0804b19c 0x0804b19c 0x00000 0x00000 RW 0x1000 ``` 所以要把這兩項用 hexeditor 改掉,改成 `13EA`,注意是 Little Endian ![](https://i.imgur.com/TnMyK2G.png) 然而拖入 ida 後什麼都沒有,所以直接執行看看。執行後,出現 `Input : `,隨便輸入後噴出 `Wrong` 用 gdb 打開它,`run` 之後 ctrl + C,接著打 `vmmap` 看它的記憶體配置,可以看到 `0x8048000 ~ 0x804b000` 這一段,用 `dump memory mem 0x8048000 0x804b000` 把這段記憶體 dump 出來。這段應該就是主要被執行的程式碼,那為什麼是這段呢? 純粹用猜的,一開始我把每一段都 dump 出來看過了 把 dump 出來的 mem 丟到 ida 後可以正常反組譯,接著查詢字串可以發現 `Input :` 在 `8048556` 中,於是現在要做的事情就是分析它。 ![](https://i.imgur.com/VZ8geRG.png) 首先,輸入 `Input` 那行看起來就是使用 `write`,那其他的呢? 可以透過位址的 offset 來判斷其他 libcall 是誰。在 gdb 中輸入 `vmmap` 後,會出現 libc 的基底位址,拿 `write` 的位址減去這個基底位址就是 offset ``` gdb-peda$ vmmap Start End Perm Name 0x00c01000 0x00c02000 rwxp /tmp/SimpleVM 0x08048000 0x0804b000 r-xp mapped 0x0804b000 0x0804c000 rwxp mapped 0xf7dd4000 0xf7fa6000 r-xp /lib32/libc-2.27.so 0xf7fa6000 0xf7fa7000 ---p /lib32/libc-2.27.so 0xf7fa7000 0xf7fa9000 r-xp /lib32/libc-2.27.so 0xf7fa9000 0xf7faa000 rwxp /lib32/libc-2.27.so 0xf7faa000 0xf7fad000 rwxp mapped 0xf7fb4000 0xf7fb6000 rwxp mapped 0xf7fb6000 0xf7fdc000 r-xp /lib32/ld-2.27.so 0xf7fdc000 0xf7fdd000 r-xp /lib32/ld-2.27.so 0xf7fdd000 0xf7fde000 rwxp /lib32/ld-2.27.so 0xf7fde000 0xf7fe2000 r--p [vvar] 0xf7fe2000 0xf7fe4000 r-xp [vdso] 0xfffbe000 0xfffdf000 rwxp [stack] ``` 但是那個 `8048460` 並不是 `write` 在 libc 的位址,而是會先跳到 `8048460` 後再去 call libc 的 `write`。所以在 gdb 中輸入 `x/i 0x8048460` 會出現 `0x8048460: jmp DWORD PTR ds:0x804b018`,就是說位址會跳到 `0x804b018` 中存的值。那再看看 `0x804b018` 中存的值是什麼,輸入 `x/wx` 可以得到 `0xf7eb96f0`,拿這個位址再去剪掉剛剛說的 libc 的基底位址就可以算出 offset。同理,所以若要知道其他 libcall 是什麼,就只要做相同的步驟後,拿得到的位址減去基底位址就可以算出 offset。 之後,用 `objdump -M intel -d /lib32/libc-2.27.so`,就可以利用剛剛算出來的 offset 得到對應的 libcall。整理之後會發現各位址對應的 libcall 如下: ``` 80489FE: getuid() 8048400: read() 8048460: write() 8048470: pipe() 8048480: fork() ``` 到這裡就能大概看懂程式在做什麼了,首先會先 fork,造出一個 child process,接著這個 process 會先讀我們輸入的 flag,並檢查這個 `flag <= 8 (含換行)`。然後把 `0x804B0A0` 中的 200 bytes 每個 byte 做一些操作,這部分不逆沒關係,因為檢查 flag 正不正確的地方在 parent process。最後把 flag 和 `0x804B0A0` 的 200 bytes 傳給 parent process ![](https://i.imgur.com/7lbVelW.png) 再來是 parent process 的部分,首先它在接收到 child process 送來的 flag 和 `0x804B0A0` 後,會把 `0x804B0A0` 的每個 byte xor 0x20,接著把 flag 塞在 `0x804B0A0` 後的前八個 byte,最後再把 `0x804B0A0` 的每個 byte xor 0x10 做完上述操作後,透過 `804BC6D` 這個 function 檢查 flag 的正確性,正確就輸入 correct,否則 wrong ![](https://i.imgur.com/VDEcuwt.png) 關於 `8048C6D` 這一段,裡面有一個 while 和一個 switch,基本上就是判斷 `dword_804B190` 中的值做判斷,如果是 0xB 就離開迴圈,0xA 也會離開迴圈,但是是失敗的 ![](https://i.imgur.com/1SISmMo.png) 再來是裡面這些 function,有看懂它們在做什麼,不過想不出在不知道 flag 的情況下逆回去的方法。所以我的策略就是,假設 flag 越接近正確答案就會在迴圈內執行越多次,那只要每次只換 flag 的其中一個字元,其他字元不變,選擇執行次數最高的當正確的。雖然這做法可能會有例外,不過值得一試,最後也成功了。 不過現在還差一樣東西,就是 `0x804B0A0` 那 200 bytes 是什麼。這可能就必須動態分析去拿了,先執行 SimpleVM,然後在另一個 terminal 輸入 `ps auwxx | grep SimpleVM` 會出現兩行 ``` root 55 0.0 0.0 2236 528 pts/1 S+ 17:06 0:00 ./SimpleVM root 56 0.0 0.0 2236 68 pts/1 S+ 17:06 0:00 ./SimpleVM ``` 選擇第一個 attach 上去 `gdb attach [PID]`,因為第二個是 child process,之後輸入 `b *0x8048C6D`,把斷點斷在 `0x8048C6D`,最後輸入 `c`。在剛剛執行 SimpleVM 的那個 terminal 應該目前是需要我們輸入,於是隨便輸入 `aaaaaaa`,這時 gdb 應該就會斷在 `0x8048C6D`。這是輸入 `x/25gx 0x804b0a0` 就可以把 `0x804b0a0` 的 200 bytes dump 下來 `dump memory 804b0a0.mem 0x804b0a0 0x804b0a0+200` 現在有了 0x804b0a0 的 200 bytes 和前面剛說的那個假設,就可以嘗試寫 script 去爆破,程式碼看起來很長,但是其實都只是把 ida 上的每個 function 改成 python 而已。最後成功得到 flag `id3*ndh` ```::python import string printable = string.printable[:-6] backup_dword_804B0A0 = [ord(i) for i in open('804b0a0.mem').read()] def sub_8048A48(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 pass_dword_804B190 = dword_804B0A0[dword_804B0A0[9] ^ 0x10] ^ 0x10 dword_804B0A0[9] = (((dword_804B0A0[9] ^ 0x10) + 1) & 0xff) ^ 0x10 return dword_804B0A0[9] def sub_8048B92(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 sub_8048A48() dword_804B198 = pass_dword_804B190 sub_8048A48() dword_804B194 = pass_dword_804B190 pass_dword_804B190 = dword_804B198 dword_804B198 = dword_804B194 return sub_8048A2F() def sub_8048ABB(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 dword_804B198 = pass_dword_804B190 dword_804B18C = dword_804B194 sub_8048A48() dword_804B18C = pass_dword_804B190 sub_8048A48() sub_8048A0B() dword_804B194 = pass_dword_804B190 pass_dword_804B190 = dword_804B18C sub_8048A0B() pass_dword_804B190 ^= dword_804B194 dword_804B198 = pass_dword_804B190 pass_dword_804B190 = dword_804B18C return sub_8048A2F() def sub_8048B31(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 sub_8048A48() sub_8048A0B() dword_804B198 = pass_dword_804B190 sub_8048A48() sub_8048A0B() dword_804B194 = pass_dword_804B190 if dword_804B198 == pass_dword_804B190: dword_804B198 = 1 else: dword_804B198 = 0 #dword_804B198 = (dword_804B198 == pass_dword_804B190) pass_dword_804B190 = 8 return sub_8048A2F() def sub_8048BCE(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 dword_804B198 = pass_dword_804B190 sub_8048A48() dword_804B198 = pass_dword_804B190 pass_dword_804B190 = 8 sub_8048A0B() result = pass_dword_804B190 if not pass_dword_804B190: pass_dword_804B190 = dword_804B198 result = sub_8048A92() return result def sub_8048C13(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 sub_8048A48() return sub_8048A92() def sub_8048C22(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 pass_dword_804B190 = 0 sub_8048A0B() dword_804B198 = pass_dword_804B190 pass_dword_804B190 = 1 sub_8048A0B() dword_804B194 = pass_dword_804B190 pass_dword_804B190 = dword_804B198 result = dword_804B194 dword_804B198 = dword_804B194 return result def sub_8048A2F(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 result = pass_dword_804B190 dword_804B0A0[pass_dword_804B190] = dword_804B198 ^ 0x10 return result def sub_8048A0B(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 v0 = dword_804B0A0[pass_dword_804B190] ^ 0x10 result = v0 pass_dword_804B190 = v0 return result def sub_8048A92(): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 v0 = (pass_dword_804B190 + (dword_804B0A0[10] ^ 0x10)) & 0xff result = v0 ^ 0x10 dword_804B0A0[9] = v0 ^ 0x10 return result def check_8048C6D(tried): global dword_804B0A0, dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198 count = 0 path = [] while True: count += 1 sub_8048A48() #print(dword_804B18C, pass_dword_804B190, dword_804B194, dword_804B198) if pass_dword_804B190 == 2: #print('sub_8048B92') sub_8048B92() path.append(2) continue elif pass_dword_804B190 == 6: #print('sub_8048ABB') sub_8048ABB() path.append(6) continue elif pass_dword_804B190 == 7: #print('sub_8048B31') sub_8048B31() path.append(7) continue elif pass_dword_804B190 == 9: #print('sub_8048BCE') sub_8048BCE() path.append(9) continue elif pass_dword_804B190 == 10: #print('sub_8048C13') sub_8048C13() path.append(10) continue elif pass_dword_804B190 == 11: #print('sub_8048C22') sub_8048C22() if pass_dword_804B190: # id3*ndh print(''.join([chr(i ^ 0x10) for i in tried]), 'Correct') break else: #print('Wrong') break return count, path #dword_804B0A0 = backup_dword_804B0A0[:] #dword_804B18C = 0 #pass_dword_804B190 = 0 #dword_804B194 = 0 #dword_804B198 = 0 #check_8048C6D() for i in printable: for j in range(8): dword_804B0A0 = backup_dword_804B0A0[:] dword_804B18C = 0 pass_dword_804B190 = 0 dword_804B194 = 0 dword_804B198 = 0 dword_804B0A0[j] = ord(i) #dword_804B0A0[0] = ord('y') #dword_804B0A0[1] = ord('t') #dword_804B0A0[2] = ord('#') #dword_804B0A0[3] = ord(':') #dword_804B0A0[4] = ord('~') #dword_804B0A0[5] = ord('t') #dword_804B0A0[6] = ord('x') count, path = check_8048C6D(dword_804B0A0[:8]) if count > 8: print(j, i, count, path) ``` ## AutoHotkey2 32-bit exe,有 UPX 殼 ![](https://i.imgur.com/y4O22TX.png) ## x64 Lotto 看起來是 C++ 寫的 x64 執行檔 ![](https://i.imgur.com/rGhLEJw.png) 拿去 ida64 分析,先看 wmain。基本上就是要輸入六個數字,拿去跟亂數產生的六個數字做比對 ![](https://i.imgur.com/qcegemo.png) 解到這裡有兩個想法,第一個是直接靜態分析在跳出迴圈後它做了什麼事情;第二個是動態分析,查看那六個亂數後輸入,以符合條件。兩種方法應該都可以,但是我選擇第二種。 打開 x64dbg,一步一步跟著,直到跟到要輸入數字的地方,先隨便輸入 6 個 1 ![](https://i.imgur.com/WK8jZuR.png) 接著要找到 rand() 產生的 6 個亂數,所以直接執行到跑完產生亂數的迴圈後,查看 stack,比對 ida64 的位址(esp + 0x54),就可以找到我們的輸入和產生的亂數 ![](https://i.imgur.com/g0wFhfU.png) 之後把這六個亂數都改成 1,繼續執行就會拿到 flag `Password is from_GHL2_-_!` ## CSHARP 又是一題 C# ![](https://i.imgur.com/POW9TYd.png) 執行後會要求輸入,隨意輸入後按 Check 會出現 `Wrong` ![](https://i.imgur.com/wyVKO3t.png) 一樣丟到 dnspy 反編譯,在 `InitializeComponent()` 中發現 `Check` button 按下後的 function ` btnCheck_Click()`,其中又發現它 call 了 `MetMetMet` ![](https://i.imgur.com/GjwuRFx.png) 寫了很長一段,其實就是把 `[1, 2] + base64(input)` 當作參數丟入 `form1.bb` 這個函數,而 form1.bb 則是在 `Form1()` 定義的 ![](https://i.imgur.com/CzXvL8j.png) 而 `Form1()` 中對 `form1.bb` 就是把 `MetMett()` 修改了幾個字元,難怪原本的 `MetMett()` 在反編譯時會出錯 ![](https://i.imgur.com/2hsPJVh.png) 所以這邊可以用動態分析,一步一步跟到修改後的函數內,在 `invoke` 時 F11 跟入 ![](https://i.imgur.com/m9Gy9HH.png) 如果跟入之後的程式看不懂,可以去查那是什麼再決定要不要跟入,不然就都跟入最保險。總之最後會到達 `MetM()` ![](https://i.imgur.com/NY5QsCz.png) 其實就是比對輸入的每個字元是否等於某個字元而已,所以逆回來後 base64 decode 就拿到 flag `dYnaaMic` ```::python flag = '' for i, j in zip([16, 17, 33, 51, 68, 102, 51, 160, 144, 181, 238, 17], [74, 87, 77, 70, 29, 49, 117, 238, 241, 226, 163, 44]): flag += chr(i ^ j) print(flag) ``` ## Flash Encrypt 給了一個 [swf 檔](http://www.swffileplayer.com/),所以去下載一個 [Adobe Flash Player](https://freewarehome.tw/pc/adobe-flash-player/) 來執行,再下載 [JPEXS Free Flash Decompiler](https://www.azofreeware.com/2015/11/jpexs-free-flash-decompiler.html) 做分析。載入 player 後,畫面如下 ![](https://i.imgur.com/ErpM47T.png) 丟入 Decompiler 後,可以看到很多元件,例如 buttons, texts, frames, ... 在 frames 中可以看到這個 flash 總共有七個畫面,展開後可以看到每個畫面下所包含的元件,這些元件可以用後面的括弧數字來辨別 ![](https://i.imgur.com/A4Zp5Hv.png) 最重要的部分在 scripts 中,裡面包含著點選每個 button 後所要做的動作,但是看起來有做過混淆,因此可以在 Settings 將 Automatic deobfuscation 選項勾選 ![](https://i.imgur.com/BiNS6kF.png) 勾選完畢後就可以清楚看到每個 button 要做的動作,例如點選 DefineButton2(4) 可以看到 `if(spw == 1456)`,那 `spw` 是什麼?點選剛剛 frames 中的 frame1,可以看到對應的 PlaceObject2(4) 和 PlaceObject2(6),再點選 texts 的 DefineButton2(6),可以看到 `variablename` 是 `spw` ![](https://i.imgur.com/HnbQDcv.png) 也就是說在第一格輸入 `1456`,就可以通過判斷到第三個畫面(因為 `gotoPlay(3)`),而第三個畫面也是有一個 `if`,只是這次變數名稱變為 `spwd`,用剛剛同樣的方式可以找到對應的 texts,而通過判斷後會有做一些操作並到下一個畫面,以次類推 ![](https://i.imgur.com/Hd64ZlI.png) 所以根據程式流程,最後發現順序為 1, 3, 4, 2, 6, 5,分別對應到的數字為 1456, 25, 44, 8, 88, 20546 使用 Player 跑一次,分別輸入上述數字,最後可以拿到 flag `16876` ![](https://i.imgur.com/jeqGGhz.png) PS. 不確定有沒有人跟我一樣發現自己算的結果卻跟實際執行算出來的不同,這部分我的想法是根據[這篇文章](https://paper.seebug.org/224/),因為代碼有混淆過,所以 JPEXS 解錯了,或是漏了一些東西,不過沒有實際驗證過 ## MetroApp ## CRC1 樸實無華的 32-bit exe ![](https://i.imgur.com/oNJOsEf.png) 用 ida 打開看看,程式邏輯很簡單。輸入長度 8 的字串,然後把這八個字元分別塞進一個長度為 256 的 aHelloWelcomeTo 字串中。接著跑 256 次迴圈遍歷剛剛改的字串,做一些跟 `sub_401000` 產生的值做運算後,把結果比對判斷對錯。 ![](https://i.imgur.com/ylFIC5r.png) 這題就是一題數學題,基本上運算過程不可逆,因此得用爆破的方式。但是 8 個字元複雜度太高,所以得用前後夾擊的方式算。也就是說,從前面算爆破 4 個 bytes 到中間,另一方面又從後面爆破 4 個 bytes 到中間,然後比對兩者的值,如果一樣就代表正確。 從前面爆破到後面很簡單,就只要照著 ida 的程式照寫就好,但是從後面爆破回來就會面臨到不知道 v11 是多少的困境。 一開始只用 ida 逆向會覺得第 63 行的 `v12 = v9 >> 8` 只會影響後面那行的 `LODWORD(v9) = v12 ^ dword_4085E8[2 * v11];`,但是其實後面那行的 `HIDWORD(v9)` 也要右移 8 bits 後才做 xor。這部分我是用 x32dbg 去追才知道的。 知道了這點之後,可以確定的是 v9 最高的那 byte 在 xor 前一定都是 0x00。xor 之後就會變成 `dword_4085EC` 的最高位,因此我們就能透過比對 `dword_4085EC` 的 index 知道當次迴圈的 v11。 舉第一個例子來說,最後比對的 v13 為 `1735352167` 也就是 `0x676f5f67`。要從後面逆回去時,找 `dword_4085EC` 中誰的最高位是 0x67,就可以得出 index。 那有沒有可能會從 `dword_4085EC` 對應到兩個 index? 答案是沒有,原因應該是可以從 `sub_401000` 知道,但是我是直接用行動證明了,確定 `dword_4085EC` 的每個最高位不重複 最後寫了 python script,跑了十幾分鐘終於找到 flag `CrCA1g@!` ```::python import string import pickle printable = string.printable[:-6] backup_hello = [i for i in open('hello.mem', 'rb').read()] mem = open('mem_4085E8.mem', 'rb').read() table = [] for m in range(0, len(mem), 8): table.append(int.from_bytes(mem[m:m+8], 'little')) assert len(table) == 256 print('table ok') def testUnique(): test = [] for i in table: test.append(i>>56) print(len(set(test))) v11_table = [0 for i in range(256)] for i in range(len(table)): v11_table[table[i] >> 56] = i assert len(v11_table) == 256 print('v11_table ok') def front(hello): v9 = 0 for i in range(64): v11 = (v9 & 0xff) ^ hello[i] v9 = (v9 >> 8) ^ table[v11] return v9 def rev(hello): v9 = 0x676F5F675F695F6C for i in range(192): v11 = v11_table[v9 >> 56] v9 = (((v9 ^ table[v11]) << 8) & 0xffffffffffffffff) + (v11 ^ hello[255 - i]) return v9 def getCRC(): froCRC = {} for i in printable: print(i) for j in printable: for k in printable: for l in printable: hello = backup_hello.copy() hello[0] = ord(i) hello[16] = ord(j) hello[32] = ord(k) hello[48] = ord(l) froCRC['{}{}{}{}'.format(i, j, k, l)] = front(hello) with open('froCRC', 'wb') as f: pickle.dump(froCRC, f) froCRC = sorted(froCRC.items(), key = lambda x:x[1]) print('froCRC ok') revCRC = {} for i in printable: print(i) for j in printable: for k in printable: for l in printable: hello = backup_hello.copy() hello[64] = ord(i) hello[80] = ord(j) hello[96] = ord(k) hello[112] = ord(l) revCRC['{}{}{}{}'.format(i, j, k, l)] = rev(hello) with open('revCRC', 'wb') as f: pickle.dump(revCRC, f) revCRC = sorted(revCRC.items(), key = lambda x:x[1]) print('revCRC ok') return froCRC, revCRC def load(): with open('froCRC', 'rb') as f: froCRC = pickle.load(f) froCRC = sorted(froCRC.items(), key = lambda x:x[1]) print('froCRC load ok') with open('revCRC', 'rb') as f: revCRC = pickle.load(f) revCRC = sorted(revCRC.items(), key = lambda x:x[1]) print('revCRC load ok') return froCRC, revCRC froCRC, revCRC = getCRC() #froCRC, revCRC = loadExist() i, j = 0, 0 while i < len(froCRC) and j < len(revCRC): try: if froCRC[i][1] == revCRC[j][1]: print(froCRC[i], revCRC[j], i, j) #('CrCA', 10897544430546154721) ('1g@!', 10897544430546154721) 46123335 46125300 i += 1 j += 1 elif froCRC[i][1] < revCRC[j][1]: i += 1 else: j += 1 except: print(froCRC[i], revCRC[j], i, j, 'error') ``` ## Multiplicative 拿到一個 jar 檔案,直接拿去[線上 decompiler](http://www.javadecompilers.com/)(要選 CFR),反編譯後會有一個 java 檔案,其中只有一個 class ```::java import java.io.PrintStream; public class JavaCrackMe { public static final synchronized /* bridge */ strictfp /* synthetic */ void main(String ... arrstring) { try { System.out.println("Reversing.Kr CrackMe!!"); System.out.println("-----------------------------"); System.out.println("The idea came out of the warsaw's crackme"); System.out.println("-----------------------------\n"); long l = Long.decode(arrstring[0]); if ((l *= 26729L) == -1536092243306511225L) { System.out.println("Correct!"); } else { System.out.println("Wrong"); } } catch (Exception exception) { System.out.println("Please enter a 64bit signed int"); } } } ``` 很明顯就是要輸入一個 64-bit 的 singed int l,讓 `l * 26729 = -1536092243306511225`。然而 `-1536092243306511225` 無法整除 `26729`,可能是 overflow 了。所以就跑一個迴圈,初始值為 `-1536092243306511225`,每次加 `2^64`,直到這個數可以整除 `26729` ```::python start = -1536092243306511225 while start % 26729: start += pow(2, 64) print(start, 26729, start//26729) ``` 跑出來的結果是 `9468659231510783855`,交出去發現不對。因為要轉成 singed int,所以直接拿這個數減 `2^64` 就是 flag `-8978084842198767761` ## CRC2 ## Adventure ## CustomShell