CURRENT WORK ITEM - PREVIEW ONLY

PS_SYSTEM_DLL_INIT_BLOCK

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.

Documentation Status

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.

Variability

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.

Layout

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.

Mitigation Options

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.