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

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

2021-4-8 23:10
12137

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

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

我的理解是反射注入实际上就是手工加载模块。通过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文件的格式网上有比较多的图片,这里就不贴了。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

需要NtFlushInstructionCache刷新指令缓存。

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。

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

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

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

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

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

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

见实现代码注释。

完成重定位过程。

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

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

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

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

最后返回entrypoint。

https://github.com/rapid7/ReflectiveDLLInjection

 
 
 
 
 
 
(前置条件序号) 序号 流程内容
 
============
注入器:
============
(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地址
(前置条件序号) 序号 流程内容
 
============
注入器:
============
(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地址
hFile = CreateFileW(dllPathname, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
dwDllLen = GetFileSize(hFile, 0);
lpDll = HeapAlloc(GetProcessHeap(), 0, dwDllLen);
hFile = CreateFileW(dllPathname, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
dwDllLen = GetFileSize(hFile, 0);
lpDll = HeapAlloc(GetProcessHeap(), 0, dwDllLen);
 
 
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;
}
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;
}
 
 
 
#include<intrin.h>
#pragma intrinsic(_ReturnAddress)
__declspec(noinline) PVOID NextAddr()
{
    return (PVOID)_ReturnAddress();
}
#include<intrin.h>
#pragma intrinsic(_ReturnAddress)
__declspec(noinline) PVOID NextAddr()
{
    return (PVOID)_ReturnAddress();
}
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--;
}
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--;
}
 
 
 
 
 
 
 
// 找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))
// 找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);

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 6
支持
分享
最新回复 (4)
雪    币: 389
活跃值: (967)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
2

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

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