IAT隐藏与混淆

文摘   2023-12-29 15:41   广东  

简介

在我们的导入表中有一些PE相关的信息,比如我们写了一个loader,里面有申请内存的操作,或者有文件读取的操作,这些函数都会在导入表中出现,为了避免蓝队分析我们的二进制文件,所以我们必须将这些函数给他隐藏掉。

比如如下代码是一个APC注入的代码,里面用到了VirtualAlloc,memcpy,LoadLibraryA等函数。

我们来使用dumpbin来查看一下导入表,发现里面导入了我们这些代码中相关的函数。置于其他为什么有很多我们不知道的这些函数,这些函数是编译器自己添加的。

dumpbin.exe /IMPORTS ".exe"

混淆的一些方式

对于混淆的方式来说我们可以直接使用GetPorcAddress,GetModuleHanle,LoadLibrary这些函数来进行动态获取,这样的话我们的一些VirtualAlloc这一类的一些函数就不会出现在IAT表中了。

但是需要注意的是虽然这些函数不会出现在IAT表中,但是GetProcAddress,GetModuleHanle,LoadLibrary这些函数会出现在IAT表中。

所以我们可以自定义GetProcAddress和GetModuleHandle这两个函数。来达到IAT隐藏。

自定义GetProcAddress

在自定义GetProcAddress函数之前,我们需要知道它的原理是什么。

GetProcAddress的工作原理

GetProcAddress是一个Windows API函数,他有两个参数,一个是hmodule,另一个就是函数的名称了,hmodule参数表示是加载的DLL的基地址,之前我们说过DLL的基地址就是进程中加载这个DLL的地址,也就是说进程空间中找到DLL模块的基地址。

那么我们现在有一个思路就是通过遍历目标进程DLL内的导出函数来判断目标函数的名称来找到真正函数的地址,如果不存在的话返回NULL。

要访问导出的函数的地址,就需要访问DLL的导出表并循环查找目标的函数名称。

导出表结构
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory typedef struct _IMAGE_EXPORT_DIRECTORY {                  DWORD   Characteristics;        // 未使用          DWORD   TimeDateStamp;          // 时间戳          WORD    MajorVersion;           // 未使用          WORD    MinorVersion;           // 未使用          DWORD   Name;                   // 指向该导出表的文件名字符串RVA        DWORD   Base;                   // 导出函数的起始序号          DWORD   NumberOfFunctions;      // 所有导出函数的个数          DWORD   NumberOfNames;          // 以函数名字导出的函数个数          DWORD   AddressOfFunctions;     // 导出函数地址表RVA                  DWORD   AddressOfNames;         // 导出函数名称表RVA                  DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA               } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

看过前面基础的应该都知道如何去遍历导出表了。

实现

这里我们来一步一步实现。

首先我们肯定要通过GetModuleHandle函数来拿到DLL模块的基地址之后才能去操作。

拿到DLL基地址之后,我们将类型转换成PBYTE类型。

#include <iostream>#include <Windows.h>int main(){  HANDLE handle = GetModuleHandle(L"kernel32.dll");  PBYTE pBase = (PBYTE)handle;  getchar();}

可以看到这是典型PE结构 4d 5a

获取到基地址之后我们首先需要去获取DOS头。然后判断如果DOS头中的e_magic不等于5A4D的话,那么就返回NULL,这里IMAGE_DOS_SIGNATURE就是5A4D。

#include <iostream>#include <Windows.h>int main(){  HANDLE handle = GetModuleHandle(L"kernel32.dll");  PBYTE pBase = (PBYTE)handle;  PIMAGE_DOS_HEADER pdosHader = (PIMAGE_DOS_HEADER)pBase;  if(pdosHader->e_magic != IMAGE_DOS_SIGNATURE) {    return NULL;  }  getchar();}

获取DOS头之后接下来就是获取NT头了。NT头需要DOS头的e_lfanew属性加上基地址就可以了。然后判断如果NT头的Signature,其实就是它的一个标识如果不等于IMAGE_NT_SIGNATURE的话返回NULL。

#include <iostream>#include <Windows.h>int main(){
HANDLE handle = GetModuleHandle(L"kernel32.dll"); PBYTE pBase = (PBYTE)handle; PIMAGE_DOS_HEADER pdosHader = (PIMAGE_DOS_HEADER)pBase; if(pdosHader->e_magic != IMAGE_DOS_SIGNATURE) { return NULL; }
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pdosHader->e_lfanew); if (pImageNtHeaders->Signature !=IMAGE_NT_SIGNATURE) { return NULL; } getchar();}

其实这里判断就是是否是4550。

获取到NT头之后,我们都知道NT头中包含了标准PE头和可选PE头。

接下来我们需要去获取到可选PE头。

IMAGE_OPTIONAL_HEADER imageoptiopn =  pImageNtHeaders->OptionalHeader;

如上图可选PE头中有很多属性。

如下为可选PE头的解释。

WORD Magic 说明文件类型,如果是32位下的PE文件它的值是10B,如果是64位下的PE文件他的值是20B(重点)SectionAlignment 内存对齐 1000HFileAlignment 文件对齐 200HDWORD SizeOfCode  所有代码节的和,必须是FileAlignment的整数倍,编辑器填的(这里指的是PE结构分很多节,比如说我其中有一个节中有10字节的代码,那么就是10 * FileAlignment 也就是文件对齐,一般文件对齐的话都是200H,而内存对齐的是话是1000H,所以它存储的值就是1000)DWORD SizeOfInitializedData 已初始化数据大小的和,必须是FileAlignment的整数倍 编辑器填的。DWORD SizeOfUninitializedData 未初始化数据大小的和,必须是FileAlignment的整数倍 编辑器填的。
AddressOfEntryPoint 程序入口点(重点) 程序入口点 + 内存镜像基址 (我们可以发现在使用OD或者xdebg的时候它断点断的那个位置就是程序入口点这个值 + 内存镜像基址)//注意:一个exe文件由多个PE文件组成,比如说dll文件,每一个dll都是一个模块。DWORD BaseOfCode 代码开始的基址 编辑器填的 没用DWORD BaseOfData 数据开始的基址 编译器填的 没用ImageBase 内存镜像DWORD SizeOfImage 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍。DWORD SizeOfHeaders 所有头+节表按照文件对齐后的大小,否则加载会出错。DWORD CheckSum 校验和,一些系统文件有要求,用来判断文件是否被修改。DWORD SizeOfStackReserve; 初始化时保留的堆栈大小DWORD SizeOfStackCommit; 初始化时实际提交的大小DWORD SizeOfHeapReserve; 初始化时保留堆的大小DWORD SizeOfHeapCommit; 初始化时实际提交堆的大小DWORD NumberOfRvaAndSizes; 目录项数(比如说存储了一个0x10,那么就表示在他后面还有10个结构。)DWORD _IMAGE_DATA_DIRECTORY DataDirectory[16];

在可选PE头中的最后一项就是我们的表目录了,这样的话就可以通过OptionalHeader(可选PE头)的DataDirectory第0个来定位到导出表的VirtualAddress地址了。

这里的IMAGE_DIRECTORY_ENTRY_EXPORT其实就是0.

#include <iostream>#include <Windows.h>int main(){
HANDLE handle = GetModuleHandle(L"kernel32.dll"); PBYTE pBase = (PBYTE)handle; PIMAGE_DOS_HEADER pdosHader = (PIMAGE_DOS_HEADER)pBase; if(pdosHader->e_magic != IMAGE_DOS_SIGNATURE) { return NULL; }
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pdosHader->e_lfanew); if (pImageNtHeaders->Signature !=IMAGE_NT_SIGNATURE) { return NULL; } IMAGE_OPTIONAL_HEADER imageoptiopn = pImageNtHeaders->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pimageexport = (PIMAGE_EXPORT_DIRECTORY)(pBase + imageoptiopn.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
getchar();}

如上图的地址指向的就是导出表的地址。

获取到导出表的地址之后,然后去获取导出表中的函数名称但是存储的是内存地址,也就是这个地址指向的是函数的名称。

PDWORD FunctionNameArray = (PDWORD)(pBase + pimageexport->AddressOfNames);

紧接着来获取序号表:

PWORD ordinArray = (PWORD)(pBase + pimageexport->AddressOfNameOrdinals);

紧接着使用for循环来进行获取,这里的次数是由导出表的NumberOfFunctions来决定的,也就是所有导出函数的个数。

然后通过pBase加上FunctionNames中函数的RVA地址来获取到内存中函数执行的真实位置最后强制转换成char类型。

#include <iostream>#include <Windows.h>#include <winternl.h>
PVOID GetProcAddressTest(HMODULE handle, LPCSTR Name) {
PBYTE pBase = (PBYTE)handle; PIMAGE_DOS_HEADER pdosHader = (PIMAGE_DOS_HEADER)pBase; if (pdosHader->e_magic != IMAGE_DOS_SIGNATURE) { return NULL; }
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pdosHader->e_lfanew); if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) { return NULL; } IMAGE_OPTIONAL_HEADER imageoptiopn = pImageNtHeaders->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pimageexport = (PIMAGE_EXPORT_DIRECTORY)(pBase + imageoptiopn.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD FunctionNameArray = (PDWORD)(pBase + pimageexport->AddressOfNames);

PDWORD FunctionAddressArray = (PDWORD)(pBase + pimageexport->AddressOfFunctions);
PWORD ordinArray = (PWORD)(pBase + pimageexport->AddressOfNameOrdinals);
for (DWORD i = 0; i < pimageexport->NumberOfFunctions; i++) { CHAR* pFunctionName = (CHAR*)(pBase + FunctionAddressArray[i]); PVOID functionAddress = (PVOID)(pBase + FunctionAddressArray[ordinArray[i]]);
} return NULL;}int main(){

}

然后使用同样的方式去获取到函数的地址。

PVOID functionAddress = (PVOID)(pBase + FunctionAddressArray[ordinArray[i]]);

接下来我们直接使用字符串比较函数去比较就行了,如果对比成功的话那么我们直接返回相应的地址就可以了,否则返回NULL。

if (strcmp(Name,pFunctionName) == 0) {      return functionAddress;}

这样我们就可以封装成一个函数了如下完整代码:

#include <iostream>#include <Windows.h>#include <winternl.h>
PVOID GetProcAddressTest(HMODULE handle, LPCSTR Name) {
PBYTE pBase = (PBYTE)handle; PIMAGE_DOS_HEADER pdosHader = (PIMAGE_DOS_HEADER)pBase; if (pdosHader->e_magic != IMAGE_DOS_SIGNATURE) { return NULL; }
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pdosHader->e_lfanew); if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) { return NULL; } IMAGE_OPTIONAL_HEADER imageoptiopn = pImageNtHeaders->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pimageexport = (PIMAGE_EXPORT_DIRECTORY)(pBase + imageoptiopn.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD FunctionNameArray = (PDWORD)(pBase + pimageexport->AddressOfNames);

PDWORD FunctionAddressArray = (PDWORD)(pBase + pimageexport->AddressOfFunctions);
PWORD ordinArray = (PWORD)(pBase + pimageexport->AddressOfNameOrdinals);
for (DWORD i = 0; i < pimageexport->NumberOfFunctions; i++) { CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]); PVOID functionAddress = (PVOID)(pBase + FunctionAddressArray[ordinArray[i]]);
if (strcmp(Name, pFunctionName) == 0) { return functionAddress; }

} return NULL;}int main(){
printf("使用原始的GetProcAddress : 0x%p \n", GetProcAddress(GetModuleHandleA("NTDLL.DLL"), "NtAllocateVirtualMemory")); printf("使用自定义的GetProcAddressTest : 0x%p \n", GetProcAddressTest(GetModuleHandleA("NTDLL.DLL"), "NtAllocateVirtualMemory"));

getchar();
}

测试:

自定义GetModuleHandle

GetModuleHandle函数用于检索指定的DLL句柄,这个函数会返回DLL的句柄,如果调用进程中不存在你要获取的DLL,他将会返回NULL。

GetModuleHandle工作原理

它返回的类型是HANDLE类型,HANDLE数据类型是加载DLL的基地址,也就是这个DLL在进程的地址空间中所处的位置,因此替换函数的目标就是检索指定DLL的基地址。PEB中包含有关加载的DLL的信息,特别是PEB_LDR_DATA Ldr结构。

实现

首先我们需要去检索PEB,他会根据你VS使用X86或x64运行来进行自动切换。

#ifdef _WIN64     PPEB pPeb = (PEB*)(__readgsqword(0x60));#elif _WIN32    PPEB pPeb = (PEB*)(__readfsdword(0x30));#endif

获取到PEB结构之后,从PEB结构中去获取PEB_LDR_DATA Ldr成员,PEB_LDR_DATA Ldr结构如下:

typedef struct _PEB_LDR_DATA {  BYTE       Reserved1[8];  PVOID      Reserved2[3];  LIST_ENTRY InMemoryOrderModuleList;} PEB_LDR_DATA, *PPEB_LDR_DATA;

这个结构中我们主要关注的是LIST_ENTRY  InMemoryOrderModuleList成员,它也是一个结构体:

LIST_ENTRY结构如下: 它是一个双向链表,本质上其实和数组是相同的。

typedef struct _LIST_ENTRY {  struct _LIST_ENTRY *Flink;  struct _LIST_ENTRY *Blink;} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

双链接列表分别使用Flink和Blink元素作为头部和尾指针。这意味着Flink指向列表中的下一个节点,而Blink元素指向列表中的前一个节点。这些指针用于在两个方向上遍历链表。了解了这一点,要开始枚举这个列表,应该首先访问它的第一个元素,InMemoryOrderModuleList.Flink。根据微软对内存或模块列表成员的定义,它声明列表中的每个项目都是一个指向LDR_DATA_TABLE_ENTRY结构的指针。

LDR_DATA_TABLE_ENTRY结构:

LDR_DATA_TABLE_ENTRY结构表示进程加载DLL链接列表中的DLL。每个LDR_DATA_TABLE_ENTRY都代表一个唯一的DLL。

我们来获取Ldr成员:

PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);

获取到Ldr之后我们来查找双向链表中的第一个元素。

//获取链表中包含关于第一给模块信息的第一个元素。 PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink); //由于每个pDte在链表中都代表一个唯一的DLL,所以可以使用以下一行代码访问下一个元素。 pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);

如下代码:

#include <iostream>#include <Windows.h>#include <winternl.h>HMODULE GetModuleHandleRelaysec(IN LPCWSTR szModuleName) {    //获取PEB结构#ifdef _WIN64     PPEB pPeb = (PEB*)(__readgsqword(0x60));#elif _WIN32    PPEB pPeb = (PEB*)(__readfsdword(0x30));#endif
//获取Ldr PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
//获取链表中包含关于第一给模块信息的第一个元素。 PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
//由于每个pDte在链表中都代表一个唯一的DLL,所以可以使用以下一行代码访问下一个元素。 pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);}int main(){ std::cout << "Hello World!\n";}

遍历DLL名称:

#include <iostream>#include <Windows.h>#include <winternl.h>HMODULE GetModuleHandleRelaysec(IN LPCWSTR szModuleName) {    //获取PEB结构#ifdef _WIN64     PPEB pPeb = (PEB*)(__readgsqword(0x60));#elif _WIN32    PPEB pPeb = (PEB*)(__readfsdword(0x30));#endif
//获取Ldr PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
//获取链表中包含关于第一给模块信息的第一个元素。 PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
//由于每个pDte在链表中都代表一个唯一的DLL,所以可以使用以下一行代码访问下一个元素。 while (pDte) { if (pDte->FullDllName.Length !=NULL) { wprintf(L" \"%s\"\n", pDte->FullDllName.Buffer); } else { break; } pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte); } return NULL;}int main(){ GetModuleHandleRelaysec(L"kernel32.dll");}

获取DLL的基地址:

获取DLL基地址需要引用LDR_DATA_TABLE_ENTRY结构,如下:

  struct LDR_DATA_TABLE_ENTRYtypedef struct _LDR_DATA_TABLE_ENTRY{     LIST_ENTRY InLoadOrderLinks;     LIST_ENTRY InMemoryOrderLinks;     LIST_ENTRY InInitializationOrderLinks;     PVOID DllBase;     PVOID EntryPoint;     ULONG SizeOfImage;     UNICODE_STRING FullDllName;     UNICODE_STRING BaseDllName;     ULONG Flags;     WORD LoadCount;     WORD TlsIndex;     union     {          LIST_ENTRY HashLinks;          struct          {               PVOID SectionPointer;               ULONG CheckSum;          };     };     union     {          ULONG TimeDateStamp;          PVOID LoadedImports;     };     _ACTIVATION_CONTEXT * EntryPointActivationContext;     PVOID PatchInformation;     LIST_ENTRY ForwarderLinks;     LIST_ENTRY ServiceTagLinks;     LIST_ENTRY StaticLinks;} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

DLL的基地址是InInitializationOrderLinks.Flink。

完整代码:

BOOL StringEq(IN LPCWSTR s1, IN LPCWSTR s2) {
WCHAR lStr1[MAX_PATH], lStr2[MAX_PATH];
int len1 = lstrlenW(s1), len2 = lstrlenW(s2);
int i = 0, j = 0;
if (len1 >= MAX_PATH || len2 >= MAX_PATH) return FALSE;
for (i = 0; i < len1; i++) { lStr1[i] = (WCHAR)tolower(s1[i]); } lStr1[i++] = L'\0';

for (j = 0; j < len2; j++) { lStr2[j] = (WCHAR)tolower(s2[j]); } lStr2[j++] = L'\0';

if (lstrcmpiW(lStr1, lStr2) == 0) return TRUE;
return FALSE;}HMODULE GetModuleHandleRelaysec(IN LPCWSTR szModuleName) { //获取PEB结构
#ifdef _WIN64 PPEB pPeb = (PEB*)(__readgsqword(0x60));#elif _WIN32 PPEB pPeb = (PEB*)(__readfsdword(0x30));#endif
//获取Ldr PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
//获取链表中包含关于第一给模块信息的第一个元素。 PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
//由于每个pDte在链表中都代表一个唯一的DLL,所以可以使用以下一行代码访问下一个元素。
while (pDte) { if (pDte->FullDllName.Length != NULL) { if (StringEq(pDte->FullDllName.Buffer, szModuleName)) { #ifdef STRUCTS return (HMODULE)(pDte->InMemoryOrderLinks.Flink);#else return (HMODULE)(pDte->Reserved2[0]);#endif
} } else { break; } pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte); } return NULL;
}

moonsec
暗月博客
 最新文章