MapViewOfSection and Shared-Memory

之前对于 UnmapViewOfFile 的分析中留了一个坑,即使用 SECTION 在进程之间共享内存的情况。拖了几个星期,终于把这个问题稍稍搞明白了一点。这里将相关的知识点记录一下。

问题描述

微软对于 UnmapViewOfFile 绕过 ACG 这种攻击方式的补丁是在 JIT 进程端移除了 VirtualAlloc/VirtualProtect 操作,从而切断了将其他属性内存转化为可执行页的路径。于是笔者产生了疑问:在不使用 VirtualAlloc/VirtualProtect 的情况下 Render 中的内存页是如何 commit 的

在打上补丁之后,内存的分配操作只剩下 AllocLocalView ,查看函数源码

1
2
3
4
5
6
AllocLocalView{
//...
process = GetCurrentProcess();
NtdllLibrary::Instance->MapViewOfSection(sectionHandle, process, &address, NULL, viewSize, &mapOffset, &viewSize, NtdllLibrary::ViewUnmap, NULL, flags);
//...
}

代码中调用 MapViewOfSection 在当前进程中为这个用来共享内存的 Section ,其中 CommitSize 参数为 viewSize。实际调试 Edge, 在这个调用上下断点 JIT 进程断在此处,此时 Render 中的虚拟内存地址已经由之前那次 Map 操作确定了,查看 Render 进程中对应的虚拟地址空间

1
2
3
4
5
6
7
8
9
0:001> dd 1ec`e6240000
000001ec`e6240000 ???????? ???????? ???????? ????????
000001ec`e6240010 ???????? ???????? ???????? ????????
000001ec`e6240020 ???????? ???????? ???????? ????????
000001ec`e6240030 ???????? ???????? ???????? ????????
000001ec`e6240040 ???????? ???????? ???????? ????????
000001ec`e6240050 ???????? ???????? ???????? ????????
000001ec`e6240060 ???????? ???????? ???????? ????????
000001ec`e6240070 ???????? ???????? ???????? ????????

执行这次函数调用,再次查看 Render 进程地址,发现此时对应的虚拟地址已经映射了物理地址

1
2
3
4
5
6
7
8
9
0:001> dd 1ec`e6240000
000001ec`e6240000 00000000 00000000 00000000 00000000
000001ec`e6240010 00000000 00000000 00000000 00000000
000001ec`e6240020 00000000 00000000 00000000 00000000
000001ec`e6240030 00000000 00000000 00000000 00000000
000001ec`e6240040 00000000 00000000 00000000 00000000
000001ec`e6240050 00000000 00000000 00000000 00000000
000001ec`e6240060 00000000 00000000 00000000 00000000
000001ec`e6240070 00000000 00000000 00000000 00000000

内存区对象

这个问题需要了解内存区对象 SECTION。SECTION 是在 Windows 平台上两个或多个进程之间共享内存的的一种常用方法,它可以被映射到系统的页面文件或者其他的文件中,它是 Windows 中的一种内核对象,由对象管理器来分配和释放。以下是内存区对象的定义

1
2
3
4
5
6
7
8
9
10
typedef struct _SECTION {
MMADDRESS_NODE Address;
PSEGMENT Segment;
LARGE_INTEGER SizeOfSection;
union {
ULONG LongFlags;
MMSECTION_FLAGS Flags;
} u;
MM_PROTECTION_MASK InitialPageProtection;
} SECTION, *PSECTION;

Address 成员是内存区的 VAD 节点,用于把所有特定类型的内存区对象组织成一颗平衡树;Segment 成员指向一个段对象,段对象是内存区对象的主体;SizeOfSection 成员是内存区的大小;Flag 表示内存区的一系列标志;InitialPageProtection 指定了内存区中页面的保护属性。

内存区的创建是由 MmCreateSection 函数来完成的,用户态调用函数 ZwCreateSection。内存区对象有两种,一种建立在页面文件的基础之上,称为页面文件支撑的内存区,另一种被映射到其他文件中,称为文件支撑的内存区,也称为文件映射对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NTSYSAPI NTSTATUS ZwCreateSection(
PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PLARGE_INTEGER MaximumSize,
ULONG SectionPageProtection,
ULONG AllocationAttributes,
HANDLE FileHandle
);
NTKERNELAPI NTSTATUS MmCreateSection (
__deref_out PVOID *SectionObject, // 接受返回值
__in ACCESS_MASK DesiredAccess, // 指定内存的访问方式 READ WRITE EXECUTE,但是在 WRK 的源码中没看到这个参数被使用~
__in_opt POBJECT_ATTRIBUTES ObjectAttributes, // 指定这个 SECTION 在对象管理器中的名称、安全描述符等
__in PLARGE_INTEGER InputMaximumSize, // 指定 SECTION 所表示的内存大小
__in ULONG SectionPageProtection, // 指定 SECTION 所表示页面的访问属性
__in ULONG AllocationAttributes, // 指定 SECTION 中内存段的分配方式 SEC_BASED SEC_RESERVE SEC_COMMIT
__in_opt HANDLE FileHandle,
__in_opt PFILE_OBJECT FileObject
);

函数的大体逻辑可以分为两部分,对象的创建 和 对象之间关联的创建。首先根据参数创建 SEGMENT 对象,SEGMENT 对象是真正描述内存区数据的对象。查看 MmCreateSection 这部分的代码可以发现,共有三个函数可以用来创建段对象,分别是 MiCreateImageFileMap、MiCreateDataFileMap、MiCreatePagingFileMap。SEGMENT 对象创建完成之后,调用对象管理器 ObCreateObject 创建一个 SECTION 对象。 最后将这些对象相互关联起来。 这里我们只关注页面文件支撑的内存区,其中的 SEGMENT 调用函数 MiCreatePagingFileMap 创建。

函数原型如下

1
2
3
4
5
6
7
NTSTATUS
MiCreatePagingFileMap (
OUT PSEGMENT *Segment, // 返回段对象
IN PUINT64 MaximumSize, // 待创建的段对象大小
IN MM_PROTECTION_MASK ProtectionMask, // ZwCreateSection 的 SectionPageProtection 转变而来
IN ULONG AllocationAttributes // ZwCreateSection 的 AllocationAttributes
)

SEGMENT 对象本身是在换页内存池中分配的,它包含了段内页面数量以及字节数,用于描述此段内页面映射的原型 PTE 阵列等信息,并且有一个指针指向一个控制区对象 CONTROL_AREA,其数据结构如下所示

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
typedef struct _SEGMENT {
struct _CONTROL_AREA *ControlArea; // 控制区对象
ULONG TotalNumberOfPtes; // Pte 数量
ULONG NonExtendedPtes;
ULONG Spare0;
UINT64 SizeOfSegment; // SEGMENT 大小 = Pte 数量 * PageSize
MMPTE SegmentPteTemplate;
SIZE_T NumberOfCommittedPages;
PMMEXTEND_INFO ExtendInfo;
SEGMENT_FLAGS SegmentFlags;
PVOID BasedAddress;
union {
SIZE_T ImageCommitment; // for image-backed sections only
PEPROCESS CreatingProcess; // for pagefile-backed sections only // 记录创建这个 SEGMENT 的进程
} u1;
union {
PSECTION_IMAGE_INFORMATION ImageInformation; // for images only
PVOID FirstMappedVa; // for pagefile-backed sections only
} u2;
PMMPTE PrototypePte; // 指向 ThePtes
MMPTE ThePtes[MM_PROTO_PTE_ALIGNMENT / PAGE_SIZE]; // 原型 PTE 阵列
} SEGMENT, *PSEGMENT;
typedef struct _CONTROL_AREA {
PSEGMENT Segment;
LIST_ENTRY DereferenceList;
ULONG NumberOfSectionReferences; // All section refs & image flushes
ULONG NumberOfPfnReferences; // valid + transition prototype PTEs
ULONG NumberOfMappedViews; // total # mapped views, including
// system cache & system space views
ULONG NumberOfSystemCacheViews; // system cache views only
ULONG NumberOfUserReferences; // user section & view references
union {
ULONG LongFlags;
MMSECTION_FLAGS Flags;
} u;
PFILE_OBJECT FilePointer;
PEVENT_COUNTER WaitingForDeletion;
USHORT ModifiedWriteCount;
USHORT FlushInProgressCount;
ULONG WritableUserReferences;
#if !defined (_WIN64)
ULONG QuadwordPad;
#endif
} CONTROL_AREA, *PCONTROL_AREA;

查看 MiCreatePagingFileMap 函数逻辑,函数首先确定原型 PTE 阵列中需要多少个 PTE 表项,即计算这个 SEGMENT 管理几个内存页。如果此时传入的参数 AllocationAttributes 中含有 COMMIT 属性,函数会直接调用 MiChargeCommitment 确认有足够的物理页面文件。如果 AllocationAttributes 同时含有 LARGE_PAGE 属性,则会调用 MiAllocateLargeZeroPages 直接在物理内存中分配大块空内存页。

接着函数会计算 SEGMENT 对象本身需要的内存大小,包括原型 PTE 阵列所占用的空间,然后调用 ExAllocatePoolWithTag 申请一块换页内存池中的内存。MiCreatePagingFileMap 又申请一个 CONTROL_AREA 对象,包括一个子内存区对象 SUBSECTION,这是在非换页内存中申请的。然后初始化 SEGMENT 和 CONTROL_AREA 中的成员,将 SECTION 和 CONTROL_AREA/SUBSECTION 相互关联起来。最后初始化原型 PTE。 将传入的 ProtectionMask 参数保存在 SEGMENT 的 SegmentPteTemplate->Protection 中。 如果 AllocationAttributes 含有 COMMIT 属性,将原型 PTEs 的 Protection 属性设置为 ProtectionMask,如果还含有 SEC_LARGE_PAGES属性,函数则直接映射好物理内存。其他情况下,所有的原型 PTE 内容至空。

SECTION、SEGMENT、CONTROL_AREA 之间的相互关联如图

仅有 SECTION 对象是无法访问内存的,还需要为内存区对象在应用程序中映射一个视图。使用函数 NtMapViewOfSection 对应于内存管理器函数 MmMapViewOfSectionMmMapViewOfSection 根据 SECTION 所对应的 CONTROL_AREA 中的标志信息 ControlArea->u.Flags 来确定映射 SECTION 的函数。对于页面支撑的内存区通过函数 MiMapViewOfDataSection 来映射内存区。

现在我们来看 MiMapViewOfDataSection 函数的逻辑,函数原型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS
MiMapViewOfDataSection (
IN PCONTROL_AREA ControlArea,
IN PEPROCESS Process,
IN PVOID *CapturedBase,
IN PLARGE_INTEGER SectionOffset,
IN PSIZE_T CapturedViewSize,
IN PSECTION Section,
IN SECTION_INHERIT InheritDisposition,
IN ULONG ProtectionMask,
IN SIZE_T CommitSize,
IN ULONG_PTR ZeroBits,
IN ULONG AllocationType
)

函数的主要任务是把 SECTION 中描述的存储资源映射到指定进程的地址空间中。函数首先需要确定 SECTION 在目标进程中的地址范围,调用函数 MiFindEmptyAddressRange/MiFindEmptyAddressRangeDown 在进程的虚拟地址空间中找到一段能包含指定大小的地址范围。接着申请一个 MMVAD 对象,使用刚刚找到的地址初始化 VAD 对象,并将其和 SECTION 以及 CONTROL_AREA 关联起来。然后记录各种系统开销,并且把 VAD 节点插入到目标进程的 VAD 树中。如果参数中 CommitSize 不为 0 ,那么函数还需要保证这部分大小的内存页已经被 commit 了,这里的做法是依次将所有要提交部分 PTE 设置成 Segment->SegmentPteTemplateMiMapViewOfDataSection 最后在 PROCESS 中更新虚拟地址的信息,完成整个函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vad->StartingVpn = MI_VA_TO_VPN (StartingAddress);
Vad->EndingVpn = MI_VA_TO_VPN (EndingAddress);
Vad->FirstPrototypePte = TheFirstPrototypePte; // TheFirstPrototypePte = &Subsection->SubsectionBase[PteOffset];
// Subsection->SubsectionBase = &NewSegment->ThePtes[0];
// NewSegment->PrototypePte = PointerPte;
Vad->ControlArea = ControlArea;
Vad->u.VadFlags.Protection = ProtectionMask; // MiMapViewOfDataSection 参数
// ......
PointerPte = Vad->FirstPrototypePte;
LastPte = PointerPte + BYTES_TO_PAGES(CommitSize);
TempPte = Segment->SegmentPteTemplate;
while (PointerPte < LastPte) {
if (PointerPte->u.Long == 0) {
MI_WRITE_INVALID_PTE (PointerPte, TempPte); // 这里实际修改的就是 SEGMENT 对象中的 PTES
}
else {
QuotaExcess += 1;
}
PointerPte += 1;
}

结合前面的描述可以看出,在 CreateSection 和 MapView 阶段,设置 COMMIT 属性只是在对应 SEGMENT 的原型 PTE 中设置好 protection 属性,并没有实际的映射物理内存页。实际的内存映射工作由缺页异常来处理。

内存页面交换

当 x86 处理器在执行一个执行流过程中需要翻译一个虚拟地址引用时,如果该地址的 PTE 中的有效位为 0 ,则处理器会触发一个异常,也称为 Page-Fault。这个异常会交由缺页异常处理程序,分配一个物理页面,并设置好 PTE。这里我们不会过分关注缺页异常处理的所有细节,重点查看其对于 SECTION->SEGMENT->PrototypePTE 的处理。

缺页异常处理函数为 MmAccessFault,其主要工作是进行各种检查,判断缺页地址对应的 PTE 是否有效,是否为空等等。在当前场景下 PTE 中仅有 Protection 属性,即非有效的 PTE 也不是全零的 PTE。此时考虑其为一个原型 PTE。 记录下原型 PTE 的 Protection。经过层层检查之后函数最终会调用 MiDispatchFault 去实际处理缺页异常。函数对于原型 PTE 调用函数 MiResolvePotoPteFault 进行处理。MiResolvePotoPteFault 根据原型 PTE 的状态做相应的处理,它可能已经有效了,也可能是一个“要求零页面”的 PTE 了,或者是一个映射文件 PTE,或者在转移状态,或者在页面文件中。所以它会根据具体情况选择调用 MiResolveDemandZeroFault(向内存要求一个页面)、MiResolveTransitionFaultMiResolvePageFileFault或者 MiResolveMappedFileFault

《Windows 内核原理与实现》书上的例子可以很好的说明,原型 PTE 在 SECTION 中的用法。如图所示,两个进程 A 和 B 共享一个 SECTION,它们分别拥有该内存区对象的一个 View。SECTION 中的页面 P1 已经被两个进程都访问过了,因此该页面对应的原型 PTE 是一个有效的 PTE,指向物理内存中的页面;P1 在两个进程中的 PTE 也是有效的,指向物理页面。页面 P2 位于页面文件中,所以它对应的 PTE 是无效的,两个进程中对应于 P2 的 PTE 也因该是无效的,它们指向该页面的原型 PTE。 此时 A 进程访问了 P2,那么内存管理器会为 P2 分配一个物理页面,并且让页面 P2 在进程 A 中的 PTE 指向该物理页面,并且 P2 在 SEGMENT 中的原型 PTE 也指向该物理页面。以后, B 进程要访问页面 P2 时,依然会产生缺页异常,但是中断响应程序会发现这是一个指向原型 PTE 的无效 PTE,它检查原型 PTE,知道页面 P2 已经在物理内存了,于是让 P2 在进程 B 中的 PTE 也指向此物理页面,从而两个进程访问的是同一个物理页面 P2。

实际调试

笔者按照 Edge 中共享内存的方式,编写了一个测试程序进行实际调试。首先调用 ZwCreateSection 创建一个 SEC_RESERVE 状态的 Section,接着使用这个 SECTION 在另一个进程中 Map 一块虚拟地址空间,此时远程进程的虚拟地址空间仍然没有 COMMIT。最后在本地进程中 Map 这个 SECTION,并为其提供 CommitSize 参数,在本地进程中 COMMIT 一块内存。

1
2
3
4
5
6
7
8
9
10
hTarget = GetProcessHandle("SectionTest2.exe");
NtCreateSection(&hSection, SECTION_MAP_EXECUTE | SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_QUERY, &objAttrs, &sectionSize, PAGE_EXECUTE_READWRITE, SEC_RESERVE, 0);
ZwMapViewOfSection(hSection, hTarget, &remoteAddr, NULL, NULL, NULL, &viewSize, ViewUnmap, NULL, PAGE_EXECUTE_READWRITE);
LARGE_INTEGER mapOffset = { 0 };
ZwMapViewOfSection(hSection, GetCurrentProcess(), &localAddr, NULL, 0x1000, &mapOffset, &viewSize, ViewUnmap, NULL, PAGE_EXECUTE_READWRITE);
*(PDWORD)localAddr = 0xffffff;

调试环境: Win8 x86。 内核调试,先在 ZwCreateSection 上下断点

1
2
3
4
5
6
7
8
9
kd> !process 0 0 SectionViewTest.exe
PROCESS 9a46b040 SessionId: 1 Cid: 0af0 Peb: 7f15d000 ParentCid: 0a44
DirBase: 3e1d24e0 ObjectTable: a3487b00 HandleCount: <Data Not Accessible>
Image: SectionViewTest.exe
kd> .process /p /i 9a46b040
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> bp /p 9a46b040 nt!MmCreateSection

记录下函数返回的 SECTION 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kd> dt nt!_SECTION_OBJECT a11273c8
+0x000 StartingVa : 0x00018981 Void
+0x004 EndingVa : 0x00018982 Void
+0x008 Parent : (null)
+0x00c LeftChild : (null)
+0x010 RightChild : 0xffd10b48 Void
+0x014 Segment : 0x9f6825f0 _SEGMENT_OBJECT
kd> dt nt!_SEGMENT 0x9f6825f0
+0x000 ControlArea : 0x8fe62bd8 _CONTROL_AREA
+0x004 TotalNumberOfPtes : 0x10
+0x008 SegmentFlags : _SEGMENT_FLAGS
+0x00c NumberOfCommittedPages : 0
+0x010 SizeOfSegment : 0x10000
+0x018 ExtendInfo : (null)
+0x018 BasedAddress : (null)
+0x01c SegmentLock : _EX_PUSH_LOCK
+0x020 u1 : <unnamed-tag>
+0x024 u2 : <unnamed-tag>
+0x028 PrototypePte : 0x8a2dd848 _MMPTE
kd> dd 0x8a2dd848
8a2dd848 00000000 00000000 00000000 00000000
8a2dd858 00000000 00000000 00000000 00000000

可以看到对象结构与 WRK 中相比有些变化,但是关键部分不影响,这里可以看到,以 SEC_RESERVE 状态创建的 SECTION 其原型 PTE 被初始化为空。

在 MapView 函数上下断点,执行完函数,返回远程虚拟地址 0xD00000,查看 SECTION 状态以及远程进程中对应的虚拟地址空间,原型 PTE 依然为空

remote.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kd> dt nt!_SEGMENT 0x9f6825f0
+0x000 ControlArea : 0x8fe62bd8 _CONTROL_AREA
+0x004 TotalNumberOfPtes : 0x10
+0x008 SegmentFlags : _SEGMENT_FLAGS
+0x00c NumberOfCommittedPages : 0
+0x010 SizeOfSegment : 0x10000
+0x018 ExtendInfo : (null)
+0x018 BasedAddress : (null)
+0x01c SegmentLock : _EX_PUSH_LOCK
+0x020 u1 : <unnamed-tag>
+0x024 u2 : <unnamed-tag>
+0x028 PrototypePte : 0x8a2dd848 _MMPTE
kd> dd 0x8a2dd848
8a2dd848 00000000 00000000 00000000 00000000
8a2dd858 00000000 00000000 00000000 00000000
kd> .process /i /p 8e93fa40
kd> dd d00000
00d00000 ???????? ???????? ???????? ????????
00d00010 ???????? ???????? ???????? ????????

在本地进程上 MapView,返回本地地址空间 0x90000 ,查看 SECTION 状态以及虚拟地址

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
kd> dt nt!_SECTION_OBJECT a11273c8
+0x000 StartingVa : 0x00018981 Void
+0x004 EndingVa : 0x00018982 Void
+0x008 Parent : (null)
+0x00c LeftChild : (null)
+0x010 RightChild : 0xffd10b48 Void
+0x014 Segment : 0x9f6825f0 _SEGMENT_OBJECT
kd> dt nt!_SEGMENT 0x9f6825f0
+0x000 ControlArea : 0x8fe62bd8 _CONTROL_AREA
+0x004 TotalNumberOfPtes : 0x10
+0x008 SegmentFlags : _SEGMENT_FLAGS
+0x00c NumberOfCommittedPages : 1 ; Changed
+0x010 SizeOfSegment : 0x10000
+0x018 ExtendInfo : (null)
+0x018 BasedAddress : (null)
+0x01c SegmentLock : _EX_PUSH_LOCK
+0x020 u1 : <unnamed-tag>
+0x024 u2 : <unnamed-tag>
+0x028 PrototypePte : 0x8a2dd848 _MMPTE
kd> dd 0x8a2dd848
8a2dd848 000000c0 00000000 00000000 00000000 ; Changed
8a2dd858 00000000 00000000 00000000 00000000
kd> .process /p /i 8e93fa40
kd> ?? (char *)@$proc->ImageFileName
char * 0x8e93fbb0
"SectionTest2.e"
kd> dd d00000
00d00000 ???????? ???????? ???????? ????????
00d00010 ???????? ???????? ???????? ????????
kd> .process /p /i 9a46b040
kd> ?? (char *)@$proc->ImageFileName
char * 0x9a46b1b0
"SectionViewTes"
kd> dd 900000
00900000 ???????? ???????? ???????? ????????
00900010 ???????? ???????? ???????? ????????

可以看到 MapView 之后,虽然 SECTION 已经产生了相应更改,但是原型 PTE 中只是记录了 Protection 属性,在进程空间中,相应的虚拟地址空间也都没有映射到物理内存。

在 SectionViewTest 的 MmAccessFault 上下断点,访问 SectionViewTest 地址 0x900000,函数最终调用 MiResolveDemandZeroFault 函数完成任务,函数从物理内存管理器中申请内存页然后修改 PTE,最后调用 MiCompleteProtoPteFault 将原型 PTE 应用到进程的 PTE 中。查看 SECTION 中的原型 PTEs,此时已经被修改为一个有效的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kd> ?? (char *)@$proc->ImageFileName
char * 0x9a46b1b0
"SectionViewTes"
kd> k
# ChildEBP RetAddr
00 a14cf618 81a57d45 nt!MiResolveDemandZeroFault+0x743
01 a14cf688 81a55ef0 nt!MiResolveProtoPteFault+0x5a9
02 a14cf748 81a543af nt!MiDispatchFault+0x246
03 a14cf7d0 81b88654 nt!MmAccessFault+0x232
kd> dd 0x8a2dd848
8a2dd848 022cb963 00000000 00000000 00000000
8a2dd858 00000000 00000000 00000000 00000000
8a2dd868 00000000 00000000 00000000 00000000

单步调试执行完 MiCompleteProtoPteFault 之后,查看本地地址 0x900000 和远程地址 0xd00000,情况如下

1
2
3
4
5
6
7
8
9
10
11
kd> dd 900000
00900000 00000000 00000000 00000000 00000000
00900010 00000000 00000000 00000000 00000000
kd> .process /p /i 8e93fa40
kd> ?? (char *)@$proc->ImageFileName
char * 0x8e93fbb0
"SectionTest2.e"
kd> dd d00000
00d00000 ???????? ???????? ???????? ????????
00d00010 ???????? ???????? ???????? ????????

对 SectionTest2 的 MmAccessFault 下断点,访问其地址 0xd00000 ,函数检查原型 PTE,知道页面已经在物理内存了,于是让 SectionTest2 地址 0xd00000 的 PTE 也指向此物理页面,从而两个进程访问的是同一个物理页面。查看地址以及地址对应的 PTE

1
2
3
4
5
6
7
8
9
10
11
kd> ?? (char *)@$proc->ImageFileName
char * 0x8e93fbb0
"SectionTest2.e"
kd> dd d00000
00d00000 00000000 00000000 00000000 00000000
00d00010 00000000 00000000 00000000 00000000
kd> dd c0005480
c0005480 022cb825 00000000 00000000 00000000
c0005490 00000000 00000000 00000000 00000000

总结

这次的内容涉及到了很多内核部分代码,调试起来很复杂,前后断断续续花了很多时间,不过也算是解决了我心中的一点疑惑。

最后在调试的时候被川神批评了看起来这些东西意思不大的样子

Reference

[1] https://kelvinhack127.blogspot.com/2016/03/windows-sectionmmcreatesectionmmmapview.html
[2] https://blog.csdn.net/qq_18218335/article/details/65626899
[3] https://blog.csdn.net/qq_18218335/article/details/65635039