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

本文通过实战演示如何使用OllyDBG的内存断点功能定位并解析一个crackme程序中的注册码生成算法,详细介绍了设置内存断点的方法及程序算法分析过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

00401323|.E84C010000CALL<JMP.&USER32.GetWindowTextA>;GetWindowTextA
00401328|.E8A5000000CALLCrackHea.004013D2;关键,要按F7键跟进去
0040132D|.3BC6CMPEAX,ESI;比较
0040132F|.7542JNZSHORTCrackHea.00401373;不等则完蛋

我们重新用OllyDBG载入目标程序,F9运行来到上面代码所在的地方(你上次设的断点应该没删吧?),我们向上看看能不能找到那个ESI寄存器中最近是在哪里赋的值。哈哈,原来就在附近啊:

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

00401000>/$6A00PUSH0;pModule=NULL

现在我们就要来设内存断点了。在OllyDBG中一般我们用到的内存断点有内存访问和内存写入断点。内存访问断点就是指程序访问内存中我们指定的内存地址时中断,内存写入断点就是指程序往我们指定的内存地址中写东西时中断。更多关于断点的知识大家可以参考论坛精华7->基础知识->断点技巧->断点原理这篇Lenus兄弟写的《如何对抗硬件断点之一---调试寄存器》文章,也可以看这个帖:http://bbs.pediy.com/showthread.php?threadid=10829。根据当前我们调试的具体程序的情况,我们选用内存写入断点。还记得前面我叫大家记住的那个40339C内存地址吗?现在我们要用上了。我们先在OllyDBG的数据窗口中左键点击一下,再右击,会弹出一个如下图所示的菜单。我们选择其中的转到->表达式(也可以左键点击数据窗口后按CTR G组合键)。如下图:
http://bbs.pediy.com/upload/2006/4/image/3_3.gif
现在将会出现这样一个对话框:
http://bbs.pediy.com/upload/2006/4/image/3_4.gif
我们在上面那个编辑框中输入我们想查看内容的内存地址40339C,然后点确定按钮,数据窗口中显示如下:
http://bbs.pediy.com/upload/2006/4/image/3_5.gif
我们可以看到,40339C地址开始处的这段内存里面还没有内容。我们现在在40339C地址处后面的HEX数据或ASCII栏中按住左键往后拖放,选择一段。内存断点的特性就是不管你选几个字节,OllyDBG都会分配4096字节的内存区。这里我就选从40339C地址处开始的四个字节,主要是为了让大家提前了解一下硬件断点的设法,因为硬件断点最多只能选4个字节。选中部分会显示为灰色。选好以后松开鼠标左键,在我们选中的灰色部分上右击:
http://bbs.pediy.com/upload/2006/4/image/3_6.gif
经过上面的操作,我们的内存断点就设好了(这里还有个要注意的地方:内存断点只在当前调试的进程中有效,就是说你如果重新载入程序的话内存断点就自动删除了。且内存断点每一时刻只能有一个。就是说你不能像按F2键那样同时设置多个断点)。现在按F9键让程序运行,呵,OllyDBG中断了!

7C932F398808MOVBYTEPTRDS:[EAX],CL;这就是我们第一次断下来的地方
7C932F3B40INCEAX
7C932F3C4FDECEDI
7C932F3D4EDECESI
7C932F3E^75CBJNZSHORTntdll.7C932F0B
7C932F408B4D10MOVECX,DWORDPTRSS:[EBP 10]

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

00401431|.8D359C334000LEAESI,DWORDPTRDS:[40339C];ALT F9返回后来到的位置
00401437|.0FB60DEC334000MOVZXECX,BYTEPTRDS:[4033EC]
0040143E|.33FFXOREDI,EDI

我们把反汇编窗口往上翻翻,呵,原来就在我们上一篇分析的代码下面啊?

现在我们在0040140C地址处那条指令上按F2设置一个断点,现在我们按CTR F2组合键重新载入程序,载入后按F9键运行,我们将会中断在我们刚才在0040140C地址下的那个断点处:

0040140C/$60PUSHAD
0040140D|.6A00PUSH0;/RootPathName=NULL
0040140F|.E8B4000000CALL<JMP.&KERNEL32.GetDriveTypeA>;/GetDriveTypeA
00401414|.A2EC334000MOVBYTEPTRDS:[4033EC],AL;磁盘类型参数送内存地址4033EC
00401419|.6A00PUSH0;/pFileSystemNameSize=NULL
0040141B|.6A00PUSH0;|pFileSystemNameBuffer=NULL
0040141D|.6A00PUSH0;|pFileSystemFlags=NULL
0040141F|.6A00PUSH0;|pMaxFilenameLength=NULL
00401421|.6A00PUSH0;|pVolumeSerialNumber=NULL
00401423|.6A0BPUSH0B;|MaxVolumeNameSize=B(11.)
00401425|.689C334000PUSHCrackHea.0040339C;|VolumeNameBuffer=CrackHea.0040339C
0040142A|.6A00PUSH0;|RootPathName=NULL
0040142C|.E8A3000000CALL<JMP.&KERNEL32.GetVolumeInformationA>;/GetVolumeInformationA
00401431|.8D359C334000LEAESI,DWORDPTRDS:[40339C];把crackme程序所在分区的卷标名称送到ESI
00401437|.0FB60DEC334000MOVZXECX,BYTEPTRDS:[4033EC];磁盘类型参数送ECX
0040143E|.33FFXOREDI,EDI;把EDI清零
00401440|>8BC1MOVEAX,ECX;磁盘类型参数送EAX
00401442|.8B1EMOVEBX,DWORDPTRDS:[ESI];把卷标名作为数值送到EBX
00401444|.F7E3MULEBX;循环递减取磁盘类型参数值与卷标名值相乘
00401446|.03F8ADDEDI,EAX;每次计算结果再加上上次计算结果保存在EDI中
00401448|.49DECECX;把磁盘类型参数作为循环次数,依次递减
00401449|.83F900CMPECX,0;判断是否计算完
0040144C|.^75F2JNZSHORTCrackHea.00401440;没完继续
0040144E|.893D9C334000MOVDWORDPTRDS:[40339C],EDI;把计算后值送到内存地址40339C,这就是我们后来在ESI中看到的值
00401454|.61POPAD
00401455/.C3RETN

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

0040339C47414D45000000000000000000000000GAME............

大家可以看出来内存地址40339CH到40339FH分别按顺序存放的是47414D45。
如下图:
http://bbs.pediy.com/upload/2006/4/image/3_10.gif_805.gif
系统存储的原则为“高高低低”,即低字节存放在地址较低的字节单元中,高字节存放在地址较高的字节单元中。比如一个字由两个字节组成,像这样:1234,这里的高字节就是12,低字节就是34。上面的那条指令MOVEBX,DWORDPTRDS:[ESI]等同于MOVEBX,DWORDPTRDS:[40339C]。注意这里是DWORD,即“双字”,由4个连续的字节构成。而取地址为40339C的双字单元中的内容时,我们应该得到的是“454D4147”,即由高字节到低字节顺序的值。因此经过MOVEBX,DWORDPTRDS:[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|.81F653757A79XORESI,797A7553;把ESI中的值与797A7553H异或

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

00401419|.6A00PUSH0;/pFileSystemNameSize=NULL
0040141B|.6A00PUSH0;|pFileSystemNameBuffer=NULL
0040141D|.6A00PUSH0;|pFileSystemFlags=NULL
0040141F|.6A00PUSH0;|pMaxFilenameLength=NULL
00401421|.6A00PUSH0;|pVolumeSerialNumber=NULL
00401423|.6A0BPUSH0B;|MaxVolumeNameSize=B(11.)
00401425|.689C334000PUSHCrackHea.0040339C;|VolumeNameBuffer=CrackHea.0040339C
0040142A|.6A00PUSH0;|RootPathName=NULL
0040142C|.E8A3000000CALL<JMP.&KERNEL32.GetVolumeInformationA>;/GetVolumeInformationA

把上面代码后的OllyDBG自动添加的注释与MSDN中的函数原型比较一下:
BOOLGetVolumeInformation(
LPCTSTRlpRootPathName,//addressofrootdirectoryofthefilesystem
LPTSTRlpVolumeNameBuffer,//addressofnameofthevolume
DWORDnVolumeNameSize,//lengthoflpVolumeNameBuffer
LPDWORDlpVolumeSerialNumber,//addressofvolumeserialnumber
LPDWORDlpMaximumComponentLength,//addressofsystem'smaximumfilenamelength
LPDWORDlpFileSystemFlags,//addressoffilesystemflags
LPTSTRlpFileSystemNameBuffer,//addressofnameoffilesystem
DWORDnFileSystemNameSize//lengthoflpFileSystemNameBuffer
);

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

0040140F|.E8B4000000CALL<JMP.&KERNEL32.GetDriveTypeA>;/GetDriveTypeA
00401414|.A2EC334000MOVBYTEPTRDS:[4033EC],AL;磁盘类型参数送内存地址4033EC

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

ValueMeaning返回在EAX中的值
DRIVE_UNKNOWNThedrivetypecannotbedetermined.0
DRIVE_NO_ROOT_DIRTherootdirectorydoesnotexist.1
DRIVE_REMOVABLEThediskcanberemovedfromthedrive.2
DRIVE_FIXEDThediskcannotberemovedfromthedrive.3
DRIVE_REMOTEThedriveisaremote(network)drive.4
DRIVE_CDROMThedriveisaCD-ROMdrive.5
DRIVE_RAMDISKThedriveisaRAMdisk.6

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值