内存直接加载运行
病毒木马具有模拟PE加载器的功能, 它们可以把DLL或者exe等PE文件从内存中直接加载到病毒木马的内存中去执行, 不需要通过LoadLibrary等现成的API函数去操作; 假如程序需要动态调用DLL文件, 内存加载运行技术可以把DLL作为资源插入到自己的程序中, 此时可直接在内存中加载运行即可; 内存加载技术的核心就在于模拟PE加载器PE文件, 也就是对导入表、导出表、重定位表的操作过程;
实现原理 首先, 需要将DLL文件加载到内存中, 按照映像大小进行对齐后映射到内存中, 然后根据重定位表修改硬编码数据, 最后根据导出表函数地址修正导入表;
流程总结
根据映像大小SizeOfImage申请可读可写可执行的内存空间, 首地址即DLL加载基址; 获取其映像对齐大小SectionAlignment
并复制到该内存空间中(FileBuffer => ImageBuffer); 修正重定位表; 修正导入表, 根据PE结构中的导入表, 加载所需的Dll, 并获取导入函数的地址将其写入导入表中; 修改DLL的加载基址ImageBase; 获取DLL入口地址, 构造DllMain函数实现加载;
对于exe文件, 重定位表不是必须的。因为对于exe进程来说, 进程最早加载的模块是exe模块, 所以它可以按照默认加载基址加载到内存中; exe和Dll唯一的区别在于构造入口函数的差别, exe不需要构造入口函数, 而是根据PE结构获取exe的入口地址偏移AddressOfEntryPoint并计算出入口地址, 然后直接跳转。
1. 将Dll文件读入内存 PELodader_Demo.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 wchat_t szFileName[MAX_PATH] = L"C:\\Users\\dell\\OneDrive\\桌面\\哔哩哔哩学习\\PE Learn\\PELoader_Demo\\Debug\\TestDLL_01.dll" ;HANDLE hFile = CreateFile ( szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL , OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL ); if (hFile == INVALID_HANDLE_VALUE){ printf ("CreateFile Failed\n" ); return 1 ; } DWORD dwFileSize = GetFileSize (hFile, NULL ); printf ("GetFileSize: %d\n" , dwFileSize);PBYTE lpData = new BYTE[dwFileSize]; if (NULL == lpData){ printf ("申请内存出错\n" ); return 1 ; } DWORD dwRead = 0 ; if (FALSE == ReadFile (hFile, lpData, dwFileSize, &dwRead, NULL )){ printf ("ReadFile Failed\n" ); return 1 ; } printf ("lpData: %08x\n" , *(short *)lpData);
CreateFile
创建或打开文件或I/O设备, 此函数区分多字节和Unicode两种模式
1 2 3 4 5 6 7 8 9 10 11 12 13 HANDLE WINAPI CreateFileW ( _In_ LPCWSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile ) ;
如果函数成功, 则返回值是指定文件、设备、命名管道或邮件槽的打开句柄; 如果函数失败, 则返回值为INVALID_HANDLE_VALUE;
GetFileSize
检索指定文件的大小(以字节为单位)
1 2 3 4 5 6 DWORD WINAPI GetFileSize ( _In_ HANDLE hFile, _Out_opt_ LPDWORD lpFileSizeHigh ) ;
如果函数成功, 则返回值为文件大小的低位双字, 如果lpFileSizeHigh为非NULL, 则该函数会将文件大小的高位双字放入该参数指向的变量中; 如果函数失败且lpFileSizeHigh为NULL, 则返回值INVALID_FILE_SIZE;
ReadFile
从指定的文件或输入/输出 (I/O) 设备读取数据
1 2 3 4 5 6 7 8 9 BOOL WINAPI ReadFile ( _In_ HANDLE hFile, _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped ) ;
如果函数成功,则返回值为非零 (TRUE); 如果函数失败或正在异步完成,则返回值为零 (FALSE);
2. FileBuffer => ImageBuffer
将FileBuffer转换成ImageBuffer可以分为两步:
先将PE头部复制至内存中; 然后循环将节内容复制过去;
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 BOOL MmMapFile (LPVOID lpData, LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)lpData); DWORD dwSizeOfHeaders = pNt->OptionalHeader.SizeOfHeaders; DWORD dwNumOfSections = pNt->FileHeader.NumberOfSections; RtlCopyMemory (lpBaseAddress, lpData, dwSizeOfHeaders); PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION (pNt); for (size_t i = 0 ; i < dwNumOfSections; i++) { if ((0 == pSec[i].VirtualAddress) || (0 == pSec[i].PointerToRawData)) { continue ; } LPVOID lpSrcMem = (LPVOID)(pSec[i].PointerToRawData + (DWORD)lpData); LPVOID lpDesMem = (LPVOID)(pSec[i].VirtualAddress + (DWORD)lpBaseAddress); DWROD dwSizeOfRawData = pSec[i].SizeOfRawData; RtlCopyMemory (lpDesMem, lpSrcMem, dwSizeOfRawData); } return TRUE; }
3. 修正重定位表 重定位表
Relocation(重定位)是一种将程序中的一些地址修正为运行时可用的实际地址的机制。在程序编译过程中, 由于程序中使用了各种全局变量和函数, 这些变量和函数的地址还没有确定, 因此它们的地址只能暂时使用一个相对地址。当程序被加载到内存中运行时, 这些相对地址需要被修正为实际的绝对地址, 这个过程就是重定位; 在Windows操作系统中, 程序被加载到内存中运行时, 需要将程序中的各种内存地址进行重定位, 以使程序能正确运行。Windows系统使用PE(Portable Executable)文件格式来存储可执行程序, 其中包括重定位信息。当程序被加载到内存中时, 系统会解析这些重定位信息, 并将程序中的各种内存地址进行重定位。 重定位表一般出现在Dll
中, 因为Dll
都是动态加载, 所以地址不固定, Dll的入口点在整个执行过程中至少要执行2次, 一次时在开始时执行初始化工作, 一次则是在结束时做最后的收尾工作, 重定位表则是解决Dll的地址问题;
重定位表的结构
重定位结构
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 BOOL DoRelocationTable (LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)lpBaseAddress); PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC); PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)(pDataDir->VirtualAddress + (DWORD)lpBaseAddress); if ((LPVOID)pLoc == lpBaseAddress) { return FALSE; } while ((pLoc.VirtualAddress + pLoc->SizeOfBlock) != 0 ) { PWORD pLocData = (PWORD)((PBYTE)pLoc + sizeof (IMAGE_BASE_RELOCATION)); DWORD dwNumOfpLoc = (pLoc->SizeOfBlock - sizeof (IMAGE_BASE_RELOCATION)) / sizeof (WORD); for (size_t i = 0 ; i < dwNumOfpLoc; i++) { if ((DWORD)(pLocData[i] & 0x0000F000 ) == 0x00003000 ) { PDWORD pAddress = (PDWORD)((PBYTE)pDos + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF )); DWORD dwDelta = (DWORD)pDos + pNt->OptionalHeader.ImageBase; *pAddress += dwDelta; } } pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock); } return TRUE; }
4. 修正导入表 PE加载器在加载PE的时候会将导入函数的地址填入导入地址表中, 导入表结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
主要用到的是OriginalFirstThunk和FirstThunk; 这两个表用到的结构体是一样的(IMAGE_THUNK_DATA):
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
如果指向导入名称表; 则内容是AddressOfData, 指向IMAGE_IMPORT_BY_NAME结构体; 如果指向导入地址表:
序号导入的话, Ordinal首位是1, 低4位是导入序号;
名称导入的话, Function的值是函数地址;
这里我们需要将导入函数的地址填入导入地址表中, 所以需要知道这个函数是怎么导入的, 然后通过GetProcAddress API获取函数地址, 然后将函数地址填入导入地址表中;
通过GetProcAddress获取函数地址, 需要知道Dll名称, 通过Dll名称获取模块句柄
所以代码流程是:
修正导入表流程
先获取导入表数组的数量和第一个成员的地址; 根据导入表的数量, 进行循环遍历;获取导入名称表; 获取导入地址表; 进行导入名称表的遍历(导入名称表数组以0作为最后一个成员结束);获取导入函数的名称或序号; 加载这个Dll, 通过名称或序号, 获取其函数地址; 将地址填入导入地址表; 进入下一次循环; 进入下一次循环; 两次遍历完成后, 导入表就已经完成了修正;
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 BOOL DoImportTable (LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)pDos); PIMAGE_DATA_DIRECTORY pDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT); PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pDir->VirtualAddress + (DWORD)pDos); char * szDllName = NULL ; HMODULE hDll = NULL ; PIMAGE_THUNK_DATA pImportNameArray = NULL ; PIMAGE_IMPORT_BY_NAME pImportByName = NULL ; PIMAGE_THUNK_DATA pImportFuncAddrArray = NULL ; FARPROC pfFuncAddress = NULL ; DWORD i = 0 ; while (TRUE) { if (0 == pImport->OriginalFirstThunk) { break ; } szDllName = (char *)(pImport->Name + (DWORD)pDos); hDll = GetModuleHandleA (szDllName); if (hDll == NULL ) { hDll = LoadLibraryA (szDllName); if (hDll == NULL ) { pImport++; continue ; } } i = 0 ; pImportNameArray = (PIMAGE_THUNK_DATA)(pImport->OriginalFirstThunk + (DWORD)pDos); pImportFuncAddrArray = (PIMAGE_THUNK_DATA)(pImport->FirstThunk + (DWORD)pDos); while (TRUE) { if (0 == pImportNameArray[i].u1.AddressOfData) { break ; } pImportByName = (PIMAGE_IMPORT_BY_NAME)(pImportNameArray[i].u1.AddressOfData); if (0x80000000 & pImportNameArrar[i].u1.Ordinal) { pfFuncAddress = GetProcAddress (hDll, (LPCSTR)(pImportNameArray[i].u1.Ordinal & 0x0000FFFF )); } else { pfFuncAddress = GetProcAddress (hDll, (LPCSTR)pImportByName->Name); } pImportFuncAddrArray[i].u1.Function = (DWORD)pfFuncAddress; i++; } pImport++; } return TRUE; }
5. 修改Dll的加载基址ImageBase
PE加载器在加载PE的时候会将进程分配的基地址填入扩展头的ImageBase中;
1 2 3 4 5 6 7 8 BOOL SetImageBase (LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)pDos); pNt->OptionalHeader.ImageBase = (DWORD)lpBaseAddress; return TRUE; }
6. 修改DllMain入口点
调用Dll的入口函数DllMain, 函数地址则是PE文件的入口点;
1 2 3 4 5 6 7 8 9 10 11 BOOL CallDllMain (LPVOID lpBaseAddress) { typedef_DllMain DllMain = NULL ; PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)pDos); DllMain = (typedef_DllMain)(pNt->OptionalHeader.AddressOfEntryPoint + (DWORD)pDos); BOOL bRet = DllMain ((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL ); return bRet; }
7. 获取Dll导出函数 导出表
PE文件运行, 需要依赖Dll; 系统Dll包括Kernel32.dll、User32.dll等; 导出表时当前PE文件提供了哪些函数给别人使用; 不管是exe还是Dll, 本质都是PE文件; exe文件也可以导出函数给别人使用; 一般exe没有, 但不是不可以;
导出表结构
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
遍历流程
获取导出函数名称; 比较是否是要找函数名称; 如果是, 则获取函数序号(2字节); 根据函数序号获取函数地址;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 LPVOID MmGetProcAddress (LPVOID lpBaseAddress, wchar_t * lpszFuncName) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (DWORD)pDos); PIMAGE_DATA_DIRECTORY pDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT); PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDir->VirtualAddress + (DWORD)pDos); PDWORD dwAddressOfFunctions = (PDWORD)(pExport->AddressOfFunctions + (DWORD)pDos); PDWORD dwAddressOfNames = (PDWORD)(pExport->AddressOfNames + (DWORD)pDos); PWORD pwAddressOfNameOrdinals = (PWORD)(pExport->AddressOfNameOrdinals + (DWORD)pDos); DWORD dwNumberOfNames = pExport->NumberOfNames; for (size_t i = 0 ; i < dwNumberOfNames; i++) { if (!dwAddressOfFunctions[i]) { continue ; } PWCHAR szFuncName = (PWCHAR)(dwAddressOfNames[i] + (DWORD)pDos); if (lstrcmpi (lpszFuncName, szFuncName) == 0 ) { return (LPVOID)(dwAddressOfFunctions[pwAddressOfNameOrdinals[i]] + (DWORD)pDos); } } return LPVOID (); }
8. 释放内存加载的Dll 释放资源
1 2 3 4 5 6 7 8 9 10 11 BOOL MmFreeLibrary (LPVOID lpBaseAddress) { BOOL bRet = NULL ; if (NULL == lpBaseAddress) { return bRet; } bRet = VirtualFree (lpBaseAddress, 0 , MEM_RELEASE); lpBaseAddress = NULL ; return bRet; }
内存加载Dll执行演示 示例Dll源代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "pch.h" extern "C" __declspec(dllexport)void ShowMessage () { MessageBox (NULL , L"I'm DLL File" , L"HELLO" , MB_OK); } BOOL APIENTRY DllMain ( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break ; } return TRUE; }
PELoader_Demo.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <iostream> #include <Windows.h> #include "MmLoadDll.h" int main () { wchar_t szFileName[MAX_PATH] = L"C:\\Users\\dell\\OneDrive\\桌面\\哔哩哔哩学习\\PE Learn\\PELoader_Demo\\Debug\\TestDLL_01.dll" ; HANDLE hFile = CreateFile ( szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL , OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL ); if (INVALID_HANDLE_VALUE == hFile) { printf ("CreateFile Failed: %d\n" , GetLastError ()); return 0 ; } DWORD dwFileSize = GetFileSize (hFile, NULL ); printf ("FileSize: %d\n" , dwFileSize); PBYTE lpData = new BYTE[dwFileSize]; if (lpData == NULL ) { printf ("申请内存出错: %d\n" , GetLastError ()); return 0 ; } DWORD dwRet = 0 ; BOOL bRet = ReadFile (hFile, lpData, dwFileSize, &dwRet, NULL ); if (!bRet) { printf ("ReadFile Failed: %d\n" , GetLastError ()); return 0 ; } printf ("lpData: %08x\n" , *(short *)lpData); LPVOID lpBaseAddress = MmLoadLibrary (lpData, dwFileSize); if (lpBaseAddress == NULL ) { printf ("MmLoadLibrary Failed: %d\n" , GetLastError ()); return 0 ; } printf ("DLL加载成功\n" ); typedef void (*typedef_ShowMessage) () ; const char * szName = "ShowMessage" ; typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MmGetProcAddress (lpBaseAddress, (wchar_t *)szName); if (NULL == ShowMessage) { printf ("MmGetProcAddress Failed\n" ); return 0 ; } ShowMessage (); BOOL bRet1 = MmFreeLibrary (lpBaseAddress); if (FALSE == bRet1) { printf ("MmFreeLibrary Failed\n" ); return 0 ; } delete [] lpData; lpData = NULL ; CloseHandle (hFile); return 0 ; }
调用导出函数 调用导出函数
内存加载exe执行 待补充…
仓库地址
参考资料
参考Blog