Geoff Chappell - Software Analyst
CURRENT WORK ITEM - PREVIEW ONLY
The PS_SYSTEM_DLL_INIT_BLOCK structure (formally _PS_SYSTEM_DLL_INIT_BLOCK) is the type of a variable named LdrSystemDllInitBlock that is exported by name from NTDLL.DLL in version 6.2 and higher. NTDLL exports this variable not so that it is imported by any user-mode software but by the kernel. Having loaded NTDLL into a newly created process, the kernel locates this variable and populates the structure with information for NTDLL to use when it gets to execute.
It happens, but is surely not necessary to the design, that NTDLL treats this structure not just as input for wider configuration but as data for continued use. That this data is then easily located (for being exported) seems to have been soon regarded as problematic. Starting with version 6.3, NTDLL is built with the LdrSystemDllInitBlock variable and other mutable read-only data in a section named .mrdata. The whole section is initially read-write, but very soon into NTDLL’s initialisation it is made read-only—and failure to make it so is fatal.
That “m” in the section’s name stands for mutable is inferred from the name of a relevant internal routine, LdrpProtectMutableReadOnlyData, as known from public symbols for the Windows 8.1 NTDLL. Its point is that NTDLL itself has occasional need to change the data and so the read-only protection is not permanent. Whenever NTDLL seeks write access to data in this section, it unprotects the data and then re-protects it. That protection has page granularity means that changes to other data in this section can have as a side-effect that the LdrSystemDllInitBlock also can be temporarily writable. This side-effect is perhaps even more likely in Windows 10 which unprotects and re-protects not just one page each time but the whole section. Either way, though the LdrSystemDllInitBlock is ordinarily read-only in version 6.3 and higher, it can sometimes be written to.
Neither the PS_SYSTEM_DLL_INIT_BLOCK structure nor the LdrSystemDllInitBlock variable is documented. This is no surprise: they are plainly intended as a private matter between the kernel and NTDLL.
The variable has an extern declaration in a header that Microsoft published in the Windows Driver Kit (WDK) for Windows 10 in its original and Version 1511 editions. Publication was almost certainly an oversight. The header, named wow64t.h, does not compile without error. One cause is that the variable’s declaration gives PS_SYSTEM_DLL_INIT_BLOCK as the variable’s type but this type is not referenced in this or any other WDK header—not even to be defined as opaque.
The practical equivalent of a C-language definition is available as type information in symbol files that Microsoft publishes for debugging support. Here too there is some sense of the obscure. Although the structure exists only for the kernel and NTDLL, it does not appear in the symbol files for either the kernel or NTDLL. It can instead be found in a smattering of symbol files for higher-level user-mode modules. These actually are private symbols whose distribution in downloadable packages of public symbols and through Microsoft’s public symbol server may be another oversight. Still, published they are.
Unsurprisingly for a structure that’s surely intended as private to two vital Windows components that ought never to be mismatched, the PS_SYSTEM_DLL_INIT_BLOCK varies between versions. As seen below, the structure’s first member in all versions is a size in bytes. The kernel checks the size, and It has never yet happened that a change within the structure has not also changed the size:
Versions | Size |
---|---|
6.2 | 0x60 |
6.3 | 0x70 |
10.0 to 1607 | 0x80 |
1703 | 0xD0 |
1709 to 1903 | 0xE0 |
2004 | 0xF0 |
Note that the structure is identical for 32-bit and 64-bit Windows.
The sizes in the preceding table and the offsets and definition in the table that follows are from public symbols as noted above.
Offset | Definition | Versions |
---|---|---|
0x00 |
ULONG Size; |
6.2 and higher |
0x04 (6.2 to 1607); 0x08 |
ULONG SystemDllWowRelocation; |
6.2 to 1607 |
ULONGLONG SystemDllWowRelocation; |
1703 and higher | |
0x08 (6.2 to 1607); 0x10 |
ULONGLONG SystemDllNativeRelocation; |
6.2 and higher |
0x10 (6.2 to 1607); 0x18 |
ULONG Wow64SharedInformation [0x10]; |
6.2 to 1607 |
ULONGLONG Wow64SharedInformation [0x10]; |
1703 and higher | |
0x50 (6.2 to 1607); 0x98 |
ULONG RngData; |
6.2 and higher |
0x9C |
union { ULONG Flags; struct { ULONG CfgOverride : 1; ULONG Reserved : 31; }; }; |
1703 and higher |
0x58 (6.2 to 1607); 0xA0 |
ULONGLONG MitigationOptions; |
6.2 to 1607 |
PS_MITIGATION_OPTIONS_MAP MitigationOptionsMap; |
1703 and higher | |
0x60 (6.2 to 1607); 0xB0 (1703 to 1903); 0xB8 |
ULONGLONG CfgBitMap; |
6.3 and higher |
0x68 (6.2 to 1607); 0xB8 (1703 to 1903); 0xC0 |
ULONGLONG CfgBitMapSize; |
6.3 and higher |
0x70 (6.2 to 1607); 0xC0 (1703 to 1903); 0xC8 |
ULONGLONG Wow64CfgBitMap; |
10.0 and higher |
0x78 (6.2 to 1607); 0xC8 (1703 to 1903); 0xD0 |
ULONGLONG Wow64CfgBitMapSize; |
10.0 and higher |
0xD0 (1709 to 1903); 0xD8 |
PS_MITIGATION_AUDIT_OPTIONS_MAP MitigationAuditOptionsMap; |
1709 and higher |
The Size must be correct by the time the kernel inspects it in the loaded NTDLL. This means in practice that it must be set in the static initialisation of the LdrSystemDllInitBlock variable. Everything else in the variable is set or cleared by the kernel before NTDLL gets to execute.
Some members, e.g., the MitigationOptions, are read by NTDLL only while initialising. Others, notably the RngData and the CfgBitMap, are live. For instance, NTDLL’s own tests for Control Flow Guard read the CfgBitMap from the LdrSystemDllInitBlock.
The MitigationOptions are bits, at first in one ULONGLONG but eventually spreading to a second and then to a third. To go by names, Microsoft defines some seemingly applicable bits in WINBASE.H from the Software Development Kit (SDK). They are, of course, not defined there for their role in the PS_SYSTEM_DLL_INIT_BLOCK. Which of them are meaningful in the MitigationOptions of a PS_SYSTEM_DLL_INIT_BLOCK, to whom in what circumstances in which versions, requires some study. For now, the table below offers only the few that are obviously meaningful to the initialising NTDLL.
Mask | Name | Versions |
---|---|---|
0x00000000`00000010 | unknown | 6.2 and higher |
0x00000000`00000020 | unknown | 6.2 and higher |
0x00000000`00000030 | unknown | 6.2 and higher |
0x00000000`00001000 | PROCESS_CREATION_MITIGATION_POLICY_HEAP_TERMINATE_ALWAYS_ON | 6.2 and higher |
A comment in WINBASE.H has it that “Bits 0-5 are legacy bits” but then does not define any use of bits 3 and 4.