【原创】DMDE 许可证算法分析与通用注册机(win32/win64)
附件:注册机完整源码(C++ 源文件 + manifest + 编译脚本)。DMDE 试用版请自行到官网下载:https://dmde.com/download.html(本文实测 4.5.0.849 beta win32/win64、4.4.4.842 win64、4.4.0.828 win32)。本文完整分析整条授权链,并说明注册机如何从目标程序里现场提取全部常量,而不是按版本硬编码。仅供学习研究,请支持正版。
1. 目标与保护概览
DMDE(Disk Editor and Data Recovery Software,磁盘编辑与数据恢复软件)采用纯离线的分层授权方案,没有加密狗,手动激活也不需要联网:
- base64url 编码的许可证数据块,内含一条二进制记录
- 数据块上有一层 RSA-2048 签名(唯一的非对称校验)
- 数据块尾部有 CRC-32 校验
- 由被授权人信息和机器码派生出的密钥材料
- 自定义 base32 激活码,由四个建立在 SHA-1 和程序内置常量表之上的半字节方程把关
层次依次是:解析(结构 + 版本范围)、RSA 验签、CRC 校验、密钥材料派生、方程组、机器绑定。下文逐层分析。全程静态逆向(IDA + capstone 脚本)加动态确认(隐藏启动、内存扫描、WOW64 断点跟踪器、以及驱动真实对话框的 UI 自动化)。
实测版本:4.5.0.849 beta(win32、win64)、4.4.4.842(win64 GUI 和控制台)、4.2.4.818(win64)、4.4.0.828(win32)。除控制台版只验证了补丁与常量提取外,其余全部通过真实对话框流程激活成功。
2. 工具
- IDA(win64 与 win32 两个库)
- python + capstone,对 PE 做模式扫描
- 一个小型 Win32 UI 驱动程序(EnumWindows / PostMessage),自动应答对话框、无头驱动手动激活流程
- 一个支持 WOW64 的 int3 跟踪器(WOW64 下断点异常码是 0x4000001F 而不是 0x80000003,这点值得知道,我为此搭进去一下午)
- ReadProcessMemory,观察活进程里的许可证结构体
3. 定位许可证解析函数
锚点是版本比较指令簇。解析函数用 cmp word ptr [reg+disp], imm16 成对校验记录里的版本字:
; 4.5.0.849 win64,解析函数 0x4C4830 内
4C4DD0 movzx rax, byte ptr [rax+10h]
4C4DD5 test al, 10h ; 版本标志位
4C4DD8 je reject
4C4DE2 movzx rax, byte ptr [rax+12h]
4C4DE7 cmp al, byte ptr [rbp+7Eh] ; 49
4C4E06 cmp word ptr [rax+14h], 25Eh ; >= 606
4C4E0C jb reject
4C4E16 cmp word ptr [rax+14h], 260h ; <= 608
4C4E1C ja reject
4C4E26 movzx rax, byte ptr [rax+10h]
4C4E2B xor al, 10h ; 掩码折回版本半字节
编码是 66 81 /7 带 ModRM 位移。解码 ModRM 而不是匹配固定字节序列,才能在每个版本上都找到这组比较(寄存器和记录偏移在版本和架构之间都会变)。持有这组比较的函数就是数据块解析函数。4.4.0.828 win32 上同一段代码位于 0x464020,记录偏移同样是 +14h。
4. 许可证数据块
许可证密钥文本是 base64url(字母表
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_,填充符 .)。
解码后的布局:
word 0 = 4 数据块版本
word 2 = l2 字符串区长度
bytes 4..l2 = 四个 NUL 结尾的字符串(name、str2、str3、str4),
长度上限 512/200/255/31;NUL 后紧跟 0xFF 表示提前进入
下一个字符串;l2-2 处的 word 必须为 0(扩展探测)
header2 @l2 = 44 字节:+0 是被授权人(12 字节),+18 是标志字,
+28 的 qword -> cfg+1044,+40 的 dword -> cfg+1056
record @l2+44 = 66 字节(见下)
sig @-256 = RSA-2048 签名
最后 4 字节 = 前面全部内容的 CRC-32(poly 0xEDB88320)
66 字节的记录:
+0 word size = 66
+2 word number -> cfg+972
+4 char magic[8] "w3ha.606"
+12 dword 自由
+16 dword 版本标志位 0x10 win64 GUI,0x20 win64 控制台,0x01 win32
+18 word 49
+20 dword 版本号 接受 606..608,高字是有效期代码(0 = 永久)
+24 qword nonce
+32 qword 自由 -> cfg+976/980
+40 qword 自由 -> cfg+1116
+48 dword 自由 -> cfg+1124
+52 14 字节 template 28 个半字节,低位在前
+16 处的字节是解析函数测试的版本标志位。两个架构上指令对相同,掩码不同:
; 4.4.4.842 win64
4C1376 movzx rax, byte ptr [rax+10h]
4C137B test al, 10h
4C137E je reject
; 4.4.0.828 win32
46458C test byte ptr [eax+10h], 1
464590 je reject
版本比较之后,同一个掩码会被折进版本半字节(xor al, 10h / xor al, 1),所以"括住"版本比较的 test/xor 指令对是一个稳定的提取锚点。
一个值得记录的版本差异:4.4.0.828 上对话框报的 "Inappropriate license key (wrong version)" 是 RSA 阶段之后一切解析失败的总 catch-all。版本标志位写错和真正的版本不匹配显示同一句话,在这版上这条报错对定位失败检查点毫无用处。
5. RSA 校验与补丁
签名校验是一个独立函数,由解析函数直接调用。观察过的每个版本里,它都紧贴在解析函数体下方:
- 4.5.0.849 win64:0x4C4720(解析函数 0x4C4830)
- 4.4.4.842 win64:0x4C0A90(解析函数 0x4C0BC0)
- 4.4.0.828 win32:0x463F48(解析函数 0x464020,经一次 .itext E9 跳板)
它对签名块做哈希并与内置公钥比对,返回布尔值。win32(FPC 编译)的函数头:
463F48 push ebp
463F49 mov ebp, esp
463F4B add esp, 0FFFFFE74h
...
463FEC cmp dword ptr [ebp-14h], 14h ; SHA-1 摘要长度 20
463FF0 jne 46400Dh
...
46400D mov al, [ebp-0Dh]
464013 ret
FPC 的调用约定把布尔返回值放在 AL 里、裸 ret 返回,所以三个字节的补丁在两个架构上通用:
B0 01 C3 ; mov al, 1 ; ret
签名是唯一的非对称校验。其余部分(CRC-32、CRC-16、方程组、解析遍历)全部照常满足而不是绕过,因此对打过补丁的程序来说,伪造的许可证与真许可证没有区别。
通用定位方法:在解析函数体里扫描 E8 rel32 调用,找目标函数结束地址落在解析函数起点下方 0x400 以内、且函数体内含 SHA-1 长度常量 0x14(cmp eax/reg/mem, 14h 或 imm32 14 00 00 00)的那个。win32 版本带增量链接,调用可能落到 .itext 跳板(单条 E9 jmp)上,需要跟一跳。
6. 密钥材料
激活校验从许可证结构体里读两个缓冲区:
k986 (cfg+986, 40 字节) = zeros(8) || 被授权人(12) || vars80(8)
|| le16(606) || le16(0) || nonce(8)
key40 (cfg+1076, 40 字节) = 机器码(8) || k986[8..40]
vars80 = SHA1(MD5(被授权人12 || number_le16 || salt5))[0..7],其中 salt5 = 18 E3 20 A1 44,是 .text 里的 5 字节二进制常量。它通过 .data 里的一对指针找到:两个连续指针,第二个指向第一个之后 6 字节(win64)或 8 字节(win32,字符串对齐不同)处,两者都指向 5 字节、NUL 结尾、至少两个字节最高位为 1 的字符串。前面 6/8 字节处的兄弟字符串属于标志路径。
; 4.4.0.828 win32
45F178 C5 C1 D9 75 98 00 .. 00 ; 标志路径盐值
45F180 18 E3 20 A1 44 00 .. 00 ; 许可证盐值
6773CC dd 45F178, 45F180 ; 指针对,间距 8
解析函数把记录版本拷到 cfg+984,然后用一个编译期常量(606)覆盖密钥材料里 cfg+1014 处的版本字。生成时必须用这个字,不能用记录版本:
4C1498 mov word ptr [rax+3F6h], 25Eh ; 4.4.4 win64
结构体偏移在 win32 和 win64 上完全一致:结构体是字节数组,架构不移动任何东西。magic 在 +1060,number 在 +972,k986 在 +986,版本在 +984,密钥版本字在 +1014,key40 在 +1076。
7. 半字节方程组
激活码解码器是自定义 base32,字母表 123456789ABCDEFGHYJKLMNPQR0TZVWX,按位低位在前消费,每 5 个字符插一个 -。解码器的容错:小写折叠,I->Y、U->Z、O/S->0,字母表之外的字符跳过。
解码出 15 字节 = 13 字节载荷 + LE 的 CRC-16。载荷是 26 个半字节(低位在前)。CRC-16:反射多项式 0xA001,初值 0xFFFF,异或输出 0xFFFF,覆盖 key40 || payload13。
令 H(t, key) = SHA1(le32(t) || key) 前 4 字节按 LE dword 读取。校验在 26 个半字节 N 和 28 元视图 V = N || [0,0] 上跑四个方程:
eqA: XOR(N[0..25]) == (XOR H(t, key40), t 取遍 T1, & 0xF) ^ 0xF
eqB: XOR(N[i], 0x3577CAF 的第 i 位) == (XOR H(t, key40), t 取遍 T2, & 0xF) ^ 5
eqC: XOR(V[0..27]) == (XOR H(t, k986), t 取遍 T3, & 0xF) ^ 0xC
eqD: XOR(V[i], 0xE9D4FED 的第 i 位) == (XOR H(t, k986), t 取遍 T4, & 0xF) ^ 9
eqA/eqB 跑在激活码校验里(手动激活),eqC/eqD 跑在许可证解析里(记录里的 template 半字节)。按位选择的循环很好认:1 << i 移位后与掩码立即数做 test:
; 4.4.0.828 win32,eqB
462F5F mov esi, 1
462F64 shl esi, cl
462F66 test esi, 3577CAFh
462F6C je @skip
462F6E movzx ecx, byte ptr [eax] ; 运行时半字节缓冲区
462F71 xor ebx, ecx
四张表紧跟在方程函数后面,是非零 dword 连续段,中间以零分隔:
T1 = 8A417373 0AFA4AAE E23719AA B451AEA4 7C862632 (5 个 dword,eqA/eqB 函数之后)
T2 = 9CC08591 263F668B 707C27D2 16BD9EAA (4 个 dword)
T3 = 7B347482 D2A33A04 D28B439E E71A7C30 (4 个 dword,eqC/eqD 函数之后)
T4 = D72C8085 6DB3EA40 26C87890 E8F90944 180A085A (5 个 dword)
这些值在检查过的所有版本里(4.2.4 到 4.5.0,win32 和 win64)完全相同。注册机仍然每次从二进制里读出来:win64 上从 .pdata 给出的函数边界之后开始找;win32 没有展开表,从掩码循环的收尾 ret 之后开始找。给想复刻 win32 扫描的人提个醒:掩码指令后遇到的第一个 C3 通常不是 ret,而是收尾阶段 mov eax, ebx 的 ModRM 字节。要把每个 C3 候选都试一遍,用 dword 连续段的形状来定夺。
求解:eqA 和 eqC 的系数全为一,所以搜索 nonce 直到两个右边相等;剩下两个方程在 GF(2) 上对选定的四个半字节线性解出。
8. 机器绑定
key40 的开头 8 字节是机器码。填充时机各版本不同:4.4.4 在普通启动时就计算;4.5.0 在许可证安装时计算;4.4.0 在启动对话框全部应答完之后,靠内置免费版许可证填入。同一台机器上三种情况的值完全相同。
注册机不维护"版本到绑定方式"的表,而是运行时直接决定:往目标目录丢一个探测用 license.key(这个数据块与机器无关),隐藏启动目标,自动应答启动对话框(语言选择、旧版本提示、许可协议:只点 OK/Yes/Continue/Ignore/Accept,复选框只在未勾选时点),在内存里扫描许可证结构体(magic w3ha. + +14 处标志字 31 00 + +24 处可打印的被授权人),然后按多数表决读出 magic+16 处的头槽。全零也是合法答案,走同一条路径。
这样读到的就是激活码校验实际使用的那个 key40,所以生成的激活码必然匹配目标程序将要校验的值。
9. 激活文件
acode.txt 把两半捆在一起:
activatekey=<激活码>\r\nLICDATA:<许可证密钥>\r\n
手动激活对话框的 Open... 一步完成:LICDATA: 之前的空白处截断激活码部分,先解析并安装许可证,再校验激活码。4.4.4 及更早版本只在换行符之后找 LICDATA:,新版本接受任意空白,所以文件统一用 CRLF。activatekey= 前缀在所有观察过的版本上都被接受,包括那些自身 ini 关键字是 activatecode[0]= 的版本。
激活成功后 DMDE 在 dmde.ini 里写入 activatecode[0]=<激活码>:<第二激活码>。第二激活码由 DMDE 自己生成,只依赖许可证和机器:同一份伪造许可证在 4.5.0 win64、4.2.4 win64、4.4.0 win32 上存下的第二激活码完全相同,与版本和架构无关。
10. 通用化的做法
注册机内部没有任何按版本硬编码的常量。所有常量在运行时通过锚点从目标程序提取,这些锚点在检查过的每个版本上都成立:
| 常量 |
锚点 |
| 版本范围 |
解析函数内的 66 81 /7 比较,与 magic 数字交叉验证 |
| 版本标志位 |
括住版本比较、立即数相等的 test/xor 对 |
| 密钥版本字 |
离版本比较最近的 66 C7 /0 存储 |
| 掩码 |
方程循环里对立即数的字节探测 |
| T1..T4 |
方程函数之后的 dword 连续段 |
| 盐值 |
.data 指针对,间距 6(win64)/ 8(win32) |
| RSA 校验 |
解析函数下方含 SHA-1 长度 0x14 的 E8 调用目标 |
| 机器码 |
运行时探测头槽 |
win32 支持要点:PE32 镜像没有 .pdata,函数边界来自 .text 的调用目标;解析调用时跟一次 .itext 跳板;FPC 用 AL 返回布尔值、调用方清栈,补丁不用改。一遍扫描同时处理两种架构。
锚点失效时,工具会大声停下并打印它找到的候选(例如所有形状像版本比较的指令),而不是伪造错误的常量。有这份输出加上源码里的锚点逻辑,重新推导是个短活。
11. 验证
生成器在写出任何文件之前先验证自己的输出:
- 按解析函数的方式重新遍历数据块(字符串遍历、边界、CRC-32)
- 用提取的表和派生的密钥材料重算 template 与半字节方程
- 重新解码激活码并重查 CRC-16
- 把试用许可证种进一次隐藏启动,逐字节比对目标程序实际派生的 k986 与生成值
端到端结果:标题栏的 "[Not Activated]" 消失,结构体里是被伪造的被授权人、number、nonce 和机器码,dmde.ini 记录下激活,且第二激活码由 DMDE 自己生成。下图输出以演示参数 --device 0011223344556677 运行,机器码相关字段均为演示值。
12. 4.0.0 的边界
4.0.0.800 win64 检查过,未支持。数据块解析、magic、唯一接受的版本 606、盐值、结构体几何、RSA 锚点全部沿用,但半字节方程机制在整个镜像里不存在:没有掩码立即数,没有 T 表。它的激活码校验是更老的方案,需要单独推导。注册机对此明确报告,而不是瞎猜。
结束语
- 从版本比较入手,其余一切都挂在解析函数上。
- 常量靠推导,不要列表:同一个版本的两个架构,光版本标志位就不一样。
- 凡是机器绑定的东西,运行时观察胜过静态猜测。
- 对话框报错文本会跨版本撒谎;能报告"哪个分支拒绝了"的跟踪器,胜过读报错字符串。
仅供学习交流,试用程序请到官网下载,喜欢请支持正版。