Intro.
ShadowPad 是我在實習公司分析的惡意程式,這隻惡意程式已被許多分析人員推測是中國駭客集團常用的入侵工具,最具代表的就是 Winnti group (APT41)[1],因此基本上各大安全廠商都有公布他們針對 ShadowPad 的分析文章,其最具特色的地方在於幾乎所有程式碼都是由 shellcode 堆砌而成,在分析上有相當的複雜度,本篇文章主要想利用這隻程式來記錄自己在分析 shellcode 的一些方法
Sample
Original Name | MD5 |
---|---|
hpqhvsei.dll | c51dca3089a4fd46984adee584cdf84a |
為了不和工作的樣本重複,這次要拿來分析的樣本是我在 Dr.WEB 的 ShadowPad 技術文章中提到的樣本[2],而這隻樣本的完整攻擊途徑已公布在 Freebuf[3]。
0x00 Init
常見的 DLL 惡意程式都是透過執行檔將該 DLL 載入並執行某個導出函式,因此要下斷點就必須先找到函式,從下圖可以看到導出函數只有DllEntryPoint 所以可以推測載入 DLL 時就會執行惡意程式碼
Debugging
通常我都是使用 IDA pro 和 x64dbg 搭配來做 shellcode 的分析,主要原因在shellcode 會有較多需要 dump memory 的情況所以常用到 x64dbg,一開始前置作業:
- 關閉 ALSR
- x64dbg 搭 rundll32.exe 讓debugger 跳進 DLL entrypoint
在 PE header 的 Dll Characteristics 中會紀錄這隻PE在執行時期的一些設定,可以利用 PEbear 這隻軟體來查看,如下圖可以看到 0x40 DLL can move
,把這個給刪除即可
接著為了執行 DLL 檔案因此我們需要用到 wow64 底下的 rundll32.exe 來執行指定的 DLL,下面為執行方式
1
2
3
4
<x64dbg> <rundll32> targetDLL.dll,<target function>
example:
<path>\x32dbg.lnk <path>\rundll32.exe <path>\hpqhvsei_noalsr.dll,DllMain
執行後到 x64dbg 將 DLL Entry 和 DLL Load 選項開啟,這樣執行到目標DLL時debugger 會自動斷在 entrypoint
0x01 Loader
到這邊就基本可以開始真正的逆向分析了,順便釐清一下術語 Loader:通常為了防止病毒遭到防毒軟體查殺,大部分的惡意程式都會在外層包裹一層外殼,那這層外殼對外可能就會以多種不同形式呈現,像是 word、excel、dll 檔之類的,而對內就必須負責把加密過的 shellcode(真實惡意程式碼) 給解密並執行
接下來的步驟如下
- 找到 解密 shellcode 的function
- 解出 shellcode
- 去掉 shellcode 的混淆
DllMain 只呼叫了一個 function,往下追發現他會針對呼叫的 module 進行檢查,檢查 base_addr + 0x10BA 的位置是否為 test eax,eax; jz loc_401207
,這樣做惡意程式作者可以確保呼叫這隻DLL的程式是他想要的,不是用其他 rundll32 之類的程式所執行。
通過檢查後就會將 jmp 0x100054e2 寫進指定位址,這個指定位置會在呼叫完這隻DLL 後被執行到,透過這個方式來間接呼叫 0x100054e2 這個 function
跳到 0x100054e2 function 後,就可看到非常經典的三塊程式碼,1. VirtualAlloc 創建一塊空間存放 shellcode,權限是 RWX;2. 解密 shellcode;3. call shellcode (可以直接忽略 GetForegroundWindow(),這東東是拿來混淆用的)
接下來就是分析 shellcode,為了能夠分析,通常會將 shellcode 的部分萃取出來,進行動靜態分析,這部分可以利用 x64dbg 跳到指定的位置並把他 dump 下來,也可以撰寫 ida python 腳本來順著解密流程來解析,以下附上 ida python 腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from idaapi import *
def LOBYTE(v):
return v & 0xFF
def CINT(v):
return v & 0xffffffff
def next_key_gen(key):
return CINT((CINT(key << 0x10) + CINT(key>> 0x10 ))*0x77+0x13)
def dump_shellcode(addr, size, key):
buffer = get_bytes(addr, size)
print(buffer)
result = b""
for x in buffer:
result += (x ^ LOBYTE(key).to_bytes(1,byteorder="little"))
key = next_key_gen(key)
i=0
return result
key = 0x0A42622A8
shell_addr = 0x1000780C
shell_len = 0x2310B
res = dump_shellcode(shell_addr,0x2310B,key)
fd = open('./shellcode1','wb')
fd.write(res)
fd.close()
0x02 shellcode
剛萃取出來的 shellcode 程式碼部分做了混淆,作者在不固定的地方插入三行指令,而第三行jmp 後面故意不放數值,導致 ida 解析不正確,如下圖,這部分可以直接用腳本把這三行 patch 成 nop 即可
1
2
3
jns $eip + 3
js $eip + 1
jmp
通常到了這一層,shellcode 就會開始做壞事了,但 ShadowPad 這隻惡意程式還會再包裹一層,因此接下來 shellcode 會開始解析第二層 shellcode,第二層 shellcode 通常會被稱作 root module,原因會在後面做解釋。
在 debugging shellcode 時假如每次都要從最一開始dll, 解密再到執行shekllcode ,每次的前置作業相當長,為了節省這些前置作業,可以使用兩種方法,一種是直接使用 VM 的快照每次重來就直接切回上一個快照,而另一種是直接寫一個簡單的 shellcode loader 就可以客製化將自己的 shellcode 執行起來,程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<memoryapi.h>
int main(){
int (*funcPtr)(void);
char* shellPtr;
int shelLength,ret;
FILE* filePtr = fopen(".\\shellcode.bin","rb");
fseek(filePtr, 0, SEEK_END);
shelLength = ftell(filePtr);
rewind(filePtr);
shellPtr = VirtualAlloc((void*)0x900000,shelLength, MEM_COMMIT|MEM_RESERVE, 0x40);
int readSize = fread(shellPtr, sizeof(char), shelLength, filePtr);
printf("%d",readSize);
funcPtr = (void *)shellPtr;
funcPtr();
}
建立起 shellcode 執行環境後,就可以開始分析了,整體步驟如下,先列出來比較方便讀者閱讀,假如有實力的也可以試著先分析再看我的解說:
- 從 PEB->LDR-> InLoadOrderModuleList 中撈出裝載的 Module
- 從 Kernel32.dll 找到 LoadLibraryA, GetProcAddress, VirtualAlloc
- 讀取 Header struct
- 將區段資料映射到新的區段
- 使用 key 修復 Relocation,最後清掉 buffer
- 使用 key 修復 Import table,最後清掉 buffer
- 執行第二段 shellcode
1. Module Search
第一塊程式碼如下圖,這邊做了惡意程式中很經典的行為,爬取PEB結構塊找 Module,下面是整個爬取的過程 FS:[0] -> TEB; TEB+0x30-> PEB; PEB+0xC -> LDR; LDR+0xC -> InLoadOrderModuleList InLoadOrderModuleList + 0x30 -> BaseDllName
這邊的做法就是遍歷所有執行的module 取出DllName 後去做 Hash 當值為0xFD5B1261 時就停止。藉由動態方式就可以看出他是在爬 Kernel32.dll
2. Export Function Search
找到 module 後接著利用 PE Header 結構來爬導出函數表,一樣透過 Hash 的方式來尋找 LoadLibraryA ,GetProcAddress, VirtualAlloc, Sleep
這四個 API,下方程式碼即為其 Hash 的方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def CINT(v):
return v & 0xffffffff
def ModuleProcHash(moduleName,Module=1):
result = 0
word = moduleName.encode()
index = 0
for each in word:
if Module == 1:
each |= 0x20
result = CINT(each+ CINT(result >> 8 | result << 24))
print(hex(result))
result ^= 0x7C35D9A3
# if Module == 1:
# result = result ^ 0x12345678
return result
#---Module
print(hex(ModuleProcHash('kernel32.dll')))
#---Proc
print(hex(ModuleProcHash('LoadLibraryA',0)))
print(hex(ModuleProcHash('GetProcAddress',0)))
print(hex(ModuleProcHash('VirtualAlloc',0)))
print(hex(ModuleProcHash('Sleep',0)))
Concolution
本文利用 ShadowPad 這隻程式簡單的介紹 shellcode 方面的分析,主要希望讓新手分析師能夠對惡意程式分析更好上手,但目前都還沒到 ShadowPad 這隻惡意程式最有趣的地方,為了不讓文章過於攏長,因此後續的部分就留到下一次的文章再繼續說明。
Ref
[1] https://www.ptsecurity.com/ww-en/analytics/pt-esc-threat-intelligence/shadowpad-new-activity-from-the-winnti-group/
[2] https://st.drweb.com/static/new-www/news/2020/october/Study_of_the_ShadowPad_APT_backdoor_and_its_relation_to_PlugX_en.pdf
[3] https://www.freebuf.com/articles/network/226336.html