SQL Assistant 14 注册码算法分析:从汇编定位到 C++ 注册机实现
本文以 SQL Assistant 14.1.127 的 32 位 SqlAssistPrefs.dll 为样本,说明如何找到注册码校验函数、如何从汇编还原计算规则,以及如何用 C++ 生成满足这些规则的注册码。
下面的虚拟地址以首选加载基址 0x08100000 为准。本文完成的是静态还原和生成器自测,尚未在原软件中提交验证,因此不把自测通过等同于实际激活成功。
一、从注册窗口找到算法入口
1. 搜索控件和事件名称
先在程序文件中搜索注册窗口相关字符串。在 SqlAssistPrefs.dll 中,可以找到:
edRegName
edRegCompany
edRegSn
btnRegOK
btnRegOKClick
Invalid License Key!
edRegSn 是注册码输入框的名称,btnRegOKClick 是注册按钮事件名称。程序保留了 Delphi 的部分窗体与方法元数据,因此可以从名称继续找到代码地址。
注意名称可能出现在窗体资源、类型信息和方法表中。窗体资源里的名称只说明事件绑定关系;真正要找的是方法表中与名称相邻的函数指针。
在文件偏移 0x014FA560 处,有下面这条方法记录:
14 00 | 78 E8 60 09 | 0D | 62 74 6E 52 65 67 4F 4B 43 6C 69 63 6B
长度 方法地址 名称长度 btnRegOKClick
按小端序解释,78 E8 60 09 为 0x0960E878。转到这个虚拟地址并检查代码,就找到了注册按钮处理函数。
文件偏移和虚拟地址不能混用。手工换算时,先找到所属 PE 节,再使用:
VA = ImageBase + 节的 VirtualAddress + (文件偏移 - 节的 PointerToRawData)
2. 跟进决定成功与失败的调用
按钮处理函数中的关键指令:
0960E89A lea edx, [ebp-8]
0960E89D mov eax, [ebx+454h] ; 注册码输入框对象
0960E8A3 call 082FFB10h ; 读取输入框文字
0960E8A8 mov eax, [ebp-8]
0960E8AB lea edx, [ebp-4]
0960E8AE call 0844CA38h ; 去除首尾空白
0960E8B3 mov eax, [ebp-4]
0960E8B6 call 0880770Ch ; 校验注册码
0960E8BB test eax, eax
0960E8BD jne 0960EAAFh ; 非零进入成功分支
最值得跟进的是 test eax,eax 前面的调用。它的返回值直接决定成功与失败,因此 0x0880770C 是校验入口。
检查成功分支还能发现,姓名和公司是在校验返回非零之后读取的。在这条调用路径中,注册码校验函数只接收注册码,这两个字段不参与计算。
沿 0x0880770C 继续分析,会遇到 0x0880743C。前者检查外层格式与校验和,后者检查核心字段:
注册按钮 → 读取并 Trim → 外层校验 → 核心校验 → 返回类型代码
DLL 中出现 RSA、AES 等名称并不能证明注册码使用它们,判断应以这条调用路径实际执行的指令为依据。
二、外层校验:先还原字符串布局
下面的字符位置从 1 开始,C++ 下标从 0 开始。原程序使用 UTF-16 字符串,每字符占两字节;本文生成的字符都属于 ASCII,可以用 std::string 按字节重写。
1. 长度和固定标记
08807739 mov eax, [ebp-4]
0880773C call 0810CDB8h ; 字符串长度
08807741 cmp eax, 28h ; 0x28 = 40
08807744 jne 088078DEh ; 不符合则失败
0880774E mov ecx, 4 ; 取 4 个字符
08807753 mov edx, 0Bh ; 从第 11 个字符开始
08807758 mov eax, [ebp-4]
0880775B call 0810DE08h ; 子串操作
08807760 mov eax, [ebp-8]
08807763 mov edx, 08807918h ; UTF-16 字符串 "-SAN"
08807768 call 0810DDD0h ; 字符串比较
0880776D jne 088078DEh
条件就是:长度为 40,第 11~14 字符必须为 -SAN。对应 C++:
if (key.size() != 40 || key.substr(10, 4) != "-SAN") {
return 0;
}
0810CDB8h、0810DE08h 的用途需要结合函数内部实现和参数确认。这里可通过返回字符串长度、复制指定范围的 UTF-16 字符等行为识别。
2. 删除外壳字符
接下来的子串操作先取原串第 3 个字符至末尾,再拼接剩余串的第 1~12 个字符和第 14~38 个字符,得到 37 字符:
std::string body = key.substr(2);
std::string text = body.substr(0, 12) + body.substr(13);
这一步要跟清楚拼接函数的参数顺序。某个子串先被计算出来,不代表它最终排在左边。
3. 识别字符交换
下面的循环调用 0x088076C4:
088077CD mov ebx, 1
088077D2 push ebp
088077D3 mov edx, 9
088077D8 sub edx, ebx
088077DA mov eax, ebx
088077DC call 088076C4h
088077E1 pop ecx
088077E2 inc ebx
088077E3 cmp ebx, 5
088077E6 jne 088077D2h
跟进被调函数,可以看到它读取两个位置的 word 再交叉写回,因此是字符交换。循环中 EAX=i、EDX=9-i,i 从 1 到 4,得到:
(1,8)、(2,7)、(3,6)、(4,5)
随后同一函数还被调用两次,参数为 (1,36)、(2,37)。接着出现:
0880780A lea eax, [ebp-4]
0880780D call 0810CE8Ch
08807812 mov word ptr [eax+16h], 39h
0x39 是字符 9,0x16 为 22 字节,对应 UTF-16 下标 11,即第 12 个字符。它被强制写成 9。
完整变换为:
for (unsigned i = 1; i <= 4; ++i) {
std::swap(text[i - 1], text[8 - i]);
}
std::swap(text[0], text[35]);
std::swap(text[1], text[36]);
text[11] = '9';
三、外层校验和怎么算
1. 从调用参数确定范围
第一次校验和调用:
08807819 push 1Ah ; 第二段终点:26
0880781B mov ecx, 14h ; 第二段起点:20
08807820 mov edx, 12h ; 第一段终点:18
08807825 mov eax, 0Ch ; 第一段起点:12
0880782A call 0880766Ch
第二次传入 EAX=28、EDX=31、ECX=33,栈上传入 37。结合被调函数里的两个遍历循环,得到:
| 结果保存位置 |
参与求和的位置,包含两端 |
| 第 1~4 字符 |
第 12~18、20~26 字符 |
| 第 5~8 字符 |
第 28~31、33~37 字符 |
2. 从指令还原公式
函数 0x0880766C 的两个循环都把字符码累加到 BX,尾部为:
088076AA movzx eax, bx
088076AD and eax, 7
088076B0 imul ax, ax, 3E8h ; 0x3E8 = 1000
088076B5 add bx, ax
088076B8 mov eax, ebx
调用者随后取 AX,所以有效结果为 16 位。设字符码之和为 s:
s = 字符码之和 mod 65536
checksum = (s + (s & 7) × 1000) mod 65536
结果格式化为四位十六进制数,与字符串中的字段进行不区分大小写的比较。ASCII 范围的实现为:
unsigned checksum(const std::string& s) {
unsigned total = 0;
for (unsigned char ch : s) {
total = (total + ch) & 0xffffu;
}
return (total + (total & 7u) * 1000u) & 0xffffu;
}
通过两组检查后,外层取第 12~36 个字符,进入 0x0880743C。
四、核心校验:25 字符里的整数关系
1. 根据长度与分隔符拆字段
0880744C call 0810CDB8h
08807451 cmp eax, 19h ; 长度 25
08807454 jne 088075D6h
0880745A cmp word ptr [ebx+0Eh], 2Dh ; 第 8 字符为 '-'
08807465 cmp word ptr [ebx+1Eh], 2Dh ; 第 16 字符为 '-'
08807470 cmp word ptr [ebx+28h], 2Dh ; 第 21 字符为 '-'
上面省略了分隔符比较之间相同的失败跳转。结合后续读取位置,可以拆出:
ddddddd-ccccccc-TMxx-aabb
| 字符位置 |
字段 |
含义 |
| 1~7 |
ddddddd |
序列字段,经外层处理后首位为 9 |
| 9~15 |
ccccccc |
七字符编码的整数 |
| 17 |
T |
类型,允许 1/E/P/S |
| 18 |
M |
模式占位符,实际允许 L/M |
| 19~20 |
xx |
核心函数未直接解释,生成时取 00 |
| 22~23 |
aa |
序列字段的数位校验和 |
| 24~25 |
bb |
编码整数的数位校验和 |
类型与模式的允许范围来自两个字符集合检查;这里不推定它们的商业授权含义。xx 虽然没有在核心函数中被直接解释,仍参与外层校验和。
2. 数位校验和
程序解析前七位十进制数后,调用 0x0880740C:
0880740E mov ebx, eax
08807410 mov esi, 16h ; 初值 22
; 省略零值判断
08807419 mov eax, ebx
0880741B mov ecx, 0Ah
08807420 xor edx, edx
08807422 div ecx ; EAX=商,EDX=余数
08807424 add esi, edx ; 累加末位数字
08807426 mov ecx, 0Ah
0880742B mov eax, ebx
0880742D xor edx, edx
0880742F div ecx
08807431 mov ebx, eax ; 去掉末位
08807433 test ebx, ebx
08807435 ja 08807419h
08807437 mov eax, esi
公式为:
digitSum(n) = 22 + n 的十进制各位数字之和
程序用它检查 aa 和 bb:前者对应序列字段,后者对应编码字段解出的整数。它与上一节的字符码校验和是两个不同函数。
3. 自定义字符表与 32 进制位权
解码循环的关键部分:
088074E8 xor edi, edi ; 累加结果
088074EA mov ebp, 1 ; 初始位权
088074EF mov esi, 0Fh ; 从第 15 字符向前遍历
088074F4 movzx eax, word ptr [ebx+esi*2-2]
088074F9 call 0844B5A4h ; 转小写
088074FE mov edx, eax
08807500 mov ecx, 1
08807505 mov eax, 0880762Ch ; 字符表
0880750A call 0844C85Ch ; 查字符位置,从 1 起算
0880750F imul ebp ; 字符值乘位权
08807511 add edi, eax
08807513 shl ebp, 5 ; 位权乘 32
08807516 dec esi
08807517 cmp esi, 8
0880751A jne 088074F4h
0x0880762C 处的字符表为:
123456789abcdefghikjlmnopqrstuv
只有 31 个字符,位置对应数值 1~31,查询不到时返回 0。中间的顺序是 ikjl,不能替换成通常的字母顺序。生成器用不在表中的字符 0 表示零值。
原汇编从右往左计算位权和;C++ 可以等价地从左往右计算:
value = value × 32 + 当前字符值
运算按 32 位取模,C++ 用 uint32_t 保存结果。不能依赖有符号整数溢出实现这个行为。
4. 决定有效性的等式
解码结果保存在 EDI,随后执行:
08807544 lea eax, [ebx+0Ah] ; 从第 6 字符取两位十进制数
08807547 xor ecx, ecx
08807549 mov edx, 2
0880754E call 0844C498h
08807553 imul edx, [esp+4], 186A0h ; aa × 100000
0880755B sub edi, edx
0880755D sub edi, eax ; 减去序列字段末两位
0880755F cmp word ptr [ebx+22h], 4Dh ; 模式是否为 'M'
08807564 sete al
08807567 mov ecx, 10h
0880756C mov edx, 1
08807571 call 0844E130h ; M 取 1,否则取 16
08807576 mov esi, eax
08807578 mov eax, 80000000h
0880757D sub eax, esi
0880757F cmp edi, eax
08807581 je 0880758Eh
08807583 mov eax, 0C0000000h
08807588 sub eax, esi
0880758A cmp edi, eax
0880758C jne 088075D6h
设 serial 为序列字段,value 为编码值;模式为 M 时 delta=1,模式为 L 时 delta=16。必须满足:
value - aa × 100000 - serial % 100
= family - delta (按 32 位取模)
family ∈ {0x80000000, 0xC0000000}
aa = digitSum(serial)
bb = digitSum(value)
通过后,类型字符 1/P/S/E 分别返回 1/2/3/4;模式为 M 时再置上 0x100 标志位。零表示失败。
五、反向计算一组注册码
把核心等式移项:
value = family - delta + digitSum(serial) × 100000 + serial % 100
因此不需要枚举注册码。选择序列字段、类型、模式和常量组,计算整数,再按字符表编码即可。
取下面这组参数:
serial = 9123456
类型 = P
模式 = L
family = 0x80000000
计算过程:
aa = 22 + 9 + 1 + 2 + 3 + 4 + 5 + 6 = 52
delta = 16
value = 2147483648 - 16 + 52 × 100000 + 56
= 2152683688
= 0x804F58A8
bb = 22 + 2 + 1 + 5 + 2 + 6 + 8 + 3 + 6 + 8 + 8 = 71
编码时反复取最低五位 value & 31,查字符表,再右移五位,从右往左填入七个位置,得到 204um58。用解码方向核算:
| 字符 |
数值 |
上一步 × 32 + 数值 |
2 |
2 |
2 |
0 |
0 |
64 |
4 |
4 |
2052 |
u |
30 |
65694 |
m |
22 |
2102230 |
5 |
5 |
67271365 |
8 |
8 |
2152683688 |
拼成核心串:
9123456-204um58-PL00-5271
再构造尚未换位的 37 字符外层串:
00000000-SA9123456-204um58-PL00-52710
前八个零预留给两组校验和,末尾的 0 是生成器选择的附加字符,参与第二组校验和。
第一组求和文本为 9123456204um58,字符码之和为 851:
851 & 7 = 3
851 + 3 × 1000 = 3851 = 0x0F0B
第二组为 PL0052710,字符码之和为 507:
507 & 7 = 3
507 + 3 × 1000 = 3507 = 0x0DB3
填入后得到:
0F0B0DB3-SA9123456-204um58-PL00-52710
然后撤销外层校验器的操作:
- 把第 12 字符从
9 恢复为 N,使最终标记正确。
- 逆序交换
(2,37)、(1,36)、(4,5)、(3,6)、(2,7)、(1,8)。
- 在换位后字符串的前 12 字符之后插入
-,补回被删除的位置。
- 在最前面补上两个字符,生成器选择
SA。
结果为:
SA3BD0B001-SAN-123456-204um58-PL00-5270F
原始前缀的两个字符不参与上述校验,使用 SA 是构造选择;补回的被删除位置也不直接参与计算。它们与必须匹配的 -SAN 标记需要区分。
六、完整 C++ 注册机实现
下面包含生成器、核心校验器、外层校验器和自测入口。保存为 keygen.cpp 即可编译,不需要其他源码文件。程序按 ASCII 注册码处理,覆盖本文生成的字符范围;它只生成和校验字符串,不自动提交注册。
#include <string>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <iostream>
#include <random>
static const std::string alphabet = "123456789abcdefghikjlmnopqrstuv";
static unsigned digitSum(uint32_t n) {
unsigned sum = 22;
do { sum += n % 10; n /= 10; } while (n);
return sum;
}
static uint32_t decimal(const std::string& s, size_t count) {
uint32_t n = 0;
for (size_t i=0; i < count && i < s.size() && s[i]>='0' && s[i]<='9'; ++i)
n = n * 10 + s[i] - '0';
return n;
}
static unsigned checksum(const std::string& s) {
unsigned n = 0;
for (unsigned char c : s) n = (n + c) & 65535;
return (n + (n & 7) * 1000) & 65535;
}
static char lower(char c) { return c >= 'A' && c <= 'Z' ? c+32 : c; }
static std::string hex4(unsigned n) {
char buf[16]; sprintf_s(buf, "%04X", n); return buf;
}
static bool equalIgnoreCase(std::string a, std::string b) {
std::transform(a.begin(), a.end(), a.begin(), lower);
std::transform(b.begin(), b.end(), b.begin(), lower);
return a == b;
}
static unsigned coreValidate(const std::string& c) {
if (c.size()!=25 || c[7]!='-' || c[15]!='-' || c[20]!='-') return 0;
if (std::string("1EPS").find(c[16])==std::string::npos ||
std::string("LM").find(c[17])==std::string::npos) return 0;
uint32_t serial = decimal(c,7), first = decimal(c.substr(21),2), value=0;
if (!serial || digitSum(serial)!=first) return 0;
for (size_t i=8;i<15;++i) {
size_t p = alphabet.find(lower(c[i]));
value = (value<<5) + (p==std::string::npos ? 0 : static_cast<unsigned>(p)+1);
}
if (!value || digitSum(value)!=decimal(c.substr(23),2)) return 0;
uint32_t delta = c[17]=='M' ? 1 : 16;
uint32_t decoded = value - first * 100000 - decimal(c.substr(5),2);
if (decoded != 0x80000000u-delta && decoded != 0xc0000000u-delta) return 0;
unsigned type = c[16]=='P' ? 2 : c[16]=='S' ? 3 : c[16]=='E' ? 4 : 1;
return type | (c[17]=='M' ? 0x100 : 0);
}
static void swapAt(std::string& s, unsigned a, unsigned b) { std::swap(s[a-1],s[b-1]); }
static unsigned validate(std::string key) {
while (!key.empty() && static_cast<unsigned char>(key.front())<=32) key.erase(0,1);
while (!key.empty() && static_cast<unsigned char>(key.back())<=32) key.pop_back();
if (key.size()!=40 || key.substr(10,4)!="-SAN") return 0;
std::string body=key.substr(2), t=body.substr(0,12)+body.substr(13);
for (unsigned i=1;i<=4;++i) swapAt(t,i,9-i);
swapAt(t,1,36); swapAt(t,2,37); t[11]='9';
if (!equalIgnoreCase(t.substr(0,4),hex4(checksum(t.substr(11,7)+t.substr(19,7))))) return 0;
if (!equalIgnoreCase(t.substr(4,4),hex4(checksum(t.substr(27,4)+t.substr(32,5))))) return 0;
return coreValidate(t.substr(11,25));
}
static std::string generate(unsigned serial, char edition, char mode, uint32_t family) {
if (serial<9000000 || serial>9999999) throw std::runtime_error("serial must be 9000000..9999999");
if (std::string("1EPS").find(edition)==std::string::npos || std::string("LM").find(mode)==std::string::npos)
throw std::runtime_error("unsupported edition or mode");
if (family!=0x80000000u && family!=0xc0000000u) throw std::runtime_error("unsupported family");
unsigned a=digitSum(serial);
uint32_t value=family-(mode=='M'?1:16)+a*100000+serial%100, v=value;
std::string encoded(7,'0');
for (int i=6;i>=0;--i) { unsigned d=v&31; encoded[i]=d?alphabet[d-1]:'0'; v>>=5; }
char buf[80]; sprintf_s(buf,"%07u-%s-%c%c00-%02u%02u",serial,encoded.c_str(),edition,mode,a,digitSum(value));
std::string core=buf;
if (!coreValidate(core)) throw std::runtime_error("unrepresentable checksum");
std::string t="00000000-SA"+core+"0";
t.replace(0,4,hex4(checksum(t.substr(11,7)+t.substr(19,7))));
t.replace(4,4,hex4(checksum(t.substr(27,4)+t.substr(32,5))));
t[11]='N'; swapAt(t,2,37); swapAt(t,1,36);
for (unsigned i=4;i>0;--i) swapAt(t,i,9-i);
std::string key="SA"+t.substr(0,12)+"-"+t.substr(12);
if (validate(key)!=coreValidate(core)) throw std::runtime_error("internal validation failed");
return key;
}
static void selfTest() {
std::mt19937 rng(127);
unsigned count = 0;
if (generate(9123456, 'P', 'L', 0x80000000u) !=
"SA3BD0B001-SAN-123456-204um58-PL00-5270F")
throw std::runtime_error("fixed vector mismatch");
for (char edition : std::string("1EPS")) {
for (char mode : std::string("LM")) {
for (uint32_t family : {0x80000000u, 0xc0000000u}) {
for (unsigned i = 0; i < 100; ++i) {
unsigned serial = 9000000u + rng() % 1000000u;
const std::string key = generate(serial, edition, mode, family);
unsigned type = edition == 'P' ? 2u : edition == 'S' ? 3u
: edition == 'E' ? 4u : 1u;
unsigned expected = type | (mode == 'M' ? 0x100u : 0u);
if (validate(key) != expected)
throw std::runtime_error("generated key rejected");
std::string damaged = key;
damaged[10] = 'X';
if (validate(damaged) != 0 || validate(key.substr(0, 39)) != 0)
throw std::runtime_error("malformed key accepted");
++count;
}
}
}
}
std::cout << count << " C++ generation/validation cases passed.\n";
}
static unsigned parseUnsigned(const char* arg) {
std::string s(arg);
if (s.empty() || s.front() == '-') throw std::runtime_error("invalid unsigned integer");
size_t end = 0;
unsigned long long value = std::stoull(s, &end, 0);
if (end != s.size() || value > 0xffffffffull)
throw std::runtime_error("invalid unsigned integer");
return static_cast<unsigned>(value);
}
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string(argv[1]) == "--self-test") {
selfTest();
return 0;
}
if (argc == 3 && std::string(argv[1]) == "--validate") {
unsigned type = validate(argv[2]);
std::cout << "type_code=" << type << '\n';
return type ? 0 : 1;
}
unsigned serial = 9123456;
char edition = 'P', mode = 'L';
uint32_t family = 0x80000000u;
if (argc != 1) {
if (argc != 6 || std::string(argv[1]) != "--generate" ||
std::string(argv[3]).size() != 1 || std::string(argv[4]).size() != 1)
throw std::runtime_error("Usage: Keygen.exe [--generate serial edition mode family | --validate key | --self-test]");
serial = parseUnsigned(argv[2]);
edition = argv[3][0];
mode = argv[4][0];
family = parseUnsigned(argv[5]);
}
const std::string key = generate(serial, edition, mode, family);
std::cout << key << '\n' << "type_code=" << validate(key) << '\n';
return 0;
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
return 2;
}
}
七、编译与运行
在 Visual Studio 的 x64 Native Tools Command Prompt 中进入源码所在目录:
cl /nologo /std:c++17 /utf-8 /EHsc /O2 /W4 /MT keygen.cpp /Fe:Keygen.exe
默认运行:
Keygen.exe
输出:
SA3BD0B001-SAN-123456-204um58-PL00-5270F
type_code=2
指定全部参数:
Keygen.exe --generate 9123456 P L 0x80000000
参数依次为序列字段、类型、模式和常量组。序列字段取 9000000~9999999;类型为 1/E/P/S,模式为 L/M,常量组为 0x80000000 或 0xC0000000。
校验已有字符串:
Keygen.exe --validate "SA3BD0B001-SAN-123456-204um58-PL00-5270F"
类型代码为零表示未通过还原校验器,非零表示通过;模式为 M 时还包含 0x100 标志位。
运行自测:
Keygen.exe --self-test
自测使用固定随机种子,遍历 4 种类型、2 种模式、2 组常量,每个组合取 100 个序列字段,共 1600 组。同时检查固定算例、生成后的类型代码,以及损坏标记和截断长度时是否被拒绝。已完成的自测输出:
1600 C++ generation/validation cases passed.
这证明生成器与还原校验器内部自洽。原始 DLL 对相同输入的返回结果,以及原软件是否还有其他授权限制,仍需另外验证。