05月02, 2020

Windows 10 20H1.19577开始System进程内Ntdll的一点变化

背景

最近为了解决一个兼容问题,看了下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中的数据。

本文链接:https://blogs.360.cn/post/Windows10_19577_Ntdll_in_SystemProcess.html

-- EOF --

Comments