在逆向分析、协议还原与恶意软件分析中,加密参数的提取往往是突破点。大量 Windows 程序依赖系统自带的 Crypto API 完成加解密、哈希计算与签名验证,通过 Hook 这些系统接口,可以在不破解算法的前提下,直接截获明文、密钥、IV 与加密结果,大幅提升逆向效率。
本文将从原理到实战,系统讲解如何 Hook Windows Crypto API 以获取关键加密参数,并提供可直接复用的实现方案。
一、Windows Crypto API 体系与核心函数
Windows 提供两代加密接口:传统 CryptoAPI(advapi32.dll)与新一代 CNG(Cryptography Next Generation,bcrypt.dll)。绝大多数老牌软件与加壳程序仍在使用传统接口,而较新的应用则倾向于 BCrypt 系列。
1.1 传统 CryptoAPI 关键函数(advapi32.dll)
表格
| 函数名 | 作用 | 可截获参数 |
|---|---|---|
CryptEncrypt |
加密数据 | 明文、密文、密钥句柄、数据长度 |
CryptDecrypt |
解密数据 | 密文、解密后明文 |
CryptGenKey |
生成密钥 | 算法类型、密钥长度、密钥句柄 |
CryptDeriveKey |
派生密钥 | 基础密码、算法、派生后密钥 |
CryptImportKey |
导入密钥 | 原始密钥明文数据 |
CryptHashData |
计算哈希 | 输入明文、哈希结果 |
CryptSetKeyParam |
设置密钥参数 | IV、填充模式、盐值 |
其中 CryptEncrypt 是逆向中最高频的 Hook 目标,函数原型如下:
BOOL CryptEncrypt(
HCRYPTKEY hKey, // 加密密钥句柄
HCRYPTHASH hHash, // 哈希对象句柄(可NULL)
BOOL Final, // 是否最后一块数据
DWORD dwFlags, // 标志位
BYTE* pbData, // 输入明文,输出密文(原地覆盖)
DWORD* pdwDataLen, // 数据长度指针
DWORD dwBufLen // 缓冲区总大小
);
1.2 CNG 接口关键函数(bcrypt.dll)
BCryptEncrypt/BCryptDecrypt:对称加解密BCryptGenerateSymmetricKey:生成对称密钥BCryptOpenAlgorithmProvider:打开算法提供者BCryptHash:哈希计算
CNG 接口参数更清晰,Hook 后提取密钥与明文也更直接。
二、Hook 技术选型与对比
针对 Crypto API 的 Hook 主要有三种实现路线,各有适用场景:
2.1 IAT Hook(导入表 Hook)
通过修改 PE 导入地址表(IAT)中函数指针,将目标 API 指向自定义函数。优点是稳定性高、不易触发异常;缺点是只能拦截通过导入表调用的 API,对于动态加载(GetProcAddress)无效。
2.2 Inline Hook(内联 Hook)
直接在目标函数入口处改写机器码,写入跳转指令劫持执行流。优点是通用性强,无论静态导入还是动态加载均可拦截;缺点是需要处理指令对齐、DEP/NX、CFG 等系统保护,x64 下实现复杂度高于 x86。
2.3 Frida 动态插桩
基于 Frida 框架进行运行时 Hook,无需编写 DLL 注入代码,脚本化程度高,适合快速分析与原型验证。Windows、Android、iOS 多平台通用,是目前逆向分析的主流工具链。
三、实战一:Inline Hook 拦截 CryptEncrypt
下面给出 x86 环境下 Inline Hook CryptEncrypt 的完整实现,运行后可直接打印每次加密的明文与密文。
3.1 定义函数指针与全局变量
#include <windows.h>
#include <stdio.h>
typedef BOOL (WINAPI *CryptEncrypt_t)(
HCRYPTKEY hKey, HCRYPTHASH hHash, BOOL Final,
DWORD dwFlags, BYTE* pbData, DWORD* pdwDataLen, DWORD dwBufLen
);
CryptEncrypt_t g_OriginalCryptEncrypt = NULL;
BYTE g_OriginalBytes[5] = {0}; // 保存原函数前5字节
3.2 Hook 代理函数
BOOL WINAPI HookedCryptEncrypt(
HCRYPTKEY hKey, HCRYPTHASH hHash, BOOL Final,
DWORD dwFlags, BYTE* pbData, DWORD* pdwDataLen, DWORD dwBufLen)
{
// 调用前打印明文
printf("[CryptEncrypt] 调用前明文长度: %d\n", *pdwDataLen);
printf("[CryptEncrypt] 明文(Hex): ");
for (DWORD i = 0; i < *pdwDataLen; i++) {
printf("%02X ", pbData[i]);
}
printf("\n");
// 调用原始函数
BOOL result = g_OriginalCryptEncrypt(
hKey, hHash, Final, dwFlags, pbData, pdwDataLen, dwBufLen
);
// 调用后打印密文
if (result) {
printf("[CryptEncrypt] 加密后密文长度: %d\n", *pdwDataLen);
printf("[CryptEncrypt] 密文(Hex): ");
for (DWORD i = 0; i < *pdwDataLen; i++) {
printf("%02X ", pbData[i]);
}
printf("\n------------------------\n");
}
return result;
}
3.3 安装 Inline Hook
void InstallCryptEncryptHook()
{
HMODULE hAdvapi = LoadLibraryA("advapi32.dll");
g_OriginalCryptEncrypt = (CryptEncrypt_t)GetProcAddress(hAdvapi, "CryptEncrypt");
DWORD oldProtect;
VirtualProtect(g_OriginalCryptEncrypt, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
// 保存原始5字节
memcpy(g_OriginalBytes, g_OriginalCryptEncrypt, 5);
// 构造 JMP 指令: E9 + 相对偏移
BYTE jmpCode[5] = { 0xE9, 0, 0, 0, 0 };
DWORD offset = (DWORD)HookedCryptEncrypt - (DWORD)g_OriginalCryptEncrypt - 5;
memcpy(jmpCode + 1, &offset, 4);
// 写入跳转
memcpy(g_OriginalCryptEncrypt, jmpCode, 5);
VirtualProtect(g_OriginalCryptEncrypt, 5, oldProtect, &oldProtect);
}
将上述代码编译为 DLL,通过远程线程注入目标进程后,所有 CryptEncrypt 调用都会被截获,明文与密文直接输出到控制台。
注:x64 环境下不能使用 5 字节相对跳转,需改用 14 字节绝对跳转(
mov rax, addr; jmp rax),并配合 Trampoline 跳板执行被覆盖的原始指令。
四、实战二:Frida Hook 一键提取加密参数
对于快速分析场景,Frida 是效率最高的方案。以下脚本可直接 Hook advapi32.dll 中的 CryptEncrypt,并打印明文、密文与密钥信息:
// crypto_hook.js
const CryptEncrypt = Module.findExportByName("advapi32.dll", "CryptEncrypt");
Interceptor.attach(CryptEncrypt, {
onEnter: function (args) {
this.hKey = args[0];
this.pbData = args[4];
this.pdwDataLen = args[5];
this.dataLen = this.pdwDataLen.readU32();
console.log("\n[+] CryptEncrypt 被调用");
console.log(" 密钥句柄:", this.hKey);
console.log(" 明文长度:", this.dataLen);
console.log(" 明文(Hex):", hexdump(this.pbData, { length: this.dataLen }));
},
onLeave: function (retval) {
if (retval.toInt32() !== 0) {
const outLen = this.pdwDataLen.readU32();
console.log(" 密文长度:", outLen);
console.log(" 密文(Hex):", hexdump(this.pbData, { length: outLen }));
}
}
});
执行命令:
frida -p <目标PID> -l crypto_hook.js
除了 CryptEncrypt,只需替换函数名即可批量 Hook CryptDecrypt、CryptHashData、CryptImportKey 等接口,形成完整的加密监控面板。开源工具 VMP_Hunter 就是基于这一思路实现的全量 Crypto API 拦截器GitHub。
五、进阶:密钥与 IV 的提取
只截获明文密文还不够,完整还原加密算法需要拿到密钥与 IV。
5.1 Hook CryptImportKey 提取原始密钥
当程序调用 CryptImportKey 导入密钥时,pbData 参数即为密钥明文(含 BLOB 头)。Hook 该函数可直接拿到原始密钥字节。
5.2 Hook CryptSetKeyParam 获取 IV
CryptSetKeyParam 的 KP_IV 参数用于设置初始化向量。Hook 时判断 dwParam 值是否为 KP_IV(值为 1),即可捕获 IV 数据。
5.3 通过密钥句柄反查密钥
对于 CryptGenKey 生成的密钥,可以在 CryptEncrypt 被调用时,使用 CryptGetKeyParam 读取 KP_BLOCKLEN、KP_ALGID 等参数判断算法类型,配合 CryptExportKey 导出密钥 BLOB。
六、BCrypt(CNG)接口的 Hook
新一代 CNG 接口参数更规范,Hook 收益更高。以 BCryptEncrypt 为例:
const BCryptEncrypt = Module.findExportByName("bcrypt.dll", "BCryptEncrypt");
Interceptor.attach(BCryptEncrypt, {
onEnter: function (args) {
this.hKey = args[0];
this.pbInput = args[1];
this.cbInput = args[2].toInt32();
this.pbOutput = args[4];
this.pcbResult = args[6];
console.log("[BCryptEncrypt] 输入长度:", this.cbInput);
console.log(hexdump(this.pbInput, { length: this.cbInput }));
},
onLeave: function (retval) {
if (retval.toInt32() === 0) {
const outLen = this.pcbResult.readU32();
console.log("[BCryptEncrypt] 输出长度:", outLen);
console.log(hexdump(this.pbOutput, { length: outLen }));
}
}
});
CNG 接口还可以通过 BCryptGenerateSymmetricKey 的 pbKeyObject 缓冲区直接提取密钥原始字节,比传统 CryptoAPI 更加直观。
七、常见问题与对抗手段
7.1 Hook 不触发的原因
- 程序没有走系统 API,而是自带了加密库(如 OpenSSL、mbedTLS)
- 使用了动态
LoadLibrary+GetProcAddress,IAT Hook 失效 - 程序在 Hook 安装前就完成了加密调用并缓存了结果
- 目标程序运行在 WoW64 模式下,Hook 了错误位数的 DLL
7.2 反 Hook 与应对
部分保护壳会校验系统 API 入口处的哈希,检测到修改后直接退出。应对思路:
- 改用深层 Hook,Hook CSP 提供者内部函数而非 advapi32 导出层
- 使用硬件断点(Drx 寄存器)替代指令改写
- Hook 底层内核接口(如
NtQueryInformationProcess反调试对抗) - 使用 Frida 的
Interceptor.replace配合 Stalker 追踪
八、总结
Hook Crypto API 是逆向分析中性价比极高的技术手段 ------ 不需要理解复杂算法,不需要还原混淆代码,只需要在系统接口层 "搭个偷听装置",就能拿到所有关键参数。
对于快速验证,优先使用 Frida 脚本,几分钟内即可搭建完整的加密监控环境;对于需要集成到工具中的稳定方案,推荐使用 Inline Hook 并做好 x86/x64 兼容;如果目标程序仅通过导入表调用 API,IAT Hook 是最稳定、最隐蔽的选择。
在实际逆向工作中,建议从 CryptEncrypt / BCryptEncrypt 入手,先确认明文密文对应关系,再逐步扩展到密钥生成、哈希计算与数据签名接口,最终还原完整的加密链路。