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 either0
or1
.nPrimes
: We generally expect this to be0
or2
depending on the value ofhasPrivateKey
.au64PubExp
: Public exponente
. 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 withau64PubExp
pmPrimes
: Pointers to_SYMCRYPT_MODULUS
p
andq
, the prime numbers when a private key is available.piPrivExps
: Pointer to_SYMCRYPT_INT
private exponentd
. Withn
andd
, we can derive the values found inpmPrimes
.
_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.
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.
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.