启动技术-内存直接加载运行

本文最后更新于:2021-11-22 晚上

内存直接加载运行

内存直接加载运行就是,模拟PE加载器的功能,把DLL或者exe等PE文件从内存中直接加载到病毒木马的内存中去运行,不需要通过loadlibrary等现成的API函数去操作。

实现原理

构造一个PE装载器,将PE文件加载到内存中。大致过程,首先要申请一块内存,然后将PE文件按照映像对齐大小映射到内存中;根据重定位表,重定位硬编码数据;获取导入表中的函数及其地址;如果是DLL,获取导出表的相关数据(EXE一般没有导出表);获取入口点的地址,若为EXE,直接跳到入口点即可执行,DLL文件的话还需要构造一个DLLMAIN函数,实现DLL加载。

具体实现

打开文件并且获取大小

1
2
3
4
5
6
7
8
9
10
11
12
13
char* FileName = "自己的文件路径";
//打开文件
HANDLE hFile = CreateFileA(FileName, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_ARCHIVE, NULL);
//获取大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//申请内存空间
PBYTE pData = new BYTE[dwFileSize];
DWORD dwRet = 0;
//将文件读取到内存中
ReadFile(hFile, pData, dwFileSize, &dwRet, NULL);
CloseHandle(hFile);
获取sizeofimage
1
2
3
4
5
6
7
8
9
10
/*获取PE文件的镜像大小,获取加载到内存后的大小
* lpData内存中的基址
*/
DWORD GetImageSize(LPVOID lpData) {
DWORD dwSizeOfImage = 0;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
dwSizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage;
return dwSizeOfImage;
}

根据获取的sizeofimage,在进程中开辟一个内存块,权限可读可写可执行。

1
2
LPVOID lpBaseAddr = VirtualAlloc(NULL, dwImageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlZeroMemory(lpBaseAddr, dwImageSize);
加载
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
/*将PE文件的头部和节区加载到内存中
*/
BOOL LoadSection(LPVOID lpData, LPVOID lpBaseAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
//获取DOS头+NT头+节表的总大小
DWORD dwSizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders;
//获取节的数量
int NumberOfSections = pNtHeaders->FileHeader.NumberOfSections;
//获取第一个节表头的地址,通过NT头加上NT头大小就是第一个节头
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(IMAGE_NT_HEADERS));
//加载DOS头+NT头+节表
RtlCopyMemory(lpBaseAddr, lpData, dwSizeOfHeaders);
LPVOID lpSrc = NULL;
LPVOID lpDest = NULL;
DWORD dwSizeOfRawData = 0;
for (int i = 0; i < NumberOfSections; i++) {
if ((pSectionHeader->VirtualAddress == 0) || (pSectionHeader->SizeOfRawData == 0)) {
pSectionHeader++;
continue;
}
lpSrc = (LPVOID)((DWORD)lpData + pSectionHeader->PointerToRawData);
lpDest = (LPVOID)((DWORD)lpBaseAddr + pSectionHeader->VirtualAddress);
dwSizeOfRawData = pSectionHeader->SizeOfRawData;
RtlCopyMemory(lpDest, lpSrc, dwSizeOfRawData);
pSectionHeader++;
}
return TRUE;
}
重定位数据
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
/*获取重定位表的相关数据并且重定位数据
* lpBaseAddr: 内存PE数据按SectionAlignment大小对齐映射到进程内存中的内存基址
*/
BOOL DoRelocationTable(LPVOID lpBaseAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
//获取重定位表
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((unsigned long)pDosHeader +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
if ((PVOID)pReloc == (PVOID)pDosHeader) {
return TRUE;
}
//开始扫描重定位表
while ((pReloc->VirtualAddress + pReloc->SizeOfBlock) != 0) {
//重定位表的头部加上sizeof(IMAGE_BASE_RELOCATION)就是重定位数据的开始
WORD* pRelocData = (WORD*)((PBYTE)pReloc + sizeof(IMAGE_BASE_RELOCATION));
//获取需要重定位的数据的个数
int nNumberOfReloc = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

for (int i = 0; i < nNumberOfReloc; i++) {
//高位是否为3,判断是否需要修复
if ((DWORD)(pRelocData[i] & 0x0000F000) == 0x00003000) {
//获取需要重定位数据的地址
DWORD* pAddress = (DWORD*)((PBYTE)pDosHeader + pReloc->VirtualAddress + (pRelocData[i] & 0x0FFF));
//修改重定位数据,公式:地址-旧基址+新基址,地址是pAddress中的值
DWORD dwDelta = (DWORD)pDosHeader - pNtHeaders->OptionalHeader.ImageBase;
*pAddress += dwDelta;
}
}
//继续处理下一组重定位数据
pReloc = (PIMAGE_BASE_RELOCATION)((PBYTE)pReloc + pReloc->SizeOfBlock);
}
return TRUE;
}
导入表
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
/*获取导入表的相关数据
* lpBaseAddr: 内存PE数据按SectionAlignment大小对齐映射到进程内存中的内存基址
*/
BOOL DoImportTable(LPVOID lpBaseAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
//获取导入表地址
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDosHeader +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 循环遍历导入表中的DLL及获取导入表中的函数地址
char* lpDllName = NULL;
HMODULE hDll = NULL;
PIMAGE_THUNK_DATA lpImportNameArray = NULL;
PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
FARPROC lpFuncAddress = NULL;
DWORD i = 0;
while (TRUE) {
if (0 == pImportTable->OriginalFirstThunk) {
break;
}
// 获取导入表中DLL的名称并加载DLL
lpDllName = (char*)((DWORD)pDosHeader + pImportTable->Name);
hDll = GetModuleHandleA(lpDllName);
if (NULL == hDll) {
hDll = LoadLibraryA(lpDllName);
if (NULL == hDll) {
pImportTable++;
continue;
}
}
i = 0;
// 获取OriginalFirstThunk以及对应的导入函数名称表首地址
lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)pDosHeader + pImportTable->OriginalFirstThunk);
// 获取FirstThunk以及对应的导入函数地址表首地址
lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)pDosHeader + pImportTable->FirstThunk);
while (TRUE) {
if (0 == lpImportNameArray[i].u1.AddressOfData) {
break;
}
// 获取IMAGE_IMPORT_BY_NAME结构
lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDosHeader + lpImportNameArray[i].u1.AddressOfData);
// 判断导出函数是序号导出还是函数名称导出
if (0x80000000 & lpImportNameArray[i].u1.Ordinal) {
// 序号导出
// 当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时,低位被看做是一个函数序号
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
}
else {
// 名称导出
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
}
lpImportFuncAddrArray[i].u1.Function = (DWORD)lpFuncAddress;
i++;
}
pImportTable++;
}
return TRUE;
}
修改ImageBase
1
2
3
4
5
6
7
//修改ImageBase
BOOL SetImage(LPVOID lpBaseAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
pNtHeaders->OptionalHeader.SizeOfImage = (ULONG32)lpBaseAddr;
return TRUE;
}
获取入口点

如果是EXE,这一步,获取addressOfEntryPoint之后跳到入口点即可直接执行。

1
2
3
4
5
6
7
8
9
BOOL Entry(LPVOID lpBaseAddr) {
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
LPVOID Entry = (LPVOID)((ULONG32)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
__asm {
mov eax,Entry
jmp eax
}
}

现在来测试一下直接运行一个EXE,测试文件为桌面上的TestProcess.exe。源代码如下:

1
2
3
4
5
#include <stdio.h>
int main(){
printf("b1ackie!!!\n");
return 0;
}

运行程序查看效果,可以看到直接加载运行TestProcess.exe。

若是DLL文件,还需要构造一下DLLMAIN

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CallDllMain(LPVOID lpBaseAddr) {
typedef_DllMain DllMain = NULL;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
DllMain = (typedef_DllMain)((ULONG32)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
BOOL bRet = DllMain((HINSTANCE)lpBaseAddr,DLL_PROCESS_ATTACH,NULL);
if (bRet == NULL) {
printf("构造入口点失败\n");
return bRet;
}
return bRet;
}
导出表
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
/*获取导出函数及其地址
* lpBaseAddr: 内存PE数据按SectionAlignment大小对齐映射到进程内存中的内存基址
* lpszFuncName:导出函数名字
*/
LPVOID GetExFuncAddr(LPVOID lpBaseAddr,char* lpszFuncName) {
LPVOID lpFunc = NULL;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddr;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew);
//获取导出表地址
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//从导出表取出的函数名
char* lpFuncName = NULL;
//获取AddressOfNames
PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames);
//获取AddressOfNameOrdinals
PWORD lpAddressOfNameOrdinalArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals);
//索引值
WORD wHint = 0;
//获取AddressOfFunctions
PDWORD lpAddressOfFuncArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions);
//获取所有根据名称导出的函数数量
DWORD dwNumberOfNames = pExportTable->NumberOfNames;
for (int i = 0; i < dwNumberOfNames; i++) {
lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]);
if (strcmpi(lpFuncName, lpszFuncName) == 0) {
//获取索引值
wHint = lpAddressOfNameOrdinalArray[i];
//根据索引值,在AddressOfFunctions中取出RVA
lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFuncArray[wHint]);
break;
}
}
//返回函数地址
return lpFunc;
}

运行加载桌面上的TestDll.dll文件,此DLL导出函数是一个messagebox函数。

参考

参考《Windows黑客编程技术详解》一书