样本版本:v16.0.4(64位的)
CSDN真挺傻逼的,稍微有一点价值的文章都给你上付费,看不惯这种,所以自己动手搞搞
1. 定位:从验证错误提示进行切入
他这个没有壳,字符串也没有被抹掉,难度较低,授权失败时弹的是 "Invalid name or license ...", 对应的 ASCII 字面量:
| 项 |
地址 |
说明 |
字符串 aInvalidNameOrL |
0x1407C3580 |
.rdata,RVA 0x7C3580 |
| 唯一引用 |
0x1402B2DC7 |
lea rdx, aInvalidNameOrL,位于 sub_1402B24F0 |
运行时交叉验证过:该串的实际地址 0x7FF6C55E3580 减掉 ASLR 基址 0x7FF6C4E20000 = 0x7C3580,和 IDA 里的 RVA 能够对上。
2. OK 按钮处理函数 sub_1402B24F0 的整体流程
把 UI 构造、异常清理那些噪音去掉,关键骨架是这样的:
name = a1[24] // 第一个 QLineEdit
license = a1[26] // 第二个 QLineEdit
// 一组输入合法性检查:空值 / 首尾空白 / 长度,只影响提示,不影响算法
sub_1400014AB(mgr, name) // 把 name 存进授权管理器单例 qword_140FFADB8
sub_14000B9B0(mgr, license) // 把 license 预处理后存进 mgr+48,长度存 mgr+64
n231 = sub_140003D64(mgr, 17, 20300) // 主判定,返回状态码
n524 = sub_1400088A5(mgr, 17, 20300) // 次判定(内部复用主判定),返回状态码
if (n231 != 231 && mgr+124 != 0) // 条件性联网(二级/激活),不参与主判定
sub_14000EA57(...) // 连 sweetscape.com
// 状态码分支(注意 n524 优先):
if (n524 == 219) -> "License activated ... Thank you for purchasing 010 Editor!" // 成功
else if (n524 == 237 || n524 == 524) -> "license is for an earlier version"
else if (n231 != 147) -> "Invalid name or license ..." // 失败
else if (n524 != 113) -> "trial period is already over"
else -> "trial period has been extended"
// 成功后:
sub_140007789(mgr) // 把 license 写进注册表/文件
两个传入的常量 17 和 20300 一直跟着判定走:17 是当前主版本号(版本上限校验要用),20300 是个上限基准,但是我们只关心 0x9C 类型路径里不直接参与判定。
核心:授权是否通过完全由 n231 / n524 两个码决定,而这两个码是上面两个本地函数算出来的,没有走网络请求。(所以其实内存patch也行,但是我觉得不方便)
3. license 字符串的格式与解码
sub_14000B9B0 是 thunk,真身 sub_14043F090,它先调 sub_140008913(真身 sub_14043F0E0)做一次字符预处理,再把结果存到 mgr+48(QString),长度存 mgr+64。
3.1 预处理:O/l 别名(sub_14043F0E0)
很简单,逐字符替换:
'O' (79) 或 'o' (111) -> '0' (48)
'l' (108) -> '1' (49)
3.2 取字节:sub_14000E214 → sub_14043E820
判定函数一上来调它,把 license 字符串拆成一个字节数组(填进调用者栈上的 10 字节缓冲 v39..v48)。逻辑:
- 先把存好的字符串(
mgr+48)复制出来,读其长度 n24 = mgr+64。
- 只接受长度 19 或 24,且固定位置必须是
-:
- 19:
'-' 在下标 4、9、14 → 4 组、解出 8 字节
- 24:再加下标 19 的
'-' → 5 组、解出 10 字节
- 每两个字符拼成一个字节:
byte = nibble(hi) * 16 + nibble(lo)。
也就是格式是 AAAA-BBBB-CCCC-DDDD(19)或 AAAA-BBBB-CCCC-DDDD-EEEE(24),本质就是带连字符的十六进制串。
字符到半字节的映射 sub_14000B2DA → sub_14043E7D0:
'0'..'9' -> 0..9
'a'..'f' / 'A'..'F' -> 10..15 ('a'-'z' 实际返回 c-87,'A'-'Z' 返回 c-55,
超过 f 的字母会给出 >15 的"半字节",属未定义用法)
'O'/'o' -> 0 (预处理后其实已是 '0')
'l' -> 1 (同上)
其它 -> 0
还原时只用 0-9A-F 就行,标准十六进制。
4. 主判定 sub_140003D64 → sub_14043D0E0
sub_140003D64 是 thunk,真身 sub_14043D0E0。它干三件事:查母钥匙表 → 按类型字节分派算法 → 比对校验和。
4.1 母钥匙表
函数开头先拿 name 去撞两张内置表:
off_140E1C5C0 ~ unk_140E1C5D0:一组特殊用户名,QString::operator== 命中任意一个直接 return 231(在 §2 的码表里 231 不等于 147,但这条是判定内部的"有效"出口,配合次判定走成功)。
unk_140E1C5D1 ~ byte_140E1C5E9:每条 12 字节 = (用户名头 2 个字符的大写)+(10 字节 license 序列)。比对方式:name 头两个字符 toUpper 后与前 2 字节相等,且解码出的 10 字节逆序逐字节等于后 10 字节,命中即 return 231。
注册机用不上它(写出来只是想说有时候商业软件感觉也就是个草台班子,想起之前看到的一篇关于医院小程序病人数据直接明文返回没有任何脱敏我就想笑)
4.2 类型字节分派(关注 0x9C)
撞表没中就进 switch (类型字节)。三个分支:0x9C(主流永久授权)、0xFC(另一种,会走 sub_14000B0FA 二次变换)、0xAC(带 mgr+132 计数)。注册机我选走最常见的 0x9C。0x9C 分支完整逻辑:
// b0..b7 已由 §3.2 解码到 v39,v40,v41,v42(=0x9C),n5999655_1,v44,v45,v46
v20 = (uint8)(v44 ^ v41) + ((uint8)(v46 ^ v40) << 8); // 16 位参数
mgr+108 = (uint8) ad76(v45 ^ v39); // 版本上限 n2
v21 = c5b(v20); // 另一参数,模 11
mgr+112 = v21;
n2 = mgr+108;
if (n2 != 0 && (uint32)(v21 - 1) <= 0x3E7) { // 要求 1 <= v21 <= 1000
n255 = 0;
if (n2 < 2) n255 = mgr+108; // n2>=2 时 n255 恒为 0
utf8 = name.toUtf8();
chk = checksum(utf8, /*flag*/ (v42 != 0xFC), n255, v21); // 0x9C -> flag=1
if ( n5999655_1 == (uint8)(chk ) // b4 == chk[0]
&& v44 == (uint8)(chk >> 8) // b5 == chk[1]
&& v45 == (uint8)(chk >> 16) // b6 == chk[2]
&& v46 == (uint8)(chk >> 24) ) // b7 == chk[3]
{
if (v42 == 0x9C) {
if (version_arg /*=17*/ > mgr+108) return 78; // "earlier version"
else return 45; // 成功
}
...
}
return 231;
}
return 231;
0x9C 要过校验需要同时满足:
- 版本上限够大:
n2 = ad76(b6 ^ b0) 必须 >= 17(传入的当前版本号)。n2 >= 2 时 n255 恒为 0。
- 参数
v21 合法:v21 = c5b(v20) 落在 [1, 1000],其中 v20 = (b5 ^ b2) | ((b7 ^ b1) << 8)。
- 校验和逐字节相等:对 name 算出的 4 字节校验和,正好等于 license 里的
b4 b5 b6 b7。
b5 b6 b7 既是校验和的目标,又通过第 1、2 条参与 n2 和 v20 的计算——看着是循环依赖,其实不是,因为 b0 b1 b2 是三个完全自由的字节,可以反过来"吸收"掉 b5 b6 b7 的取值。
4.3 两个小变换
都短到可以直接抄进注册机:
// sub_14000AD76 -> sub_14043C060 :版本上限,字节 -> 字节,是个双射(输出覆盖 0..255)
uint8 ad76(uint8 b) {
return (uint8)(((b ^ 0x18) + 61) ^ 0xA7);
}
// sub_140008C5B -> sub_14043BFD0 :参数,16 位 -> 值,不被 11 整除就作废
uint16 c5b(uint16 x) {
uint16 v = (uint16)(((x ^ 0x7892) + 19760)) ^ 0x3421;
return (v % 11) ? 0 : (uint16)(v / 11);
}
因为 ad76 是双射、值域是完整的 0–255,所以版本上限想给多大给多大(我取了最大值 255,等于对所有版本永久有效)。
4.4 校验和 sub_14000C9AF → sub_14043C0B0(核心)
这是整套算法的核心,它吃 4 个参数:name 的 UTF-8 指针、一个 flag(0x9C 时为 1)、两个字节参数 a3(= n255)和 a4(= v21),返回 32 位值,取其 4 个字节小端去和 license 比。算法是基于一张 256 项 uint32 常量表 dword_140E1C0F0 的逐字符累加,带几个步进各异的游标:
uint32 checksum(const char *name, int flag, uint8 a3, uint8 a4) {
const uint32 *T = dword_140E1C0F0; // 256 项
uint32 v5 = 0; // 累加器
uint8 g9 = 0; // 游标,步进 +19
uint8 g10 = (uint8)(15 * a4); // 游标,步进 +13 <- a4 = v21 低字节
uint8 g11 = 0; // 游标,步进 +7(仅 flag==0 用)
uint8 g12 = (uint8)(17 * a3); // 游标,步进 +9 <- a3 = n255 低字节
for (const char *p = name; *p; ++p) {
uint8 cu = toupper((uint8)*p); // C locale,逐字节大写
uint8 ia = (uint8)(cu + (flag ? 13 : 63));
uint8 ib = (uint8)(cu + (flag ? 47 : 23));
uint8 ig = flag ? g9 : g11;
uint32 t = (v5 + T[cu]) & 0xFFFFFFFF; // v17
uint32 x = (T[ia] ^ t) & 0xFFFFFFFF; // v18
v5 = (T[g10] + T[g12] + T[ig] + T[ib] * x) & 0xFFFFFFFF;
g12 += 9; g10 += 13; g9 += 19; g11 += 7; // 全部按 uint8 回绕
}
return v5;
}
这里有几个容易踩坑的实现细节,需要注意一下:
- 所有游标都是
uint8,自增后按 256 回绕;15 * a4、17 * a3 也截到低 8 位。
v5 是 32 位无符号,每一步乘加都对 2^32 取模;表项当无符号 32 位用即可(乘法对低 32 位而言有无符号一致)。
- 这一轮的
v5 用的是自增之前的 g9/g10/g12,游标自增只影响下一轮(反编译里指针在自增语句之前已取好,别搞反顺序)。
flag 在 0x9C 下恒为 1,所以走 +13 / +47、用 g9 那条;g11/另一组偏移是给别的类型字节用的。
toupper 按 C locale,只动 ASCII 的 a-z;name 里出现非 ASCII 字节时各平台 toupper 可能不同,稳妥起见注册机只支持 ASCII 用户名(这也是它实际接受的范围)。
a3 在 0x9C 路径下恒为 0(因为 n2 >= 2),所以 g12 起点就是 0;a4 = v21 & 0xFF,只有 v21 的低字节进校验和。
5. 次判定 / 成功码 sub_1400088A5 → sub_14043DC50
if (mgr+124 != 0) return 275; // 已激活/特殊状态旁路
v6 = sub_140003D64(mgr, 17, 20300); // 复用主判定
switch (v6) {
case 45: return 219; // -> "License activated"(成功)
case 78: n23 = sub_14000C8AB(mgr, 17); // 旧版本
return (n23 != 23) ? 237 : 524;
case 231: return 375;
default: // 含主判定返回 147(0xFC 那条)
n23 = sub_14000C8AB(mgr, 17);
if (n23 == 23) return 113;
if (n23 == 42) return (sub_14000EC3C(mgr) == 419) ? 249 : 375;
if (n23 == 312) return (sub_14000EC3C(mgr) == 419) ? 249 : 47;
return 375; // 试用相关码
}
整张码表里只有主判定返回 45 才映射到 219 "License activated",而能产出 45 的就只有 0x9C(版本够)和 0xAC(等级 >= 20300)两条路径。0xFC 给出的 147 落进 default,只会得到试用码。mgr+124 在正常激活流程里是 0,所以那条 275 旁路和 OK 处理函数里的联网分支都不触发。
6. 实现注册机算法
import struct
# ---- 校验和常量表 dword_140E1C0F0 (0x140E1C0F0, 256 个 uint32, 小端) ----
_TABLE_RAW = (
"b8 44 cb 39 67 4f 75 23 11 72 01 5f da 24 bb 3e c6 07 17 35 4b 77 f9 63 88 72 82 17 21 48 e7 0f"
" 0f 67 5f 5b e8 5a 31 48 69 77 5b 78 47 15 7a 2b 92 12 d1 38 32 1b a1 42 44 22 33 35 60 7b 43 77"
" 10 3b ab 1e 00 00 81 53 ae 12 02 1d a8 77 03 6f 92 30 c0 43 8e 0a 3c 2d bf 0c 95 62 fa 6f f0 30"
" e0 10 f7 34 fb 17 f4 28 95 2f 0d 35 5a 1d 36 5a 0b 06 cc 15 cc 13 fd 0a cf 3b 60 28 6b 06 71 33"
" e4 14 cd 30 67 3a 5d 17 13 6a d6 6d f9 09 34 2d 82 7b 1e 58 99 6b 52 76 88 51 8d 5c 71 79 85 2c"
" c0 1f f5 15 11 0d cc 68 5c 5e f5 49 64 43 5e 27 bc 0d 1e 2d e3 7c ee 4c 40 58 55 32 08 2e 2e 11"
" 5a 06 78 69 06 14 92 72 e7 78 45 31 b7 21 56 17 bf 1d 77 40 d6 38 c2 3f 8a 12 31 4a 6e 03 ad 2d"
" d6 69 a0 41 92 01 40 25 67 46 dd 00 4f 1f fc 6a ce 40 10 57 df 66 fe 62 3e 4b db 41 1f 23 82 35"
" 9a 07 f6 55 44 06 a7 1c d2 43 16 1b c9 28 72 3f 70 10 14 5f ab 74 14 3e 6e 25 4b 44 d9 50 70 53"
" 4b 09 42 0f e6 20 d8 2f 5e 2e 8b 77 02 6d 17 71 69 7a ea 7f 28 46 b5 5b 71 6c ba 19 99 3a 76 39"
" cd 54 8d 17 88 6e 24 01 7e 53 13 33 17 2d 8e 2b be 10 3d 2a 82 05 d1 59 db 63 a1 37 9a 48 d6 30"
" 46 5c 21 6a 76 7a 1c 0e e7 60 c7 1f 65 0c b8 79 b4 59 f4 27 26 73 9a 79 82 17 ba 50 5c 6d 11 2a"
" 1b 6e 86 63 3c 0e 92 3f 90 34 02 55 89 60 b5 55 d1 1f 39 2c c2 35 80 2f 7a 2b fd 64 9a 75 e8 4c"
" f0 04 85 51 a8 01 95 79 ad 2c 5b 3f 60 01 e6 38 d8 41 76 63 42 2a 35 33 19 2c a2 51 51 58 5c 08"
" ab 17 29 03 c7 0a 77 2b b3 77 ac 30 07 19 ec 2b d0 02 52 03 d3 33 a9 0f f3 5d 25 61 bf 06 ad 22"
" 71 69 b8 58 e5 0d ca 5f 56 64 0d 70 db 73 a9 56 fd 59 b7 5a e2 0b 0e 33 dd 0d 3c 5b 60 3c 5d 49"
" a6 59 bd 53 91 6d 5e 4c 8d 31 d9 49 79 50 3d 10 e3 42 ce 61 1d 12 d5 7e ed 60 e1 14 f2 4e 2d 21"
" f0 33 01 27 96 5a 43 62 8b 5e a7 1f be 2f 09 6f 49 0d 00 4a 70 1c ae 57 77 24 4e 00 72 7e 1e 56"
" 33 00 8c 46 02 24 cc 5d c6 7a 50 78 c7 24 af 58 34 2d f6 0d 08 47 8a 35 11 1e fb 3c 1c 45 71 2b"
" 95 52 a7 77 21 07 89 56 f3 75 ef 0f f1 24 0f 12 e7 0a 99 01 52 44 9c 33 8e 5b a1 27 6d 27 a7 0b"
" 7b 1b dc 60 82 7f 4b 4f 07 70 db 67 d9 57 4a 4f e8 52 12 62 fc 2c 53 20 06 03 39 6a 23 04 80 18"
" 8a 77 f3 19 f0 16 23 46 37 09 ae 56 5c 67 c2 43 fd 45 ca 65 f2 4f 60 0d cb 22 fd 0b 3b 64 fe 3a"
" a6 7f f6 3b 79 35 62 44 f8 31 40 18 97 4f 17 32 2a 09 6a 4c 61 02 b5 5f 74 01 65 01 f1 4a 63 33"
" f4 18 2d 71 69 71 99 6e fe 7a ab 5d e8 2e 2b 7c b4 75 db 6e b6 6f 83 5f d6 6d 2a 3c c2 05 2d 29"
" db 44 22 05 4f 5f 9a 14 40 65 48 5d ea 15 1d 33 20 69 45 4f 9f 69 3a 48 05 0f 45 3b 6c 7c 20 3b"
" fe 70 9d 74 f6 61 74 41 f1 31 b0 62 7b 57 50 27 33 15 13 29 08 38 8c 58 56 34 ef 1a ec 00 3c 0f"
" 42 47 a7 7d 6c 7a 79 4b 87 32 bb 5e b8 58 65 78 f2 4f ed 00 1e 69 69 62 5f 25 a2 24 7e 1f c1 62"
" cd 7d 8a 2f fe 17 3b 64 b8 18 83 77 fe 60 3b 25 a3 63 bb 34 4f 21 03 5b f4 71 15 5f 9f 6e 31 1a"
" 04 27 cf 7a 38 68 89 28 77 46 61 18 eb 69 f5 1b c9 5e a8 0b 46 6b ca 6a 2a 42 43 1e 0e 5f 4d 51"
" 8c 01 3e 41 e9 26 76 30 fa 1d ed 01 5a 6f f4 49 2b 64 1b 46 f2 07 70 7d 57 26 65 13 c5 0b 16 6b"
" 49 48 e0 65 1c 6e 52 1f b6 51 02 5a 69 3f d7 2b cd 7a bf 2d 80 3e e6 51 0f 67 f2 5c 03 0a cd 21"
" 61 02 ff 5c 1e 06 ae 33 5f 34 b6 3b 75 4a 81 5d f4 5d 7b 25 5b 2c 5c 0a 27 55 a4 16 45 39 f2 16"
)
TABLE = list(struct.unpack("<256I", bytes.fromhex(_TABLE_RAW.replace(" ", ""))))
TYPE_BYTE = 0x9C
VERSION_ARG = 17
def toupper(b: int) -> int:
return b - 32 if 0x61 <= b <= 0x7A else b
def checksum(name: str, flag: int, a3: int, a4: int) -> int:
data = name.encode("utf-8")
v5 = 0
v9 = 0
v10 = (15 * (a4 & 0xFF)) & 0xFF
v11 = 0
v12 = (17 * (a3 & 0xFF)) & 0xFF
for b in data:
cu = toupper(b)
idx_a = (cu + (13 if flag else 63)) & 0xFF
idx_b = (cu + (47 if flag else 23)) & 0xFF
v17 = (v5 + TABLE[cu]) & 0xFFFFFFFF
v18 = (TABLE[idx_a] ^ v17) & 0xFFFFFFFF
v20 = v9 if flag else v11
v5 = (TABLE[v10] + TABLE[v12] + TABLE[v20] + TABLE[idx_b] * v18) & 0xFFFFFFFF
v12 = (v12 + 9) & 0xFF
v10 = (v10 + 13) & 0xFF
v9 = (v9 + 19) & 0xFF
v11 = (v11 + 7) & 0xFF
return v5
def ad76(b: int) -> int:
return (((b ^ 0x18) + 61) ^ 0xA7) & 0xFF
def c5b(v20: int) -> int:
v1 = ((((v20 & 0xFFFF) ^ 0x7892) + 19760) & 0xFFFF) ^ 0x3421
if v1 % 11:
return 0
return (v1 // 11) & 0xFFFF
def _best_v20_for_param():
for v20 in range(0x10000):
r = c5b(v20)
if 1 <= r <= 1000:
return v20, r
raise RuntimeError("v20 不合法")
def _best_b_for_version():
best_b, best_n2 = 0, -1
for b in range(256):
n2 = ad76(b)
if n2 > best_n2:
best_b, best_n2 = b, n2
if best_n2 < VERSION_ARG:
raise RuntimeError("不满足版本要求")
return best_b, best_n2
def keygen(name: str) -> str:
v20, v21 = _best_v20_for_param()
b_ver, _n2 = _best_b_for_version()
chk = checksum(name, flag=1, a3=0, a4=v21 & 0xFF)
b4 = chk & 0xFF
b5 = (chk >> 8) & 0xFF
b6 = (chk >> 16) & 0xFF
b7 = (chk >> 24) & 0xFF
b2 = (v20 & 0xFF) ^ b5
b1 = ((v20 >> 8) & 0xFF) ^ b7
b0 = b_ver ^ b6
raw = [b0, b1, b2, TYPE_BYTE, b4, b5, b6, b7]
hexs = "".join("%02X" % x for x in raw)
return "-".join(hexs[i:i + 4] for i in range(0, 16, 4))
def _nibble(c: str) -> int:
o = ord(c)
if 0x30 <= o <= 0x39:
return o - 0x30
if (o - 79) & 0xDF == 0:
return 0
if o == 0x6C:
return 1
if 0x61 <= o <= 0x7A:
return (o - 87) & 0xFF
if 0x41 <= o <= 0x5A:
return (o - 55) & 0xFF
return 0
def _decode_license(lic: str):
s = lic.replace("o", "0").replace("O", "0").replace("l", "1")
if len(s) != 19 or s[4] != "-" or s[9] != "-" or s[14] != "-":
return None
chars = s.replace("-", "")
if len(chars) != 16:
return None
out = []
for i in range(0, 16, 2):
hi = _nibble(chars[i])
lo = _nibble(chars[i + 1])
out.append(((16 * hi) + lo) & 0xFF)
out += [0, 0]
return out
def verify(name: str, lic: str) -> bool:
"""复刻主判定 0x9C 分支,返回 True 表示会得到成功码 45 -> 219。"""
by = _decode_license(lic)
if by is None:
return False
v39, v40, v41, v42, n5999655_1, v44, v45, v46 = by[:8]
if v42 != TYPE_BYTE:
return False
v20 = ((v44 ^ v41) + ((v46 ^ v40) << 8)) & 0xFFFF
a1_108 = ad76(v45 ^ v39)
v21 = c5b(v20)
n2 = a1_108
if not (n2 != 0 and (v21 - 1) <= 0x3E7):
return False
n255 = n2 if n2 < 2 else 0
chk = checksum(name, flag=1, a3=n255 & 0xFF, a4=v21 & 0xFF)
ok = (n5999655_1 == (chk & 0xFF)
and v44 == ((chk >> 8) & 0xFF)
and v45 == ((chk >> 16) & 0xFF)
and v46 == ((chk >> 24) & 0xFF))
if not ok:
return False
# 0x9C: 需要 版本参数 <= 版本上限
return VERSION_ARG <= a1_108
if __name__ == "__main__":
name = "Evan"
lic = keygen(name)
print("Name : %s" % name)
print("License : %s" % lic)
print("Verify : %s" % ("PASS" if verify(name, lic) else "FAIL"))
# Name : Evan
# License : 7AE5-159C-9814-796D
# Verify : PASS