首页
社区
课程
招聘
[原创]OllyDBG 入门系列(四)-内存断点
发表于: 2006-2-15 23:22 468784

[原创]OllyDBG 入门系列(四)-内存断点

2006-2-15 23:22
468784

OllyDBG 入门系列(四)-内存断点

作者:CCDebuger

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

00401323 |. E8 4C010000         CALL <JMP.&USER32.GetWindowTextA>           ; 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 <JMP.&KERNEL32.GetDriveTypeA>          ; \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 <JMP.&KERNEL32.GetVolumeInformationA>  ; \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 <JMP.&KERNEL32.GetVolumeInformationA>  ; \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 system's 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 <JMP.&KERNEL32.GetDriveTypeA>          ; \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 兄指出本文中的错误!

--------------------------------------------------------------------------------
【版权声明】 本文纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 12
支持
分享
最新回复 (490)
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
楼主,谢谢你啦,希望你的精华不断.这几天天天就看你的资料出现.
下载,学习中.
建议坛主设立职称评定委员会,让你晋升高级职称.行不?
2006-2-15 23:43
0
雪    币: 215
活跃值: (40)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
确实是篇篇精华
2006-2-15 23:46
0
雪    币: 251
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
大家说说,不顶行吗?

楼主辛苦了!!!
2006-2-15 23:58
0
雪    币: 2506
活跃值: (1030)
能力值: (RANK:990 )
在线值:
发帖
回帖
粉丝
5
最初由 无梦徽州 发布
楼主,谢谢你啦,希望你的精华不断.这几天天天就看你的资料出现.
下载,学习中.
建议坛主设立职称评定委员会,让你晋升高级职称.行不?

呵呵,高级职称就不要了 在此谢过各位的支持了。
2006-2-16 00:00
0
雪    币: 110
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
确实值得顶的,希望继续下去,DDDDDDDDDDDDDDD
2006-2-16 00:07
0
雪    币: 222
活跃值: (40)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
7
楼主辛苦了!顶
2006-2-16 00:14
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
等待5的到来!
2006-2-16 01:30
0
雪    币: 3
活跃值: (394)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
辛苦了,真的感谢
2006-2-16 02:06
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
真的是好文章呀!
收获很大!
不过我想知道,OD有没有记录功能呀?就是把一段代码运行时的寄存器以及堆栈中的数据都记录到一个文件中
我有一个程序,用OD载入可以,一中断程序就“没有响应了”!
2006-2-16 07:23
0
雪    币: 442
活跃值: (1216)
能力值: ( LV12,RANK:1130 )
在线值:
发帖
回帖
粉丝
11
最初由 无梦徽州 发布
楼主,谢谢你啦,希望你的精华不断.这几天天天就看你的资料出现.
下载,学习中.
建议坛主设立职称评定委员会,让你晋升高级职称.行不?


再多灌些水就达到标准了,支持CCDebuger
2006-2-16 08:12
0
雪    币: 242
活跃值: (135)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
12
"系统存储的原则为“高高低低”,即低字节存放在地址较低的字节单元中,高字节存放在地址较高的字节单元中。如下图"
“47414D45”按“高高低低”的原则在存储器中的存放的情况如上图所示,即高字节存放在高位地址,低字节存放在低位地址。因此经过 MOV EBX,DWORD PTR DS:[ESI] 这条指令,就是把从地址 40339C 开始处的值送到 EBX,所以我们得到了“454D4147”。

图和解释有点疑问

高高低低:x86采用little endian方式,即”小端结束“或者”小尾“,它的意义在于指出数据在内存中存储的次序,会在寄存器和内存交换数据以及对内存数据读取、运算时体现出来

所谓小端结束,就是存储常数或寄存器时,低字节存储在低地址,读取的时候内存高地址字节放在前面(高位)

比如
内存地址   存储数据
0          0x12      
1          34
2          56
3          78

如果有一条指令 MOV EBX,DWORD PTR DS:[00000000]
此时ebx=78563412

以下内容来自Pconline:

“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

对你的工作表示敬意!!

2006-2-16 09:03
0
雪    币: 273
活跃值: (250)
能力值: ( LV9,RANK:410 )
在线值:
发帖
回帖
粉丝
13
现在很少有这么系统的教程了!。。经典哦。。可以系统学习哦!。。支持 CCDebuger 兄!!
2006-2-16 09:15
0
雪    币: 2506
活跃值: (1030)
能力值: (RANK:990 )
在线值:
发帖
回帖
粉丝
14
最初由 mirrormask 发布
"系统存储的原则为“高高低低”,即低字节存放在地址较低的字节单元中,高字节存放在地址较高的字节单元中。如下图"
“47414D45”按“高高低低”的原则在存储器中的存放的情况如上图所示,即高字节存放在高位地址,低字节存放在低位地址。因此经过 MOV EBX,DWORD PTR DS:[ESI] 这条指令,就是把从地址 40339C 开始处的值送到 EBX,所以我们得到了“454D4147”。

图和解释有点疑问

........

实在抱歉,一时疏忽,弄错了。感谢指正!等会我修改一下,否则要误人子弟了
2006-2-16 09:21
0
雪    币: 603
活跃值: (617)
能力值: ( LV12,RANK:660 )
在线值:
发帖
回帖
粉丝
15
好详细,CCDebuger兄辛苦了!
2006-2-16 09:24
0
雪    币: 333
活跃值: (40)
能力值: ( LV9,RANK:730 )
在线值:
发帖
回帖
粉丝
16
经典教程,功德无量!
2006-2-16 09:30
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
有时间也应该写点东西为论坛做点贡献
不能总是潜水啊
虽然咱现在功力尚浅 努力学习中
向论坛的高手们致敬并学习
2006-2-16 10:05
0
雪    币: 212
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
18
好文,顶一下!
2006-2-16 10:21
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
经典教程
2006-2-16 11:10
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
我顶。。。。
2006-2-16 12:35
0
雪    币: 313
活跃值: (250)
能力值: ( LV9,RANK:650 )
在线值:
发帖
回帖
粉丝
21
一篇一篇又一篇,楼主好精力。希望楼主等出完之后,把所有的文件排版打个包上来,不就更好。
2006-2-16 12:39
0
雪    币: 2506
活跃值: (1030)
能力值: (RANK:990 )
在线值:
发帖
回帖
粉丝
22
最初由 hbqjxhw 发布
一篇一篇又一篇,楼主好精力。希望楼主等出完之后,把所有的文件排版打个包上来,不就更好。

呵呵,那有那么好精力,主要是马上就要开始忙了,趁有空先写点吧。现在是骑虎难下了。等全部写完再把写的东西打包吧。
2006-2-16 12:46
0
雪    币: 108
活跃值: (42)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
支持,.,,
2006-2-16 12:50
0
雪    币: 207
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
24
确实值得顶的,希望继续下去,DDDDDDDDDDDDDDD
2006-2-16 13:06
0
雪    币: 207
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
辛苦,谢谢了
2006-2-16 13:21
0
游客
登录 | 注册 方可回帖
返回
//