A quick insight into the Driver Signature Enforcement

I have recently had some fun playing around with driver signing on Windows x64, and so I like to share some matters that have came into my head ;) Therefore, let me briefly describe some internal mechanisms lying behind well known Driver Signature Enforcement, a significant part of the Code Integrity feature introduced by Microsoft in Windows Vista and Windows 7. Understanding the underlying system behavior would let us think of possible attack vectors against the protection, as well as better apprehend the existing techniques, such as the ones developed by Joanna Rutkowska or Alex Ionescu. Let the fun begin!

Note: All the information included therein are based upon Windows 7 executables and are not guaranteed to be valid for previous Windows NT systems.

Introduction

According to Microsoft:

Code Integrity is a feature that improves the security of the operating system by validating the integrity of a driver or system file each time it is loaded into memory. Code Integrity detects whether an unsigned driver or system file is being loaded into the kernel, or whether a system file has been modified by malicious software that is being run by a user account with administrative permissions. On x64-based versions of the operating system, kernel-mode drivers must be digitally signed.

What the above quotation really informs about, is that the only device drivers that can be actually executed within ring-0 must be digitally signed – otherwise, the nt core simply refuses to load such an image. This rule applies to every single module, which desires to run with the kernel privileges. In fact, launching a device driver is the only legitimate way, in which a system user can execute ring-0 code – and this very only way is disabled for unsigned executable images. The most interesting thing, however, is that not only restricted users are forbidden to load unsigned code (which they couldn’t do before anyway, neither in x64 nor x86 mode), but the entire Administrator’s group, as well. This, in turn, means that the privileges assigned to a user account don’t play an important role anymore, in this context – the ability to load unsigned code was taken away from every system user!

The official purpose of introducing such restrictions was to make the OS more secure (by preventing ring-0 malware from pwning the system from inside), get rid of possible anti-DRM solutions and overall, make our computers a better place to work. As a few years have passed and we’re still alive, it seems like Code Integrity is doing well. Even though this feature is obligatory, and running on most of the systems nowadays, it is still possible to temporarily turn the mechanism off (which is the only reasonable idea for device driver developers, testing their work out):

  • Pressing F8 during Boot Time, and choosing the Disable Driver Signature Enforcement boot option or,
  • Specifying the /DEBUG boot flag using bcdedit.exe plus attaching a Remote Kernel Debugger during, of after the system boot-up.

Both alternatives obviously require very high user privileges, as well as rely on rebooting the machine in order to take effect. As turning the machine off and on again might be somewhat inconvenient (while attaching a remote, physical debugger is even worse), as well as is relatively hard to perform programatically (unless someone decides to create a bootkit, which would choose the right boot option without the user’s knowledge). Due to this situation, a brand new type of Elevation of Privileges attack arises: “Admin to Kernel transition”. Some experts tend to consider security flaws belonging to this class as “bugs only”, while others see them as a full-fledged vulnerabilities.

A few attacks against Code Integrity have been performed in the past, involving design and implementation flaws found in certain parts of the Windows kernel. These include:

  1. Employing paged-out kernel code (i.e. executable parts of drivers, found in Pageable sections), which has been previously overwritten inside pagefile.sys (by utilizing raw disk access).
  2. Exploiting security flaws existing in common, already-signed device drivers, such as graphic card drivers (e.g. ATI, nVidia vendors).

As we already know what we’re dealing with, let’s take a look at how the mechanism works internally.

Initialization

The actual heart of Code Integrity lies inside a single executable image, called CI.dll (you can find it inside your \Windows\system32 directory). If we take a look at the list of imported symbols, we will most likely see the following names:

  • CiCheckSignedFile
  • CiFindPageHashesInCatalog
  • CiFindPageHashesInSignedFile
  • CiFreePolicyInfo
  • CiGetPEInformation
  • CiInitialize
  • CiVerifyHashInCatalog

What shouldn’t be a surprise, the first function within our interest is the initialization routine, CI!CiInitialize. This routine is imported by the NT core (ntoskrnl.exe), and called during system initialization:

VOID SepInitializeCodeIntegrity()
{
  DWORD CiOptions;

  g_CiEnabled = FALSE;
  if(!InitIsWinPEMode)
    g_CiEnabled = TRUE;

  memset(g_CiCallbacks,0,3*sizeof(SIZE_T));

  CiOptions = 4|2;

  if(KeLoaderBlock)
  {
    if(*(DWORD*)(KeLoaderBlock+84))
    {
      if(SepIsOptionPresent((KeLoaderBlock+84),L"DISABLE_INTEGRITY_CHECKS"))
        CiOptions = 0;
      if(SepIsOptionPresent((KeLoaderBlock+84),L"TESTSIGNING"))
        CiOptions |= 8;
    }

    CiInitialize(CiOptions,(KeLoaderBlock+32),&g_CiCallbacks);
  }
}

The above C-like pseudocode presents the general idea of the SepInitializeCodeIntegrity routine. As can be seen, some global nt!g_CiEnabled variable is being set to FALSE / TRUE, depending on whether the machine is booting up in the WinPE mode. Furthermore, CiOptions is initialized accordingly to the system boot options, and finally passed to the CiInitialize routine, together with a pointer to KeLoaderBlock and a global g_CiCallbacks array. A complete call-stack, from the very beginning of the Phase1 thread initialization follows:

nt!SepInitializeCodeIntegrity
nt!SepInitializationPhase1+0x1a1
nt!SeInitSystem+0x29
nt!Phase1InitializationDiscard+0x7ce
nt!Phase1Initialization+0xd
nt!PspSystemThreadStartup+0x9e
nt!KiThreadStartup+0x19

If we decide to go one level deeper, inside the CI!CiInitialize function, several actions taken by the initialization routine can be observed:

  • First of all, a self-integrity test is being performed, by calling the CI!CiFipsCheck function. This function is a wrapper of CI!MincrypK_SelfTest, which eventually verifies the digital signature assigned to the module. If any anomaly is encountered, the 0xc0000428 (STATUS_INVALID_IMAGE_HASH) error code is returned,
  • If the self-test passes, the nt!g_CiCallbacks, passed by ntoskrnl.exe is filled in the following way:
    g_CiCallbacks[0] = CI!CiValidateImageHeader;
    g_CiCallbacks[1] = CI!CiValidateImageData;
    g_CiCallbacks[2] = CI!CiQueryInformation;
  • In the very end, the CI!PESetPhase1Initialization function is called, which aims to validate the signature of every single driver present on the Boot Driver List. In case of any errors, an adequate error code is returned and the booting process is halted.

From this point now on, the Code Integrity mechanism can be considered pretty much initialized. The most important thing about CiInitialize is that it passes three function pointers back to the nt core, which can be later used to validate respective, custom drivers as they try to be loaded.

Driver Signature Verification

Having the essential pointers already initialized, let’s take a look at which point the driver loading is disrupted by Code Integrity. We can safely assume that the nt!MmLoadSystemImage routine is reached before the loading process bails out – this is pretty much the first function to call if one wants to load a device driver. The function is used by nt!NtSetSystemInformation (when SystemInformationClass is equal either 28 or 38), so that it can be easily taken advantage of by a user-mode applications. When trying to load an unsigned driver, we end up with the following call-stack, until an error is encountered:

    1. nt!MmLoadSystemImage
      The routine loads a specific executable image into kernel memory.
    2. nt!MiObtainSectionForDriver
    3. nt!MiCreateSectionForDriver
    4. nt!MmCheckSystemImage
      The routine ensures that the system module to be loaded has a correct checksum, which matches the data in the image.
    5. nt!NtCreateSection
    6. nt!MmCreateSection
    7. nt!MiValidateImageHeader
    8. nt!SeValidateImageHeader
      The routine ensures that the system module has a valid digital signature appended.
    9. nt!_g_CiCallbacks[0]
      e.g. CI!CiValidateImageData

As the above shows, after going through a long chain of internal calls, the execution reaches the nt!SeValidateImageHeader routine, which performs the following:

  1. Checks, whether nt!g_CiEnabled is set to TRUE
    1. If so, compares the nt!g_CiCallbacks[0] pointer to NULL
      1. If not empty, calls the nt!g_CiCallbacks[0] function and quits,
      2. Otherwise, returns 0xc0000428.
    2. Otherwise:
      1. Allocates one byte on the Paged Pools,
      2. Puts the resulting address into memory pointed to by the first argument,
      3. Returns STATUS_SUCCESS (or whatever zero means here).

Surprisingly, that’s the end of the signature validation (the actual code responsible for performing the verification lies inside CI.dll). Conclusions:

The decision on whether a driver can or cannot be launched is up to one function, checking one, single variable. If one wanted to turn the mechanism off, it would be up to altering one single byte (or even better, bit!) within the ntoskrnl image. This could prove useful for a malicious, already-signed driver aiming at getting rid of Code Integrity mechanism, overall. Furthermore, potential attackers could also utilize a previously-found write-what-where vulnerability in one of the common device drivers, in order to overwrite the flag and open a gate straight into ring-0 execution – the latter option could be utile in terms of admin->kernel escalation, where disabling CI is the final goal of the exploit.

Neither nt!g_CiEnabled nor nt!g_CiCallbacks are exported symbols. Thus, it might be relatively hard to overwrite either of these values reliably, in a cross-version exploit (as long as hardcoded offsets are not provided for every single Windows version).

The two remaining item – nt!g_CiCallbacks[0] and nt!g_CiCallbacks[1] – filled inside CI!CiInitialize are used by the nt!SeValidateImageData and nt!SeCodeIntegrityQueryInformation functions, respectively.

Driver Verification Debugging Options

Even tough it is impossible to disable Code Integrity by any other way than the two described at the beginning (F8 option or attached debugger), one is able to do the opposite – enforce the mechanism to work, even if a remote debugger is attached to the machine – certainly a fair option for some of the device driver developers.

As MSDN states:

In some cases, developers may want to enforce mandatory kernel mode code signing policy even when a debugger is attached. An example of this is when a driver stack has an unsigned driver (such as a filter driver) that fails to load, which may invalidate the entire stack. Since attaching a debugger allows the unsigned driver to load, the problem appears to vanish as soon as the debugger is attached. Debugging this type of issue may be difficult. In order to facilitate debugging in this case, Code Integrity supports a registry key that can be set to enforce kernel mode signing enforcement even when a debugger is attached.

There are two flags defined in the registry that control Code Integrity behavior under the debugger. The flags are not defined by default.

Create registry value as follows:

Key:   HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CI
Value:   DebugFlags      REG_DWORD

Possible values:

00000001
Results in a debug break into the debugger and unsigned driver is allowed to load with g.
00000010
CI will ignore the presence of the debugger and unsigned drivers are blocked from loading.

Any other value results in unsigned drivers loading—this is the default policy.

Conclusion

In this short post, I wanted to introduce bits of information that brought my attention during the last one or two days. Obviously, there is still a lot of interesting material to cover, related to Code Integrity subject, which I might further describe in the near future. As for now – feel encouraged to read the articles listed below… and that’s it.

Have fun && Leave comments!

References & Links

  1. Joanna Rutkowska, Subverting Vista Kernel for Fun and Profit
  2. Joanna Rutkowska, Vista RC2 vs. pagefile attack (and some thoughts about Patch Guard)
  3. ZDNET, UPDATE: ATI driver flaw exposes Vista kernel to attackers
  4. MSDN, Digital Signatures for Kernel Modules on Systems Running Windows Vista
  5. Microsoft, Code Integrity (ci.dll) Security Policy
  6. Matthew Conover, Assessment of Windows Vista Kernel-Mode Security

2 thoughts on “A quick insight into the Driver Signature Enforcement”

Comments are closed.