OllyDBG 入门系列(四)-内存断点
我们现在知道?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?兄指出本文中的错误!
--------------------------------------------
【版权声明】?本文纯属技术交流,?转载请注明作者并保持文章的完整,?谢谢!? <!-- google_ad_section_end -->