aboutsummaryrefslogtreecommitdiffstats
diff options
authorJason A. Donenfeld <Jason@zx2c4.com>2021-08-01 23:54:06 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2021-08-02 18:15:33 +0200
commit50704cf42ba536255992e80395a7bfda2b5390b3 (patch)
tree8c9c35e88eccf9bfec0d95e8cfb95731b88496da
downloaddownlevel-driver-enabler-50704cf42ba536255992e80395a7bfda2b5390b3.tar.xz
downlevel-driver-enabler-50704cf42ba536255992e80395a7bfda2b5390b3.zip
Initial commit
-rw-r--r--Makefile13
-rw-r--r--README.md191
-rw-r--r--exports.def3
-rw-r--r--shim.c34
4 files changed, 241 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..80a7ffc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+CC := x86_64-w64-mingw32-clang
+LDFLAGS += -shared
+CFLAGS += -O3
+
+all: shim.dll
+
+shim.dll: shim.c exports.def
+ $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
+
+clean:
+ $(RM) shim.dll
+
+.PHONY: all clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..58e16c6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,191 @@
+# Downlevel Driver Enabler
+**by Jason A. Donenfeld (zx2c4)**
+
+"Downlevel Driver Enabler" enables the use of Windows 10 PnP signed drivers on
+Windows 7 and 8.1.
+
+Last year, Microsoft announced that they would no longer provide intermediate
+certificates for Authenticode-signed kernel drivers after June 1, 2021. This
+prompted widespread panic, as it effectively meant that it would be now
+impossible for many drivers to issue updates — for reliability, security, or
+otherwise — on Windows 7 and 8.1. OSR ran [a blog series](https://www.osr.com/blog/2020/10/15/microsoft-driver-updates-allowed-win7-win8/)
+on this, culminating in a final post in April indicating that after much
+haggling with Microsoft PMs, the prospects of any change to the policy is
+hopeless, and the situation is a "lost cause". R.I.P. Windows 7 and 8.1 driver
+updates? Not at all, and that is what this repository presents.
+
+### Driver signing background
+
+Before Windows 10 1607, there were two ways of signing drivers: Authenticode
+signatures, in which you pay a CA for the ability to sign your own drivers, or
+Windows Hardware Compatibility Publisher signatures, in which you either run
+your driver through a battery of hardware tests, called WHLK (which OSR points
+out is impossible for most driver types), and submit the results of those tests
+to Microsoft, or more recently, simply ask Microsoft for an "attestation
+signature", which amounts to more or less the same thing without the testing
+headache. At some point Microsoft was going to require WHLK testing for Windows
+Server, but eventually gave up on that, so now attestation signatures are fine
+for both Windows 10 Client and Windows 10 Server (2016/2019). But attestation
+is only for Windows 10, which means if you want a Windows Hardware
+Compatibility Publisher signature on Windows 7 and 8.1, you must go through the
+testing that may well not be available for your driver.
+
+As an aside, it turns out you can actually use an old Authenticode certificate
+basically indefinitely — beyond the June 1, 2021 expiration date — by
+timestamping it using a bogus timestamping server, and then adding the bogus CA
+used to generate those timestamp signatures to the system's trust store.
+Evidently the requirements of timestamp CAs are less stringent than those of
+code signing CAs. While there's arguably a "safe" way of doing that, (ab)using
+expired or intermediately expired Authenticode certificates seems to go against
+the spirit of the requirements, and so it seems a bit too dirty for production.
+One could imagine getting a certificate consequently blacklisted with tricks
+like that.
+
+So, with WHLK not available for many drivers, and Authenticode no longer viable
+after June 1, 2021, it would seem the only way forward for driver updates of
+any kind is on Windows 10, using attestation signatures, and just giving up
+entirely on trying to ship security or reliability updates to Windows 7 and
+8.1. That laziness is appealing, but also not viable for real world systems
+that still require the old operating systems.
+
+It turns out that Windows 7 and 8.1 will load drivers that have been signed
+using the Windows 10 attestation service, but only if they are non-PnP (i.e. do
+not use an `.inf` and `.cat` file). That means Windows 7 and 8.1 developers of
+non-PnP drivers can simply transition to the Windows 10 attestation service
+after June 1, 2021 and all will be well. But PnP drivers — extremely common —
+are still left out in the cold. The distinction between the two driver types,
+however, provides a hint.
+
+### Driver signature verification
+
+A first inclination upon learning that non-PnP drivers can load but PnP drivers
+cannot might be that one could just write a little non-PnP rootkit driver to
+fiddle around with whatever needs fiddling with, enabling the PnP driver to
+load subsequently. That, again, seems unfortunately too dirty for production,
+and a bit intellectually lazy too. Instead it is more interesting to understand
+the _actual_ difference between the non-PnP case and PnP case.
+
+The kernel verifies drivers when they are being loaded, in order to make sure
+that untrusted code is not loaded into the most trusted part of the OS. To this
+end, the loader is concerned primarily with the signature on the `.sys` driver
+code itself, rather than any supporting userspace files around it. So, the
+signature verifier — implemented in `ci.dll` — looks at the signature in the
+`.sys` and makes sure that it chains up to a valid root in a valid way. In our
+case here, the relevant chaining is that it ends in a particular Microsoft
+certificate related to the Windows Hardware Compatibility Publisher with proper
+EKUs. If all checks out, then the driver loads. It is very simple. For this
+reason, Windows 10 attestation works on both Windows 10 and Windows 7 and 8.1.
+The kernel's verifier cares that a driver is trusted by Microsoft, since the
+relevant security boundary here involves trust, rather than which particular
+operating system it has been "certified" to run smoothly on. And if you think
+about it, that makes sense: the kernel is trying to enforce signatures as a
+means of security, in order to have a trusted boundary. The policy it cares
+about is a simple security one, rather than anything fancier or more pedantic
+about certifications or WHLK test suites or anything like that. This is a real,
+important security boundary.
+
+The userspace PnP driver store is a bit more complicated. Here, it not only
+cares about the signature of the `.sys` driver code itself, but also all of the
+other supporting userspace files, such as the `.inf` file and other programs
+the `.inf` file might instruct the OS to install. These supporting files are
+listed in a `.cat` file, and this `.cat` file is signed with the same type of
+signature as the `.sys` driver code file. But the `.cat` file also has some
+additional fields, the most relevant of which is the `OSAttr` field, which
+lists the version of Windows with which the driver has been certified or
+attested to work. The userspace PnP driver installer, `drvinst.exe`, cares
+about this, and will return `ERROR_SIGNATURE_OSATTRIBUTE_MISMATCH` (0xE0000244)
+if `OSAttr` lists a different Windows version. This is _not_ a security check.
+It is a boring policy check, and one that is not even uniformly applied, as the
+kernel's verifier does not care about it, hence the case of non-PnP drivers
+without `OSAttr` checks. And seeing that certification for Windows 7 and 8.1 is
+not even possible now, it is no longer even a _sensible_ policy check. And, to
+repeat again, this is very much _not_ a security check. It might now be
+described as an _outdated_ or _obsolete_ policy check.
+
+Many articles on similar topics would now attempt to dazzle you with colorful
+screenshots of IDA Pro, indicating the impenetrably byzantine nature of the
+following reverse engineering work. In reality, though, the analysis here is
+not overly fancy: the PnP driver installer — `drvinst.exe` — calls into
+`setupapi.dll`, which eventually finds its way to `VerifyFile`, which in turn
+calls `WinVerifyTrust(DRIVER_ACTION_VERIFY)` in `wintrust.dll`. If that
+function returns `ERROR_APP_WRONG_OS` (0x0000047F), then `VerifyFile` returns
+`ERROR_SIGNATURE_OSATTRIBUTE_MISMATCH` (0xE0000244) to its caller. Looking at
+`wintrust.dll`'s `WinVerifyTrust`, there is a dynamic function dispatch based
+on the GUID argument, which eventually leads to a call to `DriverFinalPolicy`,
+which in turn uses `CryptCATGetCatAttrInfo` and `CryptCATGetAttrInfo` to read
+the `OSAttr` field, and then sees if it matches the running OS using
+`_CheckVersionAttributeNEW`, returning a boolean. If it returns true,
+`DriverFinalPolicy` returns `ERROR_SUCCESS` (0x0); if not, it returns
+`ERROR_APP_WRONG_OS` (0x0000047F).
+
+So naturally one starts to consider different ways of injecting into system
+services or patching binaries on disk or corrupting the file system cache or
+any of the usual techniques for such things, to turn either
+`ERROR_SIGNATURE_OSATTRIBUTE_MISMATCH` or `ERROR_APP_WRONG_OS` into an
+`ERROR_SUCCESS`. But fortunately, no such dirty technique is required. The
+`wintrust.dll` framework already gives us everything we need for such
+modifications, without having to resort to the dark arts.
+
+When we call `WinVerifyTrust(DRIVER_ACTION_VERIFY)`, the `DRIVER_ACTION_VERIFY`
+constant is actually a GUID. `wintrust.dll`, in `_CheckRegisteredProviders` and
+`GetRegProvider`, then looks in
+`HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Trust\{function name}\{that guid}`
+at two values, `$DLL` and `$Function`. If `$DLL` is not `wintrust.dll`, it
+calls `LoadLibraryW` on it (not `LoadLibraryExW`! yikes, but unrelated), and
+then it calls `GetProcAddress` on `$Function`. Finally it calls the resolved
+function.
+
+Thus, all we have to do is implement our own `DriverFinalPolicy` function that
+calls the original one in `wintrust.dll`, and converts a return value of
+`ERROR_APP_WRONG_OS` (0x0000047F) into `ERROR_SUCCESS` (0x0). And presto, we
+are done, and Windows 10 drivers can load successfully on Windows 7 and 8.1. We
+do this _without_ having to break any real security barriers or do anything
+dirty. Rather, we use the nice dynamic dispatch facilities already available in
+the OS to remove a now-antiquated OS version policy check. In some sense,
+Microsoft foresaw the need for pluggable policy years in advance.
+
+### Usage
+
+So, with the above in mind, the actual implementation is trivial. Compile the
+[~20 line `shim.c` file](https://git.zx2c4.com/downlevel-driver-enabler/tree/shim.c)
+in this repository into a `shim.dll`, and then set the
+
+```
+HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Trust\FinalPolicy\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\$DLL
+```
+
+registry key to the location of your `shim.dll`. When you are done, set the key
+back to its original value. (It is not recommended to leave the registry key
+pointing to your `shim.dll` or to install your `shim.dll` into `system32`, as
+multiple parties doing that will inevitably lead to the "dll hell" of yore.)
+
+A driver installation at the command line can be easily simplified to:
+
+```bat
+> reg add HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Trust\FinalPolicy\{F750E6C3-38EE-11D1-85E5-00C04FC295EE} /v $DLL /t REG_SZ /d "%cd%\shim.dll" /f
+> pnputil -i -a mydriver.inf
+> reg add HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Trust\FinalPolicy\{F750E6C3-38EE-11D1-85E5-00C04FC295EE} /v $DLL /t REG_SZ /d WINTRUST.dll /f
+```
+
+There is one caveat to consider, which is that the registry is a _shared
+resource_, and so multiple installers all using this method at once is going to
+lead to issues. Therefore, when doing this, take a mutex in a private namespace
+(so as to mitigate the trivial unprivileged DoS). So, by convention, let us do:
+
+ - Boundary descriptor: `L"DownlevelDriverEnabler"`
+ - Boundary descriptor SID: `WinLocalSystemSid` or `WinBuiltinAdministratorsSid`
+ - Private namespace: `L"DownlevelDriverEnabler"` with security attributes
+ `O:SYD:P(A;;GA;;;SY)(A;;GA;;;BA)S:(ML;;NWNRNX;;;HI)` or
+ `O:BAD:P(A;;GA;;;SY)(A;;GA;;;BA)S:(ML;;NWNRNX;;;HI)`
+ - Mutex name: `L"DownlevelDriverEnabler\\ShimInProgress"`
+
+Take that mutex while shimming, and release it after the key has been restored
+to `WINTRUST.DLL`. If we all follow those rules, there will be safe and
+reliable support for driver updates on Windows 7 and 8.1. Hopefully this turns
+a rather hopeless situation into a productive one.
+
+<style>
+.markdown-body {
+ max-width: 720px;
+}
+</style>
diff --git a/exports.def b/exports.def
new file mode 100644
index 0000000..8dd1b77
--- /dev/null
+++ b/exports.def
@@ -0,0 +1,3 @@
+LIBRARY shim.dll
+EXPORTS
+ DriverFinalPolicy
diff --git a/shim.c b/shim.c
new file mode 100644
index 0000000..b91f0db
--- /dev/null
+++ b/shim.c
@@ -0,0 +1,34 @@
+/* Copyright (C) 2021 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. */
+
+#include <windows.h>
+#include <wintrust.h>
+
+/*
+ * HKLM\SOFTWARE\Microsoft\Cryptography\Providers\Trust\FinalPolicy\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\$DLL
+ */
+
+typedef DWORD(DRIVER_FINAL_POLICY_FN)(CRYPT_PROVIDER_DATA *);
+typedef DRIVER_FINAL_POLICY_FN *PDRIVER_FINAL_POLICY_FN;
+
+DRIVER_FINAL_POLICY_FN DriverFinalPolicy;
+
+DWORD
+DriverFinalPolicy(CRYPT_PROVIDER_DATA *ProvData)
+{
+ DWORD OriginalLastError = GetLastError();
+ HMODULE WintrustModule = GetModuleHandleA("WINTRUST.DLL");
+ if (!WintrustModule)
+ return ERROR_INVALID_LIBRARY;
+ PDRIVER_FINAL_POLICY_FN RealDriverFinalPolicy =
+ (PDRIVER_FINAL_POLICY_FN)GetProcAddress(WintrustModule, "DriverFinalPolicy");
+ if (!RealDriverFinalPolicy)
+ return ERROR_INVALID_FUNCTION;
+ DWORD Ret = RealDriverFinalPolicy(ProvData);
+ if (Ret == ERROR_APP_WRONG_OS)
+ {
+ Ret = ERROR_SUCCESS;
+ SetLastError(OriginalLastError);
+ }
+ return Ret;
+}
+