在我们的导入表中有一些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是一个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 内存对齐 1000H
FileAlignment 文件对齐 200H
DWORD 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函数用于检索指定的DLL句柄,这个函数会返回DLL的句柄,如果调用进程中不存在你要获取的DLL,他将会返回NULL。
它返回的类型是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_ENTRY
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;
};
_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;
}