DPAPI机制解析
DPAPI概述
从Windows 2000开始,Microsoft随操作系统一起提供了一种特殊的数据保护接口,称为Data Protection Application Programming Interface(DPAPI)。DPAPI是一个简单的密码学应用程序接口,并且被广泛运用在很多Windows应用程序中,例如,Windows凭据管理器、浏览器和Outlook邮箱等。DPAPI分别提供了加密函数CryptProtectData 与解密函数 CryptUnprotectData 以用作数据的加密解密。还为了防止其他人查看进程中的敏感信息,分别提供了加密函数CryptProtectMemory与解密函数 CryptUnprotectMemory来对内存进行加密与解密。
理论上,数据保护API可以实现任何类型的数据对称加密;在实践中,其在Windows操作系统中的主要用途是使用对称加密算法对其他非对称加密算法的密钥进行加密。
对于几乎所有密码系统来说,最困难的挑战之一是“密钥管理”——其中部分挑战就是如何安全地存储解密密钥。如果密钥以纯文本存储,则可以访问密钥的任何用户都可以访问加密的数据。如果密钥被加密,则又需要另一个密钥,周而复始。因此DPAPI允许开发者使用从用户的登录私钥导出的对称密钥来加密密钥,或者在系统加密的情况下使用系统的域验证私钥来加密密钥。
DPAPI中的密码
我们在上文中所说,DPAPI需要用户的登录密码来提供保护,但是实际上,DPAPI使用的是用户的登录凭据,在用户使用密码登录的操作系统中,登录凭据只是用户密码的哈希值。为了简单起见,以下将会使用“密码”、“用户密码”或“登录密码”来代指此凭据。
使用登录密码的一个小缺点是,在同一用户下运行的所有应用程序都可以访问它们知道的任何受保护数据。当然,由于应用程序必须存储自己的受保护数据,因此其他应用程序访问这些数据可能会有些困难,但并非不可能。为了解决这个问题,DPAPI 允许应用程序在保护数据时使用额外的secret,然后在取消对于该数据保护时同样也需要这个额外的secret才能取消对该数据的保护。
Master Key文件
DPAPI 最初生成一个称为 MasterKey (主密钥)的强密钥,它受用户密码的保护,通常为64字节的随机数据。DPAPI 使用称为“基于密码的密钥派生”的标准加密过程(PKCS#5)从密码生成password-derived key(密码派生密钥)。然后,将此password-derived key与3DES一起使用来加密MasterKey,最后将其存储在用户的配置文件目录中。
然而,MasterKey 并未明确用于保护数据。DPAPI使用的对称会话密钥是由Master Key、16字节的随机数据以及额外的secret(如果应用程序选择提供)生成的,这个会话密钥被用来加密数据。为了安全性考虑,该会话密钥不被保存,加密后直接清除,只保存派生会话密钥时使用的 16字节的随机数据。当需要解密数据时,从加密数据块中提取该随机数据并以同样的方式再次派生出会话密钥对数据进行解密。
· Master Key加密
Master Key 曾经使用的第一种实现方式是使用用户密码的NTLM Hash来解密。这种方式由于其存在极大的安全风险已经不再使用。由于NTLM Hash是存储在SAM文件中,只要攻击者获取到Hash就可以用来生成Master Key来解密数据了。它在技术上允许潜在的犯罪分子通过直接从 SAM 文件获取 NTLM 哈希来解密Master Key,从而解密任何 DPAPI blob,甚至不需要用户的密码。
为了防止Master Key被篡改,它现在使用HMAC(Hash-based Message Authentication Code)进行加密。
1.首先DPAPI使用SHA1作为HMAC和用户的登录密码来派生HMAC密钥。
2.然后,将上文中所述的password-derived key与3DES一起使用来加密 MasterKey 和 MasterKey 的 HMAC。
3.盐和迭代计数都是非秘密值,因此与加密的MasterKey一起存储,但未加密。这允许DPAPI在给定用户密码的情况下轻松解密MasterKey。
·Master Key文件结构
Master Key文件结构如下图:
如上图所示,一个Master Key文件由5个部分组成:
1. 标头和系统信息
2. 用户的Master Key
3. 本地备份加密密钥
4. 唯一的 CREDHIST 文件标识符
5. 域的Master Key备份
在Windows 2000中,系统可以存储本地Master Key备份,而不是CREDHIST标识符。这就是 DPAPI 系统极易受到攻击的原因。
以下是有关5个单元更多的细节:
1. 标头属性
a. dwVersion - 用于兼容性检查的Master Key文件的版本。
b. szGuid - 具有唯一Master Key标识符的字符串值,通常与文件名匹配。
c. dwPolicy - 具有各种标志的字段。例如,如果设置了该字段的位 3,则将使用 SHA1 算法创建Master Key的解密密钥。
2. 用户Master Key属性
a. dwUserKeySize - 当前槽长度。
b. dwVersion - 数据结构版本。
c. pSalt - 盐,即 16 个随机字节的数据,参与Master Key的解密并防止使用彩虹表的数据攻击。
d. dwPBKDF2IterationCount - PBKDF2 加密密钥生成函数中的迭代次数。
e. HMACAlgId - 哈希算法标识符。
f. CryptAlgId - 使用的加密算法。
g. pKey - 用户的加密Master Key。
3. 本地备份密钥属性 (Windows 2000)
a. dwLocalKeySize - 本地备份密钥的大小
b. dwVersion - 数据结构版本。
c. pSalt - 盐,即 16 个随机字节的数据,参与Master Key的解密并防止使用彩虹表的数据攻击。
d. pKey - 加密的本地备份密钥。
4. CREDHIST 文件的 GUID 属性(Windows XP 及更高版本)
a. dwLocalKeySize - 本地备份密钥的大小
b. dwVersion - 数据结构版本。
c. guidCredHist - CREDHIST 文件二进制标识符。
5. 域的Master Key备份属性
a. dwDomainKeySize - 域的Master Key备份的大小。
b. dwVersion - 数据结构版本。
c. pSalt - 盐,即 16 个随机字节的数据,参与Master Key的解密并防止使用彩虹表的数据攻击。
d. dwPBKDF2IterationCount - PBKDF2 加密密钥生成函数中的迭代次数。
e. HMACAlgId - 哈希算法标识符。
f. CryptAlgId - 使用的加密算法。
g. pKey - 加密的域备份密钥。其解密需要存储在 Active Directory 数据库中的域控制器 RSA 私钥。
·新Master Key的创建
1. 首先,调用API函数RtlGenRandom,它会返回一个伪随机生成的64位字节数的数据。
2. 使用用户的登录密码、安全标识符(SID)和额外的16位字节数的随机数据(所谓的“盐”)来加密步骤1中获得的64字节的随机数据。
3. Master Key获得一个唯一的GUID。每一个DPAPI blob都储存该唯一标识符。
4. 使用用户密码创建和加密的Master Key与其他系统数据一起存储在Master Key存储文件夹(Master Key File)中的单独文件中。
a. 用户的Master Key存储在%APPDATA%/Microsoft/Protect/%SID% 中。其中{SID}为该用户的安全标识符。
b. 系统的Master Key存储在 %WINDIR%/System32/Microsoft/Protect 中,用于解密 DPAPI blob,受本地系统帐户的保护。
·用户密码更新后Master Key的变化
修改用户密码
Windows 安全策略假定定期更改用户密码,并且 DPAPI 必须为用户提供与密码更改之前相同级别的个人加密数据访问权限。这是通过所有用户的Master Key的三步同步来实现的:
1. 所有Master Key使用旧用户密码进行解密
2. 使用新密码对所有密钥进行加密并保存到磁盘
3. 如果选择了相应的选项,则会备份它们
所有Master Key的重新加密可能需要很长时间,因此,如上文所述,这些操作是在 LSA 内的单独线程中执行的。
重置用户密码
当管理员手动重置用户密码,或者使用密码重置程序之一删除用户密码时,就会出现这种情况。
尽管此时用户仍然可以登录系统,但此时所有使用 DPAPI 加密的数据都不再可用,因为用户的旧密码对于此时的系统来说是未知的,所以 DPAPI 无法解密Master Key。如果在强制密码重置后再次设置用户旧密码,DPAPI 将返回其原始状态,并且所有 DPAPI blob 将再次可用。发生这种情况是因为即便DPAPI无法解密Master Key,但DPAPI仍然保留了Master Key的所有副本。
DPAPI加解密过程
简单来说,DPAPI中的数据加密解密分为四个阶段,如下图:
1. 首先,应用程序调用 DPAPI 函数之一,指定要加密或解密的数据以及附加secret参数(如有)。
2. 这些函数属于CryptAPI结构,位于Crypt32.dll中。进一步处理的请求会从Crypt32.dll向LSA(Local Security Authority)发出本地RPC(Remote Process Call)调用。LSA 是一个系统进程,在启动时启动并运行直到计算机关闭。本地RPC调用无需遍历网络,因此所有数据都会保留在本地计算机上。然后,这些 RPC 调用的端点调用 DPAPI 私有函数来保护或取消保护数据。
3. 然后,DPAPI 私有函数使用 Crypt32.dll 回调 CryptoAPI,以在 LSA 的安全环境中实际加密或解密数据。
4. 最后在LSA中,加密或解密过后的数据会通过一个安全的RPC通道回到Crypt.dll中,再从Crypt.dll中传回原本的应用程序。
·加密算法
不同操作系统中DPAPI默认使用的加密算法不同,具体如下表:
操作系统 |
加密算法 |
哈希算法 |
PBKDF2 中的迭代次数 |
Windows2000 |
RC4 |
SHA1 |
1 |
WindowsXP |
3DES |
SHA1 |
4000 |
WindowsVista |
3DES |
SHA1 |
24000 |
Windows7 |
AES256 |
SHA512 |
5600 |
Windows10-11 |
AES256 |
SHA512 |
8000 |
DPAPI接口调用
·Data加密与解密demo
使用以下C++代码示例可以达成DPAPI数据加解密的接口调用(CryptProtectData 与CryptUnprotectData),可更改其中参数来达成自己所需的加密效果:
1.#include "stdafx.h"
2.#include <stdio.h>
3.#include <windows.h>
4.#include <Wincrypt.h>
5.#include <dpapi.h>
6.#pragma comment(lib, "Crypt32.lib")
7.
8.void main()
9.{
10. DATA_BLOB DataIn;
11. DATA_BLOB DataOut;
12. DATA_BLOB DataVerify;
13. BYTE *pbDataInput = (BYTE *)"Hello world of data protection.";
14. DWORD cbDataInput = strlen((char *)pbDataInput) + 1;
15. DataIn.pbData = pbDataInput;
16. DataIn.cbData = cbDataInput;
17. CRYPTPROTECT_PROMPTSTRUCT PromptStruct;
18. LPWSTR pDescrOut = NULL;
19. //-------------------------------------------------------------------
20. // Begin processing.
21.
22. printf("The data to be encrypted is: %sn", pbDataInput);
23.
24. //-------------------------------------------------------------------
25. // Initialize PromptStruct.
26.
27. ZeroMemory(&PromptStruct, sizeof(PromptStruct));
28. PromptStruct.cbSize = sizeof(PromptStruct);
29. PromptStruct.dwPromptFlags = CRYPTPROTECT_PROMPT_ON_PROTECT;
30. PromptStruct.szPrompt = L"This is a user prompt.";
31.
32. //-------------------------------------------------------------------
33. // Begin protect phase.
34.
35. if (CryptProtectData(
36. &DataIn, // 指向需要加密的数据
37. L"This is the description string.", // 对于加密数据的描述
38. NULL, // 指向用于加密数据的密码或其他附加熵(如有)
39. NULL, // 该参数并未使用,必须设置为NULL
40. &PromptStruct, // 指向PromptStruct的指针,提供有关在何处和何时显示提示(可以为NULL)
41. 0, //dwFlags,具体作用可查询官方文档
42. &DataOut)) //指向接收加密数据的地方
43. {
44. printf("The encryption phase worked. n");
45. }
46. else
47. {
48. printf("Encryption error!");
49. }
50. //-------------------------------------------------------------------
51. // Begin unprotect phase.
52.
53. if (CryptUnprotectData(
54. &DataOut, // 指向保存的加密数据
55. &pDescrOut, // 指向保存的加密数据的描述
56. NULL, // 指向用于加密数据的密码或其他附加熵(如有)
57. NULL, // 该参数并未使用,必须设置为NULL
58. &PromptStruct, // 指向PromptStruct的指针,提供有关在何处和何时显示提示(可以为NULL)
59. 0, //dwFlags,具体作用可查询官方文档
60. &DataVerify)) //指向接收解密数据的地方
61. {
62. printf("The decrypted data is: %sn", DataVerify.pbData);
63. printf("The description of the data was: %Sn", pDescrOut);
64. system("pause");
65. }
66. else
67. {
68. printf("Decryption error!");
69. }
70.}
注:使用C++和C#加密数字以及英文的结果是相同的,但是加密中文的结果会不一样,原因是编码不同。
·Data demo效果演示
下图为上述示例代码执行效果:
点击设置安全级别后→选择【高】→点击下一页,则可以设置用于加密数据的密码,如下图:
若选择设置密码后,在解密时会要求输入密码,若不选择设置密码,则可以直接解密,如下图:
密码输入错误也无法进行解密,如下图:
输入正确密码后,解密成功:
·Memory加密与解密demo
使用以下C++代码示例可以达成DPAPI内存加解密的接口调用(CryptProtectMemory与CryptUnprotectMemory),可更改其中参数来达成自己所需的加密效果:
1.#include "stdafx.h"
2.#include <windows.h>
3.#include <stdio.h>
4.#include <Wincrypt.h>
5.#include <dpapi.h>
6.#pragma comment(lib, "Crypt32.lib")
7.
8.#define SSN_STR_LEN 12 // includes null
9.
10.void main()
11.{
12. HRESULT hr = S_OK;
13. LPWSTR pSensitiveText = NULL;
14. DWORD cbSensitiveText = 0;
15. DWORD cbPlainText = SSN_STR_LEN * sizeof(WCHAR);
16. DWORD dwMod = 0;
17.
18. // Memory to encrypt must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE.
19. if (dwMod = cbPlainText % CRYPTPROTECTMEMORY_BLOCK_SIZE)
20. cbSensitiveText = cbPlainText +(CRYPTPROTECTMEMORY_BLOCK_SIZE - dwMod);
21. else
22. cbSensitiveText = cbPlainText;
23.
24. pSensitiveText = (LPWSTR)LocalAlloc(LPTR, cbSensitiveText);
25. if (NULL == pSensitiveText)
26. {
27. wprintf(L"Memory allocation failed.n");
28. // return E_OUTOFMEMORY;
29. }
30. wcscpy_s(pSensitiveText, 5,L"1234");
31. // Place sensitive string to encrypt in pSensitiveText.
32.
33. if (!CryptProtectMemory(pSensitiveText, cbSensitiveText,
34. CRYPTPROTECTMEMORY_SAME_PROCESS))
35. {
36. wprintf(L"CryptProtectMemory failed: %dn", GetLastError());
37. SecureZeroMemory(pSensitiveText, cbSensitiveText);
38. LocalFree(pSensitiveText);
39. pSensitiveText = NULL;
40. // return E_FAIL;
41. }
42.
43. if (CryptUnprotectMemory(pSensitiveText, cbSensitiveText,
44. CRYPTPROTECTMEMORY_SAME_PROCESS))
45. {
46. wprintf(L"CryptUnprotectMemory successed!n");
47. // Use the decrypted string.
48. }
49. else
50. {
51. wprintf(L"CryptUnprotectMemory failed: %dn",
52. GetLastError());
53. }
54. // Call CryptUnprotectMemory to decrypt and use the memory.
55.
56. SecureZeroMemory(pSensitiveText, cbSensitiveText);
57. LocalFree(pSensitiveText);
58. pSensitiveText = NULL;
59.}
·Memory demo效果演示
使用VS对上述示例代码进行调试,我们在
该代码处打上断点,在执行完这句代码后局部变量如下图:
可以看到在内存地址0x00E1C6A0处储存了“1234”的值,那么我们查看一下内存,如下图:
可以看到0x00E1C6A0储存的值为,31,32,33,34,分别对应1234的ASCII码。
我们继续执行代码,发现在执行完
这句代码后,0x00E1C6A0储存的值变为如下图所示:
可以看到此时0x00E1C6A0储存的值已经变为了随机的数据,完成了加密。
我们继续执行代码,发现在执行完
这句代码后,0x00E1C6A0储存的值又变回了31,32,33,34。证明解密成功。
参考文献
1. DPAPI Secrets. Security Analysis and Data Recovery in DPAPI. www.passcape.com/index.php?section=docsys&cmd=details&id=28.
2. Windows系统存储区DPAPI的解密技术研究 - 百度文库. wenku.baidu.com/view/388bea4b56270722192e453610661ed9ad5155bb.html?_wkts_=1700210324209.
3. DPAPI Master Key Analysis. www.passcape.com/windows_password_recovery_dpapi_master_key.
4. Wikipedia contributors. “Data Protection API.” Wikipedia, 27 Mar. 2023, en.wikipedia.org/wiki/Data_Protection_API.
5. Windows信息搜集篇(五)之DPAPI 数据加密与Dns域传送漏洞利用 - 404p3rs0n - 博客园. www.cnblogs.com/404p3rs0n/p/15656584.html.
往期精彩合集
长
按
关
注
联想GIC全球安全实验室(中国)
原文始发于微信公众号(联想全球安全实验室):DPAPI机制解析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论