尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

010 Editor 注册算法分析与注册机实现

010 Editor 注册算法分析与注册机实现 一、定位校验逻辑1.1 从错误提示入手在 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 [rcx124], 0的判断——0x7C(124) 这个字段是联网审查开关后文会详细讲1.2 两级结果码顺着分派区继续向上找到了结果码的生产车间——下面这一整块 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 [rcx124], 0/jz——两道条件跳决定流程走向满足任一就去本地宣判红块cmp ebx, 219那里都不满足则落入另一条分支后文会讲那是联网审查通道。三段读完这块代码的全貌就出来了取输入 → 双调用产出两个结果码 → 按码路由。r15d sub_140003D64(mgr, 17, 20300); // 内层验证thunk → sub_14043D0E0 ebx sub_1400088A5(mgr, 17, 20300); // 翻译官thunk → sub_14043DC50 cmp r15d, 231 jz 本地宣判 // r15d231 → 查 ebx cmp [mgr0x7C], 0 jz 本地宣判 // 审查开关0 → 查 ebx // 否则走联网审查分支这两个结果码分工不同r15d 是路由器ebx 才是法官。凭什么这么说看它们各自的出场位置r15d只出现在cmp r15d, 231/jz和cmp [mgr0x7C], 0/jz这两道跳转里——它决定的是流程走哪条路本地宣判还是联网审查并不直接判定成败ebx出现在红块cmp ebx, 219/jnz 失败——它才是最终判定激活成功与否的那个一个管去哪一个管判什么。还有个佐证就算 r15d 231 走了本地宣判这条路由于内层验证失败231经翻译官映射后 ebx375≠219法官照样判死——所以 r15d 真的只负责指路生杀大权全在 ebx 手里。再看这两个函数的身份sub_140003D64是个 thunk真身sub_14043D0E0就是内层验证器本文主角第二章详析sub_1400088A5真身sub_14043DC50跟进去发现它内部又调了一遍内层验证然后对返回值做 switch 映射——它是个翻译官// sub_14043DC50删除噪音 __int64 __fastcall sub_14043DC50(__int64 a1, __int64 a2, __int64 a3) { v3 a2; if ( *(_DWORD *)(a1 124) ! 0 ) return 275; v6 sub_140003D64(a1, a2, a3); switch ( v6 ) { case 45: return 219; case 78: v12 sub_14000C8AB(a1, a2: v3); v13 524; if ( v12 ! 23 ) return 237; return v13; case 231: return 375; default: ... // 试用路径的辅助校验sub_14000C8AB / sub_14000EC3C 等 // 可能返回 113/249/375与激活主线无关从略 } }拿着翻译官的映射表回到分派区逐个确认各结局的提示块① ebx219内层 45→ 激活成功。cmp ebx, 219/jnz不等则跳往失败分派相等则直接落入下方的成功提示块——拼的字符串是License activated. This license entitles...② mgr0x7C ≠ 0 → 联网审查。cmp dword ptr [rcx124], 0/jz开关为 0 回本地宣判非 0 则落入联网分支——跟进发现函数sub_1402B5140在拼接https://www.sweetscape.com/开头的 URL 并发起 HTTP 请求这就是联网反盗版校验第六章详析。校验失败直接弹反盗版警告通过则回到 219 的判定③ ebx237 / 524内层 78→ 版本太旧。两个比较任一成立jz都跳到拼The license you entered is for an earlier version...的提示块④ r15d147 → 试用。cmp r15d, 147/jnz不等于 147 跳向 Invalid 块——就是最开始定位的那个青色块拼Invalid name or license激活码错误等于 147 则落入试用路径再由cmp ebx, 71h113细分两种结局ebx113不跳→License code accepted. Your trial period has been extended试用期已延长ebx≠113跳 loc_1402B2DAA→License accepted but the trial period is already over试用已结束把 switch 的分支和分派区的提示块一一对应得到完整的码表内层码外层码含义45219激活成功License activated78524 / 237license 版本太旧earlier version147113试用试用期延长 / 试用已结束231375无效Invalid name or license二、验证器 sub_14043D0E0 解剖内层验证器的参数是(mgr, 17, 20300)。读它之前先搞清楚 mgr 这个档案袋里装了什么。方法x64dbg 连拍四张快照做对比——断在验证前、弹 Invalid 后、改 license 后、改 name 后逐格对比 mgr 内存偏移① 验证前(NAME五段)② 弹Invalid后(输入没动)③ 改license后(删掉一段)④ 改name后(改成5字符)0x18/0x20name 指针…52E7EE0/E7EF0变变变0x2844450x30/0x38license 指针…5371300/371310不变变不变0x4024242020快照③关键行原貌F0 1C 72 55 E8 01 00 00 00 1D 72 55 E8 01 00 00license 的 d/ptr下一行14 00 00 00——即 0x40 0x14 20正是删了一段后的字符数。两个规律自己跳出来0x28 只在改 name 时变4→50x40 只在改 license 时变24→20——控制变量谁变跟谁走。而指针列的变与不变正好对应 QString 重新分配改谁的内容谁才重建缓冲区。由此实锤0x28 是 name 长度、0x40 是 license 长度两个 QString 都是 {d, ptr, size} 三连布局d 与 ptr 恒差 0x10即 16 字节管理头。2.1 入口检查*(mgr 128) 0; *(mgr 104) 0; if ( *(mgr 40) 0 || *(mgr 64) 0 ) return 147; // name 或 license 为空串直接拒知道了 40/64 是两个 size 字段这段就一目了然空串检查return 147。2.2 license 解析sub_14043E820hex 解码器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反编译原文v4 *(_QWORD *)(a1 64); // license 长度64 size sub_1400053F3(v37, a1 48); // 内部即 QString::toLatin1 *(_QWORD *)(a2 2) 0; *(_WORD *)a2 0; // 先清零 10 字节输出 if ( ((_DWORD)v4 19 || (_DWORD)v4 24) // 长度闸19 或 24 *(_BYTE *)QByteArray::operator[](v37, 4) 45 // 第 4 位必须是 - *(_BYTE *)QByteArray::operator[](v37, 9) 45 // 第 9 位 *(_BYTE *)QByteArray::operator[](v37, 14) 45 // 第 14 位 ((_DWORD)v4 ! 24 || *(_BYTE *)QByteArray::operator[](v37, 19) 45) ) // 五段再加第 19 位③ 逐字符解码每两行为一组——取两个字符 →B2DA转数值 →16 * 高 低拼一个字节注意下标跳过横杠v5 *(_BYTE *)QByteArray::operator[](v37, 0); v7 (unsigned __int8 *)QByteArray::operator[](v37, 1); v8 16 * sub_14000B2DA(a1, v5); *(_BYTE *)a2 sub_14000B2DA(a1, *v7) v8; // b0 v9 *(_BYTE *)QByteArray::operator[](v37, 2); v10 (unsigned __int8 *)QByteArray::operator[](v37, 3); v11 16 * sub_14000B2DA(a1, v9); *(_BYTE *)(a2 1) sub_14000B2DA(a1, *v10) v11; // b1 // [5][6]→b2跳过横杠[4]、[7][8]→b3类型字节、[10][11]→b4、 // [12][13]→b5、[15][16]→b6、[17][18]→b7写法与上面完全相同略 if ( (_DWORD)v4 24 ) // 五段再解 b8、b9[20][21]、[22][23] { ... }其中sub_14000B2DA真身 43E7D0是字符 → 数值转换表从上到下挨个分类__int64 __fastcall sub_14043E7D0(__int64 a1, char a2) { if ( (unsigned __int8)(a2 - 48) 9u ) return (unsigned int)(a2 - 48); // 0~9 → 0~9 if ( ((a2 - 79) 0xDF) 0 ) // 0xDF 大小写合一O(0x4F)/o(0x6F) return 0; if ( a2 108 ) return 1; // l → 1易混淆字符容错 if ( (unsigned __int8)(a2 - 97) 0x19u ) return (unsigned int)(a2 - 87); // a~z → 10~35 if ( (unsigned __int8)(a2 - 65) 0x19u ) return (unsigned int)(a2 - 55); // A~Z → 10~35 else return 0; }注意它收的是整个字母表不只是 A-F但只有 0-9A-F值 0~15才能在16xy里不溢出——生成码时用标准 hex 字符即可。动态验证输AAAA-BBCC-DDEE-FF00断在解码后看v39AA AA BB CC DD EE FF 00一字不差——v39 到 v46 正好就是 b0 到 b7实锤。2.3 内置黑名单两道第一道用户名撞表。拿 name 挨个QString::operator比对内置表off_140E1C5C0表内容就两条999、AnyOne——都是网上流传泄露 key 的用户名撞中 return 231。第二道前缀 指纹组合。name 长度 1 时按 12 字节表项循环v6 12// 表项 [2 字符 name 前缀][10 字节 license 指纹倒序存储] if ( toUpper(name[0]) 前缀[0] toUpper(name[1]) 前缀[1] ) { v13 v6 10; for (i 0; i 10; i, v13--) if ( b[i] ! *v13 ) break; // v39 正序读表项倒序读 if (全中 10 字节) return 231; }即名字前缀 泄露 license 指纹的组合黑名单指纹倒序存放v13 递减dump 里直接看不出是哪条 key。第一个表项前缀是OW。动态验证mgr0x18 的 name QString 与 mgr0x30 的 license QString 背靠背跟进 ptr 能看到输入的明文。2.4 类型字节分派0x9C / 0xFC / 0xACb3v42是类型字节-100/-4/-84 转 hex 即 0x9C/0xFC/0xAC驱动稀疏 switchb3类型行为0x9C正式零售版解出版本上限mgr108、盐mgr112见下0xFC试用版全写死常量108255、1121、1281不读 license 任何字节最好成绩仅 1470xAC加强版同 0x9C 公式但闸更宽1~5000额外用 b8/b9 解出等级mgr132需五段 10 字节0x9C 主分支注册机目标fall-through 路径v20 (u8)(b5 ^ b2) ((u8)(b7 ^ b1) 8); // 4 字节两两异或拼 16 位 *(mgr108) AD76( (u8)(b6 ^ b0) ); // → 授权版本上限 v21 C5B( v20 ); *(mgr112) v21; // → 盐 if ( 版本 0 || v21 - 1 999 ) return 231; // 两道闸版本非 0、盐 ∈ [1,1000] a3 (版本 2) ? 版本 : 0; // 版本 ≥ 2 时 a3 恒 0版本约束推演决定 a3 能不能焊死为 0版本0 → 231 闸死版本∈[1,16] → 后面17 版本给 78旧版本也活不成只有版本 ≥ 17 能走到最后——而 ≥17 必然 ≥2所以 a3 恒为 0。动态验证108 改为 5真码立刻报旧版本真码的 108255、112230。2.5 核心校验LABEL_26所有分支汇合到这里一句话概括name 过 hash和 license 的 b4~b7 逐字节比对。LABEL_26: QString::toUtf8(a1: a1 24, a2: v38); // name 转 UTF-8 字节 v25 *(_DWORD *)(a1 112); // a4 盐 LOBYTE(v4) v19 ! -4; // flagb3≠0xFC → 0x9C 路径恒为 1 v26 QByteArray::data(this: (QByteArray *)v38); v27 sub_14000C9AF(a1: v26, a2: v4, a3: v23, a4: v25); // hash第三章 if ( v43 (_BYTE)v27 (_BYTE)v15 BYTE1(v27) // v43~v46 b4~b7 v45 BYTE2(v27) v46 HIBYTE(v27) ) // 四字节全等小端 { if ( v19 -100 ) // 0x9C { if ( v36 *(_DWORD *)(a1 108) ) // 17 版本字段 ? { v28 78; // 是 → 版本太旧 goto LABEL_45; } LABEL_34: v28 45; // 否 → ★ 通过45 映射 219 goto LABEL_45; } if ( v19 -4 ) // 0xFC 试用 { v29 sub_14000B0FA(a1: v39 (v40 8) (v41 16), a2: v27); // hash 当密钥解期限 if ( v29 ! 0 ) { *(_DWORD *)(a1 104) v29; v28 147; goto LABEL_45; } } else if ( v31 ! 0 ) // 0xAC等级检查略 { if ( v37 v31 ) { v28 78; goto LABEL_45; } goto LABEL_34; } } v28 231; // 四字节不全等 → 无效0xFC 路径的 B0FA 也顺手贴了((k ^ key ^ 0x22C078) - 180597) ^ 0xFFE53167取低 24 位后要求能被 17 整除再除以 17——一个解密模校验的小变换解出的值存 mgr104推测是试用期限。校验等式小端(hash(name, 1, 0, 盐)) license 的 b4~b7——已用合法码动态验证hash 输出0x6D791498拆开正是 b4~b7 的98 14 79 6D。三、hash 算法sub_14043C0B0结构一句话strlen(name)→ 逐字符touppername 不区分大小写→ 查256 项 uint32 常量表dword_140E1C0F0累加出 32 位校验和。原反编译行内注释__int64 __fastcall sub_14043C0B0(__int64 a1, int a2, char a3, char a4) // name flag 盐a3 盐a4 { v5 0; v6 -1; do v6; while ( *(a1 v6) ! 0 ); // 手写 strlen v7 (int)v6; // name 长度 if ( v7 0 ) { v8 0; // i v9 0; // 游标1步进 19 v10 15 * a4; // 游标2步进 13起点由 a4 定 v11 0; // 游标3步进 7 v12 17 * a3; // 游标4步进 9起点由 a3 定 do { v13 toupper( *(unsigned __int8 *)(v8 a1) ); // 逐字符转大写 v15 dword_140E1C0F0[v12]; // ★ 先取表项地址旧游标 v16 dword_140E1C0F0[v10]; v17 v5 dword_140E1C0F0[v13]; // acc S[字符] if ( a2 ! 0 ) // flag10x9C用偏移 13/47 { v18 dword_140E1C0F0[(unsigned __int8)(v13 13)] ^ v17; v19 (unsigned __int8)(v13 47); v20 v9; } else // flag00xFC用偏移 63/23 { v18 dword_140E1C0F0[(unsigned __int8)(v13 63)] ^ v17; v19 (unsigned __int8)(v13 23); v20 v11; } v5 *v16 *v15 // ★ 用①的旧表项累加 dword_140E1C0F0[v20] dword_140E1C0F0[v19] * v18; // uint32 乘法自然截断 v12 9; v10 13; v9 19; v11 7; // ★ 算完才步进u8 回绕 v8; } while ( v8 v7 ); } return v5; }四个 u8 游标步进各异9/13/7/19使同一字符在 name 不同位置贡献不同a3/a4 只影响游标起点盐的作用flag 选择两组偏移。踩坑注意 ★ 标记的顺序——程序是先取表值、后步进游标v15/v16在 之前取址翻写时步进必须放在取值之后否则每轮拿错表项。四、字段解码器与逆变换4.1 AD76版本字段解码sub_14043C060y ((x ^ 0x18) 0x3D) ^ 0xA7xor/加/xor 三步全可逆 →双射。逆函数x ((y ^ 0xA7) - 0x3D) ^ 0x18 // 验证: AD76(3)255 ⇔ AD76_inv(255)34.2 8C5B盐解码sub_14043BFD0v1 ((x ^ 0x7892) 0x4D30) ^ 0x3421; if (v1 % 11 ! 0) return 0; return v1 / 11;前半段可逆后半段/11看似单向但函数保证 v1 是 11 的倍数——给定商 qv1 11q 唯一确定故合法输出域内可逆v1 11 * q; x ((v1 ^ 0x3421) - 0x4D30) ^ 0x7892; // mod 2^16五、注册机实现5.1 激活码逐字节怎么生成license 一共 8 个字节每个字节的来历全部已知直接列成表字节怎么生成说明b3固定写死 0x9C类型字节选正式零售版b4hash(name) 的第 1 字节最低位算出来的不能动b5hash(name) 的第 2 字节同上b6hash(name) 的第 3 字节同上b7hash(name) 的第 4 字节最高位同上b2b5 ^ (v20 低字节)反解让程序算出的盐 你选的值b1b7 ^ (v20 高字节)同上v20 8C5B逆(你选的盐)b0b6 ^ AD76逆(255)反解让程序解出的版本 255全版本通吃生成顺序注意先后依赖先把 b3 写死、用 name 算出 hash 填好 b4~b7然后b5/b7 已知了用公式反解出 b2、b1让盐合法最后b6 已知反解 b0让版本 255。5.2 源码// 算法来源: verifyLicensesub_14043D0E0, hashsub_14043C0B0 // license 结构 (0x9C 类型, 四段 8 字节): // b0 b1 b2 : 自由字节 (吸收盐/版本约束) // b3 : 0x9C 类型字节 (正式零售版) // b4..b7 : hash(name, 1, 0, a4) 的小端 4 字节 // 约束: AD76(b6^b0) 17 (取 255 全版本); 8C5B((b5^b2)((b7^b1)8)) a4 ∈ [1,1000] #include stdio.h #include stdint.h #include string.h #include ctype.h // 256 项 S-Box: IDA dword_140E1C0F0 原样导出 static const uint32_t S[256] { 0x39CB44B8, 0x23754F67, 0x5F017211, 0x3EBB24DA, 0x351707C6, 0x63F9774B, 0x17827288, 0x0FE74821, 0x5B5F670F, 0x48315AE8, 0x785B7769, 0x2B7A1547, 0x38D11292, 0x42A11B32, 0x35332244, 0x77437B60, 0x1EAB3B10, 0x53810000, 0x1D0212AE, 0x6F0377A8, 0x43C03092, 0x2D3C0A8E, 0x62950CBF, 0x30F06FFA, 0x34F710E0, 0x28F417FB, 0x350D2F95, 0x5A361D5A, 0x15CC060B, 0x0AFD13CC, 0x28603BCF, 0x3371066B, 0x30CD14E4, 0x175D3A67, 0x6DD66A13, 0x2D3409F9, 0x581E7B82, 0x76526B99, 0x5C8D5188, 0x2C857971, 0x15F51FC0, 0x68CC0D11, 0x49F55E5C, 0x275E4364, 0x2D1E0DBC, 0x4CEE7CE3, 0x32555840, 0x112E2E08, 0x6978065A, 0x72921406, 0x314578E7, 0x175621B7, 0x40771DBF, 0x3FC238D6, 0x4A31128A, 0x2DAD036E, 0x41A069D6, 0x25400192, 0x00DD4667, 0x6AFC1F4F, 0x571040CE, 0x62FE66DF, 0x41DB4B3E, 0x3582231F, 0x55F6079A, 0x1CA70644, 0x1B1643D2, 0x3F7228C9, 0x5F141070, 0x3E1474AB, 0x444B256E, 0x537050D9, 0x0F42094B, 0x2FD820E6, 0x778B2E5E, 0x71176D02, 0x7FEA7A69, 0x5BB54628, 0x19BA6C71, 0x39763A99, 0x178D54CD, 0x01246E88, 0x3313537E, 0x2B8E2D17, 0x2A3D10BE, 0x59D10582, 0x37A163DB, 0x30D6489A, 0x6A215C46, 0x0E1C7A76, 0x1FC760E7, 0x79B80C65, 0x27F459B4, 0x799A7326, 0x50BA1782, 0x2A116D5C, 0x63866E1B, 0x3F920E3C, 0x55023490, 0x55B56089, 0x2C391FD1, 0x2F8035C2, 0x64FD2B7A, 0x4CE8759A, 0x518504F0, 0x799501A8, 0x3F5B2CAD, 0x38E60160, 0x637641D8, 0x33352A42, 0x51A22C19, 0x085C5851, 0x032917AB, 0x2B770AC7, 0x30AC77B3, 0x2BEC1907, 0x035202D0, 0x0FA933D3, 0x61255DF3, 0x22AD06BF, 0x58B86971, 0x5FCA0DE5, 0x700D6456, 0x56A973DB, 0x5AB759FD, 0x330E0BE2, 0x5B3C0DDD, 0x495D3C60, 0x53BD59A6, 0x4C5E6D91, 0x49D9318D, 0x103D5079, 0x61CE42E3, 0x7ED5121D, 0x14E160ED, 0x212D4EF2, 0x270133F0, 0x62435A96, 0x1FA75E8B, 0x6F092FBE, 0x4A000D49, 0x57AE1C70, 0x004E2477, 0x561E7E72, 0x468C0033, 0x5DCC2402, 0x78507AC6, 0x58AF24C7, 0x0DF62D34, 0x358A4708, 0x3CFB1E11, 0x2B71451C, 0x77A75295, 0x56890721, 0x0FEF75F3, 0x120F24F1, 0x01990AE7, 0x339C4452, 0x27A15B8E, 0x0BA7276D, 0x60DC1B7B, 0x4F4B7F82, 0x67DB7007, 0x4F4A57D9, 0x621252E8, 0x20532CFC, 0x6A390306, 0x18800423, 0x19F3778A, 0x462316F0, 0x56AE0937, 0x43C2675C, 0x65CA45FD, 0x0D604FF2, 0x0BFD22CB, 0x3AFE643B, 0x3BF67FA6, 0x44623579, 0x184031F8, 0x32174F97, 0x4C6A092A, 0x5FB50261, 0x01650174, 0x33634AF1, 0x712D18F4, 0x6E997169, 0x5DAB7AFE, 0x7C2B2EE8, 0x6EDB75B4, 0x5F836FB6, 0x3C2A6DD6, 0x292D05C2, 0x052244DB, 0x149A5F4F, 0x5D486540, 0x331D15EA, 0x4F456920, 0x483A699F, 0x3B450F05, 0x3B207C6C, 0x749D70FE, 0x417461F6, 0x62B031F1, 0x2750577B, 0x29131533, 0x588C3808, 0x1AEF3456, 0x0F3C00EC, 0x7DA74742, 0x4B797A6C, 0x5EBB3287, 0x786558B8, 0x00ED4FF2, 0x6269691E, 0x24A2255F, 0x62C11F7E, 0x2F8A7DCD, 0x643B17FE, 0x778318B8, 0x253B60FE, 0x34BB63A3, 0x5B03214F, 0x5F1571F4, 0x1A316E9F, 0x7ACF2704, 0x28896838, 0x18614677, 0x1BF569EB, 0x0BA85EC9, 0x6ACA6B46, 0x1E43422A, 0x514D5F0E, 0x413E018C, 0x307626E9, 0x01ED1DFA, 0x49F46F5A, 0x461B642B, 0x7D7007F2, 0x13652657, 0x6B160BC5, 0x65E04849, 0x1F526E1C, 0x5A0251B6, 0x2BD73F69, 0x2DBF7ACD, 0x51E63E80, 0x5CF2670F, 0x21CD0A03, 0x5CFF0261, 0x33AE061E, 0x3BB6345F, 0x5D814A75, 0x257B5DF4, 0x0A5C2C5B, 0x16A45527, 0x16F23945 }; // hash: sub_14043C0B0 逐行翻写 // name: UTF-8 字节串; flag: 0x9C 路径恒 1; a3: 恒 0; a4: 盐 [1,1000] static uint32_t CalcHash(const char* name, int flag, uint8_t a3, uint8_t a4) { uint32_t acc 0; size_t len strlen(name); if (len 0) { uint8_t g1 0; // 游标1, 步进 19 uint8_t g2 15 * a4; // 游标2, 步进 13, 起点由 a4 定 uint8_t g3 0; // 游标3, 步进 7 uint8_t g4 17 * a3; // 游标4, 步进 9, 起点由 a3 定 for (size_t i 0; i len; i) { uint8_t c (uint8_t)toupper((uint8_t)name[i]); uint32_t t acc S[c]; uint32_t v18; uint8_t v19, v20; if (flag ! 0) { v18 S[(uint8_t)(c 13)] ^ t; v19 (uint8_t)(c 47); v20 g1; } else { v18 S[(uint8_t)(c 63)] ^ t; v19 (uint8_t)(c 23); v20 g3; } // 程序先取表值后步进 acc S[g2] S[g4] S[v20] S[v19] * v18; g4 9; g2 13; g1 19; g3 7; } } return acc; } // AD76 逆: y ((x^0x18)0x3D)^0xA7 → x ((y^0xA7)-0x3D)^0x18 static uint8_t AD76_inv(uint8_t y) { return (uint8_t)(((y ^ 0xA7) - 0x3D) ^ 0x18); } // 8C5B 逆: 正向 v1((x^0x7892)0x4D30)^0x3421, 返回 v1/11 (要求 v1%110) // 给定商 q → v1 11*q (唯一) → 逆变换 static uint16_t C5B_inv(uint16_t q) { uint32_t v1 11u * q; return (uint16_t)(((v1 ^ 0x3421) - 0x4D30) ^ 0x7892); } int main() { char name[256] { 0 }; printf(Name: ); if (!fgets(name, sizeof(name), stdin)) return 1; name[strcspn(name, \r\n)] 0; const uint16_t salt 230; // a4, [1,1000] 内任意 const uint8_t ver 255; // 版本上限, 17 uint8_t b[8] { 0 }; b[3] 0x9C; // 类型字节: 正式版 uint32_t h CalcHash(name, 1, 0, (uint8_t)salt); b[4] (uint8_t)(h); // 小端拆 4 字节 b[5] (uint8_t)(h 8); b[6] (uint8_t)(h 16); b[7] (uint8_t)(h 24); uint16_t v20 C5B_inv(salt); // 反解盐: 8C5B(v20)salt b[2] (uint8_t)(b[5] ^ (v20 0xFF)); // 程序里 v20(b5^b2)((b7^b1)8) b[1] (uint8_t)(b[7] ^ (v20 8)); b[0] (uint8_t)(b[6] ^ AD76_inv(ver)); // 程序里 verAD76(b6^b0) printf(License: %02X%02X-%02X%02X-%02X%02X-%02X%02X\n, b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]); // name 避开黑名单 AnyOne / 999, 前两字母避开 OW/JU 前缀 system(pause); return 0; }六、反盗版机制本地算法只是第一道门软件还有纵深内置黑名单泄露 key 用户名表 名字前缀/key 指纹表见 2.2联网审查mgr0x7C 开关置位时注册表持久化状态调用sub_1402B5140拼接 URL 上传 name/license 校验https://www.sweetscape.com/cgibin/010editor_check_license_9b.php?tnamesumlicenseid..chk..typ..有意思的是 URL 的拼法——被拆成 20 余段strcpy到末尾再逐段续接中间还夹着立即数直接写内存strcpy(v55, https://www.sweetscape.com/); // 此后每段都是 do p; while(*p);strlen 内联走到串尾再 strcpy 续接 strcpy(p, cgibin); *(_WORD *)p /; strcpy(p, 010editor); *(_WORD *)p _; strcpy(p, check); *(_WORD *)p _; *(_QWORD *)p 0x65736E6563696CLL; // 小端读字节: 6C 69 63 65 6E 73 65 license *(_WORD *)p _; *(_WORD *)p 9; *(_WORD *)p b; *(_WORD *)p .; *(_DWORD *)p 7366768; // php strcpy(p, ?); *(_WORD *)p t; *(_WORD *)p ; p sprintf(p, %s, urlencode(name)); // ?tname p sprintf(p, %s%s, sum, urlencode(license)); // sumlicense立即数 7173491sum p sprintf(p, id%dchk%dtyp%d, ...); // 7039075chk、7371124typ // a20 走同步 HTTPsub_140009A25否则挂异步回调 On_HttpCheckLicenseFinished这就是为什么在字符串窗口搜 sweetscape 一无所获——完整 URL 在 .rdata 里根本不存在全是运行时用立即数现场拼的0x65736E6563696C小端 license、7366768 php反静态搜索的典型手段。审查失败弹出 has detected that you have entered an invalid license 反盗版警告改值实验动态证实把 0x7C 从 0 改成 1注册时立刻触发该警告。试用状态持久化审查/试用状态写HKCU\Software\SweetScape\010 Editor——改值实验触发反盗版警告后0x7C 被持久化后续每次启动都恢复为 1甚至导致试用期作废。异常时删除该注册表项即可重置。
返回列表