In this mini-series, Part 1 discussed how we could discover RSA certificates in arbitrary processes using volatility. Additionally, the dumpcerts plugin was also able to find certificates in the PKCS #8 format:

PrivateKeyInfo ::= SEQUENCE {
    version                   Version,
    privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
    privateKey                PrivateKey,
    attributes           [0]  IMPLICIT Attributes OPTIONAL }

Version ::= INTEGER
PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
PrivateKey ::= OCTET STRING
Attributes ::= SET OF Attribute

But what about private keys that may not be in this format, particularly in Windows environments? After all, private keys at their most basic form are not much more than a combination of numbers in some structure that can be used for decryption. This entry in the series will discuss extracting private keys in arbitrary processes.


Private keys

Before going further, we need to understand what it is we’re looking for in an RSA private key. The RSA private and public key relationship is effectively this:

Private Key Public Key
p prime number
q prime number
n modulus (p * q)
e public exponent (usually 3 or 65537)
d private exponent

When we have a public key, the modulus and public exponent are available. However, the private exponent and the prime numbers are considered to be secret, and should not be exposed publicly. At a minimum for a private key, we need n and d, since that will allow p and q to be derived as well as any additional values required by the Chinese Remainder Theorem (CRT) for some private key operations.


RSA on Windows

While Linux implementations typically use OpenSSL, Windows has several of its own cryptographic service providers. The cross-platform compatability of .NET and the providers available are well-documented in the Microsoft Docs. Most notably, we care about the RSA on Windows section, which talks about the behavior of the library depending on how the code is written.

  • Windows CryptoAPI (CAPI) is used whenever new RSACryptoServiceProvider() is used.
  • Windows Cryptography API Next Generation (CNG) is used whenever new RSACng() is used.
  • The object returned by RSA.Create is internally powered by Windows CNG. This use of Windows CNG is an implementation detail and is subject to change.
  • The GetRSAPublicKey extension method for X509Certificate2 returns an RSACng instance. This use of RSACng is an implementation detail and is subject to change.
  • The GetRSAPrivateKey extension method for X509Certificate2 currently prefers an RSACng instance, but if RSACng can’t open the key, RSACryptoServiceProvider will be attempted. The preferred provider is an implementation detail and is subject to change.

The highlight of this section pulled from Microsoft Docs is that RSA objects are powered by the internal Windows CNG provider. So how can we determine what the structure of these objects look like? Many years ago, this might have required a lot of debugging, note-taking, and hours of effort to figure out, since the cryptographic libraries of Windows were closed-source. However, in 2019, Microsoft open-sourced SymCrypt, the core cryptographic function library used by Windows. Therefore by analyzing the open source code, we can determine what the structures look like.


SymCrypt

The SymCrypt library is well-documented and we can quickly locate the code and structures we’re interested in. In particular, symcrypt_internal.h holds structures for RSA Key structures (as well as many other types of encryption).

The four structures of interest here are _SYMCRYPT_RSAKEY, _SYMCRYPT_MODULUS, _SYMCRYPT_DIVISOR, and _SYMCRYPT_INT. Within the _SYMCRYPT_RSAKEY object, there are pointers which eventually lead to the other three structures, therefore we have to keep in mind the fields available in each one.

To keep this review short, I’ll only cover the main _SYMCRYPT_RSAKEY and _SYMCRYPT_INT objects, since the others are used to traverse from the RSA key to the values we want.

_SYMCRYPT_RSAKEY

typedef SYMCRYPT_ASYM_ALIGN_STRUCT _SYMCRYPT_RSAKEY {
    UINT32              cbTotalSize;        // Total size of the rsa key
    BOOLEAN             hasPrivateKey;      // Set to true if there is private key information set
    UINT32              nSetBitsOfModulus;  // Bits of modulus specified during creation
    UINT32              nBitsOfModulus;     // Number of bits of the value of the modulus (not the object's size)
    UINT32              nDigitsOfModulus;   // Number of digits of the modulus object (always equal to SymCryptDigitsFromBits(nSetBitsOfModulus))
    UINT32              nPubExp;            // Number of public exponents
    UINT32              nPrimes;            // Number of primes, can be 0 if the object only supports public keys
    UINT32              nBitsOfPrimes[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
                                            // Number of bits of the value of each prime (not the object's size)
    UINT32              nDigitsOfPrimes[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
                                            // Number of digits of each prime object
    UINT32              nMaxDigitsOfPrimes; // Maximum number of digits in nDigitsOfPrimes
    UINT64              au64PubExp[SYMCRYPT_RSAKEY_MAX_NUMOF_PUBEXPS];
    // SYMCRYPT_ASYM_ALIGN'ed buffers that point to memory allocated for each object
    PBYTE               pbPrimes[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
    PBYTE               pbCrtInverses[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
    PBYTE               pbPrivExps[SYMCRYPT_RSAKEY_MAX_NUMOF_PUBEXPS];
    PBYTE               pbCrtPrivExps[SYMCRYPT_RSAKEY_MAX_NUMOF_PUBEXPS * SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];

    // SymCryptObjects
    PSYMCRYPT_MODULUS   pmModulus;          // The modulus N=p*q
    PSYMCRYPT_MODULUS   pmPrimes[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
                                            // Pointers to the secret primes
    PSYMCRYPT_MODELEMENT peCrtInverses[SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
                                            // Pointers to the CRT inverses of the primes
    PSYMCRYPT_INT       piPrivExps[SYMCRYPT_RSAKEY_MAX_NUMOF_PUBEXPS];
                                            // Pointers to the corresponding private exponents
    PSYMCRYPT_INT       piCrtPrivExps[SYMCRYPT_RSAKEY_MAX_NUMOF_PUBEXPS * SYMCRYPT_RSAKEY_MAX_NUMOF_PRIMES];
                                            // Pointers to the private exponents modulo each prime minus 1 (for CRT)
    SYMCRYPT_MAGIC_FIELD
} SYMCRYPT_RSAKEY;

This structure containers the entirety of the RSA structure. While all the fields have some value, the following fields are the most interesting:

  • hasPrivateKey: Determines if the object contains private key values. If not, it is a public key. This value is either 0 or 1.
  • nPrimes: We generally expect this to be 0 or 2 depending on the value of hasPrivateKey.
  • au64PubExp: Public exponent e. Defaults to 65537 but may also be 3 in some cases.
  • pmModulus: Pointer to _SYMCRYPT_MODULUS n. This value can be used to match public and private keys along with au64PubExp
  • pmPrimes: Pointers to _SYMCRYPT_MODULUS p and q, the prime numbers when a private key is available.
  • piPrivExps: Pointer to _SYMCRYPT_INT private exponent d. With n and d, we can derive the values found in pmPrimes.

_SYMCRYPT_INT

SYMCRYPT_ASYM_ALIGN_STRUCT _SYMCRYPT_INT {
                                                    UINT32  type;
    _Field_range_( 1, SYMCRYPT_FDEF_UPB_DIGITS )    UINT32  nDigits;    // digit size depends on run-time decisions...
                                                    UINT32  cbSize;

    SYMCRYPT_MAGIC_FIELD
    SYMCRYPT_ASYM_ALIGN union {
        struct {
            UINT32          uint32[SYMCRYPT_ANYSIZE];   // FDEF: array UINT32[nDigits * # uint32 per digit]
        } fdef;
    } ti;                   // we must have a name here. 'ti' stands for 'Type-Int', it helps catch type errors when type-casting macros are used.
};

The primary interest here is fdef, which contains the array of 32-bit integers that represent whatever value the structure is meant to represent. As shown in _SYMCRYPT_RSAKEY, the private exponent is represented by this structure. However, this structure is also used by _SYMCRYPT_MODULUS, therefore the prime numbers and the modulus are ultimately represented by this structure.


Discovering _SYMCRYPT_RSAKEY

Discovering the location of these structures turned out to be fairly simple. The driver responsible for Windows cryptographic operations is at C:\Windows\system32\drivers.cng.sys. By loading this driver into Binary Ninja, we can grab the PDB from the Microsoft Symbol server and locate interesting functions.

As expected, we can find the MSCryptGenerateKeyPair function which calls ‘CreateAndInitializeNewKey`. Shoutout to properly named functions.

Binja_GenerateKeypair

Inside this function, we can find the struct that is created that represents the key. We could trace back the arguments to the calling functions and function tables to verify this, but that falls outside the scope of this blog entry. However, what we can see is something that appears to be a magic header KRSM, possibly standing for Microsoft/MSCrypt RSA Key. Coincidentally, the validateMSCryptRsaAlgorithm function shown in the previous screenshot checks a different struct for the string ARSM, and we can assume that means Microsoft/MSCrypt RSA Algorithm.

Binja_KRSM_Header

Additionally, there is a hardcoded value of 0x28 right before the KRSM header. This appears to be the length of the entire structure we’re looking at, which is supported by the initialization of rax members right before the assignment. Therefore, we have enough to determine that the beginning of an MSCrypt RSA Key in hex form would look like 28 00 00 00 4b 52 53 4d, and this is a significant marker for the object in memory!

Note on Mimikatz

I learned later that this is exactly how Mimikatz looks for BCrypt private keys, so this isn’t exactly a novel technique. However, by coming across this realization, it opens opportunities to discover additional credential types for parsing, such as symmetric keys. 😃


Pivoting with Yara

By using the construct module, we can recreate the SymCrypt structures of interest, as well as the MSCrypt RSA Key structure. Some of the values for the BCRYPT_RSAKEY object were discovered by looking a little further at the structure in Binary Ninja, but not all values were found.

BCRYPT_RSAKEY = Struct(
    "Length" / Int32ul,
    "Magic" / Const(b"KRSM"),
    "Algid" / Hex(Int32ul),
    "ModBitLen" / Int32ul,
    "Unknown1" / Int32sl,
    "Unknown2" / Int32sl,
    "pAlg" / Hex(Int64ul),
    "pKey" / Hex(Int64ul),
)

SYMCRYPT_RSAKEY = Struct(
    "cbTotalSize" / Int32ul,
    "hasPrivateKey" / Int32ul,
    "nSetBitsOfModulus" / Int32ul,
    "nBitsOfModulus" / Int32ul,
    "nDigitsOfModulus" / Int32ul,
    "nPubExp" / Int32ul,
    "nPrimes" / Int32ul,
    "nBitsOfPrimes" / Array(2, Int32ul),
    "nDigitsOfPrimes" / Array(2, Int32ul),
    "nMaxDigitsOfPrimes" / Array(1, Int32ul),
    "au64PubExp" / Hex(Int64ul),
    "pbPrimes" / Array(2, Hex(Int64ul)),
    "pbCrtInverses" / Array(2, Hex(Int64ul)),
    "pbPrivExps" / Array(1, Hex(Int64ul)),
    "pbCrtPrivExps" / Array(2, Hex(Int64ul)),
    "pmModulus" / Hex(Int64ul),
    "pmPrimes" / Array(2, Hex(Int64ul)),
    "peCrtInverses" / Array(2, Hex(Int64ul)),
    "piPrivExps" / Array(1, Hex(Int64ul)),
    "piCrtPrivExps" / Array(2, Hex(Int64ul)),
    "magic" / Hex(Int64ul),
)

SYMCRYPT_MODULUS_MONTGOMERY = Struct("inv64" / Hex(Int64ul), "rsqr" / Hex(Int32ul))
SYMCRYPT_MODULUS_PSUEDOMERSENNE = Struct("k" / Int32ul)

SYMCRYPT_INT = Struct(
    "type" / Int32ul,
    "nDigits" / Int32ul,
    "cbSize" / Int32ul,
    "magic" / Int64ul,
    "unknown1" / Int64ul,
    "unknown2" / Int32ul,
    "fdef" / Array((this.cbSize - 0x20) // 4, Int32ul),
)

SYMCRYPT_DIVISOR = Struct(
    "type" / Int32ul,
    "nDigits" / Int32ul,
    "cbSize" / Int32ul,
    "nBits" / Int32ul,
    "magic" / Int64ul,
    "td" / Int64ul,
    "int" / SYMCRYPT_INT,
)

SYMCRYPT_MODULUS = Struct(
    "type" / Int32ul,
    "nDigits" / Int32ul,
    "cbSize" / Int32ul,
    "flags" / Int32ul,
    "cbModElement" / Int32ul,
    "magic" / Int64ul,
    "tm"
    / Union(
        0,
        "montgomery" / SYMCRYPT_MODULUS_MONTGOMERY,
        "pseudoMersenne" / SYMCRYPT_MODULUS_PSUEDOMERSENNE,
    ),
    "pUnknown" / Hex(Int64ul),
    "pUnknown2" / Hex(Int64ul),
    "pUnknown3" / Hex(Int64ul),
    "divisor" / SYMCRYPT_DIVISOR,
)

Since Volatility allows us to pivot to other sections of memory, combining these Struct classes with Volatility 3’s YaraScanner makes it easy to parse the entire RSA key out of memory. For example, the SYMCRYPT_RSAKEY output would look something like this:

Container:
    cbTotalSize = 3488
    hasPrivateKey = 1
    nSetBitsOfModulus = 2048
    nBitsOfModulus = 2048
    nDigitsOfModulus = 4
    nPubExp = 1
    nPrimes = 2
    nBitsOfPrimes = ListContainer:
        1024
        1024
    nDigitsOfPrimes = ListContainer:
        2
        2
    nMaxDigitsOfPrimes = ListContainer:
        2
    au64PubExp = 0x0000000000010001
    pbPrimes = ListContainer:
        0x0000015FF008CBA0
        0x0000015FF008CE20
    pbCrtInverses = ListContainer:
        0x0000015FF008D0A0
        0x0000015FF008D1A0
    pbPrivExps = ListContainer:
        0x0000015FF008D2A0
    pbCrtPrivExps = ListContainer:
        0x0000015FF008D3C0
        0x0000015FF008D4E0
    pmModulus = 0x0000015FF008C920
    pmPrimes = ListContainer:
        0x0000015FF008CBA0
        0x0000015FF008CE20
    peCrtInverses = ListContainer:
        0x0000015FF008D0A0
        0x0000015FF008D1A0
    piPrivExps = ListContainer:
        0x0000015FF008D2A0
    piCrtPrivExps = ListContainer:
        0x0000015FF008D3C0
        0x0000015FF008D4E0
    magic = 0x0000000000000000

What we’re interested in here is if the structure has a 1 for hasPrivateKey, which means we can extract the public and private key material. However, we have no way of knowing what certificate it represents. At least, not at first. Other values of interest here include the prime numbers (obviously) as well as the private exponent.


Matching keys and certs

In Part 1, we were able to extract the public certificates found in a process. Therefore, we can check to see if the process we’re scanning for SymCrypt RSA keys also has the public certificate associated with it. As mentioned earlier, we can determine a key pair by comparing the value of modulus of the private key (p * q) to the modulus n of the public certs we can extract. With a little Python magic, we can determine when there’s a match:

 Offset          PID   Process     Rule               HasPrivateKey   Modulus (First 20 Bytes)                   Matching
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  0x15fef506130   872   lsass.exe   symcrypt_rsa_key   0               CCD95B6993F8A5C47177FBD5E1CA79E43672C6EC
  0x15fef50c650   872   lsass.exe   symcrypt_rsa_key   0               E44E27497E1BDF3A768BB35141057311CA367875   F42F47F1987857A58664B8830F47803669400477 -> CN=b831458c-0bd8-4f58-8c61-64838f56ba31
  0x15fef7518d0   872   lsass.exe   symcrypt_rsa_key   0               B2EDBB129B966FBFCA24893DB4D47C6D8B2643F1
  0x15fef7557a0   872   lsass.exe   symcrypt_rsa_key   0               E44E27497E1BDF3A768BB35141057311CA367875   F42F47F1987857A58664B8830F47803669400477 -> CN=b831458c-0bd8-4f58-8c61-64838f56ba31
  0x15fef755c80   872   lsass.exe   symcrypt_rsa_key   1               E44E27497E1BDF3A768BB35141057311CA367875   F42F47F1987857A58664B8830F47803669400477 -> CN=b831458c-0bd8-4f58-8c61-64838f56ba31
  0x15fefea0150   872   lsass.exe   symcrypt_rsa_key   1               E44E27497E1BDF3A768BB35141057311CA367875   F42F47F1987857A58664B8830F47803669400477 -> CN=b831458c-0bd8-4f58-8c61-64838f56ba31
  0x15feff44750   872   lsass.exe   symcrypt_rsa_key   1               B2EDBB129B966FBFCA24893DB4D47C6D8B2643F1

In this instance, we discovered four different instances of the same structure, and while all of them correspond to a certificate with the subject name CN=b831458c-0bd8-4f58-8c61-64838f56ba31, only two of them have the private key material. Since we have the public certificate and the private key material, we can combine them to dump a working PFX that includes the private key. And if there is no match but we still have private key material, as shown in the hit at 0x15feff44750, we can still dump the key material to disk, as another process might actually be handling the public certificate. What a win!


Conclusion

There’s still more work to do. By analyzing cng.sys, we should be able to find more structures that represent secrets and certificates that can be extracted from memory. And although LSASS was focused here, this technique works on any kernel or process dump. There is value in finding these certificates, particularly in cloud environments where certificate-based authentication is extremely common between identities and services. Parsing memory dumps with techniques other than Mimikatz is a skillset that can open new and unexpected paths for attackers and is an area of research that needs further exploration.

The volatility 3 plugin for this technique can be found on my Github.