当前位置:首页 > 加密解密技术 > 调试工具使用 > OllyDbg调试器 > 详细内容
OllyDBG 入门系列(四)-内存断点
发布时间:2009-8-9  阅读次数:6044  字体大小: 【】 【】【
OllyDBG  入门系列(四)-内存断点

作者:CCDebuger

还记得上一篇《OllyDBG  入门系列(三)-函数参考》中的内容吗?在那篇文章中我们分析后发现一个  ESI  寄存器值不知是从什么地方产生的,要弄清这个问题必须要找到生成这个  ESI  值的计算部分。今天我们的任务就是使用  OllyDBG  的内存断点功能找到这个地方,搞清楚这个值是如何算出来的。这次分析的目标程序还是上一篇的那个  crackme,附件我就不再上传了,用上篇中的附件就可以了。下面我们开始:
还记得我们上篇中所说的关键代码的地方吗?温习一下:

00401323  |.  E8  4C010000                  CALL                        ;  GetWindowTextA
00401328  |.  E8  A5000000                  CALL  CrackHea.004013D2                                            ;  关键,要按F7键跟进去
0040132D  |.  3BC6                                CMP  EAX,ESI                                                                  ;  比较
0040132F  |.  75  42                              JNZ  SHORT  CrackHea.00401373                                  ;  不等则完蛋

我们重新用  OllyDBG  载入目标程序,F9运行来到上面代码所在的地方(你上次设的断点应该没删吧?),我们向上看看能不能找到那个  ESI  寄存器中最近是在哪里赋的值。哈哈,原来就在附近啊:
  
我们现在知道  ESI  寄存器的值是从内存地址  40339C  中送过来的,那内存地址  40339C  中的数据是什么时候产生的呢?大家注意,我这里信息窗口中显示的是  DS:[0040339C]=9FCF87AA,你那可能是  DS:[0040339C]=XXXXXXXX,这里的  XXXXXXXX  表示的是其它的值,就是说与我这里显示的  9FCF87AA  不一样。我们按上图的操作在数据窗口中看一下:
  
从上图我们可以看出内存地址  40339C  处的值已经有了,说明早就算过了。现在怎么办呢?我们考虑一下,看情况程序是把这个值算出来以后写在这个内存地址,那我们要是能让  OllyDBG  在程序开始往这个内存地址写东西的时候中断下来,不就有可能知道目标程序是怎么算出这个值的吗?说干就干,我们在  OllyDBG  的菜单上点  调试->重新开始,或者按  CTR+F2  组合键(还可以点击工具栏上的那个有两个实心左箭头的图标)来重新载入程序。这时会跳出一个“进程仍处于激活状态”的对话框(我们可以在在调试选项的安全标签下把“终止活动进程时警告”这条前面的勾去掉,这样下次就不会出现这个对话框了),问我们是否要终止进程。这里我们选“是”,程序被重新载入,我们停在下面这一句上:

00401000  >/$  6A  00                            PUSH  0                                                                            ;  pModule  =  NULL

现在我们就要来设内存断点了。在  OllyDBG  中一般我们用到的内存断点有内存访问和内存写入断点。内存访问断点就是指程序访问内存中我们指定的内存地址时中断,内存写入断点就是指程序往我们指定的内存地址中写东西时中断。更多关于断点的知识大家可以参考  论坛精华7->基础知识->断点技巧->断点原理  这篇  Lenus  兄弟写的《如何对抗硬件断点之一  ---  调试寄存器》文章,也可以看这个帖:http://bbs.pediy.com/showthread.php?threadid=10829。根据当前我们调试的具体程序的情况,我们选用内存写入断点。还记得前面我叫大家记住的那个  40339C  内存地址吗?现在我们要用上了。我们先在  OllyDBG  的数据窗口中左键点击一下,再右击,会弹出一个如下图所示的菜单。我们选择其中的转到->表达式(也可以左键点击数据窗口后按  CTR+G  组合键)。如下图:
  
现在将会出现这样一个对话框:

我们在上面那个编辑框中输入我们想查看内容的内存地址  40339C,然后点确定按钮,数据窗口中显示如下:
  
我们可以看到,40339C  地址开始处的这段内存里面还没有内容。我们现在在  40339C  地址处后面的  HEX  数据或  ASCII  栏中按住左键往后拖放,选择一段。内存断点的特性就是不管你选几个字节,OllyDBG  都会分配  4096  字节的内存区。这里我就选从  40339C  地址处开始的四个字节,主要是为了让大家提前了解一下硬件断点的设法,因为硬件断点最多只能选  4  个字节。选中部分会显示为灰色。选好以后松开鼠标左键,在我们选中的灰色部分上右击:
  
经过上面的操作,我们的内存断点就设好了(这里还有个要注意的地方:内存断点只在当前调试的进程中有效,就是说你如果重新载入程序的话内存断点就自动删除了。且内存断点每一时刻只能有一个。就是说你不能像按  F2  键那样同时设置多个断点)。现在按  F9  键让程序运行,呵,OllyDBG  中断了!

7C932F39  8808                                      MOV  BYTE  PTR  DS:[EAX],CL                                        ;  这就是我们第一次断下来的地方
7C932F3B  40                                          INC  EAX
7C932F3C  4F                                          DEC  EDI
7C932F3D  4E                                          DEC  ESI
7C932F3E  ^  75  CB                                JNZ  SHORT  ntdll.7C932F0B
7C932F40  8B4D  10                                MOV  ECX,DWORD  PTR  SS:[EBP+10]

上面就是我们中断后反汇编窗口中的代码。如果你是其它系统,如  Win98  的话,可能会有所不同。没关系,这里不是关键。我们看一下领空,原来是在  ntdll.dll  内。系统领空,我们现在要考虑返回到程序领空。返回前我们看一下数据窗口:
  
现在我们转到反汇编窗口,右击鼠标,在弹出菜单上选择断点->删除内存断点,这样内存断点就被删除了。

现在我们来按一下  ALT+F9  组合键,我们来到下面的代码:

00401431  |.  8D35  9C334000            LEA  ESI,DWORD  PTR  DS:[40339C]                              ;  ALT+F9返回后来到的位置
00401437  |.  0FB60D  EC334000        MOVZX  ECX,BYTE  PTR  DS:[4033EC]
0040143E  |.  33FF                              XOR  EDI,EDI

我们把反汇编窗口往上翻翻,呵,原来就在我们上一篇分析的代码下面啊?
  
现在我们在  0040140C  地址处那条指令上按  F2  设置一个断点,现在我们按    CTR+F2  组合键重新载入程序,载入后按  F9  键运行,我们将会中断在我们刚才在  0040140C  地址下的那个断点处:

0040140C  /$  60                                  PUSHAD
0040140D  |.  6A  00                            PUSH  0                                                                            ;  /RootPathName  =  NULL
0040140F  |.  E8  B4000000                CALL                      ;  \GetDriveTypeA
00401414  |.  A2  EC334000                MOV  BYTE  PTR  DS:[4033EC],AL                                  ;  磁盘类型参数送内存地址4033EC
00401419  |.  6A  00                            PUSH  0                                                                            ;  /pFileSystemNameSize  =  NULL
0040141B  |.  6A  00                            PUSH  0                                                                            ;  |pFileSystemNameBuffer  =  NULL
0040141D  |.  6A  00                            PUSH  0                                                                            ;  |pFileSystemFlags  =  NULL
0040141F  |.  6A  00                            PUSH  0                                                                            ;  |pMaxFilenameLength  =  NULL
00401421  |.  6A  00                            PUSH  0                                                                            ;  |pVolumeSerialNumber  =  NULL
00401423  |.  6A  0B                            PUSH  0B                                                                          ;  |MaxVolumeNameSize  =  B  (11.)
00401425  |.  68  9C334000                PUSH  CrackHea.0040339C                                            ;  |VolumeNameBuffer  =  CrackHea.0040339C
0040142A  |.  6A  00                            PUSH  0                                                                            ;  |RootPathName  =  NULL
0040142C  |.  E8  A3000000                CALL      ;  \GetVolumeInformationA
00401431  |.  8D35  9C334000            LEA  ESI,DWORD  PTR  DS:[40339C]                              ;  把crackme程序所在分区的卷标名称送到ESI
00401437  |.  0FB60D  EC334000        MOVZX  ECX,BYTE  PTR  DS:[4033EC]                            ;  磁盘类型参数送ECX
0040143E  |.  33FF                              XOR  EDI,EDI                                                                  ;  把EDI清零
00401440  |>  8BC1                              MOV  EAX,ECX                                                                  ;  磁盘类型参数送EAX
00401442  |.  8B1E                              MOV  EBX,DWORD  PTR  DS:[ESI]                                    ;  把卷标名作为数值送到  EBX
00401444  |.  F7E3                              MUL  EBX                                                                          ;  循环递减取磁盘类型参数值与卷标名值相乘
00401446  |.  03F8                              ADD  EDI,EAX                                                                  ;  每次计算结果再加上上次计算结果保存在EDI中
00401448  |.  49                                  DEC  ECX                                                                          ;  把磁盘类型参数作为循环次数,依次递减
00401449  |.  83F9  00                        CMP  ECX,0                                                                      ;  判断是否计算完
0040144C  |.^  75  F2                          JNZ  SHORT  CrackHea.00401440                                  ;  没完继续
0040144E  |.  893D  9C334000            MOV  DWORD  PTR  DS:[40339C],EDI                              ;  把计算后值送到内存地址40339C,这就是我们后来在ESI中看到的值
00401454  |.  61                                  POPAD
00401455  \.  C3                                  RETN

通过上面的分析,我们知道基本算法是这样的:先用  GetDriveTypeA  函数获取磁盘类型参数,再用  GetVolumeInformationA  函数获取这个  crackme  程序所在分区的卷标。如我把这个  Crackme  程序放在  F:\OD教程\crackhead\  目录下,而我  F  盘设置的卷标是  GAME,则这里获取的就是  GAME,ASCII  码为“47414D45”。但我们发现一个问题:假如原来我们在数据窗口中看到的地址  40339C  处的  16  进制代码是“47414D45”,即“GAME”,但经过地址  00401442  处的那条  MOV  EBX,DWORD  PTR  DS:[ESI]  指令后,我们却发现  EBX  中的值是“454D4147”,正好把我们上面那个“47414D45”反过来了。为什么会这样呢?如果大家对  x86系列  CPU  的存储方式了解的话,这里就容易理解了。我们知道“GAME”有四个字节,即  ASCII  码为“47414D45”。我们看一下数据窗口中的情况:

0040339C          47  41  4D  45  00  00  00  00  00  00  00  00  00  00  00  00          GAME............

大家可以看出来内存地址  40339CH  到  40339FH  分别按顺序存放的是  47  41  4D  45。
如下图:
    
系统存储的原则为“高高低低”,即低字节存放在地址较低的字节单元中,高字节存放在地址较高的字节单元中。比如一个字由两个字节组成,像这样:12  34  ,这里的高字节就是  12  ,低字节就是  34。上面的那条指令  MOV  EBX,DWORD  PTR  DS:[ESI]  等同于  MOV  EBX,DWORD  PTR  DS:[40339C]。注意这里是  DWORD,即“双字”,由  4  个连续的字节构成。而取地址为  40339C  的双字单元中的内容时,我们应该得到的是“454D4147”,即由高字节到低字节顺序的值。因此经过  MOV  EBX,DWORD  PTR  DS:[ESI]  这条指令,就是把从地址  40339C  开始处的值送到  EBX,所以我们得到了“454D4147”。好了,这里弄清楚了,我们再接着谈这个程序的算法。前面我们已经说了取磁盘类型参数做循环次数,再取卷标值  ASCII  码的逆序作为数值,有了这两个值就开始计算了。现在我们把磁盘类型值作为  n,卷标值  ASCII  码的逆序数值作为  a,最后得出的结果作为  b,有这样的计算过程:
第一次:b  =  a  *  n
第二次:b  =  a  *  (n  -  1)  +  b
第三次:b  =  a  *  (n  -  2)  +  b

第  n  次:b  =  a  *  1  +  b
可得出公式为  b  =  a  *  [n  +  (n  -  1)  +  (n  -  2)  +  …  +  1]  =  a  *  [n  *  (n  -  1)  /  2]
还记得上一篇我们的分析吗?看这一句:

00401405  |.  81F6  53757A79          XOR  ESI,797A7553                                                        ;  把ESI中的值与797A7553H异或

这里算出来的  b  最后还要和  797A7553H  异或一下才是真正的注册码。只要你对编程有所了解,这个注册机就很好写了。如果用汇编来写这个注册机的话就更简单了,很多内容可以直接照抄。
到此已经差不多了,最后还有几个东西也说一下吧:
1、上面用到了两个  API  函数,一个是  GetDriveTypeA,还有一个是  GetVolumeInformationA,关于这两个函数的具体用法我就不多说了,大家可以查一下  MSDN。这里只要大家注意函数参数传递的次序,即调用约定。先看一下这里:

00401419  |.  6A  00                            PUSH  0                                                                            ;  /pFileSystemNameSize  =  NULL
0040141B  |.  6A  00                            PUSH  0                                                                            ;  |pFileSystemNameBuffer  =  NULL
0040141D  |.  6A  00                            PUSH  0                                                                            ;  |pFileSystemFlags  =  NULL
0040141F  |.  6A  00                            PUSH  0                                                                            ;  |pMaxFilenameLength  =  NULL
00401421  |.  6A  00                            PUSH  0                                                                            ;  |pVolumeSerialNumber  =  NULL
00401423  |.  6A  0B                            PUSH  0B                                                                          ;  |MaxVolumeNameSize  =  B  (11.)
00401425  |.  68  9C334000                PUSH  CrackHea.0040339C                                            ;  |VolumeNameBuffer  =  CrackHea.0040339C
0040142A  |.  6A  00                            PUSH  0                                                                            ;  |RootPathName  =  NULL
0040142C  |.  E8  A3000000                CALL      ;  \GetVolumeInformationA

把上面代码后的  OllyDBG  自动添加的注释与  MSDN  中的函数原型比较一下:
BOOL  GetVolumeInformation(
LPCTSTR  lpRootPathName,                          //  address  of  root  directory  of  the  file  system
LPTSTR  lpVolumeNameBuffer,                    //  address  of  name  of  the  volume
DWORD  nVolumeNameSize,                            //  length  of  lpVolumeNameBuffer
LPDWORD  lpVolumeSerialNumber,              //  address  of  volume  serial  number
LPDWORD  lpMaximumComponentLength,      //  address  of  systems  maximum  filename  length
LPDWORD  lpFileSystemFlags,                    //  address  of  file  system  flags
LPTSTR  lpFileSystemNameBuffer,            //  address  of  name  of  file  system
DWORD  nFileSystemNameSize                      //  length  of  lpFileSystemNameBuffer
);

大家应该看出来点什么了吧?函数调用是先把最后一个参数压栈,参数压栈顺序是从后往前。这就是一般比较常见的  stdcall  调用约定。
2、我在前面的  00401414  地址处的那条  MOV  BYTE  PTR  DS:[4033EC],AL  指令后加的注释是“磁盘类型参数送内存地址4033EC”。为什么这样写?大家把前一句和这一句合起来看一下:

0040140F  |.  E8  B4000000                CALL                      ;  \GetDriveTypeA
00401414  |.  A2  EC334000                MOV  BYTE  PTR  DS:[4033EC],AL                                  ;  磁盘类型参数送内存地址4033EC

地址  0040140F  处的那条指令是调用  GetDriveTypeA  函数,一般函数调用后的返回值都保存在  EAX  中,所以地址  00401414  处的那一句  MOV  BYTE  PTR  DS:[4033EC],AL  就是传递返回值。查一下  MSDN  可以知道  GetDriveTypeA  函数的返回值有这几个:

Value                                          Meaning                                                                                返回在EAX中的值
DRIVE_UNKNOWN                          The  drive  type  cannot  be  determined.                              0
DRIVE_NO_ROOT_DIR                  The  root  directory  does  not  exist.                                  1
DRIVE_REMOVABLE                      The  disk  can  be  removed  from  the  drive.                        2
DRIVE_FIXED                              The  disk  cannot  be  removed  from  the  drive.                  3
DRIVE_REMOTE                            The  drive  is  a  remote  (network)  drive.                          4
DRIVE_CDROM                              The  drive  is  a  CD-ROM  drive.                                              5
DRIVE_RAMDISK                          The  drive  is  a  RAM  disk.                                                      6

上面那个“返回在EAX中的值”是我加的,我这里返回的是  3,即磁盘不可从驱动器上删除。
3、通过分析这个程序的算法,我们发现这个注册算法是有漏洞的。如果我的分区没有卷标的话,则卷标值为  0,最后的注册码就是  797A7553H,即十进制  2038068563。而如果你的卷标和我一样,且磁盘类型一样的话,注册码也会一样,并不能真正做到一机一码。

感谢  mirrormask  兄指出本文中的错误!

--------------------------------------------------------------------------------
【版权声明】  本文纯属技术交流,  转载请注明作者并保持文章的完整,  谢谢!
我要评论
  • 匿名发表
  • [添加到收藏夹]
  • 发表评论:(匿名发表无需登录,已登录用户可直接发表。) 登录状态:未登录
最新评论
所有评论[0]
    暂无已审核评论!
 


设为首页 | 加入收藏 | 意见建议 | 友情链接 | 版权声明 | 管理登陆 | 编程论坛 | 给我留言

声明:本网站部分稿件来源的所有文字、图片和音视频稿件,来自互联网,若侵犯您的权利,请来信告知,我们将在第一时间内删除!


Copyright 2009-2015 redrose ( wgbcw.cn ) All rights reserved ICP备案编号:滇ICP备09007156号 Dict.cn


点击这里给我发消息
点击这里给我发消息
安全联盟站长平台