文章总结: 该文档分析了CTF竞赛中两道涉及Bcrypt加密API的逆向工程题目。通过动态调试技术追踪BCryptOpenAlgorithmProvider、BCryptGenerateSymmetricKey等Windows加密函数调用,识别出题目使用的RC4、MD5、DES、AES-CBC等加密算法。文章详细演示了如何通过硬件断点定位加密密钥、IV值和预期哈希值,并提供了Python爆破脚本的实战示例。核心结论是掌握BcryptAPI的调试方法对解决CTF加密类题目具有重要实用价值。 综合评分: 85 文章分类: CTF,逆向分析,WEB安全,二进制安全,漏洞分析
关于 CTF 中 Bcrypt 考点的思考
studying-egg studying-egg
正在思考ing
2026年4月30日 21:49 江苏
在小说阅读器读本章
去阅读
参考链接
https://xz.aliyun.com/news/91875
引言
最近在复现polarisctf的过程中,想到了软件系统安全区域赛有一道题很类似。都考察了控制流平坦化和bcrypt加密。 看题解的过程中对于预期值的寻找感觉没有说明清楚,这里分享一下个人的思考。
polarisctf2026 – easyre
通过程序的控制流程图,可以得出考察的是控制流平坦化
这里通过改变r8的来实现改变控制流 我们在Import表,发现导入了bcrypt.dll
bcrypt.dll 是 Windows 系统自带的核心加密动态链接库,我们通过劫持这里的传参,分析加密方式 我们需要重点关注下面几个API
// attributes: thunk
NTSTATUS __stdcall BCryptOpenAlgorithmProvider(
BCRYPT_ALG_HANDLE *phAlgorithm,
LPCWSTR pszAlgId,
LPCWSTR pszImplementation,
ULONG dwFlags)
{
return __imp_BCryptOpenAlgorithmProvider(phAlgorithm, pszAlgId, pszImplementation, dwFlags);
}
phAlgorithm:算法句柄
pszAlgId : 加密算法字符串
LPCWSTR pszImplementation:算法提供者(默认是微软)
dwFlags: 功能标志。默认是0
// `BCRYPT_ALG_HANDLE_HMAC_FLAG` → HMAC 模式
// `BCRYPT_ALG_HANDLE_AES_GMAC_FLAG` → AES GMAC 模式
指明加密算法
// attributes: thunk
NTSTATUS __stdcall BCryptGenerateSymmetricKey(
BCRYPT_ALG_HANDLE hAlgorithm,
BCRYPT_KEY_HANDLE *phKey,
PUCHAR pbKeyObject,
ULONG cbKeyObject,
PUCHAR pbSecret,
ULONG cbSecret,
ULONG dwFlags)
{
return __imp_BCryptGenerateSymmetricKey(hAlgorithm, phKey, pbKeyObject, cbKeyObject, pbSecret, cbSecret, dwFlags);
}
BCRYPT_ALG_HANDLE hAlgorithm, // [in] 算法句柄
BCRYPT_KEY_HANDLE *phKey, // [out] 输出:密钥句柄
PUCHAR pbKeyObject, // [out] 密钥内存缓冲区
ULONG cbKeyObject, // [in] 缓冲区大小
PUCHAR pbSecret, // [in] 原始密钥数据
ULONG cbSecret, // [in] 原始密钥长度
ULONG dwFlags // [in] 标志位
指明密钥
// attributes: thunk
NTSTATUS __stdcall BCryptEncrypt(
BCRYPT_KEY_HANDLE hKey,
PUCHAR pbInput,
ULONG cbInput,
void *pPaddingInfo,
PUCHAR pbIV,
ULONG cbIV,
PUCHAR pbOutput,
ULONG cbOutput,
ULONG *pcbResult,
ULONG dwFlags)
{
return __imp_BCryptEncrypt(hKey, pbInput, cbInput, pPaddingInfo, pbIV, cbIV, pbOutput, cbOutput, pcbResult, dwFlags);
}
NTSTATUS __stdcall BCryptEncrypt( BCRYPT_KEY_HANDLE hKey, // [in] 密钥句柄
PUCHAR pbInput, // [in] 明文数据(要加密的内容)
ULONG cbInput, // [in] 明文长度(字节)
void* pPaddingInfo, // [in] 填充信息指针(大部分传NULL)
PUCHAR pbIV, // [in] 初始化向量 IV
ULONG cbIV, // [in] IV长度(字节)
PUCHAR pbOutput, // [out] 输出:密文数据
ULONG cbOutput, // [in] 输出缓冲区大小
ULONG* pcbResult, // [out] 实际加密后的密文长度
ULONG dwFlags // [in] 功能标志 );
通过服务端和客户端两个程序可以大胆猜测,这道题的校验逻辑存在于服务端中 开始动态调试
开始是一个RC4加密 这里是将客户端的数据进行解密,这里我们直接跳过,查看下一个加密算法
这次是MD5我们需要再在BCryptHashData处打上断点
// attributes: thunk
NTSTATUS __stdcall BCryptHashData(BCRYPT_HASH_HANDLE hHash, PUCHAR pbInput, ULONG cbInput, ULONG dwFlags)
{
return __imp_BCryptHashData(hHash, pbInput, cbInput, dwFlags);
}
我们查看pbinput的值,发现是我们的输入
我们需要再在BCryptFinishHash处打上断点,查看我们最终的哈希计算值
// attributes: thunk
NTSTATUS __stdcall BCryptFinishHash(BCRYPT_HASH_HANDLE hHash, PUCHAR pbOutput, ULONG cbOutput, ULONG dwFlags)
{
return __imp_BCryptFinishHash(hHash, pbOutput, cbOutput, dwFlags);
}
运行到这里,我们查看pbOutput的值,发现其中并不是输入值(123456)的MD5哈希值,不着急,我们步进进入BCryptFinishHash函数内部
这里通过观察,pbOutput的地址指针保存在RDX指针中,我们定位到具体位置,观察变化
通过变化,最终该地址得到了正确的MD5哈希值,后面程序的逻辑应该是对该值与预期值进行比较,所以这里我们给这里打上硬件断点 PS:IDA里面不知道怎么打硬件断点,这里换到x64dbg打硬件断点吧
继续运行程序
停到了这段代码上,分析可得这段代码的作用是将输入的MD5值由ASCII值转换为16进制,最终保存到[rsp+100]地址处。同理我们对这里的地址打上硬件断点
这里我们找到了期待值E5D489FD91431D5438EB28F7490F9CE0我们通过脚本进行爆破
import hashlib
import itertools
import string
from multiprocessing import Pool
# 目标MD5哈希值
target_hash = "E5D489FD91431D5438EB28F7490F9CE0"
# 生成可能的字符串
def generate_strings(length):
chars = string.ascii_lowercase # 使用小写字母
return itertools.product(chars, repeat=length)
# 验证MD5
def verify_md5(candidate):
candidate_str = ''.join(candidate)
if candidate_str == "ctfer":
print(candidate_str)
md5_hash = hashlib.md5(candidate_str.encode()).hexdigest()
if md5_hash == target_hash.lower():
return candidate_str
returnNone
# 爆破函数
def crack_md5(length):
for candidate in generate_strings(length):
result = verify_md5(candidate)
if result:
return result
returnNone
if __name__ == "__main__":
# 设置最大尝试长度
max_length = 5
with Pool() as pool:
result = pool.apply_async(crack_md5, (max_length,))
if result.get():
print(f"找到匹配字符串: {result.get()}")
# ctfer
用户名爆破成功后,序列号采取同样的方法进行爆破 最终flag62001be6b65779c64e67deb560164745
软件系统安全赛区域赛 – crackme
思路基本同上
随意个一输入错误输入,可以看到预期值 预期值为:FC 8F 2B 91 3A 35 B5 E8 70 EB 18 63 79 F4 CB A0 C4 B0 CC 19 8D 3F A6 39 C5 4E E2 0D 1A 8E 3E 6C 79 16 BD 4C A3 BE 71 4B 95 FC A7 CD 77 73 A2 56
在BCryptOpenAlgorithmProvider和BCryptGenerateSymmetricKey以及BCryptEncrypt处打上断点
程序逻辑为先进行DES加密,再进行AES-CBC加密,并与预期值进行比较
通过BCryptGenerateSymmetricKey找到密钥
密钥值为:91adf387c9b48aeed2a19fc7b3d985e4 在BCryptEncrypt找到IV值
IV值为:6ec1a237589f03d4b5e70c92fa418b66 进行解密
检验:
结语
其实软件系统安全赛区域赛看到crackme的时候就联想到polarisctf的easyre,但那个时候没有复现,比赛的时候就没往bcrypt上去想,有点遗憾
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:正在思考ing studying-egg studying-egg《关于 CTF 中 Bcrypt 考点的思考》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论