Why use CNG instead of CryptoAPI to store keys

In the last posts, I talked a bit about using CryptoAPI and CryptoNG (CNG) to manage encryption keys, and how using CNG sometimes requires some extra work. That begs the question: is that extra work justified? If CryptoAPI is easier to use, why not just use that?

Obviously, one good reason to prefer CNG is that CryptoAPI is deprecated.

Another reason has to do with how CNG and CryptoAPI protect encryption keys: The two APIs have a very different architecture and this difference has a profound impact on how difficult (or easy) it is for a bad actor to access those keys.

To see what that means in practice, let’s use Process Monitor to see how CryptoAPI and CNG store their keys.

Tracing CryptoAPI key access

First, we need a key and store it using CryptoAPI. An easy way to do this is to create a new self-signed certificate and use the Microsoft Base Cryptographic Provider v1.0, which is one of the CryptoAPI cryptographic service providers.

New-SelfSignedCertificate `
      -KeyUsage DigitalSignature `
      -FriendlyName "Sample CryptoAPI key" `
      -KeyExportPolicy NonExportable `
      -Subject "Sample CryptoAPI key" `
      -CertStoreLocation "cert:\CurrentUser\My" `
      -Provider "Microsoft Base Cryptographic Provider v1.0" `
      -KeySpec Signature

CryptoAPI stores RSA keys in %APPDATA%\Microsoft\Crypto\RSA\[User-SID]\, but these files are encrypted, so they need to be read via CryptoAPI.

To see how CryptoAPI accesses the stored keys, let’s use Process Monitor to see when and how it accesses files in %APPDATA%\Microsoft\Crypto\RSA\[User-SID]\. To trigger a key access, we use a little .NET test program (CryptoApiNetFx.exe) that uses our self-signed certificate to sign a piece of data.

CryptoAPI trace

As expected, we see several CreateFile calls that access files in %APPDATA%\Microsoft\Crypto\RSA\[User-SID]\. The calls originate from CryptoApiNetFx.exe and if we look at the stack trace, we can see how the CLR (clr.dll) calls into CryptoAPI (cryptsp.dll, rsaenh.dll) which then makes a system call to access the file:

CryptoAPI stack

This stack trace isn’t particularly exciting – but the key thing to notice is: Our process directly accesses the stored keys. As a result, the private key ends up being loaded into our address space. If the key is marked as non-exportable, CryptoAPI won’t allow us to access the private key – but because it’s in our process address space, we can access it anyway as explained in this whitepaper.

The takeaway here is that CryptoAPI doesn’t provide particularly strong protections for stored keys: For a bad actor to exfiltrate a user’s private key, all they need to do is run a process as that user and extract the private key from memory.

Let’s see how CNG compares.

Tracing CNG key access

First, we create another self-signed certificate, this time using the Microsoft Software Key Storage Provider which is a CNG key storage provider:

New-SelfSignedCertificate `
      -KeyUsage DigitalSignature `
      -FriendlyName "Sample CNG key" `
      -Subject "Sample CNG key" `
      -KeyExportPolicy NonExportable `
      -CertStoreLocation "cert:\CurrentUser\My" `
      -Provider "Microsoft Software Key Storage Provider" `
      -KeyAlgorithm RSA `
      -KeyLength 2048

The Microsoft Software Key Storage Provider stores its keys in %APPDATA%\Microsoft\Crypto\Keys. Following the same approach as before, let’s use a test program and Process Monitor to see how our key is being accessed.

CNG trace

First thing to notice in this Process Monitor trace is that the file isn’t accessed by CryptoApiNetFx.exe, but by LSASS. If we look at the stack trace, we can also notice that the file accesses are made by keyiso.dll, which is servicing an LRPC (rpcrt4.dll):

CNG stack

keyiso.dll is the CNG key isolation service:

The CNG key isolation service is hosted in the LSA process. The service provides key process isolation to private keys and associated cryptographic operations as required by the Common Criteria. The service stores and uses long-lived keys in a secure process complying with Common Criteria requirements.

To access a CNG key, our process has to make an LRPC call to LSASS – and that’s important: LSASS is not only a different process, it also runs as a different user – so there is a security boundary that separates our process (CryptoApiNetFx.exe) from the private key. If the CNG key is marked as non-exportable, LSASS won’t give us access to it and it’ll never be loaded into the address space of our process.

For a bad actor to exfiltrate a private key from CNG, they have to compromise LSASS. That’s possible, but not exactly easy as LSASS runs as SYSTEM.

LSA protection

If you hold the debug privilege, you can attach a debugger (or mimikatz) to LSASS and thus undermine the extra protection that CNG provides over CryptoAPI. To protect against this attack vector, Windows 8.1 introduced LSA protection, which sets up LSASS to run as Protected Process Lite (PPL):

[...] a Protected Process can be accessed by an unprotected process only with very limited privileges: PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, PROCESS_TERMINATE and PROCESS_SUSPEND_RESUME. This set can even be reduced for some highly-sensitive processes.

LSA protection is off by default, but when enabled, it makes it significantly more difficult for a bad actor to compromise LSASS and extract private keys from CNG since they first have to find a way to disable PPL.

Credential Guard

LSA protection makes it more difficult to compromise LSASS, but it’s still possible. To further raise the bar, Windows 10 therefore introduced Credential Guard:

Protected processes are a kernel construct; malware running in the kernel can modify the structures necessary to mark a process as protected and, well, unprotect them. […] That’s not strictly true anymore with the introduction of Hypervisor-protected code integrity (HVCI), which is specifically designed to protect the kernel against tampering.

Credential Guard uses virtualization-based security (VBS) and runs parts of LSASS (LsaIso) at a different trust level, which is separated from the rest of the kernel by the hypervisor. When Credential Guard is enabled, even LSASS can’t access non-exportable private keys anymore.

Takeaway

Any software-based key store has its limitations, and if you’re really concerned about the safety of encryption keys, you might be better off with a hardware-based solution. But some software-based key stores clearly provide better protection than others. CNG goes through much greater lengths to protect encryption keys than CryptoAPI, and with LSA protection and Credential Guard, you can further strengthen these protections. Whenever we need to store encryption keys, we should therefore prefer CNG over CryptoAPI.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home