FastRawViewer 授权系统逆向:从位布局到零补丁激活
Sakura1618结论
授权完全本地离线校验,激活码无签名,只靠固定位模式 + 三段哈希片段 + 24 bit 到期时间。构造一个「永久 + 本机」的激活码不需要访问服务器,也不需要改二进制——只改注册表。
1 | ActivationID = base32(15 字节) |
安装目录里的 FastRawViewer.exe 逐字节未改。
1. 组件与存储
| 组件 | 作用 |
|---|---|
FastRawViewer.exe |
13.6 MB,x64 Qt5,含全部校验与注册对话框 |
frv-activate.exe |
命令行激活工具,Qt5Network |
activate.fastrawviewer.com/activate2.cgi |
激活服务器,明文 HTTP,返回 STATUS/AK/MESSAGE |
注册表布局:
1 | HKLM\SOFTWARE\LibRaw LLC\FastRawViewer\ActivationData2 |
frv-activate 激活成功时同时写 HKLM 与 HKCU;主程序启动读 HKLM,读不到才回落 HKCU。删掉这两处键就回到未激活状态。
2. 编码
自定义 Crockford 风格 Base32,字母表 A–Z2–7,别名 0→O、1→L、8→B,- 直接忽略。解码结果必须恰好 15 字节。
ProgramKey 额外做版本判别,但只比较一个异或值:
1 | if (decoded_len == 15) { |
这不验证密钥本身,真伪完全由服务器在激活时判断。
3. 位布局
15 字节 = 120 bit,实际参与校验的只有一部分:
| 字节 | 含义 |
|---|---|
| 0 | (b0 ^ b7) & 0xF0 == 0x20 |
| 1 | (b1 ^ b8):bit7 = 试用标志,低 4 位参与状态字 |
| 2 | 指纹 5 bit(b2>>3)+ 到期低字节异或项 |
| 3 | 到期中字节 b3 ^ b8 |
| 4 | b4 ^ b10 == 0x08 |
| 5 | 到期高字节 b5 ^ b10 |
| 6 | 到期低字节异或项 |
| 7 | 指纹 8 bit:b7 == (H >> 24) & 0xFF |
| 8 | 试用位 / 到期中字节异或项 |
| 10 | 固定位与到期高字节异或项 |
| 12 | 指纹 8 bit:b12 == (H >> 48) & 0xFF |
| 9/11/13/14 | 填充,不参与校验 |
到期时间 24 bit,单位 256 秒:
1 | expiry = ((b5 ^ b10) << 16) | ((b3 ^ b8) << 8) | (b2 ^ b6); |
0x800000 是永久标记——非试用时直接跳过到期分支。
4. 校验算法
主校验函数(VA 0x140155C20)只有四步:
1 | b = base32_decode(ActivationID); // 必须 15 字节 |
指纹哈希的输入是几项机器信息的拼接,各自计算 FNV-1a-64(offset 0xCBF29CE484222325,prime 0x100000001B3):
1 | HKLM\Software\Microsoft\Windows NT\CurrentVersion : DigitalProductId |
字符串本身用 byte ^ index ^ 0x4B 混淆,还原后就是上面这些路径。
通过后把状态字写进对象:
1 | [this + 0x594] = ((b4 ^ b10) << 8) | (b1 ^ b8); // 0 表示未激活 |
5. 构造永久码
约束三条:expiry = 0x800000、试用位为 0、指纹三段沿用官方为本机签发的值。
指纹字段不需要知道哈希怎么算——试用码本身就是官方按本机硬件签发的,解出来直接复用:
1 | b7 = (H >> 24) & 0xFF ← 从试用码的 b[7] 取 |
然后按位反解:
1 | b2 ^ b6 = 0x00 → b6 = b2 |
编码回 base32,双写 HKLM + HKCU 即可。
6. 两个踩过的坑
① b1 ^ b8 == 0 会被判定为无效。
第一版把 b[1] 和 b[8] 取成相等,异或为 0。顶部状态生成函数在 0x1401839BB 有一句 test al, al; je,把 0 直接判进「未激活 / 激活码无效」分支:
1 | Registered version. ACTIVATE LICENSE. |
表现就是弹窗顶部和底部文案自相矛盾。改成 0x0F 后两个分支指向同一状态。
② 不要写 ProgramKey。
ProgramKey 只做格式判别(15 字节 + b1^b14 == 8 → V2),本身不验证真伪。但状态机在 0x14015BCA4 处先看许可证输入框长度,≥15 就解码并走「有待激活的购买密钥」分支:
1 | License key exists, please activate 按钮变成 Activate license |
等于在已激活状态上又叠了一个待激活状态。真正的永久授权只需要 ActivationID。
7. 验证方式
不点按钮、不截图猜,直接读窗口的无障碍树:
1 | 变更前: |
UI 无障碍树是 Qt 程序最省事的验证面——比截图 OCR 稳,比 hook 轻。
8. 通用化:不要复刻公式,让程序自己算
把码推到别的机器上时,指纹必须由那台机器算。原计划是按公式复刻,卡在了 DigitalProductId 的变换上:164 字节原始值、十六进制串、UTF-16、Base64、去长度头、反转……十几种变换代入 FNV 都对不上运行时真值。
于是换了个思路——授权码只需要匹配三个候选哈希之一,那就让程序自己算:
- 复制一份 exe 到临时目录,清掉 PE 的
DYNAMIC_BASE,保证镜像基址固定在0x140000000; - 用
CreateDesktopW建一个隐藏桌面启动它(不会出现在用户屏幕上); - 在
0x140157CE0(三个哈希算完、开始比较的那条指令)写一段 30 字节桩:把rsi / r15 / r14 / rdi存到VirtualAllocEx的缓冲,回放原指令后跳回; - 读回哈希,用第一个候选生成 15 字节激活码 → base32 → 写注册表;
- 结束进程,删临时副本。
安装目录里的 exe 全程不动。脚本会先校验签名 44 0F B6 95 2F 09 00 00,不匹配直接报错退出,不会乱改。
9. 这套授权的强度
| 问题 | 影响 |
|---|---|
| 激活码无签名 | 可离线伪造任意机器、任意到期时间 |
| 指纹只有 8+5+8 = 21 bit 参与比较 | 暴力 2²¹ 可命中,直接构造更省事 |
| FNV-1a 非加密哈希 | 可预测、可碰撞,不构成安全边界 |
| 到期用本地时钟,无防回拨 | 改系统时间即可续期 |
| 明文 HTTP 激活 | 可被 MITM 返回任意 AK(但攻击者根本不需要服务器) |
| HKCU 副本用户可写 | 普通用户即可伪造本地授权数据 |
| 反激活只是删键 | 服务器状态与本地可脱节 |
相对可取的地方:机器绑定、支持反激活迁移、V1/V2 密钥识别、试用提醒按 20/10/5/3/1 天节流。都属于正常用户流程设计,不构成抗破解能力。
10. 要点
- 先看数据,再看代码。 注册表里那两处
ActivationData2直接给出了全部输入输出,比从WinMain往下追快得多。 - 官方签发的码是最好的样本。 试用码本身就是「合法输入」,解出来的指纹字段天然匹配本机,省掉整个哈希复刻。
- 状态机比校验函数更容易踩坑。 校验通过不等于界面正确,
b1^b8==0和ProgramKey两个坑都在状态机上。 - 公式复刻不动就换路线。 与其逆向
DigitalProductId的变换,不如让程序自己把哈希算出来。 - 改动面越小越好。 零二进制补丁意味着哈希不变、签名不失效、升级不用重做。
本文记录静态分析与本地授权数据重构过程,用于学习 PE 结构、自定义编码与授权校验设计。软件请支持正版。

