豬年送安康,祝你們新一年健康、快樂。願你們都作一個勤奮努力、真誠奉獻的人,幸運會永遠的眷顧大家。
引子:
某一天饒有興趣在卡飯上瀏覽着帖子,故事的相遇就那麼簡單。當時一條評論勾起個人好奇心,那麼好逆向開始。
根據個人習慣,拿到樣本我會線上惡意代碼分析,直接拉到virustotal之類的網站上,看看是否已經被大多數殺毒軟件所能識別,看一些有價值的數據,以下圖所示:
圖片一:基本信息
當看到這個頁面時候,看到最後的分析日期是18年11月,又看了一下導出表的函數信息,是一款老病毒。根據各大廠商對這個病毒行爲特性、分析定位爲特洛伊、假裝等,定位不一很正常......,其實興趣下降了一大半,並非新鮮品種,但不能這樣侮辱一個病毒!接着習慣性拉入到IDA中,當我看到熟悉的彙編以後,以下圖所示:
圖片二:GetProcAddress實現
當點進去其中的一個函數,看到了fs寄存器,且一大堆比較複雜的操做,看到熟悉的彙編指令之後,心中已有定數,這是一個本身實現的GetProcAddress函數。
c++
理論篇 | 彙編篇 |
---|---|
保護模式,定時器,PE雜談 | 手動實現GetProcAddress函數及Hash加密字符比對 |
1、理論篇
先來看病毒樣本中的一段代碼,以下圖所示:
圖片三:CreateTimerQueueTimer
還記着之前分析熊貓燒香時候的定時器,以下圖所示:
圖片四:SetTimer
惡意代碼大多都會利用到WinAPI提供的定時器操做,從而實現有規劃、週期性的惡意代碼,既然那麼重要,因此咱們先來聊聊那些定時器。
常常用ARK工具的朋友,應該都使用過遍歷定時器相關的功能,有用戶層定時器,IO定時器,DCP定時器,包括咱們的時鐘中斷機制,都是具備定時器相關操做的。
咱們先從用戶層入手,windbg下深刻分析一下上面提到的兩個定時器操做,NtSetTimer彙編源碼以下所示:
注:(爲何SetTimer會調用NtSetTimer,請看https://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寄存器
段描述符如何來分解?段描述符都有那些屬性呢?以下圖所示:編程
圖片九:通用描述符
介紹一些主要屬性:
windows
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版本時,系統調用機制將調用線程捕獲到內核模式,斷定參數是否來源於用戶模式標誌。app
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其中得兩個狀態值:
一、UserMode 狀態碼是1
二、KernelMode 狀態碼是0ide
定時函數分析:
因此上圖中與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文件屬性 |
補充:
一、Machine:0x014c表明i386,平時intel32爲平臺,0x0200表示Intel 64爲平臺。
二、NumberOfSymbols:這個很重要了,你遍歷節表先要獲取數量,這個就是。
三、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) | 數據目錄表 | 棧可增加大小 |
補充:
一、文件中的數據是0x200對齊的(FileAlinment),內存中是以0x1000對齊的(SectionAlignment),對齊什麼意思?打個比方,假如從0開始,數據只佔用了0x88字節,那麼下一段數據會在0x200開始,中間填充0。
二、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加密字符串進行與名稱表進行對比,理論知識先告一段落。
2、彙編篇:
經過理論篇的閱讀,熟悉瞭如何使用C/C++(其餘語言思路不變)來獲取且遍歷導出表,那麼如圖二,當分析一段惡意代碼或者正向代碼,咱們發現這些彙編指令如何去作?IDA中轉換成C語言?其實我不多使用IDA中的轉換,應爲看彙編與看c差距並非特別大,特別對於算法,想要還原規則及代碼,彙編最爲真實可靠。固然若是說有大量工做需求,沒有太多時間去研究,只是對部分規則,邏輯進行分析造成報告,那麼就另說了......
上面介紹了保護模式相關內容及fs寄存器,分析了內核態的fs:[0x124],那麼用戶態fs:[0x30]呢?,以下圖所示:
圖片二十三:TEB
圖片二十四:PEB
什麼是TEB什麼是PEB呢?在之前的博客中介紹過一些相關的內容,這裏在簡單的說一說。
TEB(Thread Environment Block),線程環境塊,也就是說每個線程都會有TEB,用於保存系統與線程之間的數據,便於操做控制,經過理論篇述保護模式知識能夠本身分析一下,用戶態取fs寄存器的段描述符的BaseAddress拼接後地址爲TEB地址,之前的NT類系統上地址是固定的,每4KB是一個TEB,經過分解的段描述符,內存中是向下擴展。
PEB(Process Environment Block),進程環境塊,保存進程相關的信息,一樣每一個進程都是由本身的進程信息的。
獲取PEB有那些途徑?
一、fs寄存器偏移+0x30 PEB的地址
二、EPROCESS+0x1a8 PEB地址
以上偏移不是必定的根據環境而定,經過以上咱們兩種方式咱們在編程中就能夠輕易的找到PEB了。
圖片二十四中,PEB結構體中標紅了+0x00c偏移處,指向的是一個_PEB_LDR_DATA的結構體,以下所示:
kd> dt _PEB_LDR_DATA nt!_PEB_LDR_DATA +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr32 Void +0x00c InLoadOrderModuleList : _LIST_ENTRY +0x014 InMemoryOrderModuleList : _LIST_ENTRY +0x01c InInitializationOrderModuleList : _LIST_ENTRY +0x024 EntryInProgress : Ptr32 Void +0x028 ShutdownInProgress : UChar +0x02c ShutdownThreadId : Ptr32 Void
這個結構意味着什麼?其實就是包含有關進程的已加載模塊的信息。並且微軟給他標記了This structure may be altered in future versions of Windows,此結構可能會在Windows的將來版本中更改。咱們在windbg下(windwos7 32bit)與官網查詢到的結構體成員數量不同,以下所示:
typedef struct _PEB_LDR_DATA { BYTE Reserved1[8]; PVOID Reserved2[3]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
前兩個參數只給了一樣的介紹,Reserved for internal use by the operating system,供系統內部使用,而第三個參數則是一個雙向鏈表頭部,包含進程的已加載模塊。 列表中的每一個項目都是指向LDR_DATA_TABLE_ENTRY結構的指針。
在windbg下+0x00c,+0x014,+0x01c三個都是雙線鏈表有什麼不一樣呢?
InLoadOrderModuleList | InMemoryOrderModuleList | InInitializationOrderModuleList |
---|---|---|
模塊加載順序 | 模塊在內存中的順序 | 模塊初始化裝載順序 |
LDR_DATA_TABLE_ENTRY是怎樣一個雙向鏈表呢?以下所示:
圖片二十五:關聯
LDR_DATA_TABLE_ENTRY結構體,以下所示:
圖片二十六:LDR_DATA_TABLE_ENTRY
代碼中會用到如下屬性,簡單理解以下,其實一個驅動的加載過程這個結構體很重要:
DLLBase | FullDllName | BaseDllName |
---|---|---|
模塊基址 | 文件路徑 | 模塊名稱 |
彙編如何獲取呢?以下圖所示:
圖片二十七:獲取DLLBase
補充:上面一段彙編代碼,咱們經過fs獲取了PEB,經過PEB偏移+0x0C獲取_PEB_LDR_DATA,加上偏移+0x1c是InInitializationOrderModuleList爲雙向鏈表進行的遍歷。
接着獲取了字符串,而後經過Hash比對,注意模塊名稱存儲是寬字符,比對成功獲取DLLBase基地址,咱們能夠遍歷獲取想要的模塊基址如krnel32.dll等。
PE獲取:
PE如何用c++獲取導出表且遍歷,理論篇已給出完整代碼。彙編如何實現呢?對於標準的PE來
說,相對於基址偏移是必定的以下:
0x3c | 0x78 |
---|---|
PE標頭 | 導出目錄表的相對虛擬地址(RVA) |
以下圖所示:
圖片二十八:獲取Export Table
由於是彙編來實現操做,關鍵的步驟都寫到了註釋當中,下面貼上完整的彙編代碼,實現函數以下:
puGetModule | puGetProcAddress |
---|---|
獲取模塊基址,參數1:Hash值 | 獲取函數地址 參數1:模塊基址,參數2:Hash值 |
關於Hash值的算法,你們能夠逆向一下下面代碼中的彙編代碼,用c語言實現一下,貼出本代碼中測試使用的Hash值,以下:
0xec1c6278; kernel32.dll 0xc0d832c7; LoadlibraryExa 0x4FD18963; ExitPorcess 0x5644673D User32.dll 0x1E380A6A MessageBoxA 0x9EBC86B RtlExitUserProcess 0xF4E2F2C8 GetModuleHandleW 0xBB7420F9 CreateSolidBrush 0xBC05E48 RegisterClassW
puGetModule彙編代碼以下:
DWORD puGetModule(const DWORD Hash) { DWORD nDllBase = 0; __asm{ jmp start /*函數1:遍歷PEB_LDR_DATA鏈表HASH加密*/ GetModulVA: push ebp; mov ebp, esp; sub esp, 0x20; push edx; push ebx; push edi; push esi; mov ecx, 8; mov eax, 0CCCCCCCCh; lea edi, dword ptr[ebp - 0x20]; rep stos dword ptr es : [edi]; mov esi, dword ptr fs : [0x30]; mov esi, dword ptr[esi + 0x0C]; mov esi, dword ptr[esi + 0x1C]; tag_Modul: mov dword ptr[ebp - 0x8], esi; // 保存LDR_DATA_LIST_ENTRY mov ebx, dword ptr[esi + 0x20]; // DLL的名稱指針(應該指向一個字符串) mov eax, dword ptr[ebp + 0x8]; push eax; push ebx; // +0xC call HashModulVA; test eax, eax; jnz _ModulSucess; mov esi, dword ptr[ebp - 0x8]; mov esi, [esi]; // 遍歷下一個 LOOP tag_Modul _ModulSucess : mov esi, dword ptr[ebp - 0x8]; mov eax, dword ptr[esi + 0x8]; pop esi; pop edi; pop ebx; pop edx; mov esp, ebp; pop ebp; ret /*函數2:HASH解密算法(寬字符解密)*/ HashModulVA : push ebp; mov ebp, esp; sub esp, 0x04; mov dword ptr[ebp - 0x04], 0x00 push ebx; push ecx; push edx; push esi; // 獲取字符串開始計算 mov esi, [ebp + 0x8]; test esi, esi; jz tag_failuers; xor ecx, ecx; xor eax, eax; tag_loops: mov al, [esi + ecx]; // 獲取字節加密 test al, al; // 0則退出 jz tag_ends; mov ebx, [ebp - 0x04]; shl ebx, 0x19; mov edx, [ebp - 0x04]; shr edx, 0x07; or ebx, edx; add ebx, eax; mov[ebp - 0x4], ebx; inc ecx; inc ecx; jmp tag_loops; tag_ends: mov ebx, [ebp + 0x0C]; // 獲取HASH mov edx, [ebp - 0x04]; xor eax, eax; cmp ebx, edx; jne tag_failuers; mov eax, 1; jmp tag_funends; tag_failuers: mov eax, 0; tag_funends: pop esi; pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x08 start: /*主模塊*/ pushad; push Hash; call GetModulVA; add esp, 0x4 mov nDllBase, eax; popad; } return nDllBase; }
puGetProcAddress函數以下:
DWORD puGetProcAddress(const DWORD dllvalues, const DWORD Hash) { DWORD FunctionAddress = 0; __asm{ jmp start // 自定義函數計算Hash且對比返回正確的函數 GetHashFunVA: push ebp; mov ebp, esp; sub esp, 0x30; push edx; push ebx; push esi; push edi; lea edi, dword ptr[ebp - 0x30]; mov ecx, 12; mov eax, 0CCCCCCCCh; rep stos dword ptr es : [edi]; // 以上開闢棧幀操做(Debug版本模式) mov eax, [ebp + 0x8]; // ☆ kernel32.dll(MZ) mov dword ptr[ebp - 0x8], eax; mov ebx, [ebp + 0x0c]; // ☆ GetProcAddress Hash值 mov dword ptr[ebp - 0x0c], ebx; // 獲取PE頭與RVA及ENT mov edi, [eax + 0x3C]; // e_lfanew lea edi, [edi + eax]; // e_lfanew + MZ = PE mov dword ptr[ebp - 0x10], edi; // ☆ 保存PE(VA) // 獲取ENT mov edi, dword ptr[edi + 0x78]; // 獲取導出表RVA lea edi, dword ptr[edi + eax]; // 導出表VA mov[ebp - 0x14], edi; // ☆ 保存導出表VA // 獲取函數名稱數量 mov ebx, [edi + 0x18]; mov dword ptr[ebp - 0x18], ebx; // ☆ 保存函數名稱數量 // 獲取ENT mov ebx, [edi + 0x20]; // 獲取ENT(RVA) lea ebx, [eax + ebx]; // 獲取ENT(VA) mov dword ptr[ebp - 0x20], ebx; // ☆ 保存ENT(VA) // 遍歷ENT 解密哈希值對比字符串 mov edi, dword ptr[ebp - 0x18]; mov ecx, edi; xor esi, esi; mov edi, dword ptr[ebp - 0x8]; jmp _WHILE // 外層大循環 _WHILE : mov edx, dword ptr[ebp + 0x0c]; // HASH push edx; mov edx, dword ptr[ebx + esi * 4]; // 獲取第一個函數名稱的RVA lea edx, [edi + edx]; // 獲取一個函數名稱的VA地址 push edx; // ENT表中第一個字符串地址 call _STRCMP; cmp eax, 0; jnz _SUCESS; inc esi; LOOP _WHILE; jmp _ProgramEnd // 對比成功以後獲取循環次數(下標)cx保存下標數 _SUCESS : // 獲取EOT導出序號表內容 mov ecx, esi; mov ebx, dword ptr[ebp - 0x14]; mov esi, dword ptr[ebx + 0x24]; mov ebx, dword ptr[ebp - 0x8]; lea esi, [esi + ebx]; // 獲取EOT的VA xor edx, edx; mov dx, [esi + ecx * 2]; // 注意雙字 獲取序號 // 獲取EAT地址表RVA mov esi, dword ptr[ebp - 0x14]; // Export VA mov esi, [esi + 0x1C]; mov ebx, dword ptr[ebp - 0x8]; lea esi, [esi + ebx]; // 獲取EAT的VA mov eax, [esi + edx * 4]; // 返回值eax(GetProcess地址) lea eax, [eax + ebx]; jmp _ProgramEnd; _ProgramEnd: pop edi; pop esi; pop ebx; pop edx; mov esp, ebp; pop ebp; ret 0x8; // 循環對比HASH值 _STRCMP: push ebp; mov ebp, esp; sub esp, 0x04; mov dword ptr[ebp - 0x04], 0x00; push ebx; push ecx; push edx; push esi; // 獲取字符串開始計算 mov esi, [ebp + 0x8]; xor ecx, ecx; xor eax, eax; tag_loop: mov al, [esi + ecx]; // 獲取字節加密 test al, al; // 0則退出 jz tag_end; mov ebx, [ebp - 0x04]; shl ebx, 0x19; mov edx, [ebp - 0x04]; shr edx, 0x07; or ebx, edx; add ebx, eax; mov[ebp - 0x4], ebx; inc ecx; jmp tag_loop tag_end : mov ebx, [ebp + 0x0C]; // 獲取HASH mov edx, [ebp - 0x04]; xor eax, eax; cmp ebx, edx; jne tag_failuer; mov eax, 1; jmp tag_funend; tag_failuer: mov eax, 0; tag_funend: pop esi; pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x08 start: pushad; push Hash; // Hash加密的函數名稱 push dllvalues; // 模塊基址.dll call GetHashFunVA; // GetProcess mov FunctionAddress, eax; // ☆ 保存地址 popad; } return FunctionAddress; }