一场有趣的猫鼠游戏:当开发者针对旧版破解方案引入了硬编码的校验逻辑,我们该如何层层剥茧,还原其背后的算法真相?

0x01 前言

在 8 月份的文章中,我分享了如何通过 mitmproxy 激活 Shottr。原本以为这只是一个简单的 HTTP 数据包篡改游戏,但作者在 v1.9.1 版本中打了一个"反击战"。

更新版本后,原本的 Mock 方案失效了。抓包发现 API 返回了一个全新的字段 ch2,其格式类似于 时间戳.Hash。如果不提供匹配的 Hash,客户端会抛出严重的 Integrity 校验错误。

这不再是修改一个布尔值就能解决的问题。这是一次需要动用静态分析动态追踪的深度逆向实战。

IMPORTANT

本文仅供网络安全技术交流与学习,请支持正版软件。

0x02 成果展示

在深入枯燥的汇编代码之前,我们先来看看最终的破解成果。

下面的截图,均采用破解后的 Shottr v1.9.1 完成的

破解前

破解前:License 验证失败

破解前:License 验证失败

破解后

破解后:激活成功,显示 Pro 订阅

破解后:激活成功,显示 Pro 订阅

0x03 初步摸底:隐藏在二进制中的密钥

当我们尝试像以前那样伪造 tier 字段(会员等级)时,客户端报错了。观察网络请求,除了 verify.php 之外,并没有其他远程校验行为。

结论很明确: 校验这个 ch2 Hash 的逻辑和所需的 Secret,全部硬编码在 Shottr 的二进制文件中。只要我们能在几万行汇编中定位到那段逻辑,算法就无所遁形。

0x04 静态分析:定位验证入口 (radare2)

我们使用 radare2 来探索 Shottr 的内心世界。

1. 寻找突破口

首先,我们搜索 API 返回的关键字段名和可能的报错信息:

1
2
# 搜索 ch2 及相关提示
r2 -q -c '/ ch2; / Integrity check failed' /Applications/Shottr.app/Contents/MacOS/Shottr

输出定位到了数据段中的字符串地址。

2. 追踪交叉引用 (X-Ref)

找到字符串后,我们通过 axt 命令寻找是谁在引用这些字符串。最终我们将目光锁定在了函数 sym.func.1000e25e0 上。

反汇编该函数,我们发现了决定性的逻辑分支:

1
2
3
0x1000e2a3c    bl sym.func.1000e7ae4    ; 核心验证函数调用
0x1000e2a40    tbnz w0, 0, 0x1000e2a4c  ; 检查返回值是否为非零(即验证通过)
0x1000e2a44    ...                      ; 验证失败分支,加载 "Integrity check failed"

通过这一步,锁定了核心验证逻辑的起始地址:0x1000e7ae4

3. 发现加密原语

在这个验证函数周边,我们发现它频繁调用了 CC_SHA256。通过搜索加密函数的 X-Ref,我们还意外在地址 0x1001f3280 附近发现了一些长得像 Secret 的 16 位硬编码字符串。

目前的战果:

  • 找到了验证函数入口。
  • 确认了使用了 SHA256 算法。
  • 拿到了几个可疑的 Secret。

难点: 静态分析很难看清 SHA256 输入字符串的精确拼接格式(比如有没有空格?先后顺序?)。

0x05 动态追踪:窥察内存真相 (Frida)

为了看清内存中生成的拼装字符串,我们需要在运行时"窃听" CC_SHA256

1. 绕过 Hardened Runtime

Shottr 启用了 macOS 的 Hardened Runtime,这会阻止调试器。我们需要先给它脱掉这层"软甲":

1
2
3
# 移除签名并重新进行 Ad-hoc 签名
codesign --remove-signature /tmp/Shottr_debug.app/Contents/MacOS/Shottr
codesign -s - /tmp/Shottr_debug.app/Contents/MacOS/Shottr

2. 编写 Frida Hook 脚本

我们使用 frida-trace 快速生成模版,并编写 Handler 打印 CC_SHA256 的第一个参数(即待计算的明文字符串)。

1
2
3
4
5
6
7
8
// __handlers__/libcommonCrypto.dylib/CC_SHA256.js
onEnter(log, args, state) {
    var dataPtr = args[0];
    var dataLen = parseInt(args[1]);
    if (dataLen < 500) {
        log("[CC_SHA256] Input: " + dataPtr.readUtf8String(dataLen));
    }
}

3.触发验证

运行 Frida,并在 Shottr 界面输入任意 License Key。惊喜出现了,Frida 捕获到了三次连续的哈希计算过程:

  1. 第一波License前缀 + SECRET_AHash1
  2. 第二波License前缀 + Hash1 + " " + 当前时间戳 + " " + SECRET_B
  3. 第三波 (关键)License前缀 + Hash1 + " " + ch2时间戳 + " " + tier + " " + SECRET_C

0x06 算法还原:最终的公式

6.1 完整公式

基于动态分析,还原出 ch2 验证算法:

1
2
3
4
5
6
ch2 格式: timestamp.hash

验证过程:
1. hash1 = SHA256(license_prefix + SECRET1)
2. expected_hash = SHA256(license_prefix + hash1 + " " + timestamp + " " + tier + " " + SECRET3)
3. 比较 expected_hash 与 ch2 中的 hash

6.2 验证算法正确性

编写 Go 程序验证:

1
2
3
4
5
func calculateCh2Hash(licensePrefix, timestamp, tier string) string {
    hash1 := sha256Hex(licensePrefix + SECRET1)
    input := licensePrefix + hash1 + " " + timestamp + " " + tier + " " + SECRET3
    return sha256Hex(input)
}

用 Frida 捕获的数据验证:

  • 输入: license前缀=“1234”, timestamp=“1767014600”, tier=“free”
  • 计算结果与 Frida 捕获的 hash 完全匹配

这意味着什么?这意味着 Shottr 的安全性完全建立在 Secret 的私密性上。而既然 Secret 是硬编码在客户端的,对于逆向工程来说,它就是透明的。

0x07. 关键发现

7.1 算法特点

  1. 双重哈希: 先计算 license 的基础 hash,再计算最终验证 hash
  2. 时间戳绑定: hash 包含时间戳,防止重放
  3. 等级绑定: hash 包含 tier,防止篡改等级
  4. License 绑定: hash 包含 license 前缀

7.2 安全弱点

  1. Secrets 硬编码: 所有 secret 都存储在客户端二进制中
  2. 算法可逆: 知道 secrets 后可以计算任意有效的 ch2
  3. 无证书固定: 可以进行中间人攻击

7.3 利用方式

基于以上分析,可以实现:

  1. 中间人攻击,动态计算并返回有效的 ch2
  2. 本地 patch,绕过验证检查

0x08. 工具和命令汇总

radare2 常用命令

命令说明
aaa完整分析
/ string搜索字符串
axt @ addr查找交叉引用
s addr跳转到地址
pd N反汇编 N 条指令
pdf反汇编整个函数
ps @ addr打印字符串
px N打印 hex

Frida 工作流程

  1. 绕过代码签名保护
  2. 使用 frida-trace 自动生成 handler
  3. 修改 handler 记录函数输入输出
  4. 触发目标功能,观察调用
  5. 分析数据,还原算法

0x09 总结与思考

本次逆向经历可以总结为标准的 “四步走” 流程:

步骤动作工具目的
1. 侦察抓包分析 APICharles确定攻击面 (ch2 字段)
2. 定位字符串搜索与交叉引用radare2找到二进制中的验证逻辑开关
3. 截获Hook 加密函数Frida还原明文字符串的拼接格式
4. 攻克算法编写与 MockGo/Python本地生成合法 Hash 完成破解