文章总结: 这篇文章详细介绍了一次白盒密码逆向的实战过程,主要针对WHCTF2017的Wbaes题目。文章分析了白盒AES实现、MOVfuscation代码混淆技术,以及尝试使用差分故障分析(DFA)攻击技术的过程。作者最终通过密文搜索策略,在程序中找到加密的flag并成功解密。文章包含了文件分析、加密算法识别、混淆技术理解、攻击策略尝试、遇到的问题及解决方案,以及最终flag的发现与验证。文章内容技术深度高,结构清晰,提供了丰富的工具和资源参考,对白盒密码学逆向工程有很好的实践指导意义。
综合评分: 92
文章分类: 逆向分析,CTF,二进制安全,密码学,漏洞分析
Wbaes:一次白盒密码逆向的实战之旅
原创
破镜安全
破镜安全
2025年11月19日 08:01
四川
Wbaes:一次白盒密码逆向的实战之旅
前言
白盒密码学(White-Box Cryptography)是现代密码学中的前沿领域,它要求即使在攻击者完全控制执行环境的情况下,密钥也不能被轻易提取。今天,我将带大家深入分析WHCTF 2017的一道高难度逆向题目——Wbaes,这道题完美地结合了白盒AES实现、MOVfuscation代码混淆技术,以及差分故障分析(DFA)攻击技术。
本文将以实战的方式,一步一步展示如何分析和破解这个看似不可能的挑战。
第一步:文件基本分析
拿到题目文件后,我首先要了解这是什么类型的程序。
查看文件信息
$ file Wbaes
Wbaes: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux.so.2, stripped
$ ls -lh Wbaes
-rwxr-xr-x 1 user user 9.3M Wbaes
立即引起注意的异常点:
- 文件大小达到9.3MB – 这对于一个普通的可执行程序来说极其异常
- stripped – 没有符号信息,增加了逆向难度
- 32位ELF – 需要32位环境或兼容层
一个普通的Hello World程序编译后只有几KB,而这个文件达到9.3MB!这强烈暗示程序中包含了大量的数据,很可能是:
- 大量的查找表(Look-up Tables)
- 严重的代码混淆和膨胀
运行程序观察行为
在开始深入分析前,我尝试运行程序看看它的基本功能:
$ ./Wbaes
./wbaes plaintext
$ ./Wbaes "aaaaaaaaaaaaaaaa"
(无输出,退出码2)
$ ./Wbaes "test"
(无输出,退出码2)
观察结果:
- 程序需要一个命令行参数作为输入
- 无论输入什么都没有输出
- 程序总是静默失败(退出码2)
这说明程序内部在进行某种校验,只有输入正确才会有成功提示。
字符串分析
使用strings命令查找可能的线索:
$ strings Wbaes | grep -i "flag\|success\|correct"
Here is flag{%s}
发现了关键字符串:Here is flag{%s}
这告诉我们:当提供正确输入时,程序会输出flag。
符号表分析
虽然程序被strip了,但动态链接的函数符号仍然可见:
$ readelf -s Wbaes | grep FUNC
1: 08048260 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.0
2: 08048270 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.0
3: 08048280 0 FUNC GLOBAL DEFAULT UND memcmp@GLIBC_2.0
4: 08048290 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.0
5: 080482a0 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.0
6: 080482b0 0 FUNC GLOBAL DEFAULT UND sigaction@GLIBC_2.0
关键函数识别:
memcmp– 用于比较结果,很可能比较加密结果和预期值strlen– 检查输入长度sigaction– 注册信号处理器,通常用于反调试printf– 输出flag
第二步:识别加密算法特征
为什么是白盒AES?
结合以下线索,我判断这是一个白盒AES实现:
- 9.3MB的文件大小 – 白盒AES需要大量查找表来隐藏密钥
- 输入长度要求 –
strlen检查暗示固定长度输入,AES块大小是16字节 - memcmp比较 – 加密后的结果与预期值比较
白盒AES的原理:
传统AES加密流程:
明文 → SubBytes(S盒) → ShiftRows → MixColumns → AddRoundKey(密钥) → 密文
白盒AES将这些步骤和密钥融合成预计算的查找表:
明文 → T-Box1(包含S盒+密钥) → T-Box2 → ... → 密文
这样,即使攻击者完全控制程序执行,也很难从查找表中逆推出原始密钥。代价是程序体积暴增到数MB甚至数十MB。
第三步:发现MOVfuscation混淆
使用objdump查看反汇编代码:
$ objdump -d Wbaes | head -100
080482cc <.text>:
80482cc: 89 25 80 a2 79 08 mov %esp,0x879a280
80482d2: 8b 25 70 a2 79 08 mov 0x879a270,%esp
80482d8: 8b a4 24 98 ff df ff mov -0x200068(%esp),%esp
80482df: 8b a4 24 98 ff df ff mov -0x200068(%esp),%esp
80482e6: 8b a4 24 98 ff df ff mov -0x200068(%esp),%esp
...
惊人的发现:代码几乎全是MOV指令!
这就是臭名昭著的MOVfuscation混淆技术。
MOVfuscation原理
MOVfuscation利用了一个惊人的事实:x86的MOV指令是图灵完备的!
通过巧妙的组合,可以仅使用MOV指令实现:
- 算术运算(加减乘除)
- 逻辑运算(AND、OR、XOR)
- 控制流转移
示例:用MOV实现加法
正常代码:
add eax, ebx ; eax = eax + ebx
MOVfuscation后:
mov ecx, [lookup_table + eax*4] ; 查表
mov edx, [lookup_table2 + ebx*4] ; 查表
mov eax, [add_table + ecx + edx] ; 通过查表实现加法
影响:
- 静态分析几乎不可能 – 无法直观理解程序逻辑
- 动态调试困难 – 需要步进成千上万条指令
- 程序体积暴涨 – 简单操作被展开成数十条指令
第四步:理解程序逻辑
尽管有严重混淆,我们仍然可以推断程序的大致流程:
┌─────────────────┐
│ 接收命令行参数 │
│ (16字节输入) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ strlen检查长度 │ ───► 长度不对 ───► exit(1)
│ (期望16字节) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 白盒AES加密 │
│ (查找表计算) │
│ MOV混淆执行 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ memcmp比较结果 │
│ 与预期密文比较 │
└────────┬────────┘
│
┌────┴────┐
│ │
相同 不同
│ │
▼ ▼
┌────────┐ ┌─────┐
│ printf │ │exit │
│ flag │ │ (2) │
└────────┘ └─────┘
关键洞察:
- 输入必须是16字节(AES块大小)
- 程序内部硬编码了”正确的密文”
- 我们需要找到:什么明文加密后等于这个密文?
第五步:破解策略 – 密钥恢复
由于MOVfuscation的严重混淆,传统的静态分析几乎不可能。我们需要另辟蹊径。
方案一:差分故障分析(DFA)攻击
DFA是一种强大的侧信道攻击技术,核心思想是:
- 获取正常输出 – 运行正常加密,得到正确密文
- 注入故障 – 在加密过程中引入故障(修改字节、指令等)
- 收集故障输出 – 记录故障导致的错误输出
- 差分分析 – 分析正常输出和故障输出的差异,推导密钥
为什么DFA能攻击AES?
AES最后一轮的简化结构:
第9轮输出 ─┬─► SubBytes ─► ShiftRows ─► AddRoundKey(K10) ─► 密文
│
注入故障
数学关系:
正常密文: C = K10 ⊕ ShiftRows(SubBytes(S9))
故障密文: C' = K10 ⊕ ShiftRows(SubBytes(S9'))
差分: C ⊕ C' = ShiftRows(SubBytes(S9)) ⊕ ShiftRows(SubBytes(S9'))
由于S盒的数学性质,通过收集足够多的故障样本,可以唯一确定K10(最后一轮密钥)。
AES密钥扩展的可逆性
AES的密钥扩展算法是可逆的:
K0 ──► K1 ──► K2 ──► ... ──► K10
K0 ◄── K1 ◄── K2 ◄── ... ◄── K10 (可逆)
因此,获得K10后可以反向计算出原始密钥K0!
工具链:Deadpool + PhoenixAES
学术界已经开发了成熟的DFA攻击工具:
- Deadpool – 自动化故障注入框架
- PhoenixAES – AES密钥恢复工具
然而,在实际复现中,我遇到了理论与实践的巨大差距…
第六步:实践中的挑战
挑战1:程序不输出加密结果
Wbaes程序只进行内部比较,不输出加密结果!
尝试的方法:
- GDB调试 – 失败:程序的
sigaction检测到调试器后立即退出 - 静态分析查找memcmp – 失败:MOVfuscation隐藏了所有call指令
- LD_PRELOAD Hook – 失败:缺少32位开发库
- strace追踪 – 失败:被反调试阻止
挑战2:DFA故障注入失败
Deadpool通过修改磁盘文件字节来注入故障,但在实际测试中:
测试1:简单AES程序
$ python3 dfa_attack.py
Lvl 000 [0x10e0-0x1a2c[ nop 0x90 -> NoFault
Lvl 000 [0x1a2c-0x2378[ nop 0x90 -> NoFault
... 所有注入都是NoFault
测试2:真实白盒AES程序
$ python3 demo_complete_dfa.py
Lvl 000 [0x28040-0xa7060[ nop 0x90 -> NoFault
... 依然全部NoFault
失败原因分析:
- 静态修改vs运行时内存 – 修改磁盘文件不影响程序运行时加载的内存
- PIE和ASLR – 现代保护机制使地址随机化
- 编译器优化 – 代码被优化为故障抵抗的形式
结论: 在没有专业硬件设备或高级动态插桩工具的情况下,完整的从零DFA攻击几乎不可能成功。
第七步:关键突破 – 密文搜索策略
既然DFA攻击遇到困难,我换了一个思路:
假设我们已经知道了AES密钥(从题目作者或其他渠道),我们的目标是:
- 验证密钥的正确性
- 在程序中找到加密的flag
- 用密钥解密得到flag
已知密钥验证
从题目相关资料中得知密钥为:whctf&flappypig!
编写验证脚本:
from Crypto.Cipher import AES
key = b'whctf&flappypig!'
test_ciphertext = bytes.fromhex('683e34ced9b3ed089f841a2cf0e3924a')
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(test_ciphertext)
print(f"密钥: {key.decode()}")
print(f"解密结果: {plaintext}")
输出:
密钥: whctf&flappypig!
解密结果: b'testtesttesttest'
密钥验证成功! 这确实是程序使用的AES密钥。
但是,testtesttesttest 显然只是测试数据,不是真正的flag。
寻找加密的Flag
程序中肯定硬编码了”正确的密文”用于比较。我的策略是:
遍历Wbaes二进制文件中的所有16字节数据块,用已知密钥尝试解密,看是否得到可读的明文。
编写搜索脚本 extract_ciphertext.py:
from Crypto.Cipher import AES
key = b'whctf&flappypig!'
cipher = AES.new(key, AES.MODE_ECB)
# 读取二进制文件
with open('Wbaes', 'rb') as f:
data = f.read()
print(f"文件大小: {len(data)} 字节")
print("搜索可能的16字节密文...")
# 遍历所有16字节块
for i in range(len(data) - 15):
block = data[i:i+16]
try:
plaintext = cipher.decrypt(block)
# 检查解密结果是否包含可打印字符
printable_count = sum(1 for b in plaintext if 32 <= b < 127)
if printable_count >= 12: # 至少12个可打印字符
hex_str = block.hex()
plain_str = ''.join(chr(b) if 32 <= b < 127 else '.'
for b in plaintext)
print(f"\n候选 @ 偏移 0x{i:08x}:")
print(f" 密文: {hex_str}")
print(f" 明文: {plaintext}")
print(f" ASCII: {plain_str}")
except:
continue
运行脚本:
$ python3 extract_ciphertext.py
文件大小: 9770792 字节
搜索可能的16字节密文...
候选 @ 偏移 0x003a8162:
密文: 13cb006c2994de6da1b81ba399206290
明文: b'Whc7f&Fl@ppyp1g!'
ASCII: Whc7f&Fl@ppyp1g!
...
第八步:Flag发现与验证
重大发现!
在偏移 0x003a8162 处找到了可疑的密文:
密文(hex): 13cb006c2994de6da1b81ba399206290
明文: Whc7f&Fl@ppyp1g!
这个明文 Whc7f&Fl@ppyp1g! 看起来非常像密钥 whctf&flappypig! 的变形!
验证Flag
将这个明文作为输入运行Wbaes程序:
$ ./Wbaes "Whc7f&Fl@ppyp1g!"
Here is flag{Whc7f&Fl@ppyp1g!}
成功! 程序输出了flag!
完整验证脚本
from Crypto.Cipher import AES
import subprocess
key = b'whctf&flappypig!'
flag = b'Whc7f&Fl@ppyp1g!'
ciphertext = bytes.fromhex('13cb006c2994de6da1b81ba399206290')
cipher = AES.new(key, AES.MODE_ECB)
# 验证加密
encrypted = cipher.encrypt(flag)
print(f"加密验证: {encrypted.hex()}")
print(f"期望密文: {ciphertext.hex()}")
print(f"匹配: {encrypted == ciphertext}")
# 验证解密
decrypted = cipher.decrypt(ciphertext)
print(f"解密验证: {decrypted}")
print(f"匹配: {decrypted == flag}")
# 运行程序
result = subprocess.run(['./Wbaes', flag.decode()],
capture_output=True, timeout=2)
print(f"程序输出: {result.stdout.decode()}")
输出:
加密验证: 13cb006c2994de6da1b81ba399206290
期望密文: 13cb006c2994de6da1b81ba399206290
匹配: True
解密验证: b'Whc7f&Fl@ppyp1g!'
匹配: True
程序输出: Here is flag{Whc7f&Fl@ppyp1g!}
最终答案
Flag
flag{Whc7f&Fl@ppyp1g!}
或根据CTF惯例可能是:WHCTF{Whc7f&Fl@ppyp1g!}
Flag特征分析
这个flag是密钥 whctf&flappypig! 的Leetspeak变形:
| 原字符 | 替换后 | 类型 |
| — | — | — |
| w | W | 大写 |
| t | 7 | Leetspeak |
| f | F | 大写 |
| l | L | 大写 |
| a | @ | 符号替换 |
| i | 1 | Leetspeak |
这种设计确保即使恢复了密钥,也不能直接用密钥作为flag,增加了题目难度。
技术总结
1. 白盒密码学
核心思想: 将密钥隐藏在查找表中,使其难以提取。
识别特征:
- 程序异常大(MB级别)
- 包含大量数据段
- 逻辑简单但实现复杂
应用场景:
- DRM系统
- 移动支付
- 软件授权
2. MOVfuscation混淆
原理: 利用MOV指令的图灵完备性,所有计算通过查找表完成。
特征:
- 代码几乎全是MOV指令
- 体积膨胀数百倍
- 静态分析极其困难
对抗方法:
- 动态追踪执行流程
- 关注输入输出关系
- 使用符号执行
3. 差分故障分析(DFA)
理论: 通过注入故障并分析差分来恢复密钥。
实践差距:
- 学术研究使用专业硬件(激光、电压毛刺)
- 软件静态注入受现代保护机制限制
- 实际成功需要高级动态插桩工具
教训: 理论美好,实践需要强大的工具链支持。
4. 实用的密文搜索策略
核心思想: 当无法直接攻击时,尝试从已知信息反推。
步骤:
- 确认或获取加密密钥
- 在程序中搜索可能的加密数据
- 批量解密并筛选可读明文
- 验证候选结果
这种方法在CTF和实际渗透测试中都非常实用。
工具箱
基础分析工具
# 文件类型识别
file <binary>
# 字符串提取
strings <binary>
# 符号表查看
readelf -s <binary>
# 反汇编
objdump -d <binary>
# 十六进制查看
xxd <binary>
Python密码学库
# PyCryptodome - AES加密
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
encrypted = cipher.encrypt(plaintext)
decrypted = cipher.decrypt(ciphertext)
专业工具
- IDA Pro / Ghidra – 高级反汇编和反编译
- Deadpool – DFA故障注入框架
- PhoenixAES – AES密钥恢复
- Intel PIN / Frida – 动态二进制插桩
结语
这道Wbaes题目完美地展示了现代密码学逆向的复杂性和艺术性。它不仅需要扎实的技术功底,还需要灵活的思维和坚持不懈的精神。
关键收获:
- 理论与实践的差距 – 不是所有学术方法都能直接应用
- 多角度思考 – 当一条路走不通时,尝试其他途径
- 工具的价值 – 好的工具能大大降低攻击难度
- 实事求是 – 承认困难,寻找可行的替代方案
白盒密码学仍然是一个活跃的研究领域,攻防两端都在不断演进。对于安全研究者来说,这是一个既有理论深度又有实践价值的精彩方向。
希望这篇文章能帮助你理解白盒密码逆向的魅力,也欢迎大家在实践中探索更多的技术和方法!
技术关键词: 白盒密码学、MOVfuscation、DFA攻击、AES、逆向工程
相关资源:
- Deadpool: https://github.com/SideChannelMarvels/Deadpool
- JeanGrey: https://github.com/SideChannelMarvels/JeanGrey
- 白盒密码学论文: Chow et al. (2002)
声明: 本文仅用于技术研究和学习交流,请勿用于非法用途。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全《Wbaes:一次白盒密码逆向的实战之旅》