Geoff Chappell, Software Analyst
SKETCH OF HOW RESEARCH MIGHT CONTINUE AND RESULTS BE PRESENTED
The RtlGetNtGlobalFlags function gets a set of flags that govern advanced diagnostics and troubleshooting.
ULONG RtlGetNtGlobalFlags (VOID);
The function returns the flags as bits in one dword.
The RtlGetNtGlobalFlags function is exported by name from NTDLL in all known Windows versions, i.e., 3.10 and higher.
All versions of the kernel also have the RtlGetNtGlobalFlags function, but originally only as an internal routine. It is exported by name from the kernel in version 5.0 and higher. Kernel-mode callers anyway have the address of the NtGlobalFlag variable, which is exported by name from the kernel in all versions.
Though the RtlGetNtGlobalFlags function itself is not documented, the flags that the function returns have long been documented, most notably with the GFLAGS.EXE utility in Microsoft’s package of Debugging Tools for Windows.
The first list that I know Microsoft published of the defined flags is Knowledge Base article Q147314: GlobalFlags for Windows NT 3.51. Metadata in the HTML source code has it that the article was created in February 1996. As with many such articles from the time, this is long gone from Microsoft’s websites. It has symbolic names such as FLG_STOP_ON_EXECUTION for all flags that version 3.51 of the kernel accepts as valid.
The NTDDK.H from the Device Driver Kit (DDK) for Windows NT 3.51, from May 1995, has a C-language declaration of the kernel’s NtGlobalFlag variable and a macro named IF_NTOS_DEBUG whose definition shows that Microsoft’s names for the defined bits start with FLG_ but without any whole name appearing in any header. To this day, the flags are not defined in any header that Microsoft publishes with any Software Development Kit (SDK) or Windows Driver Kit (WDK) and the WDK for the 1709 release of Windows 10 (build 16299) withdraws the macro that discloses the FLG_ prefix.
The whole work of the RtlGetNtGlobalFlags function is to return one dword of flags as they currently apply to the kernel or to the current process. The immediate source of these flags is different for the kernel-mode and user-mode implementations. So too is their ultimate source as a registry value. Bits within the flags may may affect kernel-mode execution or user-mode execution or both.
The kernel-mode RtlGetNtGlobalFlags function returns the current contents of the kernel’s NtGlobalFlag variable. This is an exported variable in the kernel’s read-write data section. No kernel-mode code needs to call this function to read the contents. The kernel-mode function is here thought to exist only to simplify Run Time Library (RTL) code that is written for both kernel-mode and user-mode execution.
The kernel’s NtGlobalFlag is set initially from the registry:
As with many registry values that the kernel reads while initialising, the type is irrelevant. As many as four bytes are read: REG_DWORD is at best a guess at what seems to be intended. In versions 3.51 to 4.0, the presence of any undefined bit in the registry data means the whole dword defaults to zero. In versions 5.0 and higher, undefined bits are instead ignored—but in the sense that this is trivial starting with version 6.1, since by then all bits are defined.
Though any kernel-mode module can import the address of NtGlobalFlag, no use of this to change the variable is known in any version except through the kernel-mode debugger. Indeed, Microsoft even documents that its KD debugger’s -x switch edits NtGlobalFlag.
Subsequent change of NtGlobalFlag is mostly directed from user mode through the NtSetSystemInformation function, by specifying the information class SystemFlagsInformation (0x09) and providing new flags in a SYSTEM_FLAGS_INFORMATION structure. Different versions provide very differently for which flags can change.
For some flags that affect kernel-mode execution, a change that sets the flag is not useful (or is even dangerous) since the behaviour that the set flag might enable depends on preparation to have been done while the kernel initialised. For flags whose only effect is on user-mode execution, changing the kernel’s settings affects all new user-mode processes.
In versions before 5.0, the user-mode RtlGetNtGlobalFlags function returns the current contents of an internal NTDLL variable that public symbol files confirm is also named NtGlobalFlag. In version 5.0 and higher, the function returns the current contents of the NtGlobalFlag member of the PEB.
Wherever the function gets what it returns, the initialisation of this source when the process is created in kernel mode and then as it starts in user mode is subject to several layers of configurability. Broadly speaking, in increasing order of precedence, these can be:
The kernel’s NtGlobalFlag can be learnt from user mode through the NtQuerySystemInformation function again by specifying the information class SystemFlagsInformation (0x09) and providing a SYSTEM_FLAGS_INFORMATION to receive the current flags. Originally, this was the only way but version 3.51 introduced the NtGlobalFlag member in the PEB. In all versions since, the kernel’s NtGlobalFlag variable is the seed for the NtGlobalFlag member when the kernel creates the PEB.
Historically, the per-process GlobalFlag value is in one and only one registry key whose name is selected from the process’s filename:
|Key:||HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\filename|
|Type:||REG_SZ (all versions) or REG_DWORD (5.1 and higher)|
String data can be decimal, hexadecimal or even binary or octal according to the parsing rules for RtlUnicodeStringToInteger. Version 6.1 and higher allow that the value can instead be read from a subkey that is chosen indirectly from the whole of the process’s pathname: see LdrOpenImageFileOptionsKey for details.
The Load Configuration Directory in the process’s executable is an IMAGE_LOAD_CONFIG_DIRECTORY structure whose RVA and size can be set into the PE header as the IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10) entry in the DataDirectory. Microsoft’s linkers know to set this up for a structure that has the C-language label _load_config_used. The structure has always provided GlobalFlagsClear and GlobalFlagsSet members with which a process can specify flags that are to be clear or set (in that order) no matter what NTDLL picks up from the other sources.
How these other sources interact, and even which of them can matter, varies (widely) with the Windows version.
No interface is known for changing the per-process NtGlobalFlag after NTDLL’s initialisation. As a PEB member, it is in read-write memory but changing it does not look to be intended except perhaps when debugging. For some flags, a change that sets the flag is not useful (or is even dangerous) since the behaviour that the set flag might enable depends on preparation to have been done while NTDLL initialised.
For the following table’s reckoning of which flags are defined in which version, each bit is regarded as a defined flag if the kernel accepts it for initialising the NtGlobalFlag variable from the GlobalFlag registry value. This seems appropriate for the RtlGetNtGlobalFlags function since it allows that the bit can appear in the returned dword.
Version 3.51 brought a large-scale reorganisation of the flags. Only the first retains its numerical value from version 3.10. A few others have continuity but with numerical values that are resequenced enough so that it seems better to present the early history in a separate table.
|0x00000001||FLG_STOP_ON_EXCEPTION||3.51 and higher||see below for early history|
|0x00000002||FLG_SHOW_LDR_SNAPS||3.51 and higher||see below for early history|
|0x00000004||FLG_DEBUG_INITIAL_COMMAND||3.51 and higher|
|0x00000008||FLG_STOP_ON_HUNG_GUI||3.51 and higher|
|0x00000010||FLG_HEAP_ENABLE_TAIL_CHECK||3.51 and higher||see below for early history|
|0x00000020||FLG_HEAP_ENABLE_FREE_CHECK||3.51 and higher||see below for early history|
|0x00000040||FLG_HEAP_VALIDATE_PARAMETERS||3.51 and higher|
|0x00000080||FLG_HEAP_VALIDATE_ALL||3.51 and higher|
|0x00000100||FLG_POOL_ENABLE_TAIL_CHECK||3.51 to 5.0|
|FLG_APPLICATION_VERIFIER||5.1 and higher|
|0x00000200||FLG_POOL_ENABLE_FREE_CHECK||3.51 to 5.0|
|undefined||5.1 to 6.0|
|FLG_MONITOR_SILENT_PROCESS_EXIT||6.1 and higher|
|0x00000400||FLG_POOL_ENABLE_TAGGING||3.51 and higher||see below for early history|
|0x00000800||FLG_HEAP_ENABLE_TAGGING||3.51 and higher|
|0x00001000||FLG_USER_STACK_TRACE_DB||3.51 and higher||see below for early history|
|0x00002000||FLG_KERNEL_STACK_TRACE_DB||3.51 and higher||see below for early history|
|0x00004000||FLG_MAINTAIN_OBJECT_TYPELIST||3.51 and higher|
|0x00008000||FLG_HEAP_ENABLE_TAG_BY_DLL||3.51 and higher|
|0x00010000||FLG_IGNORE_DEBUG_PRIV||3.51 to 4.0|
|FLG_DISABLE_STACK_EXTENSION||5.1 and higher|
|0x00020000||FLG_ENABLE_CSRDEBUG||3.51 and higher||see below for early history|
|0x00040000||FLG_ENABLE_KDEBUG_SYMBOL_LOAD||3.51 and higher||see below for early history|
|0x00080000||FLG_DISABLE_PAGE_KERNEL_STACKS||3.51 and higher||see below for early history|
|0x00100000||FLG_HEAP_ENABLE_CALL_TRACING||3.51 to 4.0|
|FLG_ENABLE_SYSTEM_CRIT_BREAKS||5.1 and higher|
|0x00200000||FLG_HEAP_DISABLE_COALESCING||3.51 and higher|
|0x00400000||FLG_ENABLE_CLOSE_EXCEPTIONS||4.0 and higher|
|0x00800000||FLG_ENABLE_EXCEPTION_LOGGING||4.0 and higher|
|0x01000000||FLG_ENABLE_HANDLE_TYPE_TAGGING||4.0 and higher|
|0x02000000||FLG_HEAP_PAGE_ALLOCS||4.0 and higher|
|0x04000000||FLG_DEBUG_INITIAL_COMMAND_EX||4.0 and higher|
|0x08000000||FLG_DISABLE_DBGPRINT||5.0 and higher|
|0x10000000||FLG_CRITSEC_EVENT_CREATION||5.0 and higher|
|0x20000000||FLG_LDR_TOP_DOWN||5.1 to 6.2|
|FLG_STOP_ON_UNHANDLED_EXCEPTION||6.3 and higher|
|0x40000000||FLG_ENABLE_HANDLE_EXCEPTIONS||5.1 and higher|
|0x80000000||FLG_DISABLE_PROTDLLS||5.0 and higher|
Versions from 3.51 to 5.2 before Windows Server 2003 SP1 have both an NtGlobalFlag as an internal NTDLL variable and an NtGlobalFlag member of the PEB. Before version 5.0, the member merely provides for easy initialisation of the variable without a call to kernel mode. The internal variable is what matters, both for NTDLL’s own testing of whether to apply the features that are represented by these flags and also for what the function reveals to its callers for their own tests. The later versions get confused. The internal variable remains in use for NTDLL’s own testing for FLG_ENABLE_EXCEPTION_LOGGING but the variable is never initialised. This feature does not work in these versions.
In versions before version 3.51 the kernel accepts the GlobalFlag value in its entirety and the only sense in which bits are valid or not is whether any use is known.
A few of the bits have direct continuity with flags for which Microsoft’s names are known in version 3.51 and higher. One even continues without a change in numerical value. A few more have less continuity. A factor in this seems to have been a rethink regarding the default for the GlobalFlag value. Version 3.10 is installed with a system hive in which GlobalFlag is already 0x211A0000. Three of the set bits in these installation values act to disable the corresponding features. This means that by default, as when GlobalFlag is missing, these features are enabled. Version 3.51 flips this so that these features, which impose overheads, are enabled only if GlobalFlag is present and non-zero.
Three flags are referenced one way or another but with no non-trivial effect that is yet known.
|0x00000004 (3.10)||validate heap on call||3.10 only|
|0x00000008 (3.10 to 3.50)||3.10 to 3.50|
|0x00000010 (3.10 to 3.50)||FLG_SHOW_LDR_SNAPS||3.10 to 3.50||next as 0x00000002|
|0x00000040 (3.10 to 3.50)||disable paging the executive||3.10 to 3.50|
|0x00000200 (3.10 to 3.50)||3.10 to 3.50|
|0x00020000 (3.10 to 3.50)||3.10 only||set at installation (3.10)|
|show memory descriptor list||3.50 only|
|0x00040000 (3.10 to 3.50)||FLG_DISABLE_PAGE_KERNEL_STACKS||3.10 to 3.50||next as 0x00080000|
|0x00080000 (3.10 to 3.50)||inverted FLG_ENABLE_CSRDEBUG||3.10 to 3.50||set at installation (3.10)|
|0x00100000 (3.10 to 3.50)||inverted FLG_HEAP_ENABLE_TAIL_CHECK||3.10 to 3.50||set at installation (3.10)|
|0x00200000 (3.10 to 3.50)||FLG_USER_STACK_TRACE_DB
|3.10 to 3.50|
|0x01000000 (3.10 to 3.50)||enable OS/2 subsystem||3.10 only||set at installation (3.10)|
|FLG_POOL_ENABLE_TAGGING||3.50 only||next as 0x00000400|
|0x04000000 (3.10 to 3.50)||3.10 to 3.50|
|0x08000000 (3.10 to 3.50)||FLG_ENABLE_KDEBUG_SYMBOL_LOAD||3.10 to 3.50||next as 0x00040000|
|0x20000000 (3.10 to 3.50)||inverted FLG_HEAP_ENABLE_FREE_CHECK||3.10 to 3.50||set at installation (3.10)|
|0x80000000 (3.10)||enable BreakOnDllLoad||3.10 only|
The description above for the original 0x00000004 bit is at best a guess at the intention. If this bit is set, then in both the kernel and NTDLL an internal routine for initialising the Heap Manager sets an internal variable that symbol files name as RtlpHeapValidateOnCall but which seems otherwise to be unreferenced. More research may be needed.
The original 0x00000040 bit was given its own registry value in version 3.51: as DisablePagingExecutive in the Memory Management subkey.
Though it gets ever harder to see, all versions of the kernel even to Windows 10 provide for a plain-text display while starting. Version 3.50 interprets the 0x00020000 bit as directing that the usual summary of processors and memory should be followed with a detailed dump of the memory map that the kernel received from the loader. Why this shows in this one version of the kernel, with no trace of it in any other, I don’t know. The provenance of the version 3.50 kernel that I have for inspection is anyway uncertain.
These early versions have just the one bit, 0x00200000, for what later versions separate into FLG_USER_STACK_TRACE_DB and FLG_KERNEL_STACK_TRACE_DB. This has the problem that enabling the stack trace database for kernel mode imposes it as a user-mode overhead on all processes for which it isn’t explicitly disabled, e.g., through Image File Execution Options.
Debugging CSRSS was originally something that GlobalFlag could disable. The flip to something that needed a non-zero GlobalFlag to enable came with version 3.51. The original 0x00080000 bit had informal documentation as Knowledge Base article Q105677: Debugging the Win32 Subsystem.
The original 0x01000000 bit is needed for KERNEL32 version 3.10 to recognise OS/2 binaries as things to “execute” through the OS2.EXE program. Without it, they are treated as DOS applications. The kernel reuses the 0x01000000 bit in version 3.50 to enable what was then the new feature of tagging all pool allocations. Reorganisation for version 3.51 reassigned the new meaning to 0x00000400 and left 0x01000000 unused. This change from 3.50 to 3.51 had informal documentation as Knowledge Base article Q164933: How to Allow Poolmon.exe to Run by Setting GlobalFlag Value. This article also notes indirectly that setting 0x01000000 to enable pool tagging conflicts with other advice about clearing it to disable the OS/2 subsystem. KERNEL32 version 3.51 treats the OS/2 subsystem as always enabled. Versions 4.0 and 5.0 leave the choice to the CreateProcess caller: the CREATE_FORCEDOS flag causes an OS/2 binary to be treated as a DOS application. Version 5.1 withdrew OS/2 support.