关于CTF中Bcrypt考点的思考

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

文章总结: 该文档分析了CTF竞赛中两道涉及Bcrypt加密API的逆向工程题目。通过动态调试技术追踪BCryptOpenAlgorithmProvider、BCryptGenerateSymmetricKey等Windows加密函数调用,识别出题目使用的RC4、MD5、DES、AES-CBC等加密算法。文章详细演示了如何通过硬件断点定位加密密钥、IV值和预期哈希值,并提供了Python爆破脚本的实战示例。核心结论是掌握BcryptAPI的调试方法对解决CTF加密类题目具有重要实用价值。 综合评分: 85 文章分类: CTF,逆向分析,WEB安全,二进制安全,漏洞分析


cover_image

关于 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

BCryptOpenAlgorithmProviderBCryptGenerateSymmetricKey以及BCryptEncrypt处打上断点

程序逻辑为先进行DES加密,再进行AES-CBC加密,并与预期值进行比较

通过BCryptGenerateSymmetricKey找到密钥

密钥值为:91adf387c9b48aeed2a19fc7b3d985e4 在BCryptEncrypt找到IV

IV值为:6ec1a237589f03d4b5e70c92fa418b66 进行解密

检验:

结语

其实软件系统安全赛区域赛看到crackme的时候就联想到polarisctf的easyre,但那个时候没有复现,比赛的时候就没往bcrypt上去想,有点遗憾


免责声明:

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

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

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

本文转载自:正在思考ing studying-egg studying-egg《关于 CTF 中 Bcrypt 考点的思考》

评论:0   参与:  0