模块隐藏那节课要求完成两个作业,都是隐藏模块,本文介绍两种方法分别如何实现。
方法一:往自己的进程注入游戏主模块
这个题目的意思是将程序的基址设置成高地址,将0x400000空出来,然后将游戏主模块拉伸,修复IAT之后,注入进去。这样做的好处主要是简单,因为不需要修复重定位表了。缺点也很明显,很多时候根本没办法在0x400000处申请足够大的内存容纳游戏程序。,还有就是有些程序跳转到OEP之后会卡住,具体原因我也不知道。我这里用dbgview.exe测试,能够正常启动,这足够说明我的代码没有大的错误。
任务管理器中只会看到控制台的进程,看不到dbgview的进程:
所谓修复IAT,就是指,将游戏PE的IAT表修复成函数地址,做法也很简单,遍历IAT表,loadlibary获取dll句柄,然后遍历函数名或函数序号,一个个getprocaddress,将获取到的函数地址写入到IAT表就算修复好了。
下面是我的代码,我只给出关键的两个函数,一个是修复IAT表,一个是注入的函数,完整代码放在文章最后。
注入代码
// 将游戏模块注入到我的进程
// 修改ImageBase=0x20000000,使本程序在高地址运行
// 这种方式非常不推荐,因为游戏程序只要稍微大一些,就很可能无法在0x400000处申请内存了
void GameInjectMe()
{
if ((DWORD)GetModuleHandle(NULL) != (DWORD)0x20000000)
{
printf("当前进程句柄: 0x%X\n", (DWORD)GetModuleHandle(NULL));
printf("请修改链接设置,将ImageBase设置为0x20000000\n");
return;
}
// 读取游戏PE,拉伸,修复IAT,写入到其ImageBase处
LPVOID pFileBuffer = NULL;
//FileToMemory(TEXT("C:\\Documents and Settings\\nixiang\\桌面\\DTDebug(VT-O)专业版V1.0.025\\DTDebug\\DTDebug.exe"), &pFileBuffer);
//FileToMemory(TEXT("c:\\program32\\Helloworld.exe"), &pFileBuffer);
FileToMemory(TEXT("c:\\program32\\dbgview.exe"), &pFileBuffer);
LPVOID pImageBuffer = NULL;
DWORD dwImageSize = FileBufferToImageBuffer(pFileBuffer, &pImageBuffer);
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
LPVOID pImageBase = VirtualAlloc((LPVOID)pOptionHeader->ImageBase, dwImageSize,MEM_COMMIT, PAGE_READWRITE);
if (pImageBase != (LPVOID)pOptionHeader->ImageBase)
{
printf("错误码:0x%X 在ImageBase(0x%X)处VirtualAlloc失败\n",GetLastError(),pOptionHeader->ImageBase);
return;
}
// 修复IAT表
RepairIAT(pImageBuffer);
memcpy(pImageBase, pImageBuffer, dwImageSize);
// 跳转到游戏的入口点
DWORD dwOEP = pOptionHeader->AddressOfEntryPoint + pOptionHeader->ImageBase;
__asm{
jmp dwOEP
}
}
修复IAT代码
// 传入一个imagebuffer,修复它的IAT表
void RepairIAT(LPVOID pImageBuffer)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)(pDosHeader->e_lfanew + (DWORD)pDosHeader + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
// PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + \
// RvaToFoa(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress));
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pImageBuffer + \
pOptionHeader->DataDirectory[1].VirtualAddress);
// 严格来说应该是 sizeof(IMAGE_IMPORT_DESCRIPTOR) 个字节为0表示结束
while (pImportTable->OriginalFirstThunk || pImportTable->FirstThunk)
{
// 打印模块名
//printf("%s\n", (LPCSTR)(pImportTable->Name + (DWORD)pImageBuffer));
// 获取模块句柄
HMODULE hModule = LoadLibraryA((LPCSTR)(pImportTable->Name + (DWORD)pImageBuffer));
if (NULL == hModule)
{
printf("获取模块句柄失败,模块名: %s\n",(LPCSTR)(pImportTable->Name + (DWORD)pImageBuffer));
}
// 修复IAT表
//printf("--------------FirstThunkRVA:%x--------------\n", pImportTable->FirstThunk);
PIMAGE_THUNK_DATA32 pThunkData = (PIMAGE_THUNK_DATA32)((DWORD)pImageBuffer + \
pImportTable->FirstThunk);
while (*((PDWORD)pThunkData) != 0)
{
// IMAGE_THUNK_DATA32 是一个4字节数据
// 如果最高位是1,那么除去最高位就是导出序号
// 如果最高位是0,那么这个值是RVA 指向 IMAGE_IMPORT_BY_NAME
if ((*((PDWORD)pThunkData) & 0x80000000) == 0x80000000)
{
//printf("按序号导入 Ordinal:%04x\n", (*((PDWORD)pThunkData) & 0x7FFFFFFF));
DWORD dwProcAddress = (DWORD)GetProcAddress(hModule,MAKEINTRESOURCE((*((PDWORD)pThunkData) & 0x7FFFFFFF)));
*((PDWORD)pThunkData) = dwProcAddress;
}
else
{
PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)(*((PDWORD)pThunkData) + \
(DWORD)pImageBuffer);
//printf("按名字导入 Hint:%04x Name:%s\n", pIBN->Hint, pIBN->Name);
DWORD dwProcAddress = (DWORD)GetProcAddress(hModule,(LPCSTR)pIBN->Name);
*((PDWORD)pThunkData) = dwProcAddress;
}
pThunkData++;
}
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pImportTable + sizeof(IMAGE_IMPORT_DESCRIPTOR));
}
}
其实方法一的局限性我在博客开头已经分析过了,不过我想了一下发现,其实问题关键就是0x400000处可能会申请内存失败,实际上我们不需要非得在这个地方申请内存,而是可以在任意地方申请,然后根据重定位表,修复地址即可。而这种方式,在方法二里面用到了。
以上是第一种方法的做法,下面是第二种做法,往游戏里注入自己,然后跳转到入口函数:
方法二:往游戏进程注入自己的主模块
这种方式不需要手工修复IAT,因为自身进程启动时操作系统帮我们修复了,我们只需要修复重定位表即可。
原理上图已经解释的非常明白了,下面给出关键代码:
重定位表修复代码
// 修改 ImageBase 并修复重定位表
// 内存镜像版本
VOID SetNewImageBase2(LPVOID pImageBuffer, DWORD dwNewImageBase)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)(pDosHeader->e_lfanew + (DWORD)pDosHeader + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
PIMAGE_BASE_RELOCATION pRelocationTable = (PIMAGE_BASE_RELOCATION)((DWORD)pImageBuffer + \
pOptionHeader->DataDirectory[5].VirtualAddress);
DWORD dwImageBaseDelta = dwNewImageBase - pOptionHeader->ImageBase; // 新旧ImageBase 的差值
// 重定位表的 VirtualAddress + 低12位偏移 = RVA
// RVA + ImageBase 这个内存里存储了一个“指针”
// 要修改的是这个“指针”的值,要让这个“指针”加上两个ImageBase的差值
while (pRelocationTable->VirtualAddress || pRelocationTable->SizeOfBlock)
{
size_t n = (pRelocationTable->SizeOfBlock - 8) / 2; // 可能需要修改的地址数量(高4位==0011才要修改)
PWORD pOffset = (PWORD)((DWORD)pRelocationTable + 8); // 2字节偏移的数组
for (size_t i = 0; i < n; i++)
{
// 高4位等于0011才需要重定位
if ((pOffset[i] & 0xF000) == 0x3000)
{
// 计算需要重定位的数据的RVA地址
DWORD dwRva = pRelocationTable->VirtualAddress + (pOffset[i] & 0x0FFF);
// 计算在镜像中的地址
PDWORD pData = (PDWORD)((DWORD)pImageBuffer + dwRva);
// 重定位,即修正写死的地址
*pData &#