看雪CTF.TSRC 2018 團隊賽 第四題 『盜夢空間』 解題思路
截止今天(12月9日)中午12:00,《盜夢空間》的攻擊時間停止,五支隊伍攻擊成功!
從下圖可以看到,中午放題搬磚狗哭哭繼續穩居第一位,tekkens、金左手、pizzatqul緊隨其後。 A2更是首次突出重圍,躋身前五。
最新賽況戰況一覽
《盜夢空間》一題中,團隊 中午放題搬磚狗哭哭,以50850s的速度奪得第一!
第四題攻擊結束後,攻擊團隊Top10也發生了比較大的變動,最新排行榜如下:
戰勢風起雲湧,看來,想要維持Top10的排名,需要相當功力的。
同時,也意味著,任何隊伍都有可能是一匹黑馬,
下一個黑馬,會是你嗎?
第四題 點評
crownless:
盜夢空間的主程式採用了諸多混淆方式:Constant blinding、垃圾指令、空迴圈、跳轉到計算出來的地址、自修改程式碼,讓我們不能僅用一種工具分析程式,同時考驗了參賽者的動態除錯和靜態分析的功底。
第四題 出題團隊簡介
出題團隊: 雨落星沉
業餘安全愛好者,前幾年玩dota時經常遇到全圖掛,在好奇心的驅使下在網路上尋找全圖掛的原理。由此來到了看雪論壇,被論壇中奇妙的計算機底層技術吸引。
但由於沒有受過系統的訓練,知識結構比較零散,水平也在入門邊緣徘徊。一年前接觸的看雪CTF,感覺實戰是提高水平的捷徑,就成為了CTF中的常客。
團隊中的其他人都是CTF中的大佬,有的出過CTF的入門影片教程,有的是CTF個人排行榜的前十。在團隊中希望能互相交流技術,向大佬們學習。
參賽的題目被我命名為Transformer,主要是因為裡面用到了一些程式碼的變形混淆技術,就像變形金剛。中間也借鑑了變形金剛中的一句經典的臺詞"One shall stand and one shall fall."。然而此次出題略顯倉促,從構思到實現,用了10天左右。最後只在選手的手裡存活了10餘個小時。如果以後還有機會參賽,應該會全面提高混淆的強度吧。
第四題 設計思路
主要設計思路:
一、手工patch main函式之前的vc執行庫,加入反除錯程式碼,如果沒有發現偵錯程式就Patch自身,跳轉至正確的驗證處。否則就陷入無法求解的死衚衕。
二、修改TEA演算法流程,對編譯出來的機器碼進行了混淆處理,生成內聯彙編形式的C語言檔案。
混淆流程:
1、處理函式開頭和結尾,將push ebp;mov ebp,esp轉義。
2、轉義部分關鍵指令(shr,shl)
3、將出現的常數以常數生成器代替
4、花指令
5、反除錯
6、延時(防止爆破)
7、跳轉混淆
8、隨機自解密
附件中的其他檔案:
original_opcode:
混淆前的modified_tea_encrypt的機器碼
gen_asm_c.py:
混淆程式碼
tea_asm.c:
混淆後的modified_tea_encrypt的C原始碼(內聯彙編形式)
candidate.txt:
花指令檔案
tea_encrypt.c:
modified_tea_encrypt的原始碼
tea_decrypt.c:
modified_tea_decrypt的原始碼
求解方法:
1、編寫去混淆指令碼,寫出modified_tea_decrypt函式
2、去除反除錯,尋找到真正的驗證邏輯
真正的驗證邏輯為:
char *key = "One shall stand and one shall fall."; modified_tea_encrypt(arr,(uint*)key,0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0x7380166f,0x4914b2b9,0x172442d7,0xda8a0600,(uint*)M1,0xdeadbeef); return (arr[0]==0x87654321 && arr[1]==0x12345678); 去混淆後,寫出modified_tea_decrypt() char *key = "One shall stand and one shall fall."; arr[0]==0x87654321; arr[1]==0x12345678; modified_tea_decrypt(arr,(uint*)key,0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0x7380166f,0x4914b2b9,0x172442d7,0xda8a0600,(uint*)M1,0xdeadbeef);
此時bin2hex(arr)就是正確的驗證碼。
原文完整連結:
https://bbs.pediy.com/thread-247772.htm
第四題 盜夢空間 解題思路
本題解析依然由看雪論壇 Riatre 原創。
週末了,稍微有一點時間,儘量詳細的寫了一下自己解題的過程。看著長,但實際上並不複雜,希望大家不要被嚇到。
當然,我猜其他人並不是用解混淆的方法做的,他們的方法可能更值得追求省心省力的各位學習 :)
為了照顧行文邏輯,下面貼出程式碼可能不完整,完整程式碼見附件。同時程式碼帶有不少除錯痕跡,比較亂,湊合看一下吧。
觀察
題目(又²)給出了一個 32 位控制檯 Windows 應用程式 transformer.exe,其有 824 KB。
執行一下:
N:\pediy\4>transformer.exe Plz input your serial:0123456789ABCDEF Wrong serial! You should try a bit harder!
請按任意鍵繼續. . .
看起來還是一個經典的輸入序列號,輸出對錯的題目。注意到輸入到輸出之間有一個約1秒的停頓。
初步分析
在 IDA Pro 中載入該可執行檔案之後,到處亂點點,亂標標,很容易定位到 00401AF0 處為 main 函式:
int main(int argc, const char *argv[]) { printf("Plz input your serial:"); scanf("%32s", serial); if ( CheckSerial(serial) ) printf("Congratulations! You have found the correct serial!\n"); else printf("Wrong serial! You should try a bit harder!\n"); system("pause"); return 0; }
其中呼叫的 CheckSerial 函式在 00401150,其 (表面上的) 邏輯大致為:
BOOL CheckSerial(char *serial) { int block[2] = {0}; if ( strlen(serial) != 16 ) return 0; for ( i = 0; i < 16; ++i ) {// input is uppercase hex if ( (serial[i] < '0' || serial[i] > '9') && (serial[i] < 'A' || serial[i] > 'F') ) return 0; } for ( i = 0; i < 8; ++i ) {// u32(endian='big') if ( serial[i] < '0' || serial[i] > '9' ) block[0] += (serial[i] - '7') << (28 - 4 * i); else block[0] += (serial[i] - '0') << (28 - 4 * i); if ( serial[i + 8] < '0' || serial[i + 8] > '9' ) block[1] += (serial[i + 8] - '7') << (28 - 4 * i); else block[1] += (serial[i + 8] - '0') << (28 - 4 * i); } ObfuscatedCompute(block, "One shall stand and one shall fall.", 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600, mat0, 0xDEADBEEF); if ( IsDebuggerPresent() ) { // Fake? ABloodyChainOfLoadLibraryAndGetProcAddressLikelyUsedForAntiDebug(); return block[0] == 0x87654321 && block[1] == 0x12345678; } else { // Real? int explode4[16] = {0}; int mat0[16] = {0, 0, 0, 0, 10, 15, 2, 13, 10, 8, 6, 15, 0, 5, 3, 0}; int mat1[16] = {16, 0, 16, 14, 3, 10, 6, 0, 0, 7, 13, 6, 4, 7, 0, 2}; int mat2[16] = {6, 16, 0, 12, 0, 11, 16, 12, 8, 12, 12, 0, 6, 0, 11, 13}; int mat3[16] = {16, 14, 13, 14, 4, 0, 0, 16, 5, 0, 0, 3, 5, 16, 14, 16}; int mem[16] = {0}; int expected_answer[16] = {222105, 494358, 443201, 423901, 310311, 700114, 629640, 620483, 301566, 676368, 606711, 605590, 149250, 339264, 304846, 301296}; for ( i = 0; i < 8; ++i ) { explode4[2 * i] = ((signed int)*((unsigned __int8 *)&block[0] + i) >> 4) + 1; explode4[2 * i + 1] = (*((_BYTE *)&block[0] + i) & 0xF) + 1; } mat0[0] = explode4[0]; mat0[3] = explode4[1]; mat0[15] = explode4[2]; mat0[12] = explode4[3]; mat1[1] = explode4[4]; mat1[7] = explode4[5]; mat1[14] = explode4[6]; mat1[8] = explode4[7]; mat2[2] = explode4[8]; mat2[11] = explode4[9]; mat2[13] = explode4[10]; mat2[4] = explode4[11]; mat3[5] = explode4[12]; mat3[6] = explode4[13]; mat3[10] = explode4[14]; mat3[9] = explode4[14]; MatrixMul(mat0, mat1, mem); MatrixMul(mem, mat2, mem); MatrixMul(mem, mat3, mem); for ( i = 0; i < 4; ++i ) for ( j = 0; j < 4; ++j ) if ( expected_answer[4 * i + j] != mem[4 * i + j] ) return 0; return 1; } }
表面上看起來這是一個分兩部分的問題,第一部分的入口在 00401C70 處,是一個約 400K 大的明顯加了混淆的函式。用偵錯程式大概觀察一下其行為,可以發現其是將輸入進行了變換,並且寫了 mat0 的前 4 個值,輸入了幾組值“感受”了一下,大抵是個 block cipher 吧。第二部分是把第一部分的輸入拆開之後填進四個 4x4 的矩陣的固定位置,再乘起來,然後驗證結果。注意到裡面還有一個疑似 typo (explode4[14] 用了兩次,而 [15] 沒用到),這就非常可疑,一開始覺得是題目沒有出好,這個問題的規模又比較小,總之先瞎找一組解試試唄,將 mat0 中的兩個由混淆過的函式填充的值也設為變數。掏出一個 SMT Solver 問問它怎麼看(相關程式碼見附件):
$ python part2-z3.py unsat Proof: unit-resolution(not-or-elim(mp(mp(mp(mp(mp(mp(asserted(0 + (0 + (184 + 6*x7 + 60)*6 + (0 + 10*x4 + <...略...>
結果它並不想理我,並丟出來一個說這問題沒解的證明。仔細盯著自己的程式碼看了一會兒確認沒寫錯。
也就是說它真的沒解,那這是怎麼回事呢?看來這個程式中還隱藏著更多的秘密。仔細觀察可以發現那個很不自然的 if (IsDebuggerPresent()) 之前有兩條長的很奇怪的廢指令:
.text:004016B8 B8 91 48 BD CA mov eax, 0CABD4891h .text:004016BD B8 EA AC DB DA mov eax, 0DADBACEAh .text:004016C2 FF 15 00 A0 4A 00 call ds:IsDebuggerPresent .text:004016C8 85 C0 test eax, eax
難道程式中有奇怪的地方帶自修改?拿 x32dbg 簡單看了一下(被混淆的函式中有讀 PEB 的反除錯,x32dbg 自帶的隱藏偵錯程式可過),這裡似乎並沒有神秘問題,可以走到。難道是有奇怪的地方用其他方式做了檢測不到偵錯程式才會進行的自修改?程式結尾有一個 system("pause") 會停下,正好提供了一個附加上去的時機,於是在這個時候附加上去看一下:
00DB16B8| B8 91 48 BD CA | mov eax,CABD4891 | 00DB16BD | EB 0D| jmp transformer.DB16CC | ; -------------- 00DB16BF| AC | lodsb| ; junk | 00DB16C0 | DB DA | fcmovnu st(0),st(2)| ; junk | 00DB16C2| FF 15 00 A0 E5 00 | call dword ptr ds:[<&IsDebuggerPresent>] | ; | 00DB16C8| 85 C0 | test eax,eax | ; | 00DB16CA| 74 50 | je transformer.DB171C| ; | 00DB16CC| E8 4F 1A 06 00 | call transformer.E13120 | ; <-------------
果然變了,也就是說實際上第二部分執行的檢查邏輯是 block[0] == 0x87654321 && block[1] == 0x12345678 的那個。
本節充分說明了我有多菜,這種簡單的反除錯 trick 想必各路高手們都是秒過吧。
反混淆
第二部分的謎題看起來像是解開了(還不能石錘,因為不知道混淆過的函式里到底做了什麼,也沒有直接目擊自修改現場,萬一它執行過之後又把自己改回去了呢?),接下來處理第一部分被混淆的函式。我們先觀察一下這個混淆是咋回事,大致翻一翻,可以發現其中有很多個這樣的 pattern,有趣的地方直接原地用註釋標出了:
; 以下是一段經過了 constant blinding 和加入無用垃圾指令(主要是一些影響 EFLAGS 的程式碼)的有用程式碼 .text:00401C70 F9 stc .text:00401C71 29 C1 sub ecx, eax .text:00401C73 BF 58 2C 69 B3 mov edi, 0B3692C58h .text:00401C78 81 EF 17 B1 A8 15 sub edi, 15A8B117h .text:00401C7E 81 EF 2C C5 85 8B sub edi, 8B85C52Ch .text:00401C84 8B 8C 7D CE 93 8A DB mov ecx, [ebp+edi*2-24756C32h] ; <... 略 ...> .text:00401D3A BF 7D 32 58 3F mov edi, 3F58327Dh .text:00401D3F 81 EF A6 CB 29 53 sub edi, 5329CBA6h ; -------------------------------------------------------------------------------------------------- ; 以下地址不連續的地方是省略了中間的垃圾指令,為了便於閱讀不再顯式標明。 .text:00401D45 50 push eax .text:00401D55 53 push ebx .text:00401D59 51 push ecx .text:00401D63 64 A1 18 00 00 00 mov eax, large fs:18h ; TEB .text:00401D6D 8B 40 30 mov eax, [eax+30h] ; p_PEB .text:00401D75 0F B6 40 02 movzx eax, byte ptr [eax+2] ; 讀 PEB 裡的 IsBeingDebugged .text:00401D82 BB 00 00 00 00 mov ebx, 0 ; 一個空迴圈 .text:00401D87 loc_401D87: .text:00401D87 43 inc ebx .text:00401D9D 83 C3 01 add ebx, 1 .text:00401DA4 81 FB 4C 20 0F 00 cmp ebx, 0F204Ch .text:00401DAA 72 DB jbshort loc_401D87 .text:00401DAC E8 00 00 00 00 call $+5 .text:00401DB1 5E pop esi .text:00401DB2 01 C6 add esi, eax ; IsBeingDebugged 參與地址計算 ; 一個經過了 constant blinding 的小常數 .text:00401DB4 81 C6 93 A5 E4 5B add esi, 5BE4A593h .text:00401DBA 81 C6 D1 FB 19 67 add esi, 6719FBD1h .text:00401DC0 81 C6 5B 18 64 69 add esi, 6964185Bh .text:00401DC6 81 C6 0F F4 64 EA add esi, 0EA64F40Fh .text:00401DCC 81 C6 65 52 38 E9 add esi, 0E9385265h .text:00401DD2 FF E6 jmp esi ; 根據 IsBeingDebugged 的值計算跳到哪,兼顧反除錯和對付靜態分析工具的控制流分析 ; <... 略 ...> ; --------------------------------------------------------------------------------------------------- .text:00401DE4 B9 F2 00 00 00 mov ecx, 0F2h ; 跳到了這 .text:00401DE9 E8 00 00 00 00 call $+5 .text:00401DEE 58 pop eax .text:00401DEF 83 C0 71 add eax, 71h .text:00401DF2 83 E8 0A sub eax, 0Ah .text:00401DF5 02 08 add cl, [eax] ; 讀當前地址後面一些地方的記憶體 .text:00401DF7 83 E8 0B sub eax, 0Bh .text:00401DFA 2A 08 sub cl, [eax] .text:00401DFC 83 E8 0D sub eax, 0Dh .text:00401DFF 02 08 add cl, [eax] .text:00401E01 83 E8 09 sub eax, 9 .text:00401E04 2A 08 sub cl, [eax] .text:00401E06 83 E8 0E sub eax, 0Eh .text:00401E09 2A 08 sub cl, [eax] .text:00401E0B 83 E8 0E sub eax, 0Eh .text:00401E0E 2A 08 sub cl, [eax] .text:00401E10 83 E8 0A sub eax, 0Ah .text:00401E13 02 08 add cl, [eax] .text:00401E15 83 E8 0F sub eax, 0Fh .text:00401E18 32 08 xor cl, [eax] .text:00401E1A 83 E8 07 sub eax, 7 .text:00401E1D 02 08 add cl, [eax] .text:00401E1F 83 E8 0E sub eax, 0Eh .text:00401E22 32 08 xor cl, [eax] .text:00401E24 E8 00 00 00 00 call $+5 .text:00401E29 58 pop eax .text:00401E2A 83 C0 36 add eax, 36h .text:00401E2D 83 C0 09 add eax, 9 .text:00401E30 28 08 sub [eax], cl ; 修改當前地址後面一些地方的記憶體 .text:00401E32 83 C0 08 add eax, 8 .text:00401E35 00 08 add [eax], cl .text:00401E37 83 C0 07 add eax, 7 .text:00401E3A 28 08 sub [eax], cl .text:00401E3C 83 C0 0Eadd eax, 0Eh .text:00401E3F 28 08 sub [eax], cl .text:00401E41 83 C0 0Badd eax, 0Bh .text:00401E44 00 08 add [eax], cl .text:00401E46 83 C0 0Fadd eax, 0Fh .text:00401E49 00 08 add [eax], cl .text:00401E4B 83 C0 0Fadd eax, 0Fh .text:00401E4E 28 08 sub [eax], cl .text:00401E50 83 C0 0Aadd eax, 0Ah .text:00401E53 28 08 sub [eax], cl .text:00401E55 83 C0 08 add eax, 8 .text:00401E58 30 08 xor [eax], cl .text:00401E5A 83 C0 0Aadd eax, 0Ah .text:00401E5D 30 08 xor [eax], cl .text:00401E5F 59 pop ecx .text:00401E60 5B pop ebx .text:00401E61 58 pop eax ; --------------------------------------------------------------------------------------------------- ; 這後面即為剛剛的程式碼修改過的正常程式碼,再後面跟著一些同樣模式的程式碼。
有趣的是,正常程式碼執行完之後沒有將其再反變換回去,也沒有在任何地方記錄變換是否發生過,也就是說這些程式碼只能從頭到尾被執行一次。
也說明這段被混淆的程式碼裡面很可能沒有複雜的迴圈之類的控制流邏輯 :) 否則生成混淆的時候就要精確分析出後繼是否已經被變換過了,而這至少是困難的。因此我們的解混淆程式碼不妨就先假設裡面沒什麼 if for,胡亂寫一寫試試。
總結一下這裡面的混淆方式:
Constant blinding
垃圾指令
空迴圈,估計是用來對付一些比較慢的模擬器的
跳轉到計算出來的地址,反除錯 + 混淆靜態分析工具的控制流分析
自修改程式碼
首先明確目標:我們希望將這段程式碼反混淆到可以用 Hex-Rays 分析的程度。
其中 1、2 只要我們掏一個模擬器或者最好是帶符號執行引擎的靜態分析框架出來,對我們的反混淆就不會有影響。而 Hex-Rays 自帶十分強大的常量摺疊和死程式碼消除,因此對我們最終的分析也不會有影響。
3 可能就是拿來克我們的,但它(可能是故意)加的比較弱,所有的空迴圈形式都一樣,結尾一定是一個連起來的 jb + call .+5,中間無垃圾指令,因此 72??E800000000 就成為了一個很好的特徵,直接查詢替換成 9090E800000000 即可,存為 transformer-noloop.exe。
而我們注意到,3、4、5 總是在一起出現,且乍一看總是出現在真正幹活的程式碼之前,解密這段真正幹活的程式碼。並且這一塊總是以三個連續的 pop ecx; pop ebx; pop eax 結尾,中間沒有垃圾指令。這成為了很好的一個特徵。因此我們考慮監控程式的執行,做以下事情:
記錄所有自修改的位置和值。
記錄 push eax / push ebx / push ecx 的位置。
遇到 pop ecx; pop ebx; pop eax 三連擊的時候,nop 掉當前位置和上一個 push eax / push ebx / push ecx 之間的指令。
考慮到 1 中需要監控所有的記憶體寫,最方便的 instrument 方法可能還是直接拿起一個模擬器來跑這個程式,於是選擇用 Unicorn Engine 開幹。(當然考慮到這裡面的自修改的指令形式都比較單一,直接弄個 OllyScript 之類的東西應該也行。)
import pefile from unicorn import * from unicorn.x86_const import * import struct import collections import gdt # https://github.com/unicorn-engine/unicorn/issues/522 def p32(x): return struct.pack('<I', x) def load_pe(path): STACK_LIMIT, STACK_BASE = 0x1170000, 0x1180000 pe = pefile.PE(path) IMAGE_BASE = pe.OPTIONAL_HEADER.ImageBase SIZE_OF_IMAGE = pe.OPTIONAL_HEADER.SizeOfImage mapped_image = pe.get_memory_mapped_image(ImageBase=IMAGE_BASE) mapped_size = (len(mapped_image) + 0x1000) & ~0xFFF uc = Uc(UC_ARCH_X86, UC_MODE_32) uc.mem_map(IMAGE_BASE, mapped_size) uc.mem_write(IMAGE_BASE, mapped_image) uc.mem_map(STACK_LIMIT, STACK_BASE-STACK_LIMIT) uc.mem_write(STACK_LIMIT, '\xdd' * (STACK_BASE-STACK_LIMIT)) uc.reg_write(UC_X86_REG_ESP, STACK_BASE-0x800) uc.reg_write(UC_X86_REG_EBP, STACK_BASE-0x400) return uc def init_tib_peb(uc): TEB, PEB, GDT = 0x7fded000, 0x7fdee000, 0 uc.mem_map(TEB, 0x1000) uc.mem_map(PEB, 0x1000) uc.mem_write(TEB + 0x18, p32(TEB)) uc.mem_write(TEB + 0x30, p32(PEB)) uc.mem_write(PEB + 2, '\x00') g = gdt.IGdt(uc, GDT, 0x1000) g.Setup(TEB) def push(uc, val): esp = uc.reg_read(UC_X86_REG_ESP) uc.reg_write(UC_X86_REG_ESP, esp - 4) uc.mem_write(esp - 4, p32(val)) def setup_call(uc, ret, *arguments): for x in arguments[::-1]: push(uc, x) push(uc, ret) nop_range = [] patches = [] last_instr_addr = collections.defaultdict(lambda: 0) TO_RECORD = ( ('\x50', 'push eax'), ('\x53', 'push ebx'), ('\x51', 'push ecx'), ('\x58', 'pop eax'), ('\x5b', 'pop ebx'), ('\x59', 'pop ecx'), ) def hook_code(uc, address, size, user_data): instr = uc.mem_read(address, size) if instr == '\xff\xe6': # jmp esi print 'Jmp -> %08x' % uc.reg_read(UC_X86_REG_ESI) for op, name in TO_RECORD: if instr == op: last_instr_addr[name] = address break if last_instr_addr['pop ecx'] == address - 2 and last_instr_addr['pop ebx'] == address - 1 and last_instr_addr['pop eax'] == address: # pop ecx; pop ebx; pop eax sequence print 'Nop between %08X and %08X' % (last_instr_addr['push eax'], address) nop_range.append((last_instr_addr['push eax'], address)) def hook_memory_access(uc, access, address, size, value, user_data): if access == UC_MEM_WRITE: if address >= 0x00401C70 and address < 0x4B1000: print('Patching %08x to %s (%d bytes)' % (address, value, size)) patches.append((address, size, value)) # Setup emulation p_block = 0x52520000 p_mat0 = 0x52521000 EXIT_ADDRESS = 0xfffff000 uc = load_pe('transformer-noloop.exe') init_tib_peb(uc) uc.mem_map(p_block, 0x1000) uc.mem_map(p_mat0, 0x1000) uc.mem_map(EXIT_ADDRESS, 0x1000) uc.hook_add(UC_HOOK_CODE, hook_code) uc.hook_add(UC_HOOK_MEM_WRITE, hook_memory_access) setup_call(uc, EXIT_ADDRESS, p_block, 0x4B1000, 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600, p_mat0, 0xDEADBEEF) uc.emu_start(0x00401C70, EXIT_ADDRESS) # Writeback pe = pefile.PE('transformer-noloop.exe') done = {} for a, s, v in patches: if a not in done: pe.set_bytes_at_rva(a - 0x400000, chr(v)) done[a] = v else: if done[a] != v: print 'Conflict patch: %08x %d -> %d, ignoring...' % (a, s, v) for l, r in nop_range: pe.set_bytes_at_rva(l - 0x400000, '\x90' * (r - l + 1)) pe.write('transformer-deobf-1.exe')
本來做好了要手調處理一下里面之前沒有暴露出的問題(比如條件跳指令)之類的準備,但執行一下,出乎意料的是,看起來被混淆的函式中真的沒有什麼正常程式碼中的跳轉指令,至少直到返回之前都沒有。
在偵錯程式裡觀察一下被解混淆的函式的輸出,發現和解混淆之前是一樣的,說明這一步做的OK。
遺憾的是,這裡做完之後,我們發現程式碼還是難以直接在 Hex-Rays 中看懂,由於棧幀分析不正確,Hex-Rays 基本沒有辦法識別函式引數、區域性變數和在此基礎上進行合理的死程式碼消除。稍微觀察之後發現這個函式是使用 ebp 作為 frame pointer 的,我們在 IDA Pro 裡按 Alt+P 編輯函式,選上 BP based frame試試:
.text:00401C70 ; Attributes: bp-based frame .text:00401C70 .text:00401C70 sub_401C70 proc near ; CODE XREF: sub_401150+560↑p .text:00401C70 .text:00401C70 var_7FB3C458 = dword ptr -7FB3C458h .text:00401C70 var_7F802E58 = dword ptr -7F802E58h .text:00401C70 var_7F5D631E = dword ptr -7F5D631Eh .text:00401C70 var_7F22FF9C = dword ptr -7F22FF9Ch .text:00401C70 var_7E66DBE5 = dword ptr -7E66DBE5h .text:00401C70 var_7E5C5872 = dword ptr -7E5C5872h .text:00401C70 var_7E36A138 = dword ptr -7E36A138h < ...略... >
很遺憾,由於 Hex-Rays 依賴 IDA Pro 本體進行的棧幀分析,而 IDA Pro 本體並不是一個 decompiler,沒有常量摺疊等功能,形如如下的帶 constant blinding 的訪問函式引數的程式碼成功被識別錯了:
.text:00401C73 mov edi, 0B3692C58h .text:00401C78 sub edi, 15A8B117h .text:00401C7E sub edi, 8B85C52Ch .text:00401C84 mov ecx, [ebp+edi*2+var_24756C32] ; mov ecx, [ebp+edi*2-24756C32h]
看來我們必須手動解除這樣的指令中的混淆。注意到這樣的指令的形式都比較單一,既然我們已經模擬了這個程式,不妨糙一把,在執行到這樣的指令的時候手動將發現是常數的暫存器的值換進去:(要是一開始用了個帶符號執行的靜態分析框架就好了,這裡可以寫的更魯棒,可以直接看符號執行的結果判斷ebp加上的值是不是常數):
from capstone import * from keystone import * stack_access_simplify_candidates = collections.defaultdict(lambda: []) def hook_code_simplify(uc, address, size, user_data): instr = next(cs.disasm(instr, address)) ops = instr.op_str if '[ebp + edi' in ops: pants = ops[ops.find('[')+1:ops.find(']')] val = eval(pants, {'ebp': 0, 'edi': uc.reg_read(UC_X86_REG_EDI)}) % 2**32 stack_access_simplify_candidates[address].append((instr.mnemonic, ops, pants, val, size)) uc.hook_add(UC_HOOK_CODE, hook_code_simplify) # ... for addr, vec in stack_access_simplify_candidates.items(): if len(vec) != 1: print 'Non constant stack access @ %08x, ignoring...' % (addr) continue mnem, ops, pants, val, size = vec[0] new = 'ebp ' if val < 2**31: new += '+ ' + str(val) else: new += '- ' + str(2**32 - val) ops = ops.replace(pants, new) enc, cnt = ks.asm(mnem + ' ' + ops, addr) assert cnt == 1 enc = ''.join(map(chr, enc)) if len(enc) > size: print 'Not enough space @ %08x, skipping...' % addr continue pe.set_bytes_at_rva(addr - 0x400000, enc.ljust(size, '\x90')) # ...
除函式結尾處 0045BFC5 的一個手動 ret 沒有被識別出來需要手動修復一下以外,這樣得到的程式 IDA Pro 終於可以正確分析出棧幀了。Hex-Rays 也可以正常得出結果:
然而我們驚訝的發現,Hex-Rays 的最佳化器居然沒有化簡連續的 ror / rol 的功能,導致這個結果十分難看。解決這個問題的正常方法當然是利用 7.1 版本以上新加入的 microcode API 給 Hex-Rays 寫一個新的最佳化 pass,化簡掉這些東西。但人總是懶惰的,Hex-Rays 輸出的程式碼又是幾乎可編譯的 C 程式碼,所以我們不妨將結果修改到 GCC 可編譯,然後丟進 GCC 裡試試,看看 O2 能給最佳化成什麼樣子,順便可以把這裡引數傳進去的常數都寫死。(morenicer.cpp 見附件)
$ g++ -ggdb -O2 morenicer.cpp
$
使用 Hex-Rays 分析得到的 a.out,結果令人驚訝的好:
已經能看出明顯的 Feistel cipher 的結構了。
解決
Feistel cipher 是一種只要看出了結構,並不需要把每一個操作逆回去就可以解密的東西,將上面的程式碼再複製出來,稍作整理,然後念一唸咒語:
即可解密。
反除錯藏哪了?
仔細看看,可以發現該程式修改了初始化 stack cookie 有關的函式,在其中加入了對反除錯函式 00490F70 的呼叫。這個函式里有一些花指令混淆,但全都是同一種固定 pattern,可直接替換掉。
其檢測偵錯程式的方法是呼叫
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugObjectHandle, ...)。
也就是說,其實那一條 LoadLibrary + GetProcAddress ntdll.dll 裡的所有函式的長鏈大概是為了隱藏 NtQueryInformationProcess 這個字串,使其不那麼扎眼,從而讓反除錯程式碼不那麼顯眼吧……
Trivia
1、為了便於閱讀,上文並不是按照我實際看題目的時候看到的順序寫的。實際發生的事情是:標完表面上看起來有的部分 -> 對第二部分無解感到非常困惑 -> 認為可能混淆的函式里藏了東西 -> 還原了程式碼,發現裡面真沒什麼特殊之處,順便寫出第一部分的解密 -> 撓頭若干分鐘 -> 看到關鍵邏輯處有一些奇怪的無用指令 -> 偵錯程式附加了一個已經執行完檢查再等 system("pause") 反饋的程式 -> 竟然真的有個有趣的反除錯+自修改……
2、這個躲貓貓型反除錯,沒檢測到偵錯程式的時候默默修改程式碼太可怕了,尤其是對我這種不喜歡使用各路高手們人手一個的高階神秘魔改“過檢測”偵錯程式就喜歡直接看程式碼的人來說。
3、但從它低調的行為和藏的位置來說,還真挺有趣的。
4、混淆的那裡面第一段真正幹活的程式碼之前沒有反除錯+SMC,反倒是最後一段真正幹活的程式碼之後有 (00462FE2),不知道是不是寫的有點問題……
吐槽
Unicorn Engine 的文件基本等於沒有,用起來真頭疼,早知道換一個用了……
原文連結:https://bbs.pediy.com/thread-248285.htm(含附件)
合作伙伴
騰訊安全應急響應中心
TSRC,騰訊安全的先頭兵,肩負騰訊公司安全漏洞、駭客入侵的發現和處理工作。這是個沒有硝煙的戰場,我們與兩萬多名安全專家並肩而行,捍衛全球億萬使用者的資訊、財產安全。一直以來,我們懷揣感恩之心,努力構建開放的TSRC交流平臺,回饋安全社群。未來,我們將繼續攜手安全行業精英,探索網際網路安全新方向,建設網際網路生態安全,共鑄“網際網路+”新時代。
轉載請註明:轉自看雪學院
看雪CTF.TSRC 2018 團隊賽 解題思路彙總:
相關文章
- 看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第九題『諜戰』 解題思路2018-12-19
- 看雪CTF.TSRC 2018 團隊賽 第一題 『初世紀』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第二題 『半加器』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第五題 『交響曲』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第六題 『追凶者也』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第八題 『二向箔』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十題『俠義雙雄』 解題思路2018-12-21
- 看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十一題『伊甸園』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十五題『 密碼風雲』 解題思路2019-01-02密碼
- 看雪CTF.TSRC 2018 團隊賽 第十四題『 你眼中的世界』 解題思路2018-12-29
- 看雪CTF.TSRC 2018 團隊賽 第十二題『移動迷宮』 解題思路2018-12-25
- 看雪CTF.TSRC 2018 團隊賽 第十三題『 機器人歷險記』 解題思路2018-12-27機器人
- 看雪CTF.TSRC 2018 團隊賽 獲獎名單公示2019-01-02
- 看雪·深信服 2021 KCTF 春季賽 | 第四題設計思路及解析2021-05-17
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路2017-11-02
- 看雪.WiFi萬能鑰匙 CTF 2017第四題 點評及解題思路2017-06-29WiFi
- ACM 盜夢空間2014-04-06ACM
- 看雪.紐盾 KCTF 2019 Q3 | 第四題點評及解題思路2019-09-29
- 2020 KCTF秋季賽 | 第四題點評及解題思路2020-11-24
- 看雪.萬能鑰匙 CTF 2017第一題 WannaLOL 解題思路2017-06-29
- 看雪·深信服 2021 KCTF 春季賽 | 第七題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第八題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第九題設計思路及解析2021-05-28
- 看雪·深信服 2021 KCTF 春季賽 | 第五題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第六題設計思路及解析2021-05-21
- 看雪·深信服 2021 KCTF 春季賽 | 第二題設計思路及解析2021-05-12
- 看雪·深信服 2021 KCTF 春季賽 | 第三題設計思路及解析2021-05-14
- 看雪·深信服 2021 KCTF 春季賽 | 第十題設計思路及解析2021-05-31
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第二題點評及解析思路2017-10-28