27、软件破解基础(下):算法分析、编写KeyGen与实战
好,我们接着上一讲继续。上一回我们聊了怎么定位关键代码、怎么下断点、怎么暴力修改跳转。说白了,那是「暴力破解」——改个跳转就完事了。但真正的逆向工程,核心在于理解算法。
你想想看,一个软件的作者辛辛苦苦写了个注册算法,你直接一个 jmp 就跳过去了,虽然能用,但总觉得少了点灵魂。真正的高手,是能把这个算法还原出来,然后自己写一个注册机(KeyGen)的。
我个人习惯,拿到一个CrackMe,先暴力破解看看它到底有多「硬」,然后再回头分析算法。这样心里有底。
27.1 算法分析:从汇编到逻辑
算法分析,说白了就是「逆向注册算法」。你得从一堆汇编指令里,把注册码的生成逻辑给抽出来。
常见的注册算法有几种:
- 简单比较:用户名和注册码直接比对,或者注册码是用户名的某种简单变换(比如反转、加固定值)。
- 序列号验证:注册码是一串数字,程序会检查这串数字是否符合某种数学规律(比如校验和、CRC、RSA)。
- 挑战-响应:程序给你一个「挑战码」,你算出一个「响应码」。
我遇到过最坑的一个CrackMe,它的注册算法是拿用户名每个字符的ASCII码,乘以一个随机种子,再取模。我当时盯着OD看了半小时,才反应过来那个「随机种子」其实是固定的……
嗯,这里要注意:不要被变量名迷惑。变量名叫 random_seed,不代表它真的是随机的。
27.1.1 实战:分析一个简单的注册算法
假设我们有一个CrackMe,它的注册逻辑是这样的(伪代码):
if (strlen(serial) != 10) {
return false;
}
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += serial[i] - '0';
}
if (sum == 45) {
return true;
}
这个算法很简单:注册码必须是10位数字,而且这10位数字的和必须等于45。
在OD里,你会看到类似这样的汇编:
00401000 |. 83F9 0A CMP ECX, 0A ; 检查长度是否为10
00401003 |. 75 1C JNZ SHORT 00401021 ; 不是就跳走
00401005 |. 33DB XOR EBX, EBX ; sum = 0
00401007 |. 8D3424 LEA ESI, DWORD PTR SS:[ESP] ; 指向serial
0040100A |. 8A06 MOV AL, BYTE PTR DS:[ESI] ; 取一个字符
0040100C |. 2C 30 SUB AL, 30 ; 转成数字
0040100E |. 03D8 ADD EBX, EAX ; sum += 数字
00401010 |. 46 INC ESI ; 指针后移
00401011 |. 83F9 0A CMP ECX, 0A ; 循环10次
00401014 |. 75 F4 JNZ SHORT 0040100A ; 继续循环
00401016 |. 83FB 2D CMP EBX, 2D ; sum == 45?
00401019 |. 75 06 JNZ SHORT 00401021 ; 不等就失败
看到了吗?CMP EBX, 2D,2D就是45的十六进制。这个算法,一眼就能看穿。
核心思路:逆向算法,就是把你看到的「操作」翻译成高级语言。加法就是加法,减法就是减法,异或就是异或。别想复杂了。
27.2 编写KeyGen:从逻辑到代码
算法分析完了,接下来就是写注册机。注册机(KeyGen)就是一个程序,它能根据你提供的用户名(或者什么都不提供),生成一个合法的注册码。
写KeyGen,有几种策略:
- 直接还原算法:把CrackMe里的算法用高级语言重写一遍。
- 暴力枚举:如果算法不可逆,就枚举所有可能的注册码,直到找到一个合法的。
- 调用原程序:直接调用CrackMe里的注册验证函数(这个比较高级,我们后面再讲)。
我个人建议,能用第一种就用第一种。暴力枚举虽然简单,但效率低,而且容易被人看出来你「偷懒」。
27.2.1 实战:为上面的算法写KeyGen
刚才那个算法,要求10位数字的和等于45。那怎么写KeyGen?
最简单的办法:生成10个数字,让它们的和等于45。比如:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(NULL));
char serial[11] = {0};
int sum = 0;
// 先生成前9位随机数
for (int i = 0; i < 9; i++) {
serial[i] = '0' + (rand() % 10);
sum += serial[i] - '0';
}
// 第10位用来凑和
int last = 45 - sum;
if (last >= 0 && last <= 9) {
serial[9] = '0' + last;
printf("Serial: %s\n", serial);
} else {
// 如果凑不出来,就重新生成
// 实际项目中可以加个循环
printf("Failed, try again.\n");
}
return 0;
}
这个KeyGen很简单,但很实用。它生成的前9位是随机的,第10位是算出来的。
小技巧:写KeyGen的时候,最好能生成多个合法的注册码。这样用户试的时候,总有一个能用的。
27.3 实战:为一个CrackMe编写KeyGen
好,我们来看一个稍微复杂一点的CrackMe。假设它的注册算法是这样的:
- 用户名长度必须大于等于4。
- 对用户名每个字符,取ASCII码。
- 把这些ASCII码加起来,得到一个总和。
- 把这个总和乘以0x12345678,然后取低32位。
- 把这个结果转换成十进制字符串,就是注册码。
这个算法,说白了就是:serial = (sum_of_ascii(name) * 0x12345678) & 0xFFFFFFFF。
那我们的KeyGen怎么写?
#include <stdio.h>
#include <string.h>
int main() {
char name[256];
printf("Enter your name: ");
scanf("%s", name);
if (strlen(name) < 4) {
printf("Name too short!\n");
return 1;
}
unsigned int sum = 0;
for (int i = 0; i < strlen(name); i++) {
sum += (unsigned char)name[i];
}
unsigned int serial = sum * 0x12345678;
printf("Your serial: %u\n", serial);
return 0;
}
你看,就这么简单。把汇编里的乘法、加法,原封不动地搬到C语言里就行了。
注意:在逆向的时候,一定要搞清楚数据的类型。是 signed 还是 unsigned?是 32 位还是 64 位?我曾经因为一个符号位的问题,折腾了一整个下午……
27.4 知识体系:算法分析的核心逻辑
为了让你更直观地理解整个流程,我画了一张图:
这张图把整个流程串起来了。从定位代码,到分析逻辑,再到还原成高级语言,最后写出KeyGen。每一步都离不开对汇编指令的准确理解。
27.5 避坑指南
做算法分析,有几个坑是新人最容易踩的:
- 忽略数据类型:比如把 unsigned 当成 signed,结果算出来的注册码完全不对。
- 忽略溢出:很多算法会利用整数溢出,你如果不注意,还原出来的逻辑就是错的。
- 忽略优化:编译器会优化代码,比如把乘法变成移位和加法。你看到的汇编可能和源代码长得完全不一样。
我曾经遇到一个CrackMe,它的注册算法里有一个乘法,但汇编里死活找不到 MUL 指令。后来才发现,编译器把它优化成了 SHL 和 ADD 的组合。嗯,那一次我学到了:不要只看指令的名字,要看它实际做了什么。
27.6 总结
算法分析是逆向工程的精髓。暴力破解只是入门,能写出KeyGen才算真正「破解」了一个软件。
写KeyGen的时候,记住三点:
- 准确还原算法,不要自作聪明地「优化」。
- 考虑边界情况,比如空用户名、超长用户名。
- 多测试,用CrackMe本身验证你的KeyGen生成的注册码是否合法。
好了,这一讲就到这里。拿起你的OD和编译器,找个CrackMe练练手吧。
公众号:蓝海资料掘金营,微信deep3321