深入解析病毒(一)理論篇
?
引子:
?某一天饒有興趣在卡飯上瀏覽著帖子,故事的相遇就那麽簡單。當時一條評論勾起我的好奇心,那麽好逆向開始。
?根據我的習慣,拿到樣本我會線上惡意代碼分析,直接拉到virustotal之類的網站上,看看是否已經被大多數殺毒軟件所能識別,看一些有價值的數據,如下圖所示:
??????????????????圖片一:基本信息
?當看到這個頁面時候,看到最後的分析日期是18年11月,又看了一下導出表的函數信息,是一款老病毒。根據各大廠商對這個病毒行為特性、分析定位為特洛伊、偽裝等,定位不一很正常......,其實興趣降低了一大半,並不是新鮮品種,但不能這樣侮辱一個病毒!接著習慣性拉入到IDA中,當我看到熟悉的匯編之後,如下圖所示:
??????????????????圖片二:GetProcAddress實現
?當點進去其中的一個函數,看到了fs寄存器,且一大堆比較復雜的操作,看到熟悉的匯編指令以後,心中已有定數,這是一個自己實現的GetProcAddress函數。
理論篇 | 匯編篇 | 逆向篇 |
---|---|---|
定時器,保護模式,PE雜談 | 手動實現GetProcAddress函數及Hash加密字符比對 | 逆向病毒 |
?
一、理論篇
?先來看病毒樣本中的一段代碼,如下圖所示:
??????????????????圖片三:CreateTimerQueueTimer
?
?還記著以前分析熊貓燒香時候的定時器,如下圖所示:
??????????????????圖片四:SetTimer
?惡意代碼大多都會利用到WinAPI提供的定時器操作,從而實現有規劃、周期性的惡意代碼,既然那麽重要,所以我們先來聊聊那些定時器。
?經常用ARK工具的朋友,應該都使用過遍歷定時器相關的功能,有用戶層定時器,IO定時器,DCP定時器,包括我們的時鐘中斷機制,都是具有定時器相關操作的。
?我們先從用戶層入手,windbg下深入分析一下上面提到的兩個定時器操作,NtSetTimer匯編源碼如下所示:
註:(為什麽SetTimer會調用NtSetTimer,請看http://blog.51cto.com/13352079/2343452)
函數原型如下:
UINT_PTR SetTimer( HWND hWnd, // 窗口句柄 UINT_PTR nIDEvent, // 定時器ID,多個定時器時,可以通過該ID判斷是哪個定時器 UINT nElapse, // 時間間隔,單位為毫秒 TIMERPROC lpTimerFunc // 回調函數 );
?為了更好的理解定時器的匯編代碼,簡單分析一下函數調用的過程,就是如何獲取當前線程。
kd> u PsGetCurrentProcess
nt!PsGetCurrentProcess:
mov eax,dword ptr fs:[00000124h]
mov eax,dword ptr [eax+50h]
ret
?那麽根據書籍或者相關資料,我們知道fs寄存器的值恒定(註意windows7 32位測試的),內核態是fs = 0x30,用戶態 fs = 0x3B,fs在內核態指向_KPCR,用戶態指向_TEB.。什麽依據呢?憑什麽說fs指向KPCR? 這裏屬於保護模式得內容,但是這裏還是想與大家一起分享其中的原理,那麽先說說段寄存器,為了方便理解做了一個簡陋的圖,如下所示:
??????????????????圖片五:段寄存器
?
?其實段寄存器共96位,只有其中的16位是可見的,剩余部分隱藏,可見的部分就是我們能查詢到的立即數,也叫做選擇子。隱藏部分只可以被CPU操作,不可以使用指令進行操作。
?GDT全局描述符表,系統中按照不同的屬性、類型進行描述,所以這些描述符統一存儲到內存中,並且形成了一個數組,這就是GDT。全局描述符的索引保存在了可見部分16位的選擇子中,這就是GDT與段選擇子的關聯。如何從選擇子中知道索引呢?如下圖所示:
??????????????????圖片六:選擇子
?
?高13位是索引號,也就是下標。TI = 0 代表GDT,TI = 1代表LDT。RPL是當前請求特權級別,權限檢查會用到,這裏不對權限檢測做詳細介紹。
?清楚了上面的知識後,我們分析一下內核態fs = 30,16位選擇子內容,如下圖所示:
??????????????????圖片七:解析fs寄存器
?
?通過上述分解,我們知道了fs在GDT中的第六項(0開始),接著獲取gdtr,並且獲取段描述符的屬性狀態,如下圖所示:
??????????????????圖片八:gdtr寄存器
?段描述符如何來分解?段描述符都有那些屬性呢?如下圖所示:
??????????????????圖片九:通用描述符
?
介紹一些主要屬性:
?
L | D/B | P | S | DPL | TYPE | G |
---|---|---|---|---|---|---|
64位代碼段 | 默認操作大小 | 段有效值 | 描述符類型 | 描述符特權級別 | 段類型 | 粒度 |
?
?我們按照上圖分解,取Base Address,按照想對應的規則10101100 01001000 10000100 01000000進行地址拼接,其實這個就獲取到了KPCR的結構。
?fs寄存器其實擁有那麽的數據量,本質是是從結構數據中獲取,便於操作。推薦一下bochs這款x86硬件平臺的開源模擬器,學習保護模式,除了書中獲取相關知識以外,還可以多多閱讀源碼,才能更深層的學習理解。
?
?回到主題,我們既然知道fs在內核態指向的是什麽了,我們觀察一下fs:[00000124h]是什麽?結構體相關內容以前介紹過,這裏不羅嗦,如下圖所示:
??????????????????圖片十:_KPRC
?fs寄存器內核態指向的是_KPRC,fs:[0x124]指向CurrentThread(_EPROCESS),有了這些基礎以後,我們繼續分析NtSetTimer得調用過程。
NtSetTimer匯編代碼:(因為排版 所以就上圖了)
??????????????????圖片十一:NtSetTimer解析1
?如上圖所示,先是獲取_ETHREAD,然後獲取了ETHREAD+0x13a(Previous Mode),如下圖所示:
??????????????????圖片十二:
?什麽是Previous Mode?,簡單來說調用Nt或Zw版本時,系統調用機制將調用線程捕獲到內核模式,判定參數是否來源於用戶模式標誌。
?The native system services routine checks the PreviousMode field of the calling thread to determine whether the parameters are from a user-mode source.
詳細得內容介紹參考:https://msdn.microsoft.com/zh-cn/windows/desktop/ff559860
PreviousMode其中得兩個狀態值:
?1、UserMode 狀態碼是1
?2、KernelMode 狀態碼是0
?所以上圖中與0進行判斷,判斷當前是否內核態,是則跳轉0x8402fdd。我們先來看看如果是內核態,是怎樣一條執行路線,如下圖所示:
?
??????????????????圖片十三:定時器ID判定
?第二個參數必須大於等於0,否則會拋出異常,繼續看,如下圖所示:
??????????????????圖片十四:內核態匯編解析
?OD中我們跟中一下看是否真的追加了第五個參數,如下圖所示:
??????????????????圖片十五:NtUserSetTimer
?如果為0則跳轉,跳轉位置如下圖所示:
??????????????????圖片十六:ExpSetTimer
?我們會發現,SetTimer->NtUserSetTimer->Wow64得函數(如果32位運行在64位)-->KiFastSystemCall->ExSetTimer-->ObReferenceObjectByHandle-->..........
?所以SetTimer在內核態得過曾還是比較復雜得,大家可以通過函數棧來觀察到底如何運作得,這告訴我們一個道理,誰HOOK得函數越底層,誰就有可能做更多得事情。
?如果Previous Mode = UserMode呢?如何執行?如下圖所示:
??????????????????圖片十七:用戶態匯編分析
?在做了一些判斷賦值及參數保存操作以後,又跳回了與內核態執行得流程,所以說不論怎樣最終還會調用那些函數。
?關於SetTimer函數簡單得分析到這裏,我們下面接著看CreateTimerQueueTimer函數,先來看函數原型:
BOOL WINAPI CreateTimerQueueTimer(
_Out_ PHANDLE phNewTimer,
_In_opt_ HANDLE TimerQueue,
_In_ WAITORTIMERCALLBACK Callback,
_In_opt_ PVOID Parameter,
_In_ DWORD DueTime,
_In_ DWORD Period,
_In_ ULONG Flags
);
圖三中已經對參數進行了詳細得介紹,這裏不再做介紹
OD中我們動態觀察一下,如下圖所示:
??????????????????圖片十八:CreateTimerQueueTimer
?函數內部調用了RtlCreateTimer,我們繼續動態跟蹤,如下所示:
?內部調用了大量的函數,其中包括TpSetTimer也在其中,基本確定內部是調用TpSetTimer來實現該函數功能,在windbg中簡答了分析一下,內部調用了TppTimerpSet,且使用了Slim讀寫鎖機制,因為觸碰到了盲區,感覺不太準確,也找不到相關的參考所以有興趣的朋友可以深入分析一下,這裏就不講解了。
??????????????????圖片十九:TppTimerpSet
?這裏以上是給大家提供一些函數分析的思路罷了,有時間的話寫一篇相關的話題一起討論一下。
?
PE雜談 :
?關於PE知識雖然看起來雜亂,但還是比較有序的。PE涉獵的範圍較廣,PE文件是指一種格式,如可執行文件、動態鏈接庫、驅動等等,都屬於PE格式的文件。
?想深入學習的朋友,推薦一本書籍《Windows PE權威指南》,裏面內容是win32匯編撰寫而成。
?我們這裏只對用到的基本知識和導出表做介紹,PE結構體大概分為幾個部分,如下圖所示:
??????????????????圖片二十:PE大體結構
?上面順序是一定的,PE是一個有序結構,標準的PE格式每個結構體對應的偏移是固定的,當然也有很多惡意代碼會對PE結構體進行數據壓縮等技術,達到隱匿、免殺的目的。
?我們介紹一下DOS頭的數據介紹,其實我們用VS編程的時候就可以獲取到結構體,這裏不再windbg下獲取了,如下所示:
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;
?上面結構體是DOS頭部的全部信息,其中DOS中兩個重要屬重點介紹一下:
e_magi |
---|
“魔術”標誌,判斷是否PE格式第一道防線,恒定值為0x4D5A(MZ) |
e_lfanew |
---|
Dos頭與NT頭之間有一部分Dos Stub的數據(Dos的數據)大小不確定,意味著NT頭偏移不確定,所以 e_lfanew記錄了該模塊NT的偏移 |
?如何找到NT頭?模塊基址 + e_lfanew = NT的位置。第二部分我們會用匯編獲取且深入學習,用C/C++如何實現呢?如下代碼所示:
// 1.獲取PE格式文件
m_strNamePath = PathName;
// 2.打開文件
HANDLE hFile = CreateFile(PathName, GENERIC_READ | GENERIC_WRITE, FALSE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if ((int)hFile <= 0){ AfxMessageBox(L"當前進程有可能被占用或者意外錯誤"); return FALSE; }
HANDLE hFile = NULL;
// 3.獲取文件大小
DWORD dwSize = GetFileSize(hFile, NULL);
// 4.申請堆空間
PuPEInfo::m_pFileBase = (void *)malloc(dwSize);
memset(PuPEInfo::m_pFileBase, 0, dwSize);
DWORD dwRead = 0;
OVERLAPPED OverLapped = { 0 };
void* pFileBaseAddress = nullptr;
// 5.讀取文件到內存
int nRetCode = ReadFile(hFile, pFileBaseAddress, dwSize, &dwRead, &OverLapped);
// 6.轉換成DOS頭結構體
PIMAGE_DOS_HEADER pDosHander = (PIMAGE_DOS_HEADER)pFileBaseAddress;
// 7.Dos起始地址 + e_lfanew = NT頭
PIMAGE_NT_HEADERS pHeadres = (PIMAGE_NT_HEADERS)(pDosHander->e_lfanew + (LONG)pFileBaseAddress);
?如上述代碼,獲取可執文件路徑,創建(獲取文件句柄)、打開文件、讀取文件大小、申請堆空間、讀取文件數據到內存(加載到了內存)、獲取NT頭,第7步正式上述所表達的 模塊基址 + e_lfanew。
NT頭內部是如何?如下所示:
??????????????????圖片二十一:NT結構
如上所示,NT分為三部分,介紹如下:
Signature | FileHeader | OptionalHeader |
---|---|---|
標記,判斷是否PE格式第二道防線,恒定值為0x4550(PE) | 文件頭,存儲這PE文件的基本信息 | 存儲著關於PE文件的附加信息 |
既然已經介紹了PE格式兩條應規定,兩道標桿,如果判斷是否是一個PE格式的文件呢?如下代碼所示:
//判定是否是PE文件
BOOL IsPE(char* lpBase)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE/*0x4D5A*/)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase);
if (pNt->Signature != IMAGE_NT_SIGNATURE/*0x4550*/)
{
return FALSE;
}
return TRUE;
}
FileHeader結構體如下:
// File header format.
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;
Machine | NumberOfSections | TimeDateStamp | NumberOfSymbols |
---|---|---|---|
文件運行平臺 | 區段的數量 | 文件創建時間 | 符號個數 |
SizeOfOptionalHeader | PointerToSymbolTable | Characteristics |
---|---|---|
擴展頭大小 | 符號表偏移 | PE文件屬性 |
補充:
?1、Machine:0x014c代表i386,平時intel32為平臺,0x0200表示Intel 64為平臺。
?2、NumberOfSymbols:這個很重要了,你遍歷節表先要獲取數量,這個就是。
?3、Characteristics:PE的文件屬性值,如下所示:
數值 | 介紹 | 宏定義 |
---|---|---|
0x0001 | 從文件中刪除重定位信息 | IMAGE_FILE_RELOCS_STRIPPED |
0x0002 | 可執行文件 | IMAGE_FILE_EXECUTABLE_IMAGE |
0x0004 | 行號信息無 | IMAGE_FILE_LINE_NUMS_STRIPPED |
0x0008 | 符號信息無 | IMAGE_FILE_LOCAL_SYMS_STRIPPED |
0x0010 | 強制性縮減工作 | IMAGE_FILE_AGGRESIVE_WS_TRIM |
0x0020 | 應用程序可以處理> 2GB的地址 | IMAGE_FILE_LARGE_ADDRESS_AWARE |
0x0080 | 機器字的字節相反的 | IMAGE_FILE_BYTES_REVERSED_LO |
0x0100 | 運行在32位平臺 | IMAGE_FILE_32BIT_MACHINE |
0x0200 | 調試信息從.DBG文件中的文件中刪除 | IMAGE_FILE_DEBUG_STRIPPED |
0x0400 | 如果文件在可移動媒體上,則從交換文件復制並運行。 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP |
0x0800 | 如果在網絡存儲介質中,則從交換文件中復制並運行。 | IMAGE_FILE_NET_RUN_FROM_SWAP |
0x1000 | 系統文件 | IMAGE_FILE_SYSTEM |
0x2000 | DLL文件 | IMAGE_FILE_DLL |
0x4000 | 單核CPU運行 | IMAGE_FILE_UP_SYSTEM_ONLY |
0x8000 | 機器字的字節相反的 | IMAGE_FILE_BYTES_REVERSED_HI |
?
OptionalHeader結構體介紹:
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;
挑重點介紹一下:
Magic | AddressOfEntryPoint | BaseOfData |
---|---|---|
標誌一個文件什麽類型 | 程序入口點RVA | 起始數據的相對虛擬地址(RVA) |
ImageBase | SizeOfImage | SizeOfHeaders |
---|---|---|
默認加載基址0x400000 | 文件加載到內存後大小(對齊後) | 所有頭部大小 |
NumberOfRvaAndSizes | DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] | SizeofStackReserve |
---|---|---|
數據目錄個數(一般是0x10) | 數據目錄表 | 棧可增長大小 |
補充:
?1、文件中的數據是0x200對齊的(FileAlinment),內存中是以0x1000對齊的(SectionAlignment),對齊什麽意思?打個比方,假如從0開始,數據只占用了0x88字節,那麽下一段數據會在0x200開始,中間填充0。
?2、DataDirectory這是一個數組,IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16。所以共有16項,每一項對於整個執行程序來說都有特殊的意義,當然不是每個程序每一項數據表都有內容。下面我們介紹的導出表,便是這16項中的第1項,下標為0。
?那麽DataDirectory是什麽樣結構呢?如下所示:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
?每一個數組都保存了這樣的一個結構體指針,VirtualAddress是什麽?就是相對虛擬地址RVA,而Size意味著數據的大小。
?
術語介紹:
**虛擬地址**: 在一個程序運行起來的時候,會被加載到內存中,並且每個進程都有自己的4GB,這個4GB叫做**虛擬地址**,由物理地址映射過來的,4GB的空間,並沒有全部被用到。
**物理地址**:在物理內存中存在的地址。在windows中是沒有表現出來的,因為windows使用了保護模式。
**所有的數據都存儲在了相應的區段(節)**,rdata存儲只讀數據,data存儲的全局數據,text存儲的代碼,rsrc存儲的是資源。
**入口點(OEP)**:他保存的是一個 **RVA** ,然後使用 OEP + Imagebase == 入口點的VA,通常情況下,OEP指向的不是main函數,是一個用於初始化(實際加載地址)
**加載基址**:默認由PE文件指定,但是通常開啟隨機基址後,它的位置是由系統指定的
**鏡像大小**: 就是exe在文件中展開之後的大小, = 最後一個區段的RVA + 最後一個區段的size 再按照0x1000對齊。
**代碼/數據基址**:第一個代碼區段和第一個數據區段的RVA
**虛擬地址(VA)**:在進程4GB中所處的位置。
**相對虛擬地址(RVA)**:相對於內存(映像)中<u>加載基址</u>的一個偏移,
**文件偏移(FOA)**:相對於文件(鏡像)起始位置的偏移。
**文件塊對齊:** 0x200(512),一個區段在文件的大小必須是0x200的倍數
**內存塊對齊:**0x1000(4kb),一個區段在內存中的大小必須是0x1000的倍數
**關系:** 數據段(有效數據長度是0x100) => 文件對齊 => (0x200) => 映射到內存 => 0x1000
文件對齊力度和內存對齊力度可以自己改變,但是文件對齊力度必須不大於內存對齊力度
**標誌字:**標識可運行的平臺,x86,x64
**子系統**:窗口WinMain,控制臺main
**特征值**: 對應的是文件頭中的Characteristics,標識當前模塊有哪些屬性(重定位已分離=>動態基址)
**可選頭的大小**:可選頭有多少個字節,和操作系統的位數有關,x86/x64
?節表就不再這裏過多的介紹,說說導出表,也就是數據目錄表的第1項,下標為0。
?導出表是幹什麽的?PE文件導出的供其他使用的函數、變量等行為。當查找導出函的時候,能夠方便快捷找到函數的位置。
?
看一看導出表的結構體,如下所示:
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;
圖片二十一:Export Format
Characteristics | TimeDateStamp | MajorVersion | NumberOfFunctions |
---|---|---|---|
保留值, 為0 | 時間 | 主版本號 | 函數數量 |
MinorVersion | Name | Base | NumberOfNames |
---|---|---|---|
次版本號 | PE名稱 | 序號基數 | 函數名稱數量 |
AddressOfFunctions | AddressOfNames | AddressOfNameOrdinals |
---|---|---|
函數地址表RVA | 函數名稱表RVA | 函數序號表RVA |
補充:
?導出表一般會被安排到.edata中,一般也都合並到.rdata中。上述中有三個字段分別是AddressOfFunctions,AddressOfNames和AddressOfNameOrdinals,對應著三張表,上面三個字段保存了相對虛擬地址,且有關聯性,下面來看一下三個表的關聯性,如下所示:
??????????????????圖片二十二:Table關聯
?如上圖所示,序號表與名稱表一一對應,下標與下標中存儲的值是相關聯的,這三張表設計巧妙,利用了關系型數據庫的概念。
?需要註意的是,序號不是有序的,而且會有空白。地址表中有些沒有函數名,也就是地址表有地址卻無法關聯到名稱表中,這時候用序號調用,序號內容加上Base序號基址才是真正的調用號,且註意序號表是兩個字節WORD類型。
?了解這三張表之後,C/C++代碼實際應用獲取一下,代碼如下:
// lpBase就是讀取文件申請的緩沖區(把文件讀到內存後的首地址)
// 1. 找到導出表
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase);
PIMAGE_DATA_DIRECTORY pDir =
&pNt->OptionalHeader.DataDirectory[0];
DWORD dwExportFOA = RVAtoFOA(pDir->VirtualAddress);
// 2. 導出表在文件中的位置
PIMAGE_EXPORT_DIRECTORY pExportTable =
(PIMAGE_EXPORT_DIRECTORY)
(dwExportFOA + lpBase);
printf("模塊名稱%s\n", (RVAtoFOA(pExportTable->Name) + lpBase));
// 3. 獲取函數數量
DWORD dwFunCount = pExportTable->NumberOfFunctions;
// 3.1 獲取函數名稱數量
DWORD dwOrdinalCount = pExportTable->NumberOfNames;
// 4. 獲取地址表
DWORD* pFunAddr =
(DWORD*)(RVAtoFOA(pExportTable->AddressOfFunctions) + lpBase);
// 5. 獲取名稱表
DWORD* pNameAddr =
(DWORD*)(RVAtoFOA(pExportTable->AddressOfNames) + lpBase);
// 6. 獲取序號表
WORD* pOrdinalAddr =
(WORD*)(RVAtoFOA(pExportTable->AddressOfNameOrdinals) + lpBase);
// 7. 循環遍歷
for (DWORD i = 0; i < dwFunCount; i++)
{
// 7.1 如果為0說明是無效地址,直接跳過
if (pFunAddr[i] == 0)
{
continue;
}
// 7.2 遍歷序號表中是否有此序號,如果有說明此函數有名字
BOOL bFlag = FALSE;
for (DWORD j = 0; j < dwOrdinalCount; j++)
{
if (i == pOrdinalAddr[j])
{
bFlag = TRUE;
DWORD dwNameRVA = pNameAddr[j];
printf("函數名:%s,函數序號:%04X,函數序號:%04X\n",
RVAtoFOA(dwNameRVA) + lpBase,
i + pExportTable->Base);
}
}
// 7.3 如果序號表中沒有,說明此函數只有序號沒有名字
if (!bFlag)
{
printf("函數名【NULL】,函數序號:%04X\n", i + pExportTable->Base);
}
}
?上述代碼是對導出表進行的遍歷,上述中也許有一些細節性的知識表達的不夠到位,如果你能對以上的知識都很熟悉且匯編還不錯,那麽用匯編獲取函數導出表也許對你來說是一件比較輕松的事情。
?第二部分我們一起學習一下如何用匯編手動獲取函數名稱表及對應的函數地址(上面三張表關系一定搞清楚),用匯編實現自己的GetProcAddress,且Hash加密字符串進行與名稱表進行對比,理論知識先告一段落。
深入解析病毒(一)理論篇