在複刻 C2 伺服器的功能時,需清楚惡意程式使用何種加解密演算法,才能順利與惡意程式收送指令。但我習慣在整個惡意程式其他功能都分析完畢後,再進行加解密演算法種類判定。最困難的工作要留到最後才做,才不會因為挫折感太重而放棄。在此之前,只要先標注這
些加密函式的名稱即可,如 encrypt_func001、encrypt_func002 等。
針對如何判定加密演算法種類,接下來將從最簡單的 xor、 Base64 編碼開始,再來針對一般常見的加解密演算法的判定,以及對付惡意程式客製化加解密演算法的技巧,逐一說明。
文章目錄
XOR 邏輯運算
xor 是 CPU 指令集 (Instrutction Set) 中,做 xor 邏輯運算的指令。在組合語言呈現的語法格式,如下:
xor <a>, <b>
由於重複對同一個數值做 xor 邏輯運算,也有簡單的加解密效果。因此 xor 運算也是惡意程式常用的加解密方式。xor 邏輯運算的特色,導致加解密時,會使用相同的函式與密鑰。而 xor 語句在組合語言中有 3 種使用情境範例,如下。
- xor eax, eax
- xor eax, 8
- xor eax, ebx
第一個情境,是將 eax
初始化為 0,非加解密用途。第 2、3個範例,才是用於加解密用途。而像第 2 個情境,xor eax, 8
意思就是將 eax
暫存器的值 與 8 做 xor 邏輯運算,而 8 就是 惡意程式使用的密鑰 (key)。第 3 個情境,xor eax, ebx
意即將 eax
暫存器的值 與 ebx
暫存器的值做 xor 邏輯運算,而 ebx
暫存器中的值即為惡意程式使用的密鑰 (key)。
由於 xor 一次只能運算 1 個暫存器的值,所以通常會搭配迴圈執行。惡意程式會針對欲傳輸的一大包資料 (例如字串),進行 xor 邏輯運算,達成資料加解密目的。組合語言範例程式碼如下。用途為針對 data 字串 (字串長度: 10) 進行 xor 邏輯運算,並使用 數字 8 做為密鑰。
xor ecx, ecx ; 將 ecx 指定為 0 mov edx, [esp+data] ; 將 data 字串的起始位址,指定給 edx loc_0x100: mov al, [edx] ; 將 [edx] 所在位址的值指定 1個 byte 資料給 eax xor al, 8 ; al = al ^ 8,數字 8 即是此演算法的密鑰 mov [edx], al ; 將經運算後 al 的值覆寫回 [edx] 位址的值 inc edx ; 將 edx 位址加 1 inc ecx ; 將 ecx 位址加 1 cmp ecx, A; ; 確認 ecx 的值是否等於 0xA(10) jne loc_0x100; ; 若不等於 10 ,即跳回 loc_0x100 位址繼續執行
上述範例等到迴圈結束時,data 字串已完成加密(或解密)。
Base64
Base64 演算法出自於 RFC 3548。它是一種編碼字串的方式,不是加解密演算法。在日常生活中,最常用於編碼電子郵件的附件。但是,因為 Base64 也能有效地混淆字串,也常被惡意程式使用。將指令或資料透過標準或客製的 Base64 演算法編碼後,再傳送給 C2 伺服器。
以下是 hello 字串,經由標準 Base64 演算法編碼而成。
aGVsbG8=
標準 Base64 編碼字串有 4 個特色:
- 原始字串每 3 個 bytes一組,組合計算後,將計算結果做為索引,透過索引表代換成對應的編碼。
- 編碼索引表由 64 個字元組成,依序為 A-Z、a-z、0-9、+ 與 \。
- 字串長度為 4 的倍數,不足部份會由填充字元 (=) 補足。
- 編碼後字串比原始字串長。
標準 Base64 索引編碼表
索引 | 編碼 |
0 | A |
1 | B |
… | … |
25 | Z |
26 | a |
… | … |
51 | z |
52 | 0 |
… | |
61 | 9 |
62 | + |
63 | \ |
寫成連續字串格式
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
另外,有針對 URL 與檔案系統。為避免衝突,將第62個索引編碼換成「-」 (minus),第63 個索引編碼換成「_」 (underscore),稱為 URL Safe 版本的 Base64 編碼。
由於 Base64 編碼演算法特色,識別 Base64 編碼字串方法,如下 3 點:
- 字串看起來像亂碼,但出現的字元集中在 A-Z、a-z、0-9、+ 與 \範圍內。
- 字串長度為 4 的倍數。
- 字串最後有時候會出現填充字元(=)。
而惡意程式最常搞怪的地方是自訂索引編碼表(範例如下),以致於使用標準 Base64 工具程式解碼後,產生無意義的亂碼或文字。
自訂索引編碼表的範例(連續字串格式)
KJiHbl6vpfaFN7SDxg8Ozd/IqAURGM3Cr+Y9cm1TLsyjWBVXZ5Ek0wtPhu2Qneo4
若惡意程式使用自訂索引編碼表。你在分析時,就必須追進去編碼函式 (encoding routine),找到自訂編碼規則。以下是 hello 字串,經由範例的自訂編碼表 Base64 演算法編碼而成。
U6dWR6n=
由於編碼表對應索引均為一對一,不會有重複的情形。若你已找到自訂的規則,可撰寫客製化解碼工具。解碼工具撰寫技巧分享如下:
- 產生惡意程式編碼順序 (KJiH…o4) 與標準 Base64 編碼順序 (ABCD…+\) 的對應表。
- 利用查表方式,將欲解碼的字串,如「U6dWR6n=」,查表轉對應為標準 Base64 編碼字串,如「aGVsbG8=」。
- 使用標準 Base64 演算法進行解碼。
根據上述技巧,Python3 解碼程式範例如下:
#/usr/bin/env python3 import base64 def custom_b64_decode(enc_data): transform_data = str() enc_table = { 'K':"A", 'J':"B", 'i':"C", 'H':"D", 'b':"E", 'l':"F", '6':"G", 'v':"H", 'p':"I", 'f':"J", 'a':"K", 'F':"L", 'N':"M", '7':"N", 'S':"O", 'D':"P", 'x':"Q", 'g':"R", '8':"S", 'O':"T", 'z':"U", 'd':"V", '/':"W", 'I':"X", 'q':"Y", 'A':"Z", 'U':"a", 'R':"b", 'G':"c", 'M':"d", '3':"e", 'C':"f", 'r':"g", '+':"h", 'Y':"i", '9':"j", 'c':"k", 'm':"l", '1':"m", 'T':"n", 'L':"o", 's':"p", 'y':"q", 'j':"r", 'W':"s", 'B':"t", 'V':"u", 'X':"v", 'Z':"w", '5':"x", 'E':"y", 'k':"z", '0':"0", 'w':"1", 't':"2", 'P':"3", 'h':"4", 'u':"5", '2':"6", 'Q':"7", 'n':"8", 'e':"9", 'o':"+", '4':"\\" } for i in enc_data: if i is "=": transform_data += "=" else: transform_data += enc_table[i] dec_data = base64.standard_b64decode(transform_data) return dec_data print(custom_b64_decode("U6dWR6n="))
Python3 解碼程式針對「U6dWR6n=」字串的執行結果:
$ python3 custom_base64decode.py b'hello'
一般加解密演算法
一般加解密演算法的背後原理常搭配數學理論。加密函式初始化時,常會指定某些數學常數(Constant) 至程式變數中。例如, TEA 加密演算法 (a Tiny Encryption Algorithm) 的 delta = 0x9e377b9
。AES 加密演算法 (Advanced EncryptionStandard) 的 S-BOX = 0x63, 0x7c, 0x77, 0x7b...
等。
若惡意程式自行撰寫加解密演算法,沒有引用現成函式庫。若發現該函式裡面有以下特徵,極可能就是實做某個知名的加解密演算法。
- 函式內部牽扯到多個程式變數位移 (Shift) 運算。
- 函式引用某些數學常數或數學常數陣列。
- 函式內部僅有變數交叉數學運算,沒有呼叫作業系統內建的 API。
數學常數是識別加解密演算法種類很好的特徵。把惡意程式引用的數學常數或數學常數陣列內容做為搜索引擎的關鍵字,很大機會從搜尋結果中,找到是某種知名加解密演算法使用的數學常數。詳細比對後,即可識別出惡意程式是使用哪個知名的加解密演算法。
客製化加解密演算法
若無法透過前面「一般加解密演算法」章節提到的工具判斷為何種加解密演算法,則惡意程
式可能使用是客製化加解密演算法。在此種狀況下,複刻加解密演算法有 2 種方式供參。
一、直接模仿法
若加解密演算法函式程式碼不多,可使用二進位程式除錯器 (BinaryDebugger),如 x64dbg、ollydbg 等。追進去加解密函式,一行一行追,逐步紀錄。把暫存器的運算每一步驟都紀錄下來,直到加密函式結束。歸納後,再撰寫程式加解密函式。
程式很少一次複刻就成功,如何驗證撰寫的加解密函式與惡意程式相同呢?
- 使用二進位程式除錯器,在加解密函式進入點與離開點設定斷點,觀察輸入字串與輸出字串是否與撰寫的加解密函式相同。
- 若有錯誤,則再追進去加解密函式,迭代修正直到正確為止。
二、惡意程式自解法 (self decoding)
此方法利用惡意程式內建解密函式來幫忙解密。聽起來微妙,可利用外部除錯工具 (例如 radare2) 幫忙。但此法使用時,需避免呼叫惡意程式的解密函式過程中,執行到惡意代碼,使用時需謹慎。
惡意程式自解法使用時機:
- 加解密函式程式碼行數多且複雜,無法透過「直接破解法」歸納。
- 惡意程式小幅修改標準加解密寫法,造成標準解密程式失效。
舉例來說,分析惡意程式後發現 fcn.08048681
是解密函式。使用該函式時需要 push 3 個參數進入堆疊 (stack) 中。解密結果會放在原先的 [esp]
的指向位址。3 個參數用途說明如下:
- local_8h: 欲解密字串
- local_4h: 密鑰
- [esp]: 解密完成的字串會放在此變數中
在 除錯器 radare2 中,會看到如下的組合語言程式碼:
| ; var int local_4h @ esp+0x4 | ; var int local_8h @ esp+0x8 | ; var int local_18h @ esp+0x18 | ; var int local_1ch @ esp+0x1c ....[snip].. |0x08048781 mov dword [local_8h], eax |0x08048785 mov dword [local_4h], str.pass ....[snip]... |0x08048791 mov dword [esp], eax |0x08048794 call fcn.08048681
在這個範例裡,要達成惡意程式自解的目的,需要操作除錯器 radare2 ,進行以下步驟:
- 將中斷點設定在
0x08048794
位址。radare2 操作指令為db 0x08048794
。 - 執行惡意程式,程式會停在
0x08048794
中斷點位址,此時解密函式下一步要準備執行,函式 3 個參數已存放在堆疊中。radare2 操作指令為 dc。 - 紀錄堆疊 [esp] 與 [esp+8] 指向位址,例如分別為
0x0976f008
與0x0976f210
。radare2 操作指令為pv @ esp
與pv @ esp+8
。 - 改寫堆疊中的解密函式參數
[esp+8]
指向位址(如 0x08746008)的值,替換成要解密的字串。radare2 操作指令為 wx 0x700196db60f45629a5404600 @0x0976f210。
0x700196db60f45629a5404600 代表欲解密字串的16進位編碼。0x0976f210
是 堆疊變數esp+8
指向的記憶體位址,這個位址每次執行均不同。 - 執行解密函式
call fcn.08048681
。radare2 操作指令為dso
,代表 step over 一個操作指令。 - 解密結果會放在原先的
[esp]
的指向位址直接印出[註]。radare2 操作指令為psz @ 0x0976f008
,0x0976f008
代表原先的[esp]
指向的記憶體位址。
[註] 由於堆疊的指向位址 (esp, esp+8) 會在執行 call fcn.08048681
後改變,所以是檢查呼叫函式前所紀錄的指向位址。
根據上述 6 個步驟,寫成自動化的 r2pipe 程式碼範例,如下:
#!/usr/bin/env python3 import r2pipe # secret text would like to be decrypted secret_text = 0x700196db60f45629a5404600 # -2: disable stderr printing message r = r2pipe.open('malware', flags=['-2']) # aaa : auto analysis, ood: open debug mode r.cmd('aaa;ood') # set breakpoint at 0x08048794, address of `call fcn.08048681` r.cmd('db 0x08048794') # continue to execute, r2 will stop at breakpoint(0x08048794) r.cmd('dc') # esp: location of encode string encode_str_loc = r.cmd('pv @ esp') # esp+8: location of decode string decode_str_loc = r.cmd('pv @ esp+8') # write the secret text to location of encode string r.cmd('wx %x @ %s' % (secret_text, encode_str_loc)) # step over one instruction. complete `call fcn.08048681` r.cmd('dso') # get the decode string decode_string = r.cmd('psz @ %s' % decode_str_loc).strip() print(decode_string)
複刻 C2 系列文章
- C2 複刻 01 – 為何投入複刻 C2 工作?
- C2 複刻 02 – Command and Control Server 定義
- C2 複刻 03 | Command and Control Server (C&C、C2) 通訊協定類型
- C2 複刻 04 | Command and Control Server (C&C、C2) 觀察
- C2 複刻 05 | 判斷Command and Control Server (C&C、C2)發起連線端
- C2 複刻 06 | Command and Control Server 握手 (Handshake) 與認證 (Authentication)
- C2 複刻 07 | 分析惡意程式呼叫的作業系統網路 API
- C2 複刻 08 | 找出所有 Command and Control Server 指令列表與功能函式
- C2 複刻 09 | Command and Control 資料傳輸格式
- C2 複刻 10 | 惡意程式加解密函式定位
- C2 複刻 11 | 判定惡意程式加解密演算法種類
- C2 複刻 12 | 推薦 20 個複刻 Command and Control Server 的 Python 3 擴充模組
- C2 複刻 13 | 分享 3 個複刻 Command and Control Server 程式測試環境的設定建議
- C2 複刻 14 | 3 個複刻 Command and Control Server 程式的開發原則建議
2 comments
很易懂的解說
感謝留言,有空多來坐坐。