近日发现一个数据恢复软件,小巧,但是非常好用。唯一的不足就是在未注册的时候仅能使用部分功能,而且恢复的数据不能超过一定的大小限制。在使用是深感可惜,就萌生了破解的想法,一条路是破解其序列号的生成算法,自己算一个序列号出来;要不就干脆直接更改程序机器代码,突破功能上的限制。
首选进行序列号的算法破解,用Peid查壳,发现输出结果是 “Microsoft Visual C++ 7.0”,一阵狂喜,该软件在VC++.Net环境下编写,但是没有加任何的壳。非常好,非常好,这样可以省去一大堆去壳的麻烦。
这个数据恢复软件的注册,是提交用户名,采用付费后获得注册码的程序的方式,也就是说输入用户名,再输入开发公司给的对应序列号,就可以注册成功。那么传统的破解序列号的办法就再合适不过了。
它详细的注册流程是这样:
1. 出现程序主界面,点击注册按钮出现注册对话框;
2. 输入用户名和序列号,完成注册;
乍一看,注册方法还是比较传统的,现在推测,序列号的生成和输入的用户名有某种联系。破解的切入点可以定在这个注册对话框上,如果我们可以这个对话框背后,找到序列好的生成算法,那么一切问题都解决。
在OllyICE下加载,一切顺利。花不多时间,就找到了隐藏在幕后的一片桃花源。以下是序列号生成函数的片段,前面省略掉了一段,这一段其实就干了一件事,初始化了一个7×4的二位数组,换成C语言的话说,是这样的:
int base[7][4] = { {X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X} };
我们称数组base为基准数组,数组里的X,代表一个特定的值,这个值是固定好的,不变的,考虑到我们破解的目的仅仅是技术交流,恕不给出具体数值。
初始化完成以后,就开始计算序列号。
00475A64 |. E9 1D030000 jmp 00475D86
00475A69 |> 8B45 08 mov eax, [ebp+8]
00475A6C |. 50 push eax ; |s
00475A6D |. E8 F0660000 call <jmp.&MSVCRT.strlen> ; |strlen
00475A72 |. 83C4 04 add esp, 4
00475A75 |. 3D FD030000 cmp eax, 3FD
00475A7A |. 76 07 jbe short 00475A83
00475A7C |. 33C0 xor eax, eax
00475A7E |. E9 03030000 jmp 00475D86
00475A83 |> 8B4D 08 mov ecx, [ebp+8]
00475A86 |. 51 push ecx ; |s
00475A87 |. E8 D6660000 call <jmp.&MSVCRT.strlen> ; |strlen
00475A8C |. 83C4 04 add esp, 4
00475A8F |. 8985 54F9FFFF mov [ebp-6AC], eax
00475A95 |. C685 68F9FFFF>mov byte ptr [ebp-698], 0
00475A9C |. C785 F0FBFFFF>mov dword ptr [ebp-410], 0
00475AA6 |. C785 F8FBFFFF>mov dword ptr [ebp-408], 0
00475AB0 |. 8B55 08 mov edx, [ebp+8]
00475AB3 |. 52 push edx ; |src
00475AB4 |. 8D85 FCFBFFFF lea eax, [ebp-404] ; |
00475ABA |. 50 push eax ; |dest
00475ABB |. E8 A8660000 call <jmp.&MSVCRT.strcpy> ; |strcpy
00475AC0 |. 83C4 08 add esp, 8
可能在静态情况下阅读汇编代码比较痛苦,但是在上面一段的注释中,还是可以看到,程序调用了msvcrt.dll动态连接库中的strlen和strcpy函数,熟悉C语言的朋友不会陌生,前者是求出一个字符串的长度,后者完成一次字符串的拷贝操作。而此时操作的对象是用户名字符串,看来以后要对用户名下手。
00475AC3 |. C785 74FBFFFF>mov dword ptr [ebp-48C], 0
00475ACD |. EB 0F jmp short 00475ADE
00475ACF |> 8B8D 74FBFFFF /mov ecx, [ebp-48C]
00475AD5 |. 83C1 01 |add ecx, 1
00475AD8 |. 898D 74FBFFFF |mov [ebp-48C], ecx
00475ADE |> 8B95 74FBFFFF mov edx, [ebp-48C]
00475AE4 |. 3B95 54F9FFFF |cmp edx, [ebp-6AC]
00475AEA |. 7D 35 |jge short 00475B21
00475AEC |. 8B85 74FBFFFF |mov eax, [ebp-48C]
00475AF2 |. 8A8D 68F9FFFF |mov cl, [ebp-698]
00475AF8 |. 028C05 FCFBFF>|add cl, [ebp+eax-404]
00475AFF |. 888D 68F9FFFF |mov [ebp-698], cl
00475B05 |. 8B95 68F9FFFF |mov edx, [ebp-698]
00475B0B |. 81E2 FF000000 |and edx, 0FF
00475B11 |. 8B85 F0FBFFFF |mov eax, [ebp-410]
00475B17 |. 03C2 |add eax, edx
00475B19 |. 8985 F0FBFFFF |mov [ebp-410], eax
00475B1F |.^ EB AE \jmp short 00475ACF
熟悉汇编的朋友一看,就知道这是一段循环代码,它的操作对象是用户名字符串。它计算用户名字符串usrname中各个字符的ASCII的累加和与重叠累加和。举个例子说吧,假如输入的用户名是“HQ”,那么它会得到两个计算结果,一个是“H”和“Q”字符ASCII码的累加和accumulation,accumulation=0x48+0x51=0x99;0x48和0x51分别是字符“H”和“Q”的ASCII码的十六进制形式;另一个结果是重叠累加和,sum=0x48+(0x48+0x51)=0xE1,发现不同了吗?其实这个结果是“H”+(“H”+“Q”),故谓之重叠累加,要是输入的用户名是“LHZ”,那么sum=‘L’+(‘L’+‘H’)+(‘L’+‘H’+‘Z’)。我们把这步操作换成C语言的形式,写在一个函数里:
int ModifyID( char ID[], int length, int& accumulation )
{
int i = 0, sum = 0x00;
accumulation = 0x00;
for ( i=0; i<length; i++ )
{
accumulation += ID[i];
sum += accumulation;
}
return sum;
}
这里的sum和accumulation分别对应计算得到的累加和与重叠累加和。
00475B21 |> 68 00020000 push 200 ; |n = 200 (512.)
00475B26 |. 6A 00 push 0 ; |c = 00
00475B28 |. 8D8D 70F9FFFF lea ecx, [ebp-690] ; |
00475B2E |. 51 push ecx ; |s
00475B2F |. E8 9C650000 call <jmp.&MSVCRT.memset> ; |memset
00475B34 |. 83C4 0C add esp, 0C
00475B37 |. 6A 08 push 8 ; |n = 8
00475B39 |. 8B55 0C mov edx, [ebp+C] ; |
00475B3C |. 52 push edx ; |src
00475B3D |. 8D85 70F9FFFF lea eax, [ebp-690] ; |
00475B43 |. 50 push eax ; |dest
00475B44 |. E8 13660000 call <jmp.&MSVCRT.memcpy> ; |memcpy
00475B49 |. 83C4 0C add esp, 0C
00475B4C |. 8D8D 70F9FFFF lea ecx, [ebp-690] ; // regi code ASC to NUM
00475B52 |. 51 push ecx ; |Arg1
00475B53 |. E8 FEFAFFFF call 00475656 ; |DRW.00475656
这里在一系列准备之后,就开始对输入的序列号下手了,最后一句的call指令明显和其他几个call不一样,这个函数是开发公司自己写的,不能从名字推断出他做了些什么事情。在深入调试之后,原来它取了输入的序列号的前八个字符组成一个字符串,将这个字符串传换为数值。假如输入的前八个是“12345678”,转换结果就是0x12345678;要是“EF54BCAD”,结果就是0xEF54BCAD。这都是16进制数。我们将转换得到的数字保存在变量num里,方便下面的解说。
00475B58 |. 8985 F4FBFFFF mov [ebp-40C], eax ; eax保存转换得到的num
00475B5E |. 8B95 F0FBFFFF mov edx, [ebp-410] ; edx保存刚才计算得到的用户名重叠累加和sum
00475B64 |. 0395 F4FBFFFF add edx, [ebp-40C] ; 计算 sum+num
00475B6A |. 8995 F0FBFFFF mov [ebp-410], edx ; 计算结果保存于 [ebp-410],我们取变量名r1
00475B70 |. 8B85 F4FBFFFF mov eax, [ebp-40C]
00475B76 |. 0385 F0FBFFFF add eax, [ebp-410] ; eax保存刚才计算得到r1
00475B7C |. 8B8D 68F9FFFF mov ecx, [ebp-698] ; ecx保存刚才计算得到的用户名累加和accumulation
00475B82 |. 81E1 FF000000 and ecx, 0FF ; ecx保存的值是刚才计算得到的sum的值,取低8位
00475B88 |. 03C1 add eax, ecx ; r2 = accumulation + (ecx & 0xFF)
00475B8A |. 8B95 74FBFFFF mov edx, [ebp-48C]
00475B90 |. 33C9 xor ecx, ecx
00475B92 |. 8A8C15 FBFBFF>mov cl, [ebp+edx-405] ; ecx是输入的用户名的最后一个字符的ASCII值,username[last]
00475B99 |. 03C1 add eax, ecx ; r3=r2+(int)username[last]
00475B9B |. 33D2 xor edx, edx
00475B9D |. B9 07000000 mov ecx, 7
00475BA2 |. F7F1 div ecx ; 计算r3 % 7,即r3除以7的余数
00475BA4 |. 8995 F8FBFFFF mov [ebp-408], edx ; r3除以7的余数保存于[ebp-408]中,取变量名row
00475BAA |. 8B95 F8FBFFFF mov edx, [ebp-408]
00475BB0 |. C1E2 04 shl edx, 4 ; 计算 edx×16
00475BB3 |. 8D8415 78FBFF>lea eax, [ebp+edx-488] ; 完成在基准数组base的定位,即以下取base[row]这一行的数据
00475BBA |. 8985 70FBFFFF mov [ebp-490], eax
计算序列号的过程终于拉开了序幕。上面这一段,就开始了序列号计算的一连串变换。程序大喊一声,“我要变形了!”,就开始变了。对照刚才的分析结果,具体的变法都写在了语句后面的注释里了。
00475BC0 |. 8B8D 70FBFFFF mov ecx, [ebp-490]
00475BC6 |. 8B51 0C mov edx, [ecx+C]
00475BC9 |. 3395 F0FBFFFF xor edx, [ebp-410]
00475BCF |. 8995 58F9FFFF mov [ebp-6A8], edx
00475BD5 |. 8B85 70FBFFFF mov eax, [ebp-490]
00475BDB |. 8B48 04 mov ecx, [eax+4]
00475BDE |. 338D F0FBFFFF xor ecx, [ebp-410]
00475BE4 |. 898D 5CF9FFFF mov [ebp-6A4], ecx
00475BEA |. 8B95 70FBFFFF mov edx, [ebp-490]
00475BF0 |. 8B42 08 mov eax, [edx+8]
00475BF3 |. 3385 F0FBFFFF xor eax, [ebp-410]
00475BF9 |. 8985 60F9FFFF mov [ebp-6A0], eax
00475BFF |. 8B8D 70FBFFFF mov ecx, [ebp-490]
00475C05 |. 8B11 mov edx, [ecx]
00475C07 |. 3395 F0FBFFFF xor edx, [ebp-410]
00475C0D |. 8995 64F9FFFF mov [ebp-69C], edx
这一段看着充满了一种对称的美感,就是序列号最后生成保存的地方。在base[row]中去数字,分别和刚才计算得到的变量r1做异或操作。
第一个结果是 base[row][3] XOR r1
第二个结果是 base[row][1] XOR r1
第三个结果是 base[row][2] XOR r1
第四个结果是 base[row][0] XOR r1
我们把整个过程翻译成C语言,Transform(SN, num, strlen(SN))函数将输入序列号的前八个字符组成的字符串SN转换为对应的数值,保存于变量num中:
int sum = ModifyID(ID, length, accumulation);
if ( !Transform(SN, num, strlen(SN)) )
{
cerr<<" Illegal input of SN !"<<endl;
exit(0);
}
int r1 = sum+num;
int r2 = r1+num;
r2 += (accumulation & 0xFF);
r2 += ID[length-1];
int row = r2 % 7;
SerialNumber[0] = num;
SerialNumber[1] = base[row][3] ^ r1;
SerialNumber[2] = base[row][1] ^ r1;
SerialNumber[3] = base[row][2] ^ r1;
SerialNumber[4] = base[row][0] ^ r1;
细心的你已经发现,SerialNumber数组是存放序列号的数组,但为什么这里它有5个元素呢?在对后来的代码的分析中,它将num,和刚才四次异或操作的结果都作为序列号的组成部分。而接下来,只是要将这些数值在转换回字符串而已。程序用了sprintf函数完成这一步。具体就不做分析。
用户名和最后生成的序列号就具有下列的形式:
Username : LHZ
SerialNumbe : 12345678-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX
用户名和序列号的前八个字符是可以任意输入的,而后面的四个数字是根据用户名和序列号的前八个字符算出来的,这就是这个数据恢复软件的序列号生成算法。
在C语言下实现了算法以后,计算出一组序列号,一注册,果然成功!
前前后后,总共花了三个小时,终于解除了这道题!^_^
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)