前言
之前已经实现了我们可以把dll手工模拟加载到内存,让dll在自己的进程的内存里执行,只需要定位到程序的入口点OEP的地址(DLL的OEP是DllMain函数的地址)然后去执行即可;如果是定位导出函数,也只需要通过导出表来定位函数地址即可(通过导出函数名称得到导出序号,根据导出序号从导出地址表里获取导出函数地址)。
如果想让自己加载的DLL注入到其他进程里也能执行,那么就需要进行一些处理才行
PE文件在内存里能执行主要是PE文件在内存里展开后,对重定位表和导入表进行了修正,修正重定位表是将涉及到模块基址的部分进行重定位,修正导入表则是对导入函数地址的重定位
因为修正重定位表和导入表的操作需要在dll注入到其他进程里了之后才能进行(依赖于注入的地址和系统dll的模块基址),所以通过shellcode来实现是不错的
这里以x86为例介绍本人对该目标的探索
实现目标及步骤
目标:将完整的dll注入到目标进程内并执行
步骤:
- 将dll展开到内存
- 编写shellcode修正dll的导入表和重定位表
- 注入shellcode和dll到目标内存执行
这里重点探究的是步骤2和3;如何使用shellcode去修正dll的导入表和重定位表,对于其他内容这里不做展开
具体实现
这里使用C语言编写shellcode,具体如何使用C写shellcode这里不进行展开,下文使用的
pfn
开头的函数一律是动态获取函数地址得到的函数
获取shellcode地址定位(自定位)
// 获取shellcode首地址
LPVOID lpShellcode = NULL;
// 获取DLL内存首地址,用于后续修复操作和跳转
LPVOID lpBaseAddress = NULL;
__asm {
__emit 0e8h;
__emit 00h;
__emit 00h;
__emit 00h;
__emit 00h;
pop eax;
mov dword ptr[lpShellcode], eax;
add eax, 03edh; //等写完代码回来修改,硬编码让这个地址指向dll内存首地址
mov dword ptr[lpBaseAddress], eax;
}
定位PE结构
// ==============0.定位dll PE结构=============
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((SIZE_T)lpBaseAddress + pDos->e_lfanew);
PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((SIZE_T)lpBaseAddress +
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)((SIZE_T)lpBaseAddress +
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
修正重定位表
// ==============1.修复重定位表=============
// 判断是否有重定位表,数据目录表项不存在的时候,VirtualAddress为0,也就是指向映像基址
if ((LPVOID)pLoc != lpBaseAddress) {
// 开始扫描重定位表,重定位表VirtualAddress和SizeOfBlock都为0表示重定位表结束(最后一项是全0)
while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) {
// 数据目录表里的VirtualAddress指向对应表的虚拟地址开头,重定位数据位于重定位表8字节后
PWORD pLocData = (PWORD)((SIZE_T)pLoc + sizeof(IMAGE_BASE_RELOCATION));
// 重定位数据数量,重定位表大小减掉头部8字节,剩下的内容每2字节都是一个重定位数据
SIZE_T dwNumOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (SIZE_T i = 0; i < dwNumOfReloc; i++) {
// 判断重定位类型,x86如果前4位是3就需要修正
if ((WORD)(pLocData[i] & 0xF000) == 0x3000) {
// 需要修正的数据
// 重定位表记录的是硬编码的地址,需要转换成当前内存基地址
// 需要重定位的地址 = 基地址 + 重定位偏移(于基地址的偏移) + 重定位数据偏移(二级偏移)
PSIZE_T pAddress = (PSIZE_T)((SIZE_T)lpBaseAddress + (pLoc->VirtualAddress) + (pLocData[i] & 0x0FFF));
// 需要重定位的数据 = 硬编码地址 - 硬编码映像地址 + 真实映像地址
*pAddress = *pAddress - pNt->OptionalHeader.ImageBase + (SIZE_T)lpBaseAddress;
}
}
// 移动到下一个重定位区段进行处理,每个区段都有对应一个重定位表进行修复使用
pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);
}
}
修正导入表
// ==============2.修复导入表=============
// 每个存在导入函数的DLL都会有一个导入表,导入表大小是固定的
// 遍历导入表中的DLL,获取导入表中的函数地址
while (pImp->OriginalFirstThunk != 0) { // 最后一个项为全0,退出
// 获取导入表对应的DLL名称
char* lpDllName = (char*)((SIZE_T)lpBaseAddress + pImp->Name);
// 获取DLL模块基地址, 如果已经加载了就直接获取,不然就手动加载,加载不上就跳过
HMODULE hDll = pfnGetModuleHandleA(lpDllName);
if (hDll == NULL) {
hDll = pfnLoadLibraryExA(lpDllName, NULL, NULL);
if (hDll == NULL) {
pImp++;
continue;
}
}
// 导入名称表地址, 通过导入名称表获取导入函数名称或序号获取其地址
PIMAGE_THUNK_DATA lpImpName = (PIMAGE_THUNK_DATA)(pImp->OriginalFirstThunk + (SIZE_T)lpBaseAddress);
// 导入地址表地址, 将获取的地址填入对应的导入地址表位置
PIMAGE_THUNK_DATA lpImpAddr = (PIMAGE_THUNK_DATA)(pImp->FirstThunk + (SIZE_T)lpBaseAddress);
// 开始修复导入表
int i = 0;
while (lpImpName[i].u1.AddressOfData != 0) { // 表项以0结尾
// 导入函数名称结构
PIMAGE_IMPORT_BY_NAME lpFunName = (PIMAGE_IMPORT_BY_NAME)(lpImpName[i].u1.AddressOfData + (SIZE_T)lpBaseAddress);
// 判断函数导出类型
FARPROC lpFunAddress = NULL;
if (0x80000000 & lpImpName[i].u1.Ordinal) {
// 序号导出,IMAGE_THUNK_DATA最高位为1
lpFunAddress = pfnGetProcAddress(hDll, (LPCSTR)(lpImpName[i].u1.Ordinal & 0x0000FFFF));
}
else {
lpFunAddress = pfnGetProcAddress(hDll, lpFunName->Name);
}
lpImpAddr[i].u1.Function = (SIZE_T)lpFunAddress;
i++;
}
pImp++;
}
执行DllMain
// ==============3.调用dllmain=============
// DllMain函数地址在PE结构入口点OEP
// typedef BOOL(__stdcall* selDllMain)(HINSTANCE hInstance, SIZE_T dwReason, LPVOID lpReserved);
// selDllMain DllMain = (selDllMain)((SIZE_T)lpBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
// DllMain((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL);
LPVOID selDllMain = (LPVOID)((SIZE_T)lpBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
__asm {
push 0;
push 1;
mov eax, dword ptr[lpBaseAddress];
push eax;
mov eax, dword ptr[selDllMain];
call eax;
}
效果测试
#include "ManualLoadDll.h"
#include "Common.h"
#include "DLLtoShellcode.h"
char dllpath[] = "C:\\Users\\selph\\source\\Tools\\DLL\\ManualLoadDll\\Debug\\TestDll.dll";
char dllpath64[] = "C:\\Users\\selph\\source\\Tools\\DLL\\ManualLoadDll\\x64\\Debug\\TestDll.dll";
char exepath[] = "C:\\Users\\selph\\source\\Tools\\DLL\\ManualLoadDll\\Debug\\TextExe.exe";
int main(int argc, char* argv[]) {
// 手动加载PE文件到内存
ManualLoadPE mlPE(dllpath);
// 拼接shellcode和DLL
DWORD dwSize = 1 + mlPE.stImageSize + sizeof(hexData);
PBYTE buffer = new BYTE[dwSize];
memcpy(buffer, hexData, sizeof(hexData));
memcpy(buffer + sizeof(hexData), mlPE.lpBaseAddress, mlPE.stImageSize);
// 注入整个dll作为shellcode
DWORD dwPid = 33512;
//打开进程,获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL) {
return FALSE;
}
//在注入进程中申请内存
LPVOID pShellcodeAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (pShellcodeAddr == NULL) {
return FALSE;
}
//在目标内存写入数据
if (WriteProcessMemory(hProcess, pShellcodeAddr, buffer, dwSize, NULL) == NULL) {
return FALSE;
}
//修改目标进程内存读写属性
DWORD dwOld = 0;
::VirtualProtectEx(hProcess, pShellcodeAddr, dwSize, PAGE_EXECUTE_READWRITE, &dwOld);
// 注入--初始化DllMain
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pShellcodeAddr, NULL, 0, NULL);
WaitForSingleObject(hThread,INFINITE);
//关闭句柄
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
效果