兩種姿勢(shì)批量解密惡意驅(qū)動(dòng)中的上百條字串
作者:JiaYu 轉(zhuǎn)自公眾號(hào):信口雜談
1. 概述
在 360Netlab 的舊文 《“雙槍”木馬的基礎(chǔ)設(shè)施更新及相應(yīng)傳播方式的分析》 中,提到了 雙槍 木馬傳播過(guò)程中的一個(gè)惡意驅(qū)動(dòng)程序 kemon.sys ,其中有經(jīng)過(guò)自定義加密的 Ascii 字符串和 Unicode 字符串 100+ 條:

這在 雙槍 木馬的傳播鏈條中只是一個(gè)很小的技術(shù)點(diǎn),所以文中也沒(méi)說(shuō)具體是什么樣的加密算法以及怎樣解密,供分析員更方便地做樣本分析工作。但這個(gè)技術(shù)點(diǎn)還算有點(diǎn)意思,尤其是對(duì)逆向入門階段的朋友來(lái)說(shuō),可以參考一下解法。最近又碰到了這個(gè)驅(qū)動(dòng)程序的變種,跟團(tuán)隊(duì)的老司機(jī)討教了一番,索性寫篇短文記錄一下。
感謝老司機(jī)們解惑。也歡迎各路師傅不吝賜教,提一些更快準(zhǔn)狠的解法。
2. 樣本概況
MD5: b001c32571dd72dc28fd4dba20027a88
2.1 字符串加密情況
驅(qū)動(dòng)程序中用到的 100+ 條字符串都做了自定義加密處理,在設(shè)置完各 IRP 派遣函數(shù)和卸載例程之后,依次解密這些字符串。IDA 中打開(kāi)樣本,部分解密過(guò)程如下:
整個(gè)解密過(guò)程的函數(shù)是 sub_100038 ,里面會(huì)多次調(diào)用兩個(gè)具體的解密函數(shù):sub10003871 和 sub_10003898。前者解密 Ascii 字串,后者解密 Unicode 字串,都有兩個(gè)參數(shù):arg1—>要解密的字符串地址;arg2—>字符串長(zhǎng)度。后面會(huì)把這兩個(gè)函數(shù)分別命名為 DecryptAsciiStr 和 DecryptUnicodeStr 。這兩個(gè)函數(shù)在 IDA 中看到的 xrefs 狀況如下:
2.2 加密算法
前面說(shuō)了,算法不復(fù)雜。以 DecryptAsciiStr 函數(shù)為例:
反編譯看看:
DecryptUnicodeStr 算法其實(shí)相同,只是因?yàn)樽止?jié)構(gòu)成不同,所以是兩個(gè)解密函數(shù)分開(kāi)寫:
簡(jiǎn)單總結(jié)起來(lái),這套解密過(guò)程其實(shí)就是:把當(dāng)前字節(jié)后面特定偏移處的字節(jié)與 0xC 異或,然后替換掉當(dāng)前字節(jié),把解密后的字節(jié)寫入到當(dāng)前位置,即完成解密。本人對(duì)密碼學(xué)不熟,不知道這是不是已有名號(hào)的加密算法,看起來(lái)像是 凱撒密碼 的變形加強(qiáng)版?對(duì)此有了解的朋友歡迎指教。
3. 解密
了解了上面的情況之后,就該著手解密這百十多條字符串了。既然是用 IDA 來(lái)分析這個(gè)樣本,理想的狀況應(yīng)該是把這些字串批量解出來(lái),直接在 IDA 中呈現(xiàn),然后就可以進(jìn)行后續(xù)分析了。既然是要自動(dòng)化批量解密,寫 IDAPython 應(yīng)該算是最便捷的做法了。最終效果如圖:
3.1 姿勢(shì) 1——自行實(shí)現(xiàn)解密算法
首先想到的思路是:就兩個(gè)解密算法,而且不復(fù)雜,不妨直接寫個(gè) IDAPython 腳本,實(shí)現(xiàn)這兩個(gè)解密算法。解密之后把明文字串直接寫到 IDB 文件中,在 IDA 中呈現(xiàn)。兩個(gè)解密算法的 Python 版本分別如下(附帶對(duì) IDB 的 Patch 操作):
這里稍微解釋一下 make unicode str 時(shí)的操作:
- old_type = idc.GetLongPrm(INF_STRTYPE)
- idc.SetLongPrm(idc.INF_STRTYPE, idc.ASCSTR_UNICODE)
- idc.MakeStr(argv[0], argv[0]+(argv[1]*2))
- idc.SetLongPrm(idc.INF_STRTYPE, old_type)
在 IDA 的 UI 界面中,可以選擇生成的字符串的類型(如下圖),快捷鍵只有一個(gè) A,對(duì)應(yīng)的 idc 函數(shù)是 idc.MakeStr(0。然而 ida.MakeStr() 函數(shù)默認(rèn)是生成 Ascii 字串的,要想生成 Unicode 字串,就需要調(diào)用 idc.SetLongPrm() 函數(shù)設(shè)置一下字符串的類型。
IDA 中支持的字符串類型如上圖,相應(yīng)地,在 idc 庫(kù)中的定義如下:
- ASCSTR_C = idaapi.ASCSTR_TERMCHR # C-style ASCII string
- ASCSTR_PASCAL = idaapi.ASCSTR_PASCAL # Pascal-style ASCII string (length byte)
- ASCSTR_LEN2 = idaapi.ASCSTR_LEN2 # Pascal-style, length is 2 bytes
- ASCSTR_UNICODE = idaapi.ASCSTR_UNICODE # Unicode string
- ASCSTR_LEN4 = idaapi.ASCSTR_LEN4 # Pascal-style, length is 4 bytes
- ASCSTR_ULEN2 = idaapi.ASCSTR_ULEN2 # Pascal-style Unicode, length is 2 bytes
- ASCSTR_ULEN4 = idaapi.ASCSTR_ULEN4 # Pascal-style Unicode, length is 4 bytes
- ASCSTR_LAST = idaapi.ASCSTR_LAST # Last string type
所以,要生成 Unicode 格式的字串,需要先用 idc.SetLongPrm() 函數(shù)設(shè)置一下字符串類型。其中 idc.INF_STRTYPE 即代表字符串類型的常量,在 idc 庫(kù)中的定義如下:
用 Python 實(shí)現(xiàn)了解密函數(shù)之后,如何模擬這一波解密過(guò)程把這 100+ 條字串依次解密呢?這里可以結(jié)合 IDA 中的 xrefs 和 idc.PrevHead() 函數(shù)來(lái)實(shí)現(xiàn):
- 先通過(guò) xrefs 找到調(diào)用兩個(gè)解密函數(shù)的位置;
- 再通過(guò) idc.PrevHead() 定位到兩個(gè)解密函數(shù)的參數(shù)地址,并解析出參數(shù)的值;
- 執(zhí)行解密函數(shù),將解密后的明文字串寫回 IDB 并 MakeStr。
3.2 姿勢(shì) 2——指令模擬
這個(gè)樣本中的字串解密算法并不復(fù)雜,所以可以輕松寫出 Python 版本,并直接用 IDAPython 腳本在 IDA 中將其批量解密。那如果字串解密算法比較復(fù)雜,用 Python 實(shí)現(xiàn)一版顯得吃力呢?
這時(shí)不妨考慮一下指令模擬器。
近幾年,Unicorn 作為新一代指令模擬器在業(yè)界大火。基于 Unicorn 的 IDA 指令模擬插件也不斷被開(kāi)發(fā)出來(lái),比如簡(jiǎn)捷的 IdaEmu 和 FireEye 開(kāi)發(fā)的功能強(qiáng)大的 Flare-Emu。指令模擬器可以模擬執(zhí)行一段匯編指令,而 IDA 中的指令模擬插件可以在 IDA 中模擬執(zhí)行指定的指令片段(需要手動(dòng)指定起始指令地址和結(jié)束指令地址,并設(shè)置相關(guān)寄存器的初始狀態(tài))。這樣一來(lái),我們就可以在 IDA 中,利用指令模擬插件來(lái)模擬執(zhí)行上面的批量解密指令,解密字串的匯編指令模擬執(zhí)行結(jié)束,字串也就自然都給解密了。
本文 Case 的指令模擬姿勢(shì)基于 Flare-Emu。
不過(guò),這個(gè)姿勢(shì)需要注意兩點(diǎn)問(wèn)題:
- 指令模擬器無(wú)法模擬系統(tǒng) API ,如果解密函數(shù)中有調(diào)用系統(tǒng) API 的操作,那指令模擬這個(gè)姿勢(shì)就要費(fèi)老勁了。
- 所謂模擬指令執(zhí)行,真的只是模擬,而不會(huì)修改 IDA 中的任何數(shù)據(jù)。這樣一來(lái),需要自己把指令模擬器執(zhí)行結(jié)束后的明文字串 Patch 到 IDB 文件中,這樣才能在 IDA 中看到明文字串。
3.2.1 hook api
第 1 點(diǎn)問(wèn)題,IdaEmu 中需要自己實(shí)現(xiàn)相關(guān) API 的功能,并對(duì)指令片段中相應(yīng)的 API 進(jìn)行 Hook,才能順利模擬。比如下圖示例中,指令片段里調(diào)用了 _printf 函數(shù),那么就需要我們手動(dòng)實(shí)現(xiàn) _printf 的功能并 Hook 掉指令片段中的 _printf 才行:
而 Flare-Emu 就做的更方便了,他們直接在框架中實(shí)現(xiàn)了一些基礎(chǔ)的系統(tǒng) API,而不用自己手動(dòng)實(shí)現(xiàn)并進(jìn)行 Hook 操作:
之所以提這么個(gè)問(wèn)題,是因?yàn)檫@個(gè) kemon.sys 樣本中的批量解密字串的過(guò)程中,涉及了對(duì) memcpy 函數(shù)的調(diào)用:
這樣一來(lái),直接用 Flare-Emu 來(lái)模擬執(zhí)行應(yīng)該是個(gè)更便捷的選項(xiàng)。
3.2.2 Patch IDB
第 2 點(diǎn)問(wèn)題,將模擬結(jié)果寫回 IDB 文件,在 IDA 中顯示。
首要問(wèn)題是如何獲模擬執(zhí)行成功后的結(jié)果——明文字符串。前面描述字串解密算法時(shí)說(shuō)過(guò),解密后的字節(jié)(Byte)會(huì)直接替換密文中的特定字節(jié),把密文的前 dataLen 個(gè)字節(jié)解密出來(lái),就是明文字串。這個(gè)字節(jié)替換的操作,其實(shí)對(duì)應(yīng) Unicorn 指令模擬器中定義的 MEM_WRITE 操作,即寫內(nèi)存,而且,字串解密過(guò)程中也只有這個(gè)字串替換操作會(huì)寫內(nèi)存 。恰好,F(xiàn)lare-Emu 中提供了一個(gè) memAccessHook() 接口(如下圖),可以 Hook 多種內(nèi)存操作:
- memAccessHook can be a function you define to be called whenever memory is accessed for reading or writing. It has the following prototype: memAccessHook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData).
Unicorn 支持 Hook 的的內(nèi)存操作有以下幾個(gè):
于是,我們 Hook 掉指令模擬過(guò)程中的 UC_MEM_WRITE 操作,即可獲取解密后的字節(jié),并將這些字節(jié)手動(dòng) Patch 到 IDB 中:
- def mem_hook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData):
- #if accessType == UC.UC_MEM_READ:
- # print("Read: ", hex(memAccessAddress), memAccessSize, hex(memValue))
- if accessType == UC.UC_MEM_WRITE:
- #print("Write: ", hex(memAccessAddress), memAccessSize, hex(memValue))
- if memAccessSize == 1:
- idc.PatchByte(memAccessAddress, memValue)
- elif memAccessSize == 2:
- idc.PatchWord(memAccessAddress, memValue)
- elif memAccessSize == 4:
- idc.PatchDword(memAccessAddress, memValue)
Patch IDB 的基本操作當(dāng)然是像前文中 IDAPython 腳本那樣,調(diào)用 idc.PatchXXX 函數(shù)寫入 IDB 文件。前面一個(gè)姿勢(shì)中,Patch IDB 文件,只調(diào)用了一個(gè) idc.PatchByte() 函數(shù)。其實(shí),idc 庫(kù)中共有 4 個(gè)函數(shù)可以 Patch IDB:
- idc.PatchByte(): Patch 1 Byte;
- idc.PatchWord(): Patch 2 Bytes;
- idc.PatchDword(): Patch 4 Bytes;
- idc.PatchQword(): Patch 8 Bytes;
指令模擬器中執(zhí)行 Patch 的操作,并不只有 PatchByte 這一項(xiàng)。根據(jù)我 print 出來(lái)的指令模擬過(guò)程中寫內(nèi)存操作的細(xì)節(jié),可以看到共涉及 3 種 Patch 操作(如下圖):1 byte、2 Bytes 和 4 Bytes,所有才有了上面 mem_hook() 函數(shù)中的 3 種 memAccessSize。
明確并解決了「系統(tǒng) API Hook」和「捕獲指令模擬結(jié)果并 Patch IDB」這兩點(diǎn)問(wèn)題,就可以寫出準(zhǔn)確無(wú)誤的 IDAPython 腳本了。
3.2.3 Radare2 ESIL 模擬
r2 上也有強(qiáng)大的指令模擬模塊,名為 ESIL( Evaluable Strings Intermediate Language):
在 r2 上用這個(gè)東西來(lái)模擬指令解密這一批字符串,就不用像 IDA 中那樣還要自己動(dòng)手寫 IDAPython 腳本了,只需要通過(guò) r2 指令配置好幾個(gè)相關(guān)參數(shù)即可。下面兩張圖是在 r2 中通過(guò)指令模擬批量解密這些字符串的前后對(duì)比:
具體操作方法就不細(xì)說(shuō)了,有興趣的朋友可以自行探索。
4. 總結(jié)
文中介紹兩種基本方法,在 IDA 中批量解密 雙槍 木馬傳播中間環(huán)節(jié)的惡意驅(qū)動(dòng) kemon.sys 中的大量自定義加密字串:Python 實(shí)現(xiàn)解密函數(shù)和指令模擬解密函數(shù)。
原理都很簡(jiǎn)單,介紹的有點(diǎn)啰嗦,希望把每個(gè)關(guān)鍵細(xì)節(jié)都描述清楚了。
兩種方法對(duì)應(yīng)的 IDAPython 腳本,已上傳到 Github,以供參考:https://github.com/0xjiayu/decrypt_CypherStr_kemonsys
5. 參考資料
https://en.wikipedia.org/wiki/Caesar_cipher
https://github.com/tmr232/idapython/blob/master/python/idc.py
https://unicorn-engine.org
https://github.com/36hours/idaemu
https://github.com/fireeye/flare-emu
https://github.com/unicorn-engine/unicorn/blob/master/bindings/python/unicorn/unicorn_const.py#L64



















