首页
社区
课程
招聘
[原创]萌新从反射注入学习pe文件加载的记录
2021-4-8 23:10 10546

[原创]萌新从反射注入学习pe文件加载的记录

2021-4-8 23:10
10546

从反射注入学习pe文件加载

前言?后记与总结

这是我写完代码后写的总结。重新梳理一下反射注入到底想干什么、以及怎么干。以及从中学到了什么。

收获?目标?反射注入是什么

首先是学习反射注入的收获,当然也可以作为学习的目标,同时也是反射注入实际做的东西。

 

我的理解是反射注入实际上就是手工加载模块。通过LoadLibrary加载dll会在peb中留下记录,通过手工实现加载过程,我们的dll能像正常dll那样工作,且不再peb中留下痕迹。

 

也就是说,通过学习反射注入,可以了解到windows系统加载一个pe文件的流程。其中涉及到了部分的peb以及大量的pe结构。

 

我们主要需要peb中的ldr结构,这个结构中保存了该进程已经加载了的dll。

 

既然我们的主要工作是手工加载pe文件,自然要对pe文件格式有一定了解。但笔记中不会多提pe文件结构,实际上只要大概了解pe文件格是是个什么,然后在写代码时多去看pe结构的定义,就可以对pe文件结构有一个更深的理解。

参考

在学习过程中参考了许多资料,最主要的就是msf的反射注入payload的源码。其他如有不懂通过百度也可以找到详细的解释。相关文章比较多,dddd,就不一一列举了。

关于这篇东西

这篇东西由我阅读源码,查资料时做的笔记发展而来。国内虽然少但也有一些优秀的反射注入的文章,看雪中也有类似文章,但阅读门槛稍微有点高。因为是由笔记发展而来,这更像是一个零基础初学者的学习笔记(实际上在开始学反射注入之前,只知道pe文件格是是什么东西,几乎完全不了解。对windows的机制也完全不了解),希望能帮助到初学者,这项技术对我学习windows有很大帮助,虽然我只是一个初学者,但这项实践使我之后对书本、资料上的内容有了更深的了解。可能会有错误,希望发现错误的dalao可以帮帮我这个初学者纠正。

前置要求

va、fa、rva这几个概念搞清楚好像就行。实际上就是对pe文件如何从文件映射到内存有一个大体的认识。我们的工作就是具体完成这个过程。可以参考下图。

 

pe文件到内存的映射

 

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
(前置条件序号) 序号 流程内容
 
============
注入器:
============
(0)   1 打开dll文件(CreateFile),获取dll长度(GetFileSize)
(1)   2 分配内存(HealAlloc),读取文件(ReadFile)
(0)   3 打开目标进程(OpenProcess)
(2,3) 4 调用反射注入函数(LoadLibraryR.c>LoadRemoteLibraryR)
(2)   5 获取反射加载函数的文件偏移(LoadLibraryR.c>GetReflectiveLoaderOffset)
(2,3) 6 在目标进程中分配空间(VirtualAllocEx),写入dll(WriteProcessMemory)
(6)   7 修改目标进程中的空间为可执行(VirtualProtectEx)
(5,7) 8 创建远程线程,执行反射加载函数(CreateRemoteThread)
 
=======================
反射加载函数(运行在被注入进程的新建线程中):
=======================
1 获取基地址
2 获取需要的kernel32.dll及ntdll.dll的函数的va
3 分配空间作为映像空间,并复制pe头到新的位置
4 复制所有段到映像的对应位置
5 处理导入表,填写iat
6 重定位
7 跳转到ep(_DllMainCRTStartup)
8 返回entry point地址

注入器

主要流程

首先打开dll文件,获取长度,并在堆中分配空间读取文件。

1
2
3
hFile = CreateFileW(dllPathname, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
dwDllLen = GetFileSize(hFile, 0);
lpDll = HeapAlloc(GetProcessHeap(), 0, dwDllLen);

然后找到dll中ReflectiveLoader的入口点。

 

最后以RW申请空间,写入dll后改成RX,然后以ReflectiveLoader作为线程函数创建远程线程。

计算fa

LoadLibraryR中有个函数Rva2Offset用于获取rva对应的fa。

 

原理是遍历区块获取区块的section_rva和section_fa,然后比较rva和section_rva找到rva所在的section,最后计算出fa。再用fa+baseAddr得到内存中的位置。

1
2
3
4
5
6
7
8
9
10
DWORD Rva2Fa(DWORD rva, PIMAGE_SECTION_HEADER sections, int sectionNum)
{
    for (int i = 0; i < sectionNum; i++) {
        int sectionVa = sections[i].VirtualAddress;
        if ((rva >= sectionVa) && ((sectionVa + sections[i].SizeOfRawData) > rva))
            return rva - (sectionVa - sections[i].PointerToRawData);
    }
 
    return 0;
}

获取ReflectivelLoader位置(输出表)

通过nt头,计算出sections的fa,以及通过nt头的optionalheader获取输出表的rva。

 

然后遍历section找到输出表的fa,接着遍历输出表的函数名字rva表,计算出rva对应fa得到导出函数名字,与需要的导出函数做对比,确定要找的函数在函数名表中的下标。用此下标在序号表中找到序号,最后再用序号去地址表找到地址。

反射加载函数

1. 获取基址

首先获取代码的位置,然后再往前找dos头。

 

_ReturnAddress()返回当前调用函数的返回地址。所以在loader中调用一个函数,该函数再调用_ReturnAddress(),返回调用函数的返回地址,即loader中调用函数的下一条语句的地址。
其中 __declspec(noinline) 用于防止编译器优化该函数成内联函数,否则返回的就是loader的返回地址。

 

使用_ReturnAddress需要intrin.h,并使用#pragma intrinsic防止内联优化。

1
2
3
4
5
6
#include<intrin.h>
#pragma intrinsic(_ReturnAddress)
__declspec(noinline) PVOID NextAddr()
{
    return (PVOID)_ReturnAddress();
}

根据pe格式可知,dos头(IMAGE_DOS_HEADER)中有有一个e_magic标志,值是0x5A4D(MZ)。
所以向前遍历内存,直到找到MZ标志,再检查pe头的PE标志,这样就找到dos头了。
需要注意的是,检查PE标志时要检查pe头偏移是否正确,防止错误的内存访问。

1
2
3
4
5
6
7
8
9
10
11
while (TRUE) {
    if (dosHeadAddr->e_magic == 0x5A4D) {
        LONG e_lfanew = dosHeadAddr->e_lfanew;
        if (e_lfanew >= sizeof(IMAGE_DOS_HEADER) && e_lfanew < 1024) {
            ntHeadAddr = (PIMAGE_NT_HEADERS)((PVOID)dosHeadAddr + (PVOID)e_lfanew);
            if (ntHeadAddr->Signature == 0x4550)
                break;
        }
    }
    dosHeadAddr--;
}

这里也可以取巧,远程线程是可以传递一个参数的,对于我们这个简单的dll,imagebase实际上就是分配空间的首地址,可以作为参数传入。

2. 获取需要的内核导出函数的va

目标

接下来的步骤中需要用到一些ntdll.dll,kernel32.dll中的导出函数,所以需要先找到这些函数的va。这些系统模块都是已经加载了的,可以在peb中找到其加载的位置。

 

这里利用hash避免直接比较字符串。

 

我们需要LoadLibraryA、GetProcAddress加载导入表中的dll的对应的函数。

 

需要VirtualAlloc分配内存给我们把pe文件加载到其中。

 

需要NtFlushInstructionCache刷新指令缓存。

LDR_DATA_TABLE_ENTRY

InMemoryOrderModuleList对应的链表是一个环形双向链表,且有一个头节点(或者说哨兵节点)。InMemoryOrderModuleList的Flink指向链表的第一个节点,Blink指向链表最后一个节点。头节点的Flink是第一个节点,可以以此为跳出条件遍历该链表。

 

这里借用一张网图。

 

ldr链

思路

首先从peb中找到ldr,然后遍历InMemoryOrderModuleList,通过hash(BaseName)找到kernel32.dll和ntdll.dll对应的LDR_DATA_TABLE_ENTRY结构。

 

找到dll对应的LDR_DATA_TABLE_ENTRY后,获取其imagebase,然后解析pe头,计算出导出表位置。同样利用hash比较字符串找到所需的导出函数,并计算出va。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 找pLdrDataTableEnrty
DWORD pPeb = __readfsdword(0x30);
DWORD pLdr = *(DWORD*)(pPeb + 0xc);
DWORD pInMemoryOrderModuleList = pLdr + 0x14; // 第一个节点的二级指针
DWORD pLdrDataTableEnrty = *(DWORD*)(pInMemoryOrderModuleList + 0);
// 遍历LdrDataTableEnrty
do{
    WCHAR* name = (WCHAR*)*(DWORD*)(pLdrDataTableEnrty + 0x24 + 0x4);
    hash = YourHashFun(name); // 使用你自己的函数计算hash
    if(hash == DLLHASH) { // DLLHASH由你自己的函数计算得出
        DWORD baseAddr = *(DWORD*)(pLdrDataTableEnrty + 0x10);
        // 解析pe头过程省略
        for(int i = 0; i < funcNum; i++) { // funcNum是导出函数的个数
            char* name = (char*)(baseAddr + ((DWORD*)nameRvas)[i]);
            DWORD hash = YourHashFun(name);
            if (hash == FUNCHASH) {
                pFunc = (FUNC)(baseAddr + ((DWORD*)funcRvas)[((WORD*)ordRvas)[i]]);
            }
        }
    }
}while(*(DWORD*)(pLdrDataTableEnrty) != *(DWORD*)(pInMemoryOrderModuleList))

3. 给映像分配空间,并加载pe头

新分配大小等于sizeOfImga的内存作为映像加载的空间,然后把pe头复制到新内存里,这里我只更新了新nt头的imagebase地址。太简单就不贴代码了。

4. 加载段

遍历section_header获取fa和rva,计算出section在旧内存中的va和新内存中的va。然后复制section到新内存中的对应位置。

1
2
oldVA = oldImageBase + sections[i].PointerToRawData;
newVA = newImageBase + sections[i].VirtualAddress;

5. 处理导入表

目标

找到导入表,然后遍历导入表,依次加载对应的dll,及需要的dll的导出函数,并填写对应iat。

导入表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// winnt.h
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME (补充一下,这是个rva)
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
 
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

和导出表不同,导入表是一个结构体数组。它不提供结构体数量,最后一个结构体仅作为结束标志,不包含导入信息,其成员Characteristics为0,这可以作为遍历的退出条件。

 

对于每个导入表,在文件中时OriginalFirstThunk和FirstThunk都是RVA,指向同一个IMAGE_THUNK_DATA结构体数组。
当加载到内存时,FirstThunk改为函数的VA,即iat。

 

文件中时,OriginalFirstThunk和FirstThunk指向的结构体数组中,每一个IMAGE_THUNK_DATA的成员u1都被解释为Ordinal,若该函数应该通过序号导入,则Ordinal的最高位会被置为1。

思路

见实现代码注释。

实现代码

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
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewDosHeader + pNewNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
 
for (; pImportDescriptor->Characteristics; pImportDes++) {
    // 加载dll
    HMODULE libraryAddress = pLoadLibraryA((LPCSTR)((DWORD)pNewDosHeader + pImportDes->Name));
    if (!libraryAddress)
            continue;
 
    // parsing pe structure
    PIMAGE_THUNK_DATA32 pOriginalThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes->OriginalFirstThunk);
    PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes->FirstThunk);
    PIMAGE_NT_HEADERS32 pLibNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)libraryAddress + ((PIMAGE_DOS_HEADER)libraryAddress)->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD)libraryAddress + pLibNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    PDWORD funcRvas = (PDWORD)((DWORD)libraryAddress + pExportDir->AddressOfFunctions);
 
    while (*(DWORD*)pThunk) {
        if (pOriginalThunk && pOriginalThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) {
            // import by ord
            WORD ord = pOriginalThunk->u1.Ordinal - pExportDir->Base;
            *(DWORD*)pThunk = ((DWORD)libraryAddress + funcRvas[ord]);
        }
        else {
            // import by name (this is a rva)
            *(DWORD*)pThunk = (DWORD)pGetProcAddress(libraryAddress, ((PIMAGE_IMPORT_BY_NAME)((DWORD)pNewDosHeader + pThunk->u1.AddressOfData))->Name);
        }
 
        pThunk++;
        if (pOriginalThunk)
            pOriginalThunk++;
    }
}

6. 重定位

目标

完成重定位过程。

重定位表结构

重定位表是一个结构体数组,DataDirectory中的重定位表项保存着第一个重定位表的rva,遍历每一个重定位表,并遍历重定位表中的表项,根据其重定位类型,执行重定位操作。

1
2
3
4
5
6
7
8
9
typedef struct {
    WORD offset : 12;
    WORD type : 4;
} RELOC;
typedef struct {
    DWORD VA;
    DWORD size;
    // RELOC reloc[];
} IMAGE_BASE_RELOCATION;

其中每一个重定位表保存着一个rva,重定位实际上就是遍历IMAGE_BASE_RELOCATION的成员reloc,然后执行*(rva+baseAddr+reloc[i].offset) += baseAddr - ImageBase

思路

两层循环,遍历重定位表,再遍历每个表的 RELOC reloc[]。然后根据重定位类型进行重定位。

实现代码

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
// 解析pe,并计算offset
PIMAGE_DATA_DIRECTORY pDDBaseReloc = &pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION pBaseRelocation;
ULONG_PTR offset = (ULONG_PTR)pNewDosHeader - (ULONG_PTR)pNtHeaders->OptionalHeader.ImageBase;
 
if (pDDBaseReloc->Size) {
    DWORD size = pDDBaseReloc->Size;
    pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pNewDosHeader + pDDBaseReloc->VirtualAddress);
 
    // 遍历重定位表结构体
    while (size && pBaseRelocation->SizeOfBlock) {
 
        DWORD va = (DWORD)pNewDosHeader + pBaseRelocation->VirtualAddress;
        DWORD num = (pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC); // 计算reloc[]大小
        PIMAGE_RELOC reloc = (PIMAGE_RELOC)((DWORD)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
 
        // 遍历reloc[],根据重定位类型重定位
        while (num--) {
            DWORD type = reloc->type;
            if (type == IMAGE_REL_BASED_HIGH) {
                *(WORD*)(va + reloc->offset) += HIWORD(offset);
            }
            else if (type == IMAGE_REL_BASED_LOW) {
                *(WORD*)(va + reloc->offset) += LOWORD(offset);
            }
            else if (type == IMAGE_REL_BASED_HIGHLOW) {
                *(DWORD*)(va + reloc->offset) += (DWORD)offset;
            }
 
            reloc++;
        }
 
        size -= pBaseRelocation->SizeOfBlock;
        pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pBaseRelocation + pBaseRelocation->SizeOfBlock);
    }
}

7. 跳转到ep

跳转到dll的ep。实际上就是执行dll原本的_DllMainCRTStartup函数。该函数会完成一些初始化工作并转到dllMain,让我们的dllMain像正常dllmain那样运行,但又不在peb中留下dll加载的痕迹。

1
2
3
4
5
6
7
typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);
 
PVOID entryPoint = (PVOID)((DWORD)pNewDosHeader + pNewNtHeaders->OptionalHeader.AddressOfEntryPoint);
 
pNtFlushInstructionCache((HANDLE)-1, NULL, 0);
 
((DLLMAIN)entryPoint)((HMODULE)pNewDosHeader, DLL_PROCESS_ATTACH, lpParameter);

8. 返回

最后返回entrypoint。

参考

https://github.com/rapid7/ReflectiveDLLInjection


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞5
打赏
分享
最新回复 (4)
雪    币: 523
活跃值: (827)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
笑熬浆糊 2 2021-4-11 11:35
2
0

感谢分享,不过好像还不完美,SEH 会挂吧 

最后于 2021-4-11 11:36 被笑熬浆糊编辑 ,原因: 错字
雪    币: 2819
活跃值: (3678)
能力值: ( LV5,RANK:63 )
在线值:
发帖
回帖
粉丝
wx_御史神风 2021-4-11 14:13
3
0
笑熬浆糊 感谢分享,不过好像还不完美,SEH&nbsp;会挂吧&nbsp;
刚刚开始学,还不了解seh
雪    币: 394
活跃值: (3999)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
0x太上 2021-4-11 15:49
4
0
内存注入一般不支持异常处理
雪    币: 374
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
gostrmao 2023-8-14 17:02
5
0
 给映像分配空间,并加载pe头。这句话是要重新为已经写入内存的dll在申请一段空间,重新写入吗?
游客
登录 | 注册 方可回帖
返回