题记:
从Win7开始,创建进程的函数变为了nt!NtCreateUserProcess(R0),看了科锐的《64位Windows创建64位进程逆向分析》受到了一些启发,由于自己研究了内存管理很久,所以我想尽自己一点薄力(我不是科锐的...hhhh),填补这个过程中文件映射的部分,同时也填补我上一篇《XX之NTDLL随机化“逆向”(XP系统)》文章中所留下的一些空白。
重要的事情说三遍:大佬勿喷、大佬勿喷、大佬勿喷! 自己是个业余选手..hh..,为了写这篇文章,怕嘴闲着,去买了一扎葡萄28元!!! 我家的猪都不敢这么卖....
正文:
由于是文件映射,所以必然少不了这几位主角:FILE_OBJECT、SEGMENT、SECTION、CONTROL_AREA,外加用户层访问内存的入口VAD(BITMAP就不提了它只是VAD的一种“缓存”方式)。由于上述文章中没有过多的提及关于文件映射的这块内容,所以我必须自己在nt!NtCreateUserProcess函数流程的海洋中寻找与这些对象相关的函数影子。
寻找相关流程思路:
研究系统:由于头铁,所以直接拿了一个Win10 x64 1909或20HX的版本研究(搭建虚拟机太麻烦),所以我就以此版本为研究对象。
寻找相关函数的思路有两种:
第一种,对特定对象中的特定字段进行下断点(这种需要知道相关对象的创建时机),然后进行栈回溯查看周围的堆栈情况。
第二种,直接从头开始从下找,人肉筛选其重点函数(参考之前一些前辈的文章)。
我选择的是第二种,但是也引用了一些技巧。首先我对nt!NtCreateUserProcess函数进行下断点,断下后,使用 uf /c /D 地址,查看此函数的的下一层的函数调用关系,然后进行人肉筛选一些不重要的函数。
uf /c /D 地址
结果如下图:
筛选规则很简单,就是对一些解引用、内存操作、参数检查的函数一律PASS掉,因为我们的核心是文件映射相关(这里参考《64位Windows创建64位进程逆向分析》系列)。
经过筛选最终如下图(此时还没有动态调试哦~):
开始调试验证流程和探究其细节:
提出问题和猜想:
由于在《XX之NTDLL随机化“逆向”(XP系统)》文章中我提到了,在Win7乃至Win10中,有3种情况:
将一个exe程序重复启动,查看其基址
第一次启动:
在桌面移动一下坐标再次启动:
将一个exe移动一下再回到原来位置启动,查看其基址。
第一次启动:
此时,我将这个exe移动(剪切的方式)到某个盘符下,再移动回来。
很明显,此时,对于同一个软件这个基址就发生了变化。
3. 通过修改exe一些字节,进行重新运行
没改之前运行:
修改一些字节:
很明显,此时,exe在一个位置,但是其内容发生了变化,也会导致其基址改变。
通过这三个实验我想提出的观点是:对于文件加载是存在“缓存”机制的。而问题是如何进行缓存呢?这是下文开始探讨的问题。
调试相关流程分析:
首先我给出各个对象的框架关系图:
接下来按照所过滤的函数流程开始逐层分析,验证上面图片中对象的关系和寻找如何进行“缓存”?
第一个分析的便是:IoCreateFileEx函数,该函数会生成一个FILE_OBJECT对象。通过分析可以得出IopCreateFile的第一个参数是file_object的Handle,所以只需跟踪即可。
经过分析会得出以下结果:
此时已经产生FILE_OBEJCT、SEGMENT、CONTROL_AREA对象。此时图形更新为:
由于此时还没有创建EPROCESS和SECTION对象,所以用户层是无法看到映射的内容的。所以重点就在于nt!MmCreateSpecialImageSection函数的身上。
我先简单的划分一下,可以看的更清楚一些,最终图如下:
由于我是直接运行了一个exe程序,所以必然流程走的是MiCreateImageOrDataSection函数。
struct CREATE_SECTION_PACKET
{
ULONG Flags;
DWORD Unknown04;
POBJECT_ATTRIBUTES InputObjectAttributes;
ULONG AllocateAttributes;
ULONG InputAllocationAttributes;
UCHAR InputSectionSignatureLevel;
BYTE Unknown19;
WORD Unknown1A;
ULONG InputSectionPageProtection;
ULONG PageProtectionMask;
DWORD Unknown24;
HANDLE InputFileHandle;
PFILE_OBJECT InputFileObject;
PFILE_OBJECT FileObject;
CONTROL_AREA* SectionControlArea;
KPROCESSOR_MODE InputPreviousMode;
BYTE Unknown49[67];
DWORD Unknown8C;
SECTION* SectionObject;
PLARGE_INTEGER MaximumSize;
PACCESS_TOKEN InputToken;
DWORD InputSessionId;
DWORD UnknownAC;
MI_PARTITION* Partition;
PIRP TopLevelIrp;
BYTE UnknownC0;
BYTE UnknownC1[3];
DWORD UnknownC4;
};
NTSTATUS __fastcall MiReferenceControlArea(
CREATE_SECTION_PACKET* CreateSectionPacket,
CONTROL_AREA* ControlArea,
CONTROL_AREA** ControlAreaOut)
{
CONTROL_AREA* controlArea;
//...
fileObject = CreateSectionPacket->FileObject;
/*
检索Section Object指针。 如果 SEC_IMAGE 使用 ImageSectionObject 否则使用 DataSectionObject
*/
controlArea = fileObject->SectionObjectPointer->DataSectionObject;
if ((CreateSectionPacket->AllocateAttributes & SEC_IMAGE) != 0)
{
controlArea = fileObject->SectionObjectPointer->ImageSectionObject;
}
//...
//
// 一些非常丑陋的锁循环和验证。
//
//...
*ControlAreaOut = controlArea;
return STATUS_SUCCESS;
//...
}
NTSTATUS __fastcall MiCreateImageOrDataSection(
CREATE_SECTION_PACKET* CreateSectionPacket)
{
NTSTATUS status;
PFILE_OBJECT fileObject;
CONTROL_AREA controlArea;
CONTROL_AREA* newControlArea;
//...
fileObject = CreateSectionPacket->InputFileObject;
if (fileObject)
{
//
// 已经提供了文件对象,请使用它。
//
goto HaveFileObject;
}
if ((allocationAttributes & SEC_LARGE_PAGES) == 0)
{
//
// 从输入文件句柄获取文件对象。
//
status = ObReferenceObjectByHandle(
CreateSectionPacket->InputFileHandle,
MmMakeFileAccess[CreateSectionPacket->PageProtectionMask & 7],
IoFileObjectType,
CreateSectionPacket->InputPreviousMode,
&fileObject,
NULL);
if (!NT_SUCCESS(status))
{
goto Exit;
}
if (!fileObject->SectionObjectPointer)
{
//
// 如果使用了文件句柄并且没有为它创建的节,这是一个失败条件。
//
status = STATUS_INVALID_FILE_FOR_SECTION;
goto Exit;
}
:HaveFileObject
//...
//
// 在数据包和本地 CONTROL_AREA 中存储一些信息以维护状态以供进一步调用。
//
ObfReferenceObject(fileObject);
CreateSectionPacket->FileObject = fileObject;
controlArea.u.LongFlags = 2;
controlArea.FilePointer.Value = fileObject;
newControlArea = NULL;
//...
while (1)
{
//...
//
// Go reference the correct control area.
// 去参考正确的控制区域。
//
status = MiReferenceControlArea(CreateSectionPacket,
&controlArea,
§ionControlArea);
if (NT_SUCCESS(status))
{
break;
}
if ((status == 0xC000060B) || (status == 0xC0000476))
{
//
// The control area is not charged or is invalid.
// 这个控制区域已经无效
//
goto Exit;
}
}
CreateSectionPacket->SectionControlArea = sectionControlArea;
if ((sectionControlArea->u.LongFlags & 2) != 0)
{
//
// 我们有section控制区域,其中将包含参考部分。 现在,去创建一个新的。
//
status = MiCreateNewSection(CreateSectionPacket,
&newControlArea);
if (NT_SUCCESS(status)))
{
//...
CreateSectionPacket->SectionControlArea = newControlArea;
goto Exit;
//...
Exit:
//...
return status;
}
伪代码参考:https[:]//github.com/jxy-s/herpaderping/blob/main/res/DivingDeeper.md
首先会判断是否存在FILE_OBEJCT对象,如果有的话直接跳转,不需要再通过输入的句柄获取其FILE_OBJECT对象(没有的话,需要走这一步)。
其次默认设置一些标志已经字段,但是最重要的是BeingCreated标志位。
后续会根据此标志位(BeingCreated = 1)来判断是否创建新的CONTROL_AREA。此时也会修改掉_SEGMENT.BaseAddress字段,所以这里就不使用缓存。
此时图更新为:
实验验证:
先运行一个exe一次,然后记录其基址:
然后在nt!NtCreateUserProcess中下断,再次启动该程序,跟踪上述流程,并寻找_Segment.BaseAddress。
不难发现,第二次启动程序使用了第一次启动后缓存的_SEGMENT,所以基址保持不变。并且查看BeingCreated标志,也会发现此时该标志为0。
接下来再次启动该程序,并将其BeingCreated标志置为0,查看其基址又是如何的情景呢?(此时我是将提出的问题1,2合并来进行测试)。将exe剪切移动到一个位置后,再移动回来,查看其BeingCreated标志。
为了更加了解_SEGMENT.BaseAddress如何来的,所以继续顺着流程往下跟踪。(注解:ImageBase来源于_SEGMENT.BaseAddress,所以我一般会混合称呼。
对于新创建的SEGMENT流程来说:
nt!MiCreateNewSection->nt!MiRelocateImage->nt!MiSelectImageBase最终生成_SEGMENT.BaseAddress。
其中生成的算法为:
充满好奇心的同学,就如我一样,肯定会心中产生一个疑问,那么便是为什么,这里会有两种产生随机地址的流程呢?那么让我们继续顺藤摸瓜,向上查找
接着,就会寻找到如下图的关系:
在nt! MiSelectImageBase会判断_CONTROL_AREA.u2.e2.ImageBaseOkToReuse字段是否为1,说明当这个字段为1的时候,那么也会进行重定位。这个情况是发生在_CONTROL_AREA“缓存的情况”下(为什么是这个情况呢?因为另一种情况会重新创建Segment),即发生下:
这就得出了一个结论,如果ImageBaseOkToReuse标志在置位的情况下,即时存在CONTROL_AREA的“缓存”,那么也会进行重新计算BaseAddress,所以可以看到在这个分支中会出现一个独特的函数,即:nt!MiSwitchBaseAddress。
当nt!MiSelectImageBase返回后,会返回到nt!MiRelocateImageAgain函数中,
nt!MiRelocateImageAgain函数会判断返回的BaseAddress值与原来的_SEGMENT.BaseAddress中的值是否一致,如果不一致的话,那么就会调用nt!MiSwitchBaseAddress 函数更新_SEGMENT。
但是这里还有一个小细节,就是它会判断ImageActive标志位,这个位顾名思义(我猜的),代表的是当前进程是否存活。为了突出重点,所以我标出了流程中关键的部分,具体图如下:
细分析如下:
在nt!MiRelocateImageAgain函数中,首先判断 _control_area .u2.e2.ImageActive。是否为1(即)当前进程是否存活。如果不是退出状态,则会调用nt!MiSelectImageBase函数来获取基址。
如果获取到新的BaseAddress后,则将其和老的进行比较,不相同,则调用 nt!MiSwitchBaseAddress来更新_SEGMENT。
对于其他的流程,后续总结的时候简要说明一下,nt!PspAllocateProcess函数流程可以参考《64位Windows创建64位进程逆向分析》。
总结:
对于复用CONTROL_AREA“缓存”的情况图:
对于重新创建SEGMENT的图:
简单说一句,如果想让内存在用户层可见,都需要调用nt!MiMapViewOfImageSection函数,这个函数主要作用就是创建_SECTION对象,并将_SEGMENT.BaseAddress函数映射到用户层可见部分。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2021-8-10 22:29
被烟花易冷丶编辑
,原因: