2019-07 from–https://rcoil.me/2019/07/%E3%80%90%E7%9F%A5%E8%AF%86%E5%9B%9E%E9%A1%BE%E3%80%91DPAPI%20%E8%AF%A6%E8%A7%A3/
0x00 前言
绝大多数应用程序都有数据加密保护的需求,存储和保护私密信息最安全的方式就是每次需要加密或解密时都从用户那里得到密码,使用后再丢弃。这种方式每次处理信息时都需要用户输入口令,对于绝大多数用户来说,这种方式是不可取的。因为这要求用户记住很多信息,而用户一般会反复使用同一个密码,从而降低系统的安全性和可用性。因此需要一种加密机制,再不需要用户输入任何信息的情况下也能存储秘密数据,而微软数据保护接口(Data Protection Application Programming Interface
,DPAPI)便是瞒住这种要求的程序接口。
从 Windows 2000 开始,用户程序或操作系统程序就可以直接调用 DPAPI 来加密数据。由于 DPAPI 简单易用且加密强大,大量应用程序都采用 DPAPI 加密用户的私密数据,如 Chrome 浏览器
的自动登陆密码、远程桌面
的自动登陆密码、Outlook邮箱
的账号密码、加密文件系统
的私钥等。DPAPI 内部加密流程异常复杂而且微软官方也未公布过其内部细节,这给理解该接口内部实现机制带来了极大困难。本文在已有的研究基础上对 DPAPI加密机制做了全面剖析,给出了DPAPI的离线解密方法。
0x01 DPAPI 概述
1.1 DPAPI 函数
DPAPI 由一个加密函数(CryptProtectData()
)和一个解密函数(CryptUnProtectData()
)组成,是一组跟Windows 系统用户环境上下文密切相关的数据保护接口。某个系统用户调用 CryptProtectData()
加密后的数据只能由同一系统用户调用 CryptUnProtectData()
来解密,一个系统用户无法调用 CryptUnProtectData()
来解密其他系统用户的 DPAPI
加密数据。
CryptProtectData()
的函数调用1 2 3 4 5 6 7
pDataIn:DATA_BLOB 结构指针,只想需要加密的数据明文块; szDataDescr:描述字符串,返回的数据包含该字符串,其未被加密。该参数为可选参数,可以为 NULL; pOptionslEntropy:DATA_BLOB 结构指针,指向一个额外熵参数,可以是一个加密密码。改参数为可选参数乜可以为 NULL。若加密时设置了额外熵参数,则加密时必须提供同样的熵参数,否则无法解密; pvReserved:保留,必须为 NULL; pPromptStruct:CRYPTPROTECTPROMPTSTRUCT 结构指针,用于弹出对话框与用户交互,通常为 NULL。 dwFlags:加密标识位,通常为 NULL; pDataOut:DATA_BLOB 结构指针,只想经过加密处理后的密文块。
CryptUnProtectData()
的参数跟CryptProtectData()
的参数类似,详见 MSDN 文档.aspx),这里不在说明。
1.2 基本概念介绍
1)加密应用程序编程接口
加密应用程序编程接口( cryptography application programming interface, CryptoAPI )是 Windows 平台提供的一组函数,该函数允许应用程序对用户的秘密信息进行编码、加密和数字签证等操作。CryptoAPI
内部的加密操作是在加密服务提供程序(CSP)的独立模块中执行,DPAPI 是在 CryptoAPI 的基础上实现封装。
2)加密服务提供程序
加密服务提供程序( cryptographic service provider,CSP )是一组实现标准加密和签名算法的硬件和软件的组合。每个 CSP 都包含一组它们自己定义并实现的函数。不同的 CSP 提供的安全算法不用,且 CSP 是平台相关的。不同的 Windows 操作系统提供的 CSP 的个数和类型也不同,每个 CSP 都有其对应的名称和类型,名称必须是唯一的。目前常用的 CSP 类型有9种,要指定采用哪种 CSP,只需在 CryptAcquirecContext() 中指定即可,DPAPI 默认使用 PROV_RSA_FULL
类型。
3)算法标识
算法标识( ALG_ID )是微软顶一顶一系列 32 位整型值,用于指明 CryptoAPI
所采用的加密或散列算法类型。其中以 0x66
开头的标识通常表示对称加密算法,以 0x88
开头的标识通常表示散列算法。例如,CALG_3DES
对应的值为 0x6603
,表示为三重数据加密标准。
4)安全散列算法
安全散列算法( secure hash algorithm,SHA )是散列算法中的一种,又叫摘要算法,用于产生消息摘要,也是经常使用的一种算法。在数据签名标准( digita signature standard,DSS )中,安全散列算法通常和数字签名算法( digital signature algorithm,DSA )一起用于对消息进行数字签名。每一个安全散列算法都有其对应的算法标识。在 CryptoAPI
中,SHA 对应的算法标识为 CALG_SHA
。目前,安全散列算法有 4 种:SHA-1、SHA-256、SHA-384 和 SHA-512,可分别产生 160 位、256 位、384 位和 512 位长的消息摘要,还有一个变种的SHA-224(224位)实现方为 Bouncy Castle。
5)会话密钥
会话密钥( session key )是随机产生的密钥,使用一次后,立刻被丢弃而不会被保存。在 CryptoAPI
中,会话密钥通常是对称加密算法的密钥。会话密钥由 40~2000 位随机数组成,可以通过调用 CryptoAPI 中的 CryptDeriveKey() 并传递一个散列值来生成。
6)干扰值
干扰值( salt valve )也称作 “盐”,通常是随机数,一般可看作是会话密钥的一部分。干扰值被添加到会话密钥后,通常是以明文的形式被防止在加密数据的前端。加入干扰值可以有效地防止堆成加密算法被预先计算好的彩虹表攻击。在 CryptoAPI
中,干扰值通过 CryptGenRandom() 来生成,此函数在将来的版本会进行删除,详情请看函数详情。
7)基于口令的密钥派生函数
基于口令的密钥派生函数( passwordbased key derivation function,PBKDF )通过对干扰后的用户输入口令计算多次散列来缓和字典攻击。攻击者若想确定口令的正确性,需要执行上百万条指令,导致完成一次字典攻击就需要花费大量时间。PBKDF 目前有两个版本:PBKDF1 和 PBKDF2,两个函数均以口令、干扰值和内部函数的跌代次数作为输入。在 DPAPI 中,采用的是 PBKDF2 版本,且内部做了部分改动。
8)加密散列函数
加密散列函数又叫基于散列的消息认证代码( hash-based message authentication code,HMAC )。使用加密散列函数需要一个密钥,同时还需要制定一个散列函数,可以是 MD5 或 SHA-1 等。在 DPAPI 中,HMAC 主要用于数据认证。
9)密钥分组链接
密钥分组链接( cipher-block chaining,CBC )是一种加密模式,在 CBC 模式中,每个分组完的明文块都需要与前一个经过加密后的密文块进行异或操作,然后进行加密操作。因此需要使用初始化向量。 CBC 加密模块是微软默认使用的加密模块,DPAPI 内部对所有对称加密算法默认都采用 CBC 加密模式。
10)填充
填充( padding) 是明文根据加密函数进行数据分组后,由于最后一个明文块不满足分组数据长度要求而在末尾额外添加的数据。填充的数据解密后一般会被自动移除,DPAPI 内部所有堆成加密算法默认都采用 PKCS
填充方式。
0x02 DPAPI 加密机制分析
CryptProtectData()
是对 CryptoAPI
的封装,其加密过程如下图所示。
整个加密过程大致可以分成 3 个阶段,分别为生成主密钥、解密主密钥以及使用主密钥加密数据。
2.1 生成主密钥
当前应用程序调用 CryptProtectData()
时,DPAPI 会读取主密钥存储区下的 Preferred
文件,获取当前系统使用的主密钥文件及其创建时间,如果创建时间与当前系统时间相差超过了 90 天,则重新生成一个主密钥文件。
为了防止攻击者对同一个加密主密钥进行长期的攻击,微软引入了主密钥的更新机制,更新时间微软设置为 90 天。即若 Preferred
文件中指示的主密钥创建时间与系统当前时间相差 90 天以上,将生成一个新的主密钥,新的主密钥将以同样的加密方式保护用户数据。这种主密钥更新策略有效防止了攻击者破解唯一的主密钥后即可访问用户所有的受保护数据。因为主密钥会更新,因而 DPAPI 必须提供一种机制能够解密历史主密钥加密下的数据库。其实,DPAPI 不删除任何过期的主密钥,所有的主密钥文件保存在用户的配置文件目录下,且全受到用户登陆密码的保护,并且每一个加密块都存储着当时加密它的主密钥全局唯一标识符(GUID)。当需要解密加密块时,DPAPI 从加密块中提取 GUID,找到对应主密钥文件进行相应的数据解密。
2.2 解密主密钥
若 Preferred
文件指示的主密钥没有过期,DPAPI 将解密对应的主密钥文件,获取 64 字节的主密钥。
主密钥受到用户登陆密码保护。DPAPI 首先使用 SHA-1 安全散列函数作用于用户登陆密码,然后将此密码散列和 16 字节的干扰值以跌代次数提供给基于口令的密钥派生函数 PBKDF2,用户派生一个会话密钥;然后用此会话密钥作为堆成加密算法的加密密钥,对主密钥进行加密,将加密后的主密钥存储在用户的配置文件目录下。
为了防止主密钥被篡改,主密钥将被计算 HMAC 加密散列。DPAPI 将使用 SHA 版HMAC 加密散列算法并以密码散列作为加密密钥作用于 16 字节干扰值,进而派生对应加密 散列值。该加密散列值再次作为 HMAC 的密钥计算主密钥的加密散列,计算后的加密散列同加密后的主密钥一起存于主密钥中。
由于主密钥受到用户登陆密码的保护,而用户登陆密码又是可修改的。因此,DPAPI 必须提供一种机制,使得在用户修改登陆密码后仍然可以正常解密主密钥。其实,DPAPI 对密码修改模块进行了 Hook 操作。当用户修改密码时,所有主密钥都将根据新的密码重新加密。另外,用户配置文件目录下有个历史凭据文件 CREDHIST
,当用户修改密码时,旧密码的 SHA-1 散列值会用新的密码进行加密,然后将加密后的结果存放在文件的底部。因此,如果当前系统登录密码法务解密主密钥,DPAPI 将使用当前密码解密历史凭据文件,获取上一次历史密码散列值,然后用这个历史密码散列值解密主密钥。如果解密又失败了,历史密码散列值将再次用于解密历史凭据文件,获取更旧的密码散列值。如此下去,直到成功解密主密钥为止。
2.3 使用主密钥加密数据
主密钥并不直接作为加密密钥来保护数据。DPAPI 首先将主密钥、16 字节干扰值以及应用程序提供的额外熵参数 3者组合派生一个会话密钥,然后用这个会话密钥对数据进行加密。但这个会话密钥永远不会被保存,DPAPI 选择存储用于派生会话密钥的 16 字节干扰值。这些干扰值是用来产生加密数据的关键。当 DPAPI 需要解密加密块时,便从加密块种提取这 16 字节的干扰值,并以同加密相同的方式派生出会话密钥,然后用该会话密钥对数据进行解密。
0x03 DPAPI 离线解密方法
由以上对 DPAPI 加密过程的分析容易得出 DPAPI 离线解密过程,如下图所示。
DPAPI 离线解密过程大致可以分为以下几点。
3.1 定位主密钥
由对 DPAPI 加密过程的分析可知主密钥文件会定期更新,Preferred 文件中存储这最后生成的主密钥文件 GUID,然而任意给定的一个 DPAPI 加密块并不一定时用最新的主密钥进行解密。为了快速定位 DPAPI加密块对应的主密钥文件,DPAPI 加密块种存储当时加密它的主密钥 GUID。DPAPI 加密块大致结构如下图所示。
从 DPAPI 加密块结构可知其除了存储主密钥 GUID 外,还存储解密时所需的其他关键数据信息,包括所使用的对称加密算法标识、安全散列算法标识、干扰值等。
- 第一个字段表示版本号,为固定值 0x00000001;
- 第二个字段表示加密服务提供程序的 DUID,也为固定值 D08C9DDF0115D1118C7A00C04FC297EB;
由于标准的 DPAPI 加密块总是以单个完整的文件存在,所以标准的 DPAPI 加密块有可能包含在文件中作为文件的一部分存在,甚至有些文件可以包含多个标准的 DPAPI 加密块。由于 DPAPI 加密块的前两个字段总是为固定值,因此可以以此为特征在文件中搜索和提取标准的 DPAPI 加密块。
3.2 解密主密钥
Windows 的主密钥文件和历史凭据文件有其固定的路径结构,如下所示。
- 用户主密钥文件,位于
%APPDATA%\Microsoft\Protect\%SID%
- 系统主密钥文件,位于
%WINDIR%\System32\Microsoft\Protect\S-1-5-18\User
每个系统帐号都有其对应的若干个主密钥;%SID% 是 Windows系统为区分不同账户而为它们分配的全局唯一标识符,它们的格式的固定的。和主密钥对应的历史凭据文件则位于主密钥的上一层目录。
主密钥文件共包含 5 个数据单元,分别为
主密钥头部单元
、用户主密钥单元
、本地加密密钥单元
、历史凭据标识单元
和域密钥备份单元
。结构如图所示。
其中,主密钥头部单元包括主密钥 GUID,该标识与 DPAPI 中指示的加密密钥唯一标识相对应。另外,主密钥头部单元还包含了指示其他个单元占用字节数的字段。历史凭据单元也包含一个全局唯一表示符( CREDHIST GUID ),用来指示跟主密钥文件对应的历史凭据文件。对于域密钥本分单元,只有域环境下的用户才会有,此单元的数据经过了域管理员的公钥加密处理。对于单机用户,无论什么系统都没有域密钥备份单元,这篇文章主要解析单机用户主密钥单元。用户主密钥单元包含一个经过加密的二进制加密块,其中的加密数据就是主密钥。
用户主密钥单元中包含 PBKDF2 采用的迭代次数、安全散列算法类型和对称加密算法类型等字段,不同的操作系统有着不同的值
操作系统 | PBKDF2迭代次数 | 安全散列算法类型 | 加密算法类型 |
---|---|---|---|
Windows XP | 4000 | SHA-1 | DES-3 |
Windows 2003 | 4000 | SHA-1 | DES-3 |
Windows Vista | 24000 | SHA-1 | DES-3 |
Windows 7 | 5600 | SHA-512 | SHA-256 |
Windows 8 | 8000 | SHA-512 | SHA-256 |
Windows 10 |
由 DPAPI 的加密过程分析可知,要使 DPAPI 正常运作,必须保存用户所有的历史登陆密码的 SHA-1 值。用户所有的历史登陆密码的 SHA-1 值存储在一个名为 CREDHIST 的历史凭据文件中,结构如图所示
CREDHIST 以链表的形式存储用户历史登陆密码的 SHA-1 值,每个用户的登陆密码散列值作为链表的一个节点,整个链表在文件中以反向形式存储,即表头在最后,倒数第二个记录时第一个节点,依次类推。而每个链表节点都具有相同的数据格式,并且每个节点的 SHA-1 值被前一个节点的 SHA-1 值加密处理,加密的方式与主密钥的加密方式一样。
图 2 的解密主密钥过程中涉及一个基于口令的密钥派生函数( PBKDF2 )的调用,微软的 PBKDF2 跟工业标准( PKCD#5 )不太一样,其内部做了部分改动。工业标准的 PBKDF2 函数循环内的伪随机函数的输入时上一次循环伪随机函数的输出,而微软 PBKDF2 函数伪随机函数的输入则是上两次相邻随机函数输出的异或。
3.3 解密 DPAPI 加密块
从图 2 的 DPAPI 离线解密过程可以知道,在解密 DPAPI加密块时先需要从主密钥派生一个会话密钥,派生过程中调用了改版的加密散列函数( M_HMAC() )和一个烟花密钥函数( DeriveKey() )。M_HMAC()
输入包括 DPAPI 提供的额外熵参数。
M_HMAC()
具体描述如下所示:1 2 3 4 5 6 7 8 9 10 11
M_HMAC(K, m, e, s) 算法的数据定义为: M_HMAC(K, m, e, s) = H((K ⊕ opad) // H((K ⊕ ipad) // m) // e // s) H() 表示对应的散列函数 K 表示密钥,这里对应密钥的 SHA-1 值 M 表示需要认证的数据,这里对应 Salt S 表示强密码 // 表示两个字符串的连接 ⊕ 表示异或 Opad 表示外部填充块 Ipad 表示内部填充块
DeriveKey()
对应于 CryptoAPI 的 CryptDeriveKey(),具体描述如下:1 2 3 4 5 6 7 8 9 10 11
DeriveKey(d, n) 算法的数据定义为: DeriveKey(d, n) = F-n(H( d ⊕ ipad) // H(d ⊕ opad)) H() 表示对应的散列函数 F-n() 表示取前 t 个字节数据 n 表示需要演化出的密钥长度 d 表示被演化的数据(对应 M_HMAC() 输出),并通过填充 0 补齐 64 字节长 // 表示两个字符串的连接 ⊕ 表示异或 Opad 表示外部填充块 Ipad 表示内部填充块
0x04 DPAPI 的应用
- EFS文件加密
- 存储无线连接密码
- Windows凭据管理器
- IE浏览器
- 外表
- Skype的
- Windows CardSpace
- Windows Vault
- 谷歌浏览器
0x05 结束语
在 WIndows 操作系统中,DPAPI 作为具有加密功能的最主要接口之一,保护着大量的用户私密数据。然而,本文给出的 DPAPI 离线解密方法,只适用于单机用户,无法解密域管理员控制下的 DPAPI 加密的数据。与环境控制下的 DPAPI 加密机制相对于淡季用户有很大的不同。