背景
最近为了解决一个兼容问题,看了下Windows 10 20H1的内核,发现了一处有点意思的改动。
简单来说, ntdll.dll过去映射在System进程以及其他所有进程内的时候,都是以可执行页面进行映射的。但从19577开始,在System进程内,NTDLL开始映射为只读页面,而在其他所有进程内,NTDLL仍映射为可执行页面。
初步检查VADs
这点可以用Windbg来印证:
切换到System进程后,查看VADs
1: kd> !vad
VAD level start end commit
9769a248 ( 2) e0 e0 0 Mapped READWRITE Pagefile-backed section
9769a2a0 ( 1) f0 f0 0 Mapped READWRITE Pagefile-backed section
94bdf6c0 ( 2) 100 100 0 Mapped READWRITE Pagefile-backed section
8c266770 ( 0) 3d0 579 0 Mapped Exe READONLY \Windows\System32\ntdll.dll
86177288 ( 1) 7ffe0 7ffe0 1 Private READONLY
86177078 ( 2) 7ffe7 7ffe7 1 Private READONLY
Total VADs: 6, average level: 2, maximum depth: 2
可以看到,映射的ntdll.dll是READ ONLY
那么我们切到其他进程,例如smss,查看VADs,可以看到这里ntdll.dll映射的是EXECUTE_WRITECOPY
1: kd> !vad
VAD level start end commit
90aff2a0 ( 3) 630 630 0 Mapped READONLY Pagefile-backed section
8ec7afb0 ( 2) 650 66c 0 Mapped READONLY Pagefile-backed section
90a72a08 ( 3) 670 6af 5 Private READWRITE
8ec7ada0 ( 1) 6b0 6e0 0 Mapped READONLY \Windows\System32\C_936.NLS
8ec7abe8 ( 3) 6f0 6f2 0 Mapped READONLY \Windows\System32\l_intl.nls
90a72cd8 ( 2) 700 708 1 Private READWRITE
90a72eb8 ( 4) 710 74f 5 Private READWRITE
90a72e58 ( 3) 750 78f 5 Private READWRITE
a32a52b8 ( 4) 790 7cf 5 Private READWRITE
90a72a68 ( 0) 800 9ff 5 Private READWRITE
8ec7ab38 ( 3) aa0 abb 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\smss.exe
8ec7af58 ( 2) ac0 2abf 8 Mapped NO_ACCESS Pagefile-backed section
90a72d38 ( 3) 2c00 2dff 15 Private READWRITE
8ec7ab90 ( 1) 775f0 77799 9 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
90a72d08 ( 4) 7f960 7f968 2 Private READWRITE
90a72d68 ( 3) 7f970 7f971 2 Private READWRITE
8ec7adf8 ( 4) 7f980 7f980 0 Mapped READONLY Pagefile-backed section
90a72948 ( 2) 7ffe0 7ffe0 1 Private READONLY
90a72a98 ( 3) 7ffe7 7ffe7 1 Private READONLY
Total VADs: 19, average level: 3, maximum depth: 4
Windbg的!vad指令是简单使用_MMVAD->VadFlags.Protection来判断其页面的保护属性的。我们查看对应VAD结构也可以获得相同结论:
1: kd> dt _MMVAD 8c266770 -b
nt!_MMVAD
+0x000 Core : _MMVAD_SHORT
+0x000 NextVad : 0x9769a2a0
+0x004 ExtraCreateInfo : 0x86177288
+0x000 VadNode : _RTL_BALANCED_NODE
+0x000 Children :
[00] 0x9769a2a0
[01] 0x86177288
+0x000 Left : 0x9769a2a0
+0x004 Right : 0x86177288
+0x008 Red : 0y0
+0x008 Balance : 0y00
+0x008 ParentValue : 0
+0x00c StartingVpn : 0x3d0
+0x010 EndingVpn : 0x579
+0x014 ReferenceCount : 0n0
+0x018 PushLock : _EX_PUSH_LOCK
+0x000 Locked : 0y0
+0x000 Waiting : 0y0
+0x000 Waking : 0y0
+0x000 MultipleShared : 0y0
+0x000 Shared : 0y0000000000000000000000000000 (0)
+0x000 Value : 0
+0x000 Ptr : (null)
+0x01c u : <unnamed-tag>
+0x000 LongFlags : 0xa0
+0x000 VadFlags : _MMVAD_FLAGS
+0x000 Lock : 0y0
+0x000 LockContended : 0y0
+0x000 DeleteInProgress : 0y0
+0x000 NoChange : 0y0
+0x000 VadType : 0y010
+0x000 Protection : 0y00001 (0x1)
+0x000 PreferredNode : 0y000000 (0)
+0x000 PageSize : 0y00
+0x000 PrivateMemory : 0y0
以下省略
可以看到Protection 是1 , 这里的Protection是Mm内部转换为Index的MM_XXXX保护值,具体来说:
MM_EXECUTE_WRITECOPY = 7
MM_READONLY = 1
NTDLL映射过程
那么,具体是哪里的代码影响了不同进程的NTDLL的保护属性呢?
这里要介绍下系统是如何加载映射NTDLL的。
在Windows操作系统启动过程中,OS LOADER会将OS KERNEL(ntoskrnel.exe),HAL等等,以及BOOT 驱动加载到内存中,但是BOOT驱动一开始不会得到执行, 而是会在IO系统初始化过程中,由IoInitSystem函数进行初始化并执行。
在执行BOOT和系统驱动的之前, 系统要为这些驱动创建System进程环境(在Win10中使用IoInitSystemPreDrivers实现),其中一项就是加载NTDLL.DLL到系统进程的用户内存中。
加载NTDLL.DLL使用的是PsLocateSystemDll,该函数遍历PspSystemDlls常量内的DLL(X64下还需要加载WOW64的NTDLL),找到NTDLL路径后,为其创建Section,最后使用PsMapSystemDll进行映射,实现加载。
这是NTDLL.DLL第一次被加载,其后其他进程加载NTDLL则是使用另一条路径,当非system进程创建时,系统会调用MiMapProcessExecutable,将进程的可执行文件映射到内存中,同时,也会调用PspMapSystemDlls->PsMapSystemDll将NTDLL映射到内存中,此时的section会使用第一次加载时,保存的section object(使用fast reference存储和管理)。
在WRK中,我们可以看到,PsMapSystemDll的原型为:
NTSTATUS
PsMapSystemDll (
IN PEPROCESS Process,
OUT PVOID *DllBase OPTIONAL,
IN LOGICAL UseLargePages
)
然而在Windows 10中,这个函数在最后新增了一个参数,可以称为bFirstInit吧,指示了PsMapSystemDll是从IoInitSystem过来的初次加载,还是从MiMapProcessExecutable过来的后续映射。
首先我们可以看看,上个版本(19564)是如何处理这个参数的:
status = MmMapViewOfSectionEx(
Section,
Process,
(int)&MappedBase,
(int)&ZeroBits,
(int)&CommitSize,
UserLargePages != 0 ? MEM_LARGE_PAGES : 0,
4,
(int)&v12,
2,
0,
0,
UserLargePages != 0 ? MEM_LARGE_PAGES : 0);
ObFastDereferenceObject(v5, Section);
if ( status != 1073741827 )
goto LABEL_5;
if ( Process != PsInitialSystemProcess )
{
status = STATUS_CONFLICTING_ADDRESSES;
LABEL_5:
if ( status < 0 )
return status;
}
if ( bFirstInit )
{
status = 0;
ntheaders = RtlImageNtHeader(MappedBase);
_DllInfo = DllInfo;
*(_DWORD *)(DllInfo + 20) = ntheaders->OptionalHeader.ImageBase;
*(_DWORD *)(_DllInfo + 24) = MappedBase;
return status;
}
这里可以看到对于bFirstInit的处理,仅仅是在映射后(可看到对UseLargePages的处理),如果是第一次加载,要将ImageBase和映射后的Base保存到DLL信息中。
19577的代码改动
但是检查19577开始(包括以后版本)的PsMapSystemDll代码,可以看到:
AllocationType = UseLargePages != 0 ? MEM_LARGE_PAGES : 0;
CommmitSize = 0;
v14 = 5;
v15 = 32;
v16 = 0;
v19 = 0;
if ( bFirstInit )
AllocationType |= MEM_MAPPED;
if ( !(*(v5 + 8) & 8) )
v18 = MmHighestUserAddress;
status = MmMapViewOfSectionEx(
Section,
Process,
&MappedBase,
&ZeroBits,
&CommmitSize,
AllocationType,
2,
&v12,
2,
0,
0,
AllocationType);
ObFastDereferenceObject(v5, Section);
if ( status != STATUS_IMAGE_NOT_AT_BASE )
goto LABEL_9;
if ( Process != PsInitialSystemProcess )
{
status = STATUS_CONFLICTING_ADDRESSES;
LABEL_9:
if ( status < 0 )
return status;
}
if ( bFirstInit )
{
MappedBase_1 = MappedBase;
status = 0;
v9 = RtlImageNtHeader(MappedBase);
v10 = v25;
*(v25 + 0x14) = v9->OptionalHeader.ImageBase;
*(v10 + 0x18) = MappedBase_1;
return status;
}
这里bFirstInit除了用于标记是否保存ImageBase和映射后的base外,当=TRUE时,还会将AllocationType增加MEM_MAPPED
Mm/Mi映射的处理
那么使用MEM_MAPPED又会如何影响MmMapViewOfSectionEx的行为呢?
我们一路追下去看MmMapViewOfSectionEx->MiMapViewOfSectionExCommon->MiMapViewOfSection->MiMapViewOfImageSection的行为
在映射镜像的处理中,我们可以追到相应的代码:(这部分改动早于19577)
if ( !MapSectionParameters || *(MapSectionParameters + 32) & MEM_RESET || *(v8 + 20) & MEM_MAPPED )
{
if ( a7 != 1 )
return STATUS_INVALID_PAGE_PROTECTION;
_memflags |= 0x800u;
mem_flags = _memflags;
}
可以看到,这里(MapSectionParameters是Mi处理过程中将参数AllocationType整理打包到栈上)判断AllocationType如果含有MEM_MAPPED或MEM_RESET,则会将函数的一个内部标记 增加 0x800。
而对于这个0x800的标记,其后是这样处理的:
flags = v25 & 0xFFFFF0FF | 0x80;
vad->Core.PushLock.Value = 0;
vad->Core.u.LongFlags = flags;
if ( mem_flags < 0x800 )
{
flags |= 0x380u;
vad->Core.u.LongFlags = flags;
vad->Core.u1.LongFlags1 ^= (BugCheckParameter2 ^ vad->Core.u1.LongFlags1) & 0x7FFFFFFF;
}
在这里代码我们可以看到,首先会将VadFlags初始化添加0x80,这里因为Protection的位置在Flags的第7位开始,所以0x80>>7 = 1,即为MM_READONLY
如果mem_flags没有0x800的标记,也就是正常映射,没有MEM_MAPPED的情况下,会为VadFlags添加0x380, 0x380>>7 = 7,即为MM_EXECUTE_WRITECOPY
至此,我们就弄明白了为什么System进程(首次加载)中的NTDLL会是Read Only的属性,而其他进程中的,会是Execute Write Copy
想法
至于为什么会有这个改动,猜测有可能是为了避免在System进程地址空间中遗留可定位的可执行内存?
为什么不干脆取消NTDLL在system进程的映射?猜测是还有一些第三方内核驱动还需要NTDLL中的数据。
Comments