接上文:https://www.dabaitulm.com/nxhb/1065.html
当我们获取到OPTIONAL_HEADER时,我们就可以拿到第一个需要的东西,也就是DLL的程序入口点,它就是OPTIONAL_HEADER中的AddressOfEntryPoint项。接下来,我们需要从数据目录表中找我们需要的东西,包括重定位表,导入表以及TLS(线程本地存储, Thread Local Storage),当然TLS其实并不是必须的,因为并非所有的DLL都会使用TLS,但为了能写出一个通用的注入器,我们需要考虑TLS的存在。
重定位表为 DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]也就是DataDirectory[5],通过重定位表里的内容判断DLL是否重定位,如果重定位的话需要确认重定位的位置,并且需要定位到对应的重定位项,以下的代码为一个示例:
#define RELOC_FLAG32(RelInfo) ((RelInfo) >> 0x0c == IMAGE_REL_BASED_HIGHLOW)
#define RELOC_FLAG64(RelInfo) ((RelInfo) >> 0x0c == IMAGE_REL_BASED_DIR64)
#ifdef _WIN64
#define RELOC_FLAG RELOC_FLAG64
#else
#define RELOC_FLAG RELOC_FLAG32
#endif
BYTE* LocationDelta = pBase - pOpt->ImageBase;
// 看dll是否重定位
if (LocationDelta) {
if (!pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size) {
return;
}
// 获取重定位表
/* 重定位表
typedef struct _IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; //重定位数据开始的RVA 地址
DWORD SizeOfBlock; //重定位块的长度,标识重定向字段个数
// WORD TypeOffset[]; //重定位项数组相对虚拟RVA, 个数动态分配
}IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
*/
IMAGE_BASE_RELOCATION* pRelocData = (IMAGE_BASE_RELOCATION*)
(pBase + pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (pRelocData->VirtualAddress) {
// 获取重定位项的数量(也就是重定位表的最后一部分,实际上是一个WORD数组)
UINT AmountOfEntries = (pRelocData->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
// 向后偏移一个重定位项
WORD* pRelativeInfo = (WORD*)(pRelocData + 1);
for (UINT i = 0; i < AmountOfEntries; ++i, ++pRelativeInfo) {
// 判断是否为重定位项
if (RELOC_FLAG(*pRelativeInfo)) {
// 定位到当前的重定位项
UINT_PTR* pPatch = (UINT_PTR*)(pBase + pRelocData->VirtualAddress + ((*pRelativeInfo) & 0xfff));
*pPatch += (UINT_PTR)LocationDelta;
}
}
pRelocData = (IMAGE_BASE_RELOCATION*)(((BYTE*)pRelocData) + pRelocData->SizeOfBlock);
}
}
接下来是导入表,导入表是非常关键的,通过导入表能否知道我们要注入的DLL具体要导入哪些外部函数,不过考虑到可能会出现没有导入外部函数的可能,所以需要先进行判断,以下为导入表部分的示例,这个示例使用了LoadLibraryA和GetProcAddress获取外部DLL的函数地址,也就是说在这个示例中并没有完全代替使用Windows API,但是对于某些反作弊,做到这部分就已经足够了,当然,之后会给出GetProcAddress的替代方案
if (pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size)
{
/* 导入表基址是PE文件从其他三方程序中导入API,以供本程序调用的机制
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//指向输入名称表的表(INT)的RVA
};
DWORD TimeDateStamp;// 映象绑定前,这个值是0,绑定后是导入模块的时间戳。
DWORD ForwarderChain;//转发链,如果没有转发器,这个值是-1。
DWORD Name;//指向导入映像文件的名称
DWORD FirstThunk;//指向输入地址表的表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
*/
IMAGE_IMPORT_DESCRIPTOR* pImportDescr = (IMAGE_IMPORT_DESCRIPTOR*)
(pBase + pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 将所有要导入的外部dll导入,比如ntdll.dll
while (pImportDescr->Name)
{
char* szMod = (char*)(pBase + pImportDescr->Name);
HINSTANCE hDll = _LoadLibraryA(szMod);
UINT_PTR* pThunkRef = (UINT_PTR*)(pBase + pImportDescr->OriginalFirstThunk);
UINT_PTR* pFuncRef = (UINT_PTR*)(pBase + pImportDescr->FirstThunk);
if (!pThunkRef) {
pThunkRef = pFuncRef;
}
for (;*pThunkRef; ++pThunkRef, ++pFuncRef)
{
// 判断是否按序号导入,不按序号就按名称导入(导入所有的外部函数)
if (IMAGE_SNAP_BY_ORDINAL(*pThunkRef)) {
*pFuncRef = _GetProcAddress(hDll, (char*)(*pThunkRef & 0xffff));
}
else {
IMAGE_IMPORT_BY_NAME* pImport = (IMAGE_IMPORT_BY_NAME*)(pBase + (*pThunkRef));
*pFuncRef = _GetProcAddress(hDll, pImport->Name);
}
}
++pImportDescr;
}
}
接下来是TLS,当然我们也需要最先判断DLL是否启用了TLS,以下是代码示例
if (pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].Size)
{
IMAGE_TLS_DIRECTORY* pTLS = (IMAGE_TLS_DIRECTORY*)
(pBase + pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
PIMAGE_TLS_CALLBACK* pCallback = (PIMAGE_TLS_CALLBACK*)(pTLS->AddressOfCallBacks);
// 调用所有的TLS回调函数
/*
TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,创建进程的主线程时也会自动调用回调函数,
且其调用执行先于EP代码,反调试技术利用的就是TLS回调函数的这一特征
*/
for (;pCallback && *pCallback; ++pCallback) {
(*pCallback)(pBase, DLL_PROCESS_ATTACH, nullptr);
}
}
完成这些步骤后,我们可以执行DllMain函数,这个函数的地址就是之前获取到的DLL入口点,这个函数有三个参数,第一个为DLL本身的地址,也就是DOS头的地址,第二个是CALL DllMain的原因,这里我们使用DLL_PROCESS_ATTACH,也就是需要将DLL附加到进程
using f_DLL_ENTRY_POINT = BOOL(WINAPI*)(void* hDll, DWORD dwReadson, void* pReserved);
_DllMain(pBase, DLL_PROCESS_ATTACH, nullptr);
至此,Dll的初始化步骤就已经完成,不过,这种注入方式不适用于所有的进程以及所有的DLL,而且,就算使用这种办法将DLL注入进程并且没有被检测到,也可能会被扫内存模块扫描到异常的模块,所以单在注入环节下功夫是不够的。