Loading... 本章中我将会介绍OllyDbg的使用。Olly有许多的功能,唯一学好它们的方式是实践和练习。也就是说,本教程也只是给你一个简单的概述。此教程不会涉及额外的内容,后面会进行重点讨论。到最后,你应该会比较好的掌握Olly。本章包含了一些文件。你能够下载那些文件,以及可以在这里下载到次教程的PDF版本。它们包括一个我们将在Olly中用到的二进制文件、一个Olly备忘单、我使用的外观上有些不同的Olly以及一个新的ini文件。你可以用这个ini文件替换掉Olly默认的ini,可以给新人提供一些帮助(感谢伟大的Lena151做的这些)。你可以从这里直接下载或者从教程页面下载。如果你更愿意用原版的Olly,你可以从这里下载。 # 一、载入应用 第一步是将目的二进制文件载入Olly。你可以将二进制文件拖放到Olly的反汇编窗口,或者点击顶部工具栏中的载入图标选择目的文件。我们这里载入“FirstProgram.exe”,可以从本网站下载。Olly会进行分析(Olly的底部状态栏会显示分析进程)然后停在程序的入口点(EP): ![9cb9ebd119851d35b25665c66738eafd.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1644893846.png) 需要注意的第一件事是EP的地址是401000,就是图片中的第一列。这是可执行文件的一个相当标准的起点(该可执行文件至少没有加过壳或混淆过)。如果你的看起来不太一样,并且Olly没有停在401000,你可以尝试点击Appearance菜单,然后选择debugging options,点击“Events”标签,并且确保“WinMain(if location is known)”被勾选上。然后重启应用。 让我们给“FirstProgram.exe”的内存空间占用情况来张快照。点击“Me”图标(如果你使用的是不同版本的Olly的话应该是“M”): ![ad62d59047154781eb36279a5bc48e6d.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/2996009319.png) 如果你看地址那一列,你会看到401000那行包含有大小1000、名称“FirstPro”(FirstProgram的简写形式)、区块名“.text”、包含里是“SFX,code”。随着学习进度的展开,我们就会知道exe文件中有不同的区块,包含不同的数据类型。该区块中是程序的“代码”。它有1000字节大,从内存的401000开始。 在这个的下面你会看到FirstProgram的其他区块。其中.rdata区包含着数据,其导入地址是402000,地址403000的.data区中什么都没有。最后的那个.rsrc区中存有资源(比如对话框、图片、文本等)。要注意的是这些区可以叫任何名字,这个完全依赖于程序员。你可能会问为什么.data区是空的。好吧,它事实上就是那样。它一般包含全局变量和随机数据。Olly只是选择了不显示,因为它确实不知道那里存储了哪种数据。区段的顶部是一个叫做“Pe Header”的区块。这是一个非常重要的区,一个我们会在将来文章中深入探讨的区。不过对于目前来说,我们只需要知道它对于Windows就像一本指令手册一样,用来按步将文件载入内存,程序运行需要多少空间,还有其他某些事情等。它在大约所有exe的头部(DLL也是一样)。 如果你继续往下看,你可以看到不只是FirstProgram程序,还有其他的文件。我们看到有comctl32, imm32,gdi32,kernel32等。这些DLL是我们程序运行所需要的。DLL是函数的集合,我们的程序能够调用那些Windows已经提供的(或者其他程序员提供的)函数。比如打开对话框、比较字符串、创建窗口以及类似的功能。统称为Windows API。程序使用这些函数的原因是,假如我们写每一个用到的函数,仅仅显示一个消息框就需要数千行的代码。然而,Windows已经提供了像CreateWindow这样的函数来为我们做这些工作。对于程序员来说这使得编程要简单的多。 你或许会问这些DLL是如何进入我们的地址空间的,windows是怎么知道哪一个是我们需要的。好吧,这些信息是存储在上列出的PE Header中的。当Windows将我们的exe载入内存时,它会检查头并找出DLL的名字,以及每个DLL中我们程序需要的函数,然后将这些函数载入我们的程序内存空间,以便于我们的程序调用它们。每个程序被载入内存时,它所需要的DLL也会被载入它的内存空间。可以想象得到,当有好几个程序当前都需要被载入内存并且都需要某个特定的DLL时,那么有些DLL就有可能被载入内存好几次。如果你需要准确的知道我们的程序调用了哪些函数,你可以右键点击Olly 的反汇编窗口,选择“Search for”——>““All Intermodular Calls”。会显示如下图: ![1f26c5f4254663fca257e1a6091b8ace.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/658034870.png) 这有点惊奇,不过这个列表非常的小。通常,对于一个商业产品来说,需要数百或数千函数。不过因为我们的程序太简单了,它需要的不是很多。你想想我们的程序干了什么,看起来好像是那么多的函数只完成了如此简单的功能!欢迎来到Windows。该窗口首先显示了DLL的名字,紧跟着的是函数的名字。比如,User32.LoadIconA是在DLL User32中,函数名字是LoadIconA。该函数通常用来载入窗口左上角的图标。 下一步,我们搜索下程序中的所有字符串。右键点击反汇编窗口,选择“SearchFor”-> “All Referenced Text Strings”: ![88a9366b4aca99b40e7779db9dd91951.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1844171913.png) 该窗口显示了我们程序中所有能找到的字符串。因为程序非常简单,所以这里只有一点。大多数的程序如果没有加壳或混淆的话,都有多得多的字符串(有时能达到十万)。这种情况下,你有可能一个也看不到!加壳工具这样做的原因是逆向工程师(至少新人是这样)严重依赖字符串来查找重要的函数。而删除了字符串后就会难的多。想象一下,如果你搜索字符串然后看到了“Congratulations! You entered the correct serial(恭喜!你输入了正确的序列号)”会怎么样?嗯,这对于逆向来说是巨大的帮助(我们会一次又一次的看到这个)。另外,双击其中的字符串,你会来到反汇编窗口中使用该字符串的指令那。这是一个很好的特性,你能够正确的跳转到使用字符串的代码。 --- # 二、运行程序 如果你看Olly的左上角的话,会看到一个黄色背景的小区块,里面写着“暂停(Pause)”。意思是程序已经暂停了(本例中是在开始的时候),等着你进行其他操作。所以,咱们开始干一票吧!按一下F9(或者从“Debug”菜单中选择“Run”)。一会儿后,我们的程序会弹出一个对话框(它有可能显示在Olly的后面,所以最小化Olly以确保能看见窗口)。 ![e96c30b954cb07d7474b1757c92ce326.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/314440974.png) 刚才显示“Pause”的地方现在应该显示的是“Runing”。意思是程序正在运行,不过是在Olly中运行的。你可能会与我们的程序进行一些交互,看看它是如何工作的以及它干了些什么。如果你不小心关了它的话,返回到Olly并且按下Ctrl+F2(或选择Debug->Restart)以重新载入程序,然后你可以点击F9让程序再一次运行起来。 现在照着做:程序运行的时候,点击回到Olly中,然后点击暂停图标(或点击F12,也可以点击Debug->Pause菜单)。即使我们的程序正在运行,该操作会让程序暂停在内存中的任何地方。如果这时候你想看看程序,你会发现挺有意思的(程序一点也不会显示出来)。这是因为当程序暂停的时候,Windows不会更新视图。现在再一次点击F9,你会发现你又可以和程序进行交互了。如果有什么问题的话,只需要点击那个双左尖括号图标或Debug-restart (或者ctrl-F2),程序就会重新载入并暂停在入口处。如果你需要的话,你可以再一次运行它。 --- # 三、单步运行 程序运行一个程序确实挺爽,不过你却得不到有关于程序运行的太多信息。让我们试试单步运行。重新载入应用程序(重新载入按钮、Ctrl+F2或Debug->restart),然后我们会暂停在程序的开始处。按一下F8,你就会发现当前的行选择器下移了一行。Olly运行了一行指令,然后又暂停了下来。如果你够激灵的话,就会发现堆栈区下滚了一行,并且在顶部有了一个新的入口。 ![f3da09397e880bfdc605cbf9a162c380.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/4255179976.png) 这是因为我们执行了一条指令,“PUSH 0”往堆栈里“压”了一个0。在堆栈中的显示是“pModule=NULL”。NULL是0的另一个名字。你有可能也注意到了那个寄存器区,ESP和EIP寄存器变红了。 ![fbe354a064d9317e5615fa09c426334e.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/2476534414.png) 当一个寄存器变红的时候,这就意味着最后执行的指令修改了该寄存器。本例中,ESP寄存器(用来存放指向栈顶的地址)增加了1,因为我们向栈中压了一个新值。EIP寄存器增加了2,其中存放了将要运行的指令的地址。因为我们已经不在地址401000了,而是在401002。因为上一个运行的指令是两个字节长。我们现在暂停在下一个指令处。这个指令是在401002,这正是当前EIP的值。 Olly现在暂停的指令是一个CALL。CALL指令意味着我们要临时暂停在我们当前所在的函数中,然后去运行另一个函数。这类似于高级语言中的方法调用,举个例子: ```C int main(){ int x = 1; call doSomething(); x = x + 1; } ``` 这段代码中,我们首先让x等于1,然后呢我们要在逻辑上暂停这行代码,转而去调用doSomething()。当doSomething()执行完毕后,我们会返回我们原来的逻辑,然后将x加1。 当然,在汇编语言里也是一样。我们首先往栈中压了一个0,现在呢我们又想调用一个函数,例子中调用了Kernel32 dll中的GetModuleHandleA(): ![90146dc90b40ebf9d4dfbf9d92c9c7a4.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1342407681.png) 好,再按一次F8。当前的行指示器会下移一行,而EIP仍然会保持红色并且加了5因为刚刚运行的指令是5字节大小),堆栈也回到了它原来的地方。刚刚发生的这些是从我们按下F8开始的,F8的意思是“Step-Over(单步步过)”,CALL中的代码被调用,然后Olly暂停在了CALL的下一行。也就是CALL中的程序执行了也做了某些事,但是我们跳过去了。 好了,现在我们看看其他的选项。重启程序(Ctrl+F2),按下F8步过第一条指令,不过在CALL指令上我们这次按F7。你会注意到整个窗体都变得不一样了: ![bc0c23b10cd8c2527a3a5327daa9d3ab.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/3515268501.png) 这是因为F7“Step-In(单步步入)”那个CALL,意思是Olly做了这个调用并暂停在了新函数的第一行。这种情况下,CALL跳转到了一个新的内存区域(EIP=4012d6)。理论上,如果我们按行通过这个新函数的话,我们最终还是会回到将我们带进来的那个CALL后面的语句。当然,有快捷键可以完成同样的功能,不过目前来说,咱们还是重启程序从头来吧。因为我怕教的太多容易忘。现在我们暂停在了程序的开始,按下F8(单步步过)4次,我们会停在如下图的语句处: ![7c2acf674b57ac494da4ea7bf95eefa9.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1077355635.png) 你会看到在一块的四个PUSH语句。这回当你四次按下F8的时候,注意观察堆栈区,会看到栈的增长(确实是向下增长,还记不记得那个盘子的例子?)。我觉得我们开始理解什么是压栈了......你可能会问我们为什么要将这些乱七八糟的数字往栈里压。本例中这四个数字是作为参数传递给函数的(那个函数是在地址401021处)。我们将前面的那个高级语言程序做一点点修改就会比较清楚了: ```C int main(){ int x = 1; int y = 0; call doSomething( x, y ); x = x + 1; } ``` 这里我们声明了两个变量x和y,并且将它们传递给了doSomething()函数。doSomething函数将会(可能)对这些变量做些什么,然后将控制权还给调用该函数的程序。通过堆栈是将变量传递给函数的主要方法之一:每个变量被压进堆栈,然后调用函数。然后在函数中,这些变量被访问到。通常PUSH指令的逆操作是POP。 堆栈并不是做这件事的唯一方法,它只是最常用的。这些变量也可以被放到寄存器中,然后在被调用的函数内部访问寄存器。不过本例中,我们程序的编译器选择将变量放到堆栈中。在你学了汇编语言后,这些东西都会变得清晰(你正在学习汇编语言,不是吗?)。后面我们还会复习几次的。 现在,如果我们再按一次F8,你会注意到Olly的工具栏中会显示“Runing”,我们程序的对话框就会显示。这是因为我们单步步过了那个CALL,说明那个CALL中存在程序的大部分。这个调用的代码是等待用户进行一些操作的循环,所以我们永远也不会将控制权交给CALL的下一行。那么,让我们修复它......。点击回到我们的程序,点那个关闭按钮来结束应用。Olly会立即暂停在那个CALL的下一行: ![c786465f0c2f6393dc2ad60315709d53.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1832703844.png) 你会注意到我们的程序也消失了。那是因为,在那个CALL的某个地方,对话框窗口被关闭了。如果你看下一行,你会发现我们正准备调用kernel32.dll -> ExitProcess。这是一个停止应用程序的Windows API。所以,基本上Olly在窗口被关闭了之后就暂停了,不过是在程序确实被终止之前。如果你这时按F9,程序就会终止,Olly的活动栏就会显示“Terminated(已终止)”,我们就再也不能调试任何东西了。 --- # 四、断点 我们试试别的东西,重新载入应用(Ctrl+F12),然后在地址 401011处的第二列上双击(也就是双击那个“6A 0A”操作码)。然后地址401011就会变红: ![76ed4c453b85d6e104dc6f427828c7f4.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/623488335.png) 你刚才做的就是在地址401011处设置断点。当Olly到达该处时,断点就会强制Olly暂停。有好几种不同的断点会因为不同的事件而阻止程序运行。 ## 1、软件断点(Software Breakpoints) 软件断点就是将断点所在地址处的字节用0xCC操作码替换掉,也就是int 3指令。这是一个特殊的中断,用以告知操作系统调试器希望在这里暂停,并且在执行该指令之前将控制权交给调试器。你不会看到指令被修改成0xCC,因为Olly在背后做了这个。当Olly遇到异常时它会设一个陷阱,让用户做他们希望做的事。如果你选择让程序继续运行(通过运行它或单步运行),0xCC操作码就会被原来的操作码替换回来。为了设置一个操作码,你可以双击操作码那一列,也可以先选中你想设置断点的那一行,然后右键点击它,选择Breakpoints->Troggle(或按下F2)。要删除断点你可以双击同一行,或右键点击选择Breakpoints->Remove Software Breakpoint(或再次按下F2)。 现在我们在401011处设置了一个BP(Breakpoints),让程序暂停在第一行指令处。按下F9,程序将运行并在我们设置的断点处暂停。 这里我告诉大家一些有用的东西。点击工具栏上的“Br”图标或选择菜单中的View->Breakpoints。你会看到一个断点窗口,里面显示了我们设置的断点。 ![107b6cae379e9657d130e8281c029ee0.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/519520077.png) 通过它你可以快速的浏览所有你设置的断点。你可以双击任何一个断点,然后反汇编窗口就会跳转到那个断点处(如果你没有改变程序控制流的话,EIP仍然停在原来的地方。双击EIP寄存器会回到当前的行,并准备执行下一行)。如果你选中一个断点,然后敲击空格键,断点就会在可用和禁用之间来回切换。你可以选中一个断点,然后敲一下“Del”键就会删除断点。 最后,重启程序,打开断点窗口,选中401011处的断点。敲一下空格键,然后“Active”列将会变成“Disable(禁用)”。现在运行程序(F9),你会发现Olly不会停在我们设置的断点处,因为它被禁用了。 ## 2、硬件断点(Hardware Breakpoints) 硬件断点使用的是CPU的调试寄存器。CPU内建的有8个寄存器,是R0-R7。即使芯片中内建了8个,但是我们只能使用四个。它们可以被用来断在内存区的读、写和执行。硬件断点和软件断点的不同之处在于,硬件断点不会修改进程的内存,所以它更可靠,尤其是在加壳或被保护的软件中。通过右键点击相关行可以设置硬件断点,选择Breakpoints,然后选择Hardware,on Execution。 ![9749ad380f88e47093b0140545d0e1b1.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/2621122027.png) 唯一查看你已经设置的内存断点(译者注:这里应该是硬件断点)是打开“Debug”菜单,选择“Hardware Breakpoints”。有个插件可以提供方便,不过我们后面再讨论。 ![cfcdaf2ccddc2f24c490f615e933715b.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/404807457.png) ## 3、内存断点(Memery Breakpoints) 有时候你想查找程序内存中的字符串或在常量,但是你又不知道程序在内存的什么地方。你可以用内存断点来告诉Olly只要任何一条指令读或写一个内存地址(或许多内存地址),然后暂停就行,在任何地方都无所谓。有三种方法设置内存断点。对于一条指令,右键点击该行,然后选择Breakpoint->Memory, On Access or Memory, On Write。 要在内存数据区设置断点,在数据窗口中选中一个或多个字节,然后右键选择和上面一样的操作。 你也可以对整个内存区域设置断点。打开内存映射窗口(“Me”图标或View->Memery),右键相关内存区域,在弹出菜单中选择“ Set Break On Access for either Access or Write”。 ![a8a515ce7a015be1e1d3b58ec79e33ac.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/616592272.png) # 五、内存数据面板的使用 你可以用数据面板检查被调试进程内存空间中的内容。如果反汇编窗口的指令、寄存器或堆栈中的任何一项包含了对内存位置的引用,你可以在该引用上右键然后选择“Follow in Dump”,随即数据面板就会向你显示该地址引用的内容。你也可以在数据面板的任何地方右键单击选择“GoTo”,然后输入要查看的地址。咱们现在试试。 确保FirstProgram已经载入并且停在了入口处。现在,按下F8八次,来到了401021地址指令处,该处指令是CALL FirstPro.40102c。如果你注意看这行的话,会注意到这个CALL会向下跳转到40102c处,在当前行下面的第三行的地方。按下F7我们单步步入那个跳转,然后我们就来到了40102c。记住这是一个CALL指令,所以我们最后还是会回到401021的(至少是该条指令后面的那条)。 ![a6341a08449202ca9df8df5d6b3a7570.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/3457137751.png) 现在,单步执行代码(F8)直到401062。你也可以在这行设置断点,然后按F9运行。还记得怎么设置断点吗?双击你想设置断点的那行的操作码列。你也可以选中该行,然后按F2去设置或取消断点。现在我们断在了401062: ![3b392f873a3b3be074e63ac0b3787f41.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1515885142.png) 现在,我们看看断下的那行。相关指令是MOV [LOCAL.3], FirstPro.00403009。我确定你知道(因为你已经学了汇编语言:P)这条指令是将地址00403009中的内容移动到堆栈中(这里Olly是用LOCAL.3表示的)。你可以在注释列看到Olly已经发现了该地址处的内容是字符串“MyMenu”。好,下面让我们看看。在指令上右键,选择“Follow in Dump(数据窗口跟随)”。注意这里有好几选项: ![9c8c099dbf6dfaebbc5d28346fe65101.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/3252946468.png) 这里我们选择“Immediate constant”。这回载入指令中的任何地址。如果你选择了“Selection”,数据窗口会显示高亮行所在的地址,这里是401062(也就是我们暂停的地方)。基本上我们在数据窗口中看到的就是我们在反汇编窗口中看到的。最后,如果我们选择了“Memory address”,数据窗口会显示LOCAL.3的内存区域。这会显示我们正在使用的变量(在堆栈中)的内存。下面是选择了Immediate constant之后的数据窗口的样子: ![6c66583c56d3730ed4b8047c556a3e19.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/152317737.png) 就像你看到的,数据窗口显示的内存是从403009开始的。正是Olly按指令从中载入字符串的地址。在右边你可以看到字符串“MyMenu”。左边你可以看到每个字符的十六进制数据。你可能注意到了在“MyMenu”后面有些其他的字符串。这些字符串会在程序的其他部分被用到。 # 六、最后,来点有意思的! 此次教程的最后,让我们做些有意思的事情。让我们修改二进制数据来显示我们自己的信息!我们将字符串“Dialog As Main”改成我们自己的,然后看看效果。 首先,在数据窗口的ASCII列,点那个“Dialog As Main”中的“D”: ![b4ef4c1958c6fb00e1f75858fcf54415.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/857545360.png) 注意,左边的第一个十六进制数据也高亮了。这个数字对应字母“D”。如果你查查ASCII码表的话,就会发现字母“D”的十六进制数正是0x44。现在,选中整个“Dialog As Main”字符串: ![9fc9fa0b0c75caff0bc791fe05e62cd1.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/3108177261.png) 在选中的内容上右键,选择“Binary” -> “Edit”。我们就可以修改我们程序在内存的内容: ![7d094d0a225b6cb40fda0c617ce171ea.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/2839500715.png) 然后就会弹出一个如下的窗口: ![04be56ae9848fc66586ae71ce08f4fd0.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1322866532.png) 第一个文本框以ASCII码的形式显示字符串。第二个文本框是以UNICODE形式(我们的程序用不着,所以空着),最后那个文本框是相关字符串的原始数据。好,咱们改一下。点一下字符串的第一个字母(“D”),然后输入任何你想要将“Dialog As Main”覆盖掉的内容。要注意的是你输入的长度,别多了。否则你就会覆盖掉程序需要的其他字符串,或者更糟糕是覆盖掉了程序需要的代码!!!这里呢,我输入的是“Program R4ndom”: ![6bbdcae9a1e3735ab05e185e923c8da0.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/1255540578.png) 完了之后呢点OK按钮,并允许程序(点Olly的运行按钮或按下F9)。切换到我们的程序,然后随便输入什么都行,然后选择菜单“Option”->"Get Text"。现在看看我们的对话框! ![a34d959e7b1b31cf199740001b173149.png](https://blog.cdn.fanlis.xyz/usr/uploads/2020/09/2392562977.png) 注意到对话框的标题有什么不同没有。 最后修改:2020 年 09 月 12 日 © 允许规范转载 赞 2 如果觉得我的文章对你有用,请随意赞赏