您好,登錄后才能下訂單哦!
?豬年送安康,祝大家新一年健康、快樂。愿大家都做一個勤奮努力、真誠奉獻的人,幸運會永遠的眷顧你們。
?
引子:
?某一天饒有興趣在卡飯上瀏覽著帖子,故事的相遇就那么簡單。當時一條評論勾起我的好奇心,那么好逆向開始。
?根據我的習慣,拿到樣本我會線上惡意代碼分析,直接拉到virustotal之類的網站上,看看是否已經被大多數殺毒軟件所能識別,看一些有價值的數據,如下圖所示:
??????????????????圖片一:基本信息
?當看到這個頁面時候,看到最后的分析日期是18年11月,又看了一下導出表的函數信息,是一款老病毒。根據各大廠商對這個病毒行為特性、分析定位為特洛伊、偽裝等,定位不一很正常......,其實興趣降低了一大半,并不是新鮮品種,但不能這樣侮辱一個病毒!接著習慣性拉入到IDA中,當我看到熟悉的匯編之后,如下圖所示:
??????????????????圖片二:GetProcAddress實現
?當點進去其中的一個函數,看到了fs寄存器,且一大堆比較復雜的操作,看到熟悉的匯編指令以后,心中已有定數,這是一個自己實現的GetProcAddress函數。
?
?
理論篇 | 匯編篇 |
---|---|
保護模式,定時器,PE雜談 | 手動實現GetProcAddress函數及Hash加密字符比對 |
?
一、理論篇
?先來看病毒樣本中的一段代碼,如下圖所示:
??????????????????圖片三: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寄存器
?段描述符如何來分解?段描述符都有那些屬性呢?如下圖所示:
??????????????????圖片九:通用描述符
?
介紹一些主要屬性:
?
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加密字符串進行與名稱表進行對比,理論知識先告一段落。
?
?
二、匯編篇:
?通過理論篇的閱讀,熟悉了如何使用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有那些途徑?
1、fs寄存器偏移+0x30 PEB的地址
2、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;
}
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。