首页
社区
课程
招聘
[原创]文件映射之地址随机化原理(Win10 x64)-“缓存”
2021-8-10 21:15 9764

[原创]文件映射之地址随机化原理(Win10 x64)-“缓存”

2021-8-10 21:15
9764

题记

      从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种情况:

  1. 将一个exe程序重复启动,查看其基址

    第一次启动:

         在桌面移动一下坐标再次启动:


  2. 将一个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, 
                                            &sectionControlArea);
            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 被烟花易冷丶编辑 ,原因:
收藏
点赞12
打赏
分享
最新回复 (13)
雪    币: 1290
活跃值: (2332)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
灵幻空间 2021-8-10 21:22
2
0
前来捧场,厉害!
雪    币: 1226
活跃值: (1605)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
烟花易冷丶 2021-8-10 21:25
3
0
这个话题懂了的话,对于process herpaderping来说,很好理解。就是利用了这种“缓存”的机制进行注入,此外,对于一些内存的缓存问题,也是利用的是SECTION这套等价的机制+外加集群读取(也称为预取),只不过添加了缓存管理。而对于“随机化”的深入算法逆向,可以参考一些国外的论文。这篇文章...我默认是对内存映射了解的。所以就没多提了!
雪    币: 3776
活跃值: (5379)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
独钓者OW 2 2021-8-10 21:33
4
0
立马来捧场了,每篇都值得细细研究
雪    币: 1124
活跃值: (1996)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oday小斯 2021-8-11 09:37
5
0
感谢分享
雪    币: 6977
活跃值: (1775)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
TopC 2021-8-11 10:11
6
0
感谢分享好文
雪    币: 4448
活跃值: (3481)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木志本柯 2021-8-11 10:43
7
0
呦西
雪    币: 68
活跃值: (3035)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
不懂就不懂 2 2021-8-16 14:25
8
0
捧场捧场
雪    币: 2081
活跃值: (3856)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
Jev0n 2021-8-16 14:54
9
0
感谢分享
雪    币: 7651
活跃值: (493)
能力值: ( LV9,RANK:610 )
在线值:
发帖
回帖
粉丝
achillis 15 2021-8-17 10:07
10
0
以前写过类似的文章,深知分析过程的不易,支持楼主!
雪    币: 1226
活跃值: (1605)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
烟花易冷丶 2021-8-17 16:55
11
0
achillis 以前写过类似的文章,深知分析过程的不易,支持楼主!
谢谢大佬的认可,看了您的很多文章, 也让我刚入门的学到了不少知识。
雪    币: 3373
活跃值: (3407)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2022-8-18 17:40
12
0
感谢分享!
雪    币: 1517
活跃值: (3290)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 2024-4-16 10:28
13
0
学习,感谢分享
雪    币: 215
活跃值: (429)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
maxps 2024-4-16 12:39
14
0
留个脚印 以后会用到
游客
登录 | 注册 方可回帖
返回