第十六章-序列号生成算法分析-Part1(上)

Posted 大灬白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第十六章-序列号生成算法分析-Part1(上)相关的知识,希望对你有一定的参考价值。

本章,我们分析的CrackMe与之前的不同之处在于序列号是基于名称变化的,也就是说我们将讨论序列号生成算法。

1、查看CrackMe程序信息

先运行一下CrackMe程序:

有File、Help两个下拉框,选项有Exit、Register、About:

再用PEiD查看信息:

这是一个由汇编代码编写的Win32 GUI程序。
例如:Delphi是使用Pascal语言,Masm/Tasm是使用8086汇编语言编写程序的

2、使用OllyDbg加载CrackMe程序

使用OllyDbg加载CrackMe程序:

程序停在了入口处。
EP代码非常短,这是因为CrackMe程序是使用汇编语言编写出来的可执行文件。
  在使用VC++、VC、Delphi等开发工具编写程序时,除了自己编写的代码外,还有一部分启动函数是由编译器添加的,经过反编译后,代码看上去就变得非常复杂。但是如果直接使用汇编语言编写程序,汇编代码会直接变为反汇编代码。观察上图中的代码可以看到,main()直接出现在EP中,简洁又直观,充分证明了这是一个直接用汇编语言编写的程序。
接下来查看它使用了哪些常见的Windwos API函数,鼠标右键,查找->当前模块中的名称(标签):

当前模块中的名称有:

我们看到了很多上面常见的功能:
USER32.GetDlgItemTextA:获取用户输入的文本;
USER32.EndDialogKERNEL32.ExitProcess:退出、结束进程;
USER32.MessageBoxA:弹出新的对话框。

3、GetDlgItemTextA函数设置一个内存断点

接下来我们给GetDlgItemTextA函数设置一个内存断点:

运行程序,选择Register功能,输入姓名和序列号:

点击OK按钮,程序就会停在刚刚设置的断点处:

  GetDlgItemTextA函数的参数中Buffer = CRACKME.0040218E,指向的缓冲区存放着用户输入的文本,起始地址为40218E,在Buffer参数上面单击鼠标右键选择-Follow in Dump来在数据窗口中定位到用户名缓冲区:

  里面就是我们输入的用户名“Test”, 之后程序会根据我们的输入生成一个正确的序列号,来和我们输入的输入的序列号进行比较
我们继续调试,执行到调用获取输入的Name的GetDlgItemTextA函数的位置:

  我们可以看到获取输入的Name的GetDlgItemTextA函数,接下来还有一个获取输入的Name的GetDlgItemTextA函数
我们再点击运行程序,它又停在了GetDlgItemTextA函数的断点处:

  这次GetDlgItemTextA函数的参数中Buffer = CRACKME.0040217E,指向的缓冲区存放着用户输入的序列号,起始地址为40217E,在Buffer参数上面单击鼠标右键选择-Follow in Dump来在数据窗口中定位到序列号缓冲区:

  里面就是我们输入的序列号“123456”,之后我们照样执行到GetDlgItemTextA函数返回,回到自己的代码空间调用GetDlgItemTextA函数的地方:

我们继续单步调试,步过EndDialog函数之后:

Register对话框就会退出
  现在缓冲区CRACKME.0040217E里面存放了我们输入的错误序列号,从程序的角度出发,程序就会取用户输入的错误的序列号与根据名称生成的正确序列号进行比较,所以我们可以对错误序列号设置内存访问断点,看看程序的哪些地方使用了。
  我们拖选中错误的序列号,单击鼠标右键选择-Breakpoint-Memory,on access,然后运行起来:

4、读取输入的序列号的位置

之后当程序访问读取缓冲区CRACKME.0040217E里我们输入的错误序列号时,就会停下来:

我们可以看到程序尝试从缓冲区中读取错误序列号的第一个字节,并且将保存到BL寄存器中,我们按F7键单步调试:

因为bl是39不为0,所以根据test把两个bl相与之后置标志位,je不会跳转;这里je只会在bl为0、ZF=1时跳转。
之后就会继续执行

004013EA  |.  80EB 30       |sub bl,0x30

就会得到错误序列号第一个字符的值‘1’

之后执行

004013ED  |.  0FAFF8        |imul edi,eax

al在循环开始被初始化为0xA,edi在

004013DA  |.  33FF          xor edi,edi

被初始化成了0,所以这里imul edi,eax之后edi依旧是0:

之后

004013F0  |.  03FB          |add edi,ebx

相加到edi的结果为1:

然后下面

004013F2  |.  46            |inc esi            ;  CRACKME.0040217E

  ESI递增1,然后跳转到循环开始处,读取错误序列号的下一个字符,只要读取的输入的字符bl为空就会跳出循环。
  所以这个循环的逻辑就是读取输入的字符串的每个字符,把字符的ASCII值减去30得到它的16进制值,然后逐个乘以eax(0xA,就相当于16进制的进位),再累加到edi寄存器保存:

我们直接在跳出循环后的地址:

004013F5  |> \\81F7 34120000 xor edi,0x1234

  设一个断点,然后运行程序,就会跳出循环并且停在CRACKME.004013F5了,不过我们开始在缓冲区CRACKME.0040217E的错误序列号设置了一个内存断点,所以每次循环读取

004013E4  |.  8A1E          |mov bl,byte ptr ds:[esi]

的时候,程序依旧会停下来,我们为了方便可以去掉这个内存断点:

我们拖选中错误的序列号0040217E,单击鼠标右键选择-Breakpoint-删除内存断点:

5、输入的序列号的值

然后运行起来,这次就会停在CRACKME.004013F5了:

这时候由错误序列号最终得到的edi是0x0001E240,10进制形式就是123456

之后执行edi异或0x1234

004013F5  |> \\81F7 34120000 xor edi,0x1234

得到edi的值为0x0001F074,之后

004013FB  |.  8BDF          mov ebx,edi

把返回值保存在ebx寄存器中,然后返回:

之后执行

0040123D   .  83C4 04       add esp,0x4

栈顶指针加4个字节:

6、输入的序列号计算的值保存在eax寄存器

然后

00401240   .  58            pop eax

把栈顶的值0x00005738出栈,并保存在eax寄存器

  之后比较错误序列号得到的值ebx(0x0001F074)和eax(0x00005738),根据是否相等来决定跳转到弹出正确的对话框还是错误的对话框

所以我们的目的是让ebx(错误序列号得到的)和eax(正确序列号)相等,

7、程序的计算逻辑

  ebx的值(0x1F074)是由我们输入的错误序列号字符串“123456”,逐个字符转换成16进制之后累乘以0xA,再相加的结果edi(0x123456),再和0x1234异或得到的0x1F074
  所以我们想让eax和ebx相等,就也把eax异或0x1234
0x00005738 xor 0x1234 = 0x450c(17676)
  也就是说我们输入序列号“123456”就会得到ebx=0x1F074
如果我们输入“17676”,就可以得到ebx=0x5738,此时就会ebx=eax,通过验证跳转到弹出正确的对话框。
  所以用户名的“Test”对应的序列号就是“17676”:

  到这里我们就明白了程序逻辑,通过用户名“Test”计算得到eax=0x5738,再把序列号“17676”转换之后异或得到ebx=0x5738,当eax=ebx的时候,就说明这是一对正确的序列号,注册成功。

8、计算输入的用户名

  其实同样的方法,我们也可以在输入的Name字符串的缓冲区“Test”下一个内存断点,就能找到程序读取输入的Name字符串的位置,然后就可以知道由“Test”如何计算得到eax=0x5738:

运行程序,停在读取缓冲区0040218E处:

9、验证输入的用户名字符串

我们可以看到依然是从用户名缓冲区中逐个字符读取,然后

00401385  |.  84C0          |test al,al

判断字符是否为空,为空则跳出循环,不为空则继续执行

00401389  |.  3C 41         |cmp al,0x41

比较字符和0x41(‘A’)的大小,之后

0040138B  |. /72 1F         |jb short CRACKME.004013AC

JB无符号小于则跳转当CF标志位为1的时候才会跳转。也就是字符小于0x41(‘A’)就跳出循环。接着

0040138D  |.  3C 5A         |cmp al,0x5A
0040138F  |.  73 03         |jnb short CRACKME.00401394

就是大于0x5A(‘Z’)就跳出循环到CRACKME.004013D2

00401394  |> \\E8 39000000   |call CRACKME.004013D2

CRACKME.004013D2函数就是:

  当字符大于‘A’时就减小0x20,就是把它从小写字母转换成大写字母,让他处在A-Z的范围中,可以看到执行完这个循环之后,我么你输入的用户名字符串“Test”已经变成了“TEST”
所以这个循环就是判断用户名字符是否为空且转换成A-Z的范围之内的大写字符。
全部字符通过循环之后就到了:

0040139D  |.  E8 20000000   call CRACKME.004013C2

调用CRACKME.004013C2:

10、输入的用户名的值

同样的,接下来的这个循环也是从Name缓冲区中逐个读取字符给bl,之后把值累加在edi寄存器:

最后返回时edi的值为0x54+0x45+0x53+0x54=0x140
返回到

004013A2  |.  81F7 78560000 xor edi,0x5678

将edi异或0x5678,得到0x00005738

然后把edi的值保存到eax寄存器,之后跳转到返回

这就是eax寄存器的值0x5738的由来:

11、完整的用户名和序列号的比较逻辑

所以完整的用户名和序列号的比较逻辑就是:
  通过用户名“Test”转换成字符串“TEST”,然后逐个字符累加得到edi的值0x140,在异或0x5678计算得到的值保存在eax=0x5738;再把序列号“17676”转换之后,异或得到ebx=0x5738,当eax=ebx的时候,就说明这是一对正确的序列号,注册成功。

12、用户名和序列号的注册机

由此我们可以写一个注册机:
serial.c:

#include<stdio.h>
int main()
{
    //用户名
    char name[10];
    //用户名的值
    int edi = 0;
    //序列号
    int serial = 0;
    printf("请输入你的用户名:\\n");
    scanf("%s", name);
    //判断用户名字符是否为空且转换成A-Z的范围之内的大写字符
    int i = 0;
    for (; name[i]; i++) {
        //字符小于‘A’
        if (name[i] < 65) {
            printf("输入的用户名中有不是大小写字母的非法字符!\\n");
            break;
        }
        //字符大于‘Z’
        if (name[i] > 90) {
            name[i] = name[i] - 32;
        }
        edi = edi + name[i];
    }
    serial = edi ^ 0x1234 ^ 0x5678;
    printf("计算得到的序列号是:%d", serial);
    return 0;
}

运行截图:

用户名:~~~
序列号:17750
输入到软件:

注册成功!说明我们的注册机是成功的!

以上是关于第十六章-序列号生成算法分析-Part1(上)的主要内容,如果未能解决你的问题,请参考以下文章

算法导论笔记——第十六章 贪心算法

第十六章 Caché 算法与数据结构 计数排序

算法导论_第十六章_动态规划_creatshare分享会

《JAVA编程思想》学习笔记——第十六章 数组

Linux应用开发第十六章MQTT协议分析应用开发

《构建之法》第十六章阅读与思考