首页
社区
课程
招聘
[原创]win10和win11内存区域划分及动态随机的本质
2024-6-26 14:33 5313

[原创]win10和win11内存区域划分及动态随机的本质

2024-6-26 14:33
5313

前言

在win10内存中,很多win7中的结构体都被微软废除了,取而代之的使用了一个全局的变量来存放内存相关信息。看了很多中文的资料,发现对win10内存的资料介绍甚少,仅有的几篇也是我豆总在看雪发表的,不知道是大哥们对这个不屑一顾,还是不屑一顾,win10版本马上都停止维护了,但是在我看来比较有用的知识点还是没有人点出,(英文的资料还是有的),windows虽然日薄西山把,但也不至于这么日薄西山吧。这篇文章就来说下win10 内存的一些知识点
另外丑话说前面,这篇文章算是纯逆向所得,如果出现有明显错误的观点,请直接说出你认为的正确观点及证据。

0.背景知识介绍

MiState : 这么说吧,你拿到他,win10内存基本就不用看了。 一切你认为的没有符号,都源自于你没注意到他。
MI_SYSTEM_INFORMATION : MiState的类型就是它。具体这样

MI_SYSTEM_VA_TYPE 这个是微软提供的内存类型,

引子:win10内存区域如何获取

nt内核中有一个函数为 MiGetSystemRegionType(ULONG64 va), 传入一个虚拟地址进去, 返回的是一个MI_SYSTEM_VA_TYPE枚举类型的值,该值代表了虚拟地址属于那种类型。

很明显, byte_140c6A018是一个数组,在没有对MiState的类型重新定义的时候,就是这样的,如果修改了MiState类型为MI_SYSTEM_INFORMATION,

嗯,没错是MiState.Vs.SysTemVaType,SysTemVaType ,这个就是豆总前几年说的那个0x100个标记的,另外指出一个错误。在FaEry作者的[原创]Windows内存篇Ⅱ x64内核内存布局的迁移演变-编程技术-看雪-安全社区|安全招聘|kanxue.com 文章中有错误的表示,说是从Win10 2004之后这个标记已经找不到了, 我这边拿到的是win11 23h2的内核查看的,这个依然存在。
其实到这里依旧是拾人牙慧,只不过是从符号的角度来讲,说明当初豆总说的那个mark标记在哪里。如果只是单纯的想要判断那些地址是在那个内存区域,到这里基本就不用看了,下面讲解的都是为什么能够从这个成员变量得到内存区域枚举值。

想一下,((a1 >> 0x27) & 0x1FF) - 0x100 这个偏移是如何被设计的,SysTemVaType这个成员变量是如何被设计填充。了解这些设计后面的知识,才能明白windows内存区域划分的关键。

1. SystemVaRegions成员

SystemVaRegions 是 MiState.Vs.SysTemVaType 之后的一个成员数组,这个数组的个数在win10的各个版本都存在变化的情况,我选取的win10 版本有 13个 ,SystemVaRegions 的类型为_MI_SYSTEM_VA_ASSIGNMENT ,具体定义:

1
2
3
4
5
_MI_SYSTEM_VA_ASSIGNMENT
{
  VOID* BaseAddress; //0x0 ULONGLONG
  NumberOfBytes; //0x8
};

看到这里是不是觉得开始有趣, 这个BaseAddress是不是和我们的内存区域有关。 是的,很明确的告诉,在上一节提到的 FaEry的[原创]Windows内存篇Ⅱ x64内核内存布局的迁移演变-编程技术-看雪-安全社区|安全招聘|kanxue.com,文中 提到的MiQuerySystembase函数用到的一个未导出变量,该变量为 SystemVaRegions,打开ida看一下在符号下的样子

嗯。有符号确实好看哈。但是这个结构体成员变量是如何被填充的,它是如何和SysTemVaType数组进行关联的。
这个是问题的关键。

还有一个枚举值: MI_ASSIGNED_REGION_TYPES

通过该枚举值 我们可以知道 SystemVaRegions 的13个数组成员 分别代表了什么 ,那么MI_ASSIGNED_REGION_TYPES 和 MI_SYSTEM_VA_TYPE 是如何被转换的?这也是我们需要思考的

2. MmInitSystem 函数

总所周知,windows是会分阶段初始化的, 在调用KiInitializeBootStructures过程中,会在0xff阶段的时候调用MmInitSystem函数 ,其实 MmInitSystem 函数 也是分为多个阶段被调用,现在只说和我们相关的,在0xff阶段的时候会调用 MiInitializeSystemVa
MiInitializeSystemVa 函数是负责填充 SystemVaRegions 结构体的关键函数,也是 windows内存区域能够动态初始化的关键。

MiInitializeSystemVa 函数被执行后,会调用 MiInitializeTopLevelBitmap 函数,用来初始化 一个bitmap 这个bitmap 指向了 MiState.SystemVa.SystemVaAssignment, SystemVaAssignment是一个int[8]类型的成员, 如果你留意其大小,你就会发现, SystemVaAssignment 的大小是32个字节,256bit, 还有一个 MiState.SystemVa.SystemVaAssignmentHint,请记住,这个变量是动态初始化的关键一步。
在初始化这个bitmap之后, MiInitializeSystemVa 会调用 MiAssignTopLevelRanges ,而 MiAssignTopLevelRanges 是梦最初的地方。
ps: 如果想要通过windbg 调试MmInitSystem 的0xff阶段的话,可能需要进行 额外的设置。

3. MiAssignTopLevelRanges

MiAssignTopLevelRanges 如下图所示, 其中通过逆向,对Base 这个数组的类型进行 了重定义。 通过qsort函数可知 每个BASE的结构体大小为 0x18 , 总共有13个, 上文提到过,我选取的win10内核中的SystemVaRegions也是13个。
嗯。没错,这个Base 和 SystemVaRegions 有着千丝万缕的关系。


其中我定义的Base 的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MyBase_VIsibleType
 
{
     
    unsigned __int32 index;
     
    unsigned __int32 randseed;
     
    unsigned __int64 BaseAddress;
     
    unsigned __int64 Length;
 
};  

从上面截图可以看出, Base结构体数组 ,对 结构体的 index ,randseed ,Length 都进行了 赋值。

通过MiAssignSystemVa 函数我们得到 Base数组元素中BaseAddress的值。

然后 对MiState.Vs.SystemVaRegions的数组进行初始化填充。

4 MiAssignSystemVa

MiAssignSystemVa是 动态内存区域划分的关键函数,其函数原型为
int64 MiAssignSystemVa(ULONG NumberToFind, unsigned int a2)

首先解释 MiAssignSystemVa 函数的 第一个参数,也就是我们上一节提到的。

1
2
3
v19 = (*p_Length + 0x7FFFFFFFFFi64) & 0xFFFFFF8000000000ui64;
*p_Length = v19;
vaBaseaddress = MiAssignSystemVa(v19 >> 0x27, a1);

NumberToFind 就是 length >> 0x27得到的。
MiAssignSystemVa 的主要功能是取到已经初始化好的MiState.SystemVa.SystemVaAssignment 和 已经随机化好的MiState.SystemVa.SystemVaAssignmentHint,通过RtlFindClearBitsAndSet 去查找到符合满足条件的位图索引。

其中内存区域的动态随机设置就和这个位图的find过程有关,根据以上截图我们可以看出。只有满足找到的索引值是随机值或至少满足10次循环才会跳出find过程。
最后,得到位图的索引值 ,通过 index - 0x100 ,通过左移0x27,得到baseaddress。

根据上面我们可以得出一个结论: windows内核内存区域的动态随机初始化由 两个变量影响:

  1. MiState.SystemVa.SystemVaAssignmentHint
  2. SystemVaRegions[i].NumberOfBytes
    二者通过入参的形式,影响了位图寻找索引的流程。该索引值最终影响了baseaddress 的计算。

5. MI_ASSIGNED_REGION_TYPES 和 MI_SYSTEM_VA_TYPE之间的转换

在 win10和win11 中, MiInitializeSystemVa 在调用完 MiAssignTopLevelRanges 之后,会调用 MiConvertAssignedRegionToVaType(i) 。
MiConvertAssignedRegionToVaType 的原理就是内建了一张表,通过 输入MI_ASSIGNED_REGION_TYPES类型的枚举值,然后根据表 输出 对应的MI_SYSTEM_VA_TYPE类型的值。
在win11中,MiConvertAssignedRegionToVaType被封装到了MiSetSystemRegionTypes(i)中,各个小版本之间这张表可能也不同。
放win11 23h2 和 win10的 代码。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
win10 
__int64 __fastcall MiConvertAssignedRegionToVaType(int a1)
{
 int v1; // ecx
 int v2; // ecx
 int v3; // ecx
 int v5; // ecx
 int v6; // ecx
 int v7; // ecx
 int v8; // ecx
 
 if ( a1 > 7 )
 {
   v5 = a1 - 8;
   if ( v5 )
   {
     v6 = v5 - 1;
     if ( !v6 )
       return 4i64;
     v7 = v6 - 1;
     if ( v7 )
     {
       v8 = v7 - 1;
       if ( v8 )
       {
         if ( v8 != 1 )
           return 0i64;
         return 12i64;
       }
       else
       {
         return 15i64;
       }
     }
     else
     {
       return 1i64;
     }
   }
   else
   {
     return 14i64;
   }
 }
 else if ( a1 == 7 )
 {
   return 2i64;
 }
 else if ( a1 )
 {
   v1 = a1 - 1;
   if ( v1 )
   {
     v2 = v1 - 1;
     if ( v2 )
     {
       v3 = v2 - 1;
       if ( v3 )
       {
         if ( v3 != 2 )
           return 0i64;
         return 4i64;
       }
       return 9i64;
     }
     else
     {
       return 8i64;
     }
   }
   else
   {
     return 6i64;
   }
 }
 else
 {
   return 5i64;
 }
}
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
win 11
 
__int64 __fastcall MiConvertAssignedRegionToVaType(int a1)
{
  int v1; // ecx
  int v2; // ecx
  int v3; // ecx
  int v4; // ecx
  int v6; // ecx
  int v7; // ecx
  int v8; // ecx
  int v9; // ecx
  int v10; // ecx
 
  if ( a1 > 8 )
  {
    v6 = a1 - 9;
    if ( !v6 )
      return 14i64;
    v7 = v6 - 1;
    if ( v7 )
    {
      v8 = v7 - 1;
      if ( !v8 )
        return 1i64;
      v9 = v8 - 1;
      if ( !v9 )
        return 15i64;
      v10 = v9 - 1;
      if ( !v10 )
        return 16i64;
      if ( v10 != 1 )
        return 0i64;
      return 12i64;
    }
    return 4i64;
  }
  if ( a1 == 8 )
    return 2i64;
  if ( !a1 )
    return 4i64;
  v1 = a1 - 1;
  if ( !v1 )
    return 5i64;
  v2 = v1 - 1;
  if ( !v2 )
    return 6i64;
  v3 = v2 - 1;
  if ( !v3 )
    return 8i64;
  v4 = v3 - 1;
  if ( !v4 )
    return 9i64;
  if ( v4 != 1 )
    return 0i64;
  return 17i64;
}

6. SystemVaRegions与SystemVaType的建立关联

在 MiInitializeSystemVa 函数执行完上面所提到的流程后,还会对wsl的内存区域进行一个分配。并不在我们讨论的范围,所以跳过。

随着 BaseAddress被填充到 SystemVaRegions 后, 程序会进入一个循环。 一个把 SystemVaRegions 与SystemVaType 连起来的循环。

因为ida本身反汇编识别的问题,所以我们这段是直接通过汇编手动还原成c代码。

asm:

ida 反汇编:

手动还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for ( i = 0; i < 0xD; i ++ // SystemVaRegions与SystemVaType有多少个 数组成员 就循环多少次
{
 
    BaseAddress = systemVaRegions[i].BaseAddress;
     
    Length = systemVaRegions[i].NumberOfBytes;    // 和填充长度有关
     
    offset = ((BaseAddress >> 0x27) & 0x1FF) - 0x100//这个offset 眼熟不,和MiGetSystemRegionType
                                                       // 的计算数组的偏移是一样的。
    paddinglength = Length>> 0x27
     
    value = MiConvertAssignedRegionToVaType(i); //枚举类型转换
 
    for ( j = Length >> 0x27; j; --j )
    {
     
        Systemvatype[offset] = value   // 对Systemvatype填充转换后的枚举类型
         
        offset ++ ;
     
    }
 
}

通过手动还原,我们可以知道, SystemVaRegions 与 SystemVaType 建立映射关系的步骤分为:

  1. 取出属于SystemVaRegions中数组的基地址和这个区域的长度。
  2. 根据规则算出偏移,
  3. 根据MiConvertAssignedRegionToVaType函数把 MI_ASSIGNED_REGION_TYPES 类型转换为 MI_SYSTEM_VA_TYPE 类型,
  4. 根据 offset 对Systemvatype中的数组进行填充,填充的值就是 MI_SYSTEM_VA_TYPE类型的值,填充的长度NumberOfBytes>>0x27
  5. 继续下一次循环

根据还原后的代码,我们可以看出为什么通过 MiGetSystemRegionType 函数可以获取到 指定虚拟地址的 内存区域类型。因为在 填充SystemVaType 成员的时候,就是根据规则偏移来填充的。填充的值还是SystemVaRegions经过转换后的枚举值。填充的长度也和NumberOfBytes>>0x27。

((a1 >> 0x27) & 0x1FF) - 0x100背后的设计理念:

回想一下 在引子部分提到的 :((a1 >> 0x27) & 0x1FF) - 0x100 ,我们可以进一步思考:
a1 为 一个内核的虚拟地址, a1 >> 0x27 是在取 pml4的值, (pml4 & 0x1ff) 这个是 确保能取到 pml4索引,因为pml4 占位 9bit,最终 pml4的index - 0x100 得出最终的索引。 因为虚拟地址是连续的,假设存在 nopagedpool 的内存地址 A, A必定满足 大于等于nopagedpool.baseaddress,小于等于
nopagedpool.baseaddress+nopagedpool.length,如果使用我们的符号表示:
systemVaRegions[0].BaseAddress<= A <= systemVaRegions[0].BaseAddress +systemVaRegions[0].NumberOfBytes , 所以 A地址代表的pml4索引必定Systemvatype中代表nopagedpool的连续为分区内。

所以 通过MiGetSystemRegionType去内存区域的本质是 ,通过pml4索引的值 去取已经填充好的“位图”的值。

到此 ,分析告一段落。

7. 后话

1.本篇文章通过 MiGetSystemRegionType 函数能够获取 某个虚拟地址为 引子,介绍了一下几个知识点:

  1. SystemVaType、SystemVaRegions、MI_ASSIGNED_REGION_TYPES 和 MI_SYSTEM_VA_TYPE基本类型
  2. MmInitSystem函数初始化流程。
  3. windows内核内存的区域的动态随机划分的本质是什么.
  4. MI_ASSIGNED_REGION_TYPES 和 MI_SYSTEM_VA_TYPE之间转换的关系
  5. SystemVaRegions与SystemVaType是如何建立关联的。
  6. ((a1 >> 0x27) & 0x1FF) - 0x100背后的设计理念,以及为什么可以通过MiGetSystemRegionType得出指定虚拟地址的内存类型。

本文仅用作抛砖引玉,希望广大windows内核爱好者能够基于本文的些许启发,对windows 内核的内存有更深的了解。

妈的,文邹邹写了一段,快累死了。大白话说一点。使用MiState 变量可以 搞到很多关于内存的知识点, 比如在内存初始化的0阶段, 可以通过逆向MiInitNucleus函数 清晰的看到 各个内存区域是如何被建立的,以及物理页帧的几个链表是如何被初始化的。 在逆向nt内核过程中我们其实也可以发现 也有其他的关键变量可以去读取内存的关键信息,比如 MiSystemPartition 全局变量。 一切关于内存的逆向,还看各位大佬们出手了。
(ps:使用MiState 有一个小坑, 正式版本的 MI_SYSTEM_INFORMATION 微软提供的有些许问题,需要自己改动,如果自己仔细思考的话,解决这个问题应该不在话下)。


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

收藏
免费 13
打赏
分享
最新回复 (3)
雪    币: 1365
活跃值: (1602)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
青丝梦 1 2024-6-26 21:36
2
1

我收回我说的那句话, 看来windows真的是日薄西山了。。。

最后于 2024-6-26 21:37 被青丝梦编辑 ,原因:
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_晴天_847 2024-6-28 17:29
3
0
我真不知道原来代码及长这样,看起来好深奥啊
雪    币: 2073
活跃值: (5032)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
bwner 2 5天前
4
0
感谢分享
游客
登录 | 注册 方可回帖
返回