贴主是逆向初学菜鸟,第一次逆向学习大型软件(虽然是入门级),写下这个帖子留作纪念
在 010 的注册界面中输入任意 Name/License,弹出 Invalid name or license...。字符串窗口找到该文案,xref 定位到"宣判块"(错误提示所在基本块)。
下图中异色(青)块即错误提示所在的基本块,可以看到它位于一长串分支逻辑的末尾——所有验证失败的路径最终都汇集到这一层:

放大分派区,可以看到一组连续的结果码比较:cmp ebx, 237、cmp ebx, 524、cmp r15d, 147 等——不同的结果码被分流到不同的提示分支,一个"结果码 → 提示文案"的分派区浮出水面:

继续向上追溯结果码的来源:图中红块是 cmp ebx, 219(激活成功判定),其上方是 call sub_1400088A5(参数为 mgr, 17, 20300,返回值存入 ebx),再往上还有 cmp dword ptr [rcx+124], 0 的判断——0x7C(124) 这个字段是联网审查开关,后文会详细讲:

顺着分派区继续向上,找到了结果码的"生产车间"——下面这一整块 30 多行的顺序逻辑。看着密,其实拆成三段就很好读,我们一段一段来:

第一段(取输入):块里唯一的外部库调用是 QLineEdit::text()——Qt 函数名是现成的路标,这是从注册窗口的输入框把 License 文本取出来,再经两个内部函数(sub_1400014AB、sub_14000B9B0)存进全局对象 qword_140FFADB8。这个对象后面会反复出现,我们叫它 mgr(管理器),可以把它想象成一个"档案袋",name、license、验证结果全装在里面。
第二段(双调用):接下来是肉眼可见的"复制粘贴"——mov edx, 17 / mov r8d, 20300 / call XXX 一模一样的配方出现两次,只是调用目标不同(先 sub_140003D64 后 sub_1400088A5),返回值一个进 r15d、一个进 ebx。两个吃同样参数的调用挨在一起,大概率是"一个负责验证、一个负责转述"的关系。
第三段(路由):块尾 cmp r15d, 231 / jz,加上下面小块里的 cmp dword ptr [rcx+124], 0 / jz——两道条件跳决定流程走向:满足任一就去本地宣判(红块 cmp ebx, 219 那里),都不满足则落入另一条分支(后文会讲,那是联网审查通道)。
三段读完,这块代码的全貌就出来了:取输入 → 双调用产出两个结果码 → 按码路由。
这两个结果码分工不同:r15d 是"路由器",ebx 才是"法官"。凭什么这么说?看它们各自的"出场位置":
一个管"去哪",一个管"判什么"。还有个佐证:就算 r15d == 231 走了本地宣判这条路,由于内层验证失败(231)经翻译官映射后 ebx=375≠219,法官照样判死——所以 r15d 真的只负责指路,生杀大权全在 ebx 手里。
再看这两个函数的身份:sub_140003D64 是个 thunk,真身 sub_14043D0E0 就是内层验证器(本文主角,第二章详析);sub_1400088A5 真身 sub_14043DC50,跟进去发现它内部又调了一遍内层验证,然后对返回值做 switch 映射——它是个"翻译官":
拿着翻译官的映射表,回到分派区逐个确认各结局的提示块:
① ebx==219(内层 45)→ 激活成功。 cmp ebx, 219 / jnz:不等则跳往失败分派,相等则直接落入下方的成功提示块——拼的字符串是 License activated. This license entitles...:

② mgr+0x7C ≠ 0 → 联网审查。 cmp dword ptr [rcx+124], 0 / jz:开关为 0 回本地宣判;非 0 则落入联网分支——跟进发现函数 sub_1402B5140 在拼接 123K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3g2W2N6s2y4U0j5i4m8W2i4K6u0W2j5$3!0E0i4K6u0r3 开头的 URL 并发起 HTTP 请求,这就是联网反盗版校验(第六章详析)。校验失败直接弹反盗版警告,通过则回到 219 的判定:


③ ebx==237 / 524(内层 78)→ 版本太旧。 两个比较任一成立(jz)都跳到拼 The license you entered is for an earlier version... 的提示块:

④ r15d==147 → 试用。 cmp r15d, 147 / jnz:不等于 147 跳向 Invalid 块——就是最开始定位的那个青色块,拼 Invalid name or license,激活码错误:

等于 147 则落入试用路径,再由 cmp ebx, 71h(113)细分两种结局:



把 switch 的分支和分派区的提示块一一对应,得到完整的码表:
内层验证器的参数是 (mgr, 17, 20300)。读它之前,先搞清楚 mgr 这个"档案袋"里装了什么。
方法:x64dbg 连拍四张快照做对比——断在验证前、弹 Invalid 后、改 license 后、改 name 后,逐格对比 mgr 内存:
(快照③关键行原貌:F0 1C 72 55 E8 01 00 00 00 1D 72 55 E8 01 00 00(license 的 d/ptr)下一行 14 00 00 00——即 +0x40 = 0x14 = 20,正是删了一段后的字符数。)
两个规律自己跳出来:+0x28 只在改 name 时变(4→5),+0x40 只在改 license 时变(24→20)——控制变量,谁变跟谁走。而指针列的"变与不变"正好对应 QString 重新分配(改谁的内容谁才重建缓冲区)。由此实锤:+0x28 是 name 长度、+0x40 是 license 长度,两个 QString 都是 {d, ptr, size} 三连布局(d 与 ptr 恒差 0x10,即 16 字节管理头)。
知道了 +40/+64 是两个 size 字段,这段就一目了然:空串检查(return 147)。
sub_14000E214(a1, &v39) 把 license 字符串解码成字节数组(v39 起,记 b0~b9)。三个动作:
① 转单字节:先 QString::toLatin1 把 license 从 UTF-16 压成单字节(x64 动态跟进 ptr 可看到 31 00 31 00 → 31 31 的压缩过程)。
② 格式闸机:长度必须 19(四段)或 24(五段),且第 4/9/14(/19) 位必须是 -(45 = 0x2D),反编译原文:
③ 逐字符解码:每两行为一组——取两个字符 → B2DA 转数值 → 16 * 高 + 低 拼一个字节,注意下标跳过横杠:
其中 sub_14000B2DA(真身 43E7D0)是"字符 → 数值"转换表,从上到下挨个分类:
注意它收的是整个字母表(不只是 A-F),但只有 0-9A-F(值 0~15)才能在 16x+y 里不溢出——生成码时用标准 hex 字符即可。
动态验证:输 AAAA-BBCC-DDEE-FF00,断在解码后看 &v39:AA AA BB CC DD EE FF 00 一字不差——v39 到 v46 正好就是 b0 到 b7,实锤。
第一道:用户名撞表。 拿 name 挨个 QString::operator== 比对内置表 off_140E1C5C0,表内容就两条:999、AnyOne——都是网上流传泄露 key 的用户名,撞中 return 231。

第二道:前缀 + 指纹组合。 name 长度 >1 时,按 12 字节表项循环(v6 += 12):
即"名字前缀 + 泄露 license 指纹"的组合黑名单,指纹倒序存放(v13 递减),dump 里直接看不出是哪条 key。第一个表项前缀是 OW。
动态验证:mgr+0x18 的 name QString 与 mgr+0x30 的 license QString 背靠背,跟进 ptr 能看到输入的明文。
b3(v42)是类型字节(-100/-4/-84 转 hex 即 0x9C/0xFC/0xAC),驱动稀疏 switch:
0x9C 主分支(注册机目标,fall-through 路径):
版本约束推演(决定 a3 能不能焊死为 0):版本=0 → 231 闸死;版本∈[1,16] → 后面 17 > 版本 给 78(旧版本)也活不成;只有版本 ≥ 17 能走到最后——而 ≥17 必然 ≥2,所以 a3 恒为 0。