Loading... # 一、简介 此次教程我们继续学习Olly的使用。我们将继续使用上一章的程序(我也会将它包含在下载里)。 你可以在tutorials中下载文件和PDF版的教程。 --- # 二、DLLS 就像我前面说的,当你启动程序时,DLL被系统载入器载入。这回我会细致的讲解。DLL(Dynamic Link Libraries)是函数的集合,通常由Windows提供(当任何人都可以提供),其中含有很多Windows程序要用的函数。这些函数可以让程序员更容易的完成一些乏味的重复性的任务。 例如,将字符串全部转换成大写是许多程序要实现的功能。如果你的程序要多次使用该功能的话,你有三个选择:一是在你的程序中自己编码实现;问题是,你不知道你的下一个程序是不是也会用到该功能很多次。你可能需要在你使用到的程序里复制粘贴很多次相同的代码。二是创建一个自己的库,这样任何程序都可以调用。这种情况下,你可以创建一个DLL,然后包含在程序中。该DLL可能有像convertToUpper这样的通用函数以便于程序调用,因此你只需要写一次代码就行了。这样做的另一个好处是,你可以说你为字符串转大写想到了一个很好的优化方案。第一个例子中,你需要将代码拷贝到所有要用到该代码的程序中,但是在那个通用DLL例子中,你只需要修改DLL的代码,然后所有使用该DLL的程序都可以以最快的速度获益。爽吧!这就是DLL产生的真正原因。 最后一个选择是,使用Windows提供的一堆DLL中包含的数千个函数中的一个。这样做有很多好处。第一个是,Microsoft的程序员已经花了多年时间来优化他们的函数,他们在很大程度上要比你牛逼。第二,你不需要将你的DLL包含在应用中,因为Windows操作系统已经内建了这些DLL。最后,如果Windows决定修改他们的操作系统,你自己的DLL有可能和新系统不兼容。同时,如果你使用Windows的DLL,它们肯定是兼容的。 --- # 三、如何使用DLL 现在你已经知道了什么是DLL,那就谈谈如何使用它们。DLL基本上就是一个你的程序可以调用的函数库。在你第一次载入应用程序时,Windows载入器就会检查PE头(还记不记得PE头?)的特定区段,看看你的程序调用了哪些函数,以及这些函数都在哪些DLL中。在将你的程序载入内存后,载入器就迭代这些DLL,将它们载入到你的应用程序的内存空间。然后它再仔细检查你的程序的代码,并将你的程序调用的DLL函数注入到正确的地址。例如,如果你的程序调用Kernel32 DLL中(只是一个例子啊)的StrToUpper函数来将一个缓冲区里的字母转换成大写,载入器要找到Kernel32DLL,找到StrToUpper函数的地址,并将地址注入到你的程序中调用该函数的那行代码处。你的程序就会通过调用进入到Kernel32 DLL的内存空间,执行StrToUpper函数,最后再返回到程序中。 让我们实际看看这个过程。Olly载入本教程包含的FirstProgram.exe。Olly断在了第一行代码(从现在起我们就叫它入口点(Entry Point)——这很重要,因为这是我们详细讨论PE头的时候PE头中的叫法)。 ![2d68954e1befdfedee3858fb020a1de0.png](https://image.cdn.fanlis.xyz/images/2020/09/11/a3beec48d13eb35d80c2c5582f3d1ad4.png) 如果你看第二行代码的话,你会看到一个对函数kernel32.GetModuleHandleA的调用。第一步,我们看看这个函数是干嘛的。我已经将WIN32.HLP文件以及一个教你怎样将它安装到你的Olly中的文本文档包含在了本课的下载里,就是为了防止你上一课没有拿到它。安装该文件后,你在你不熟悉的Windows API上右键,会显示一个该API是干什么的菜单。在你拷贝过去后,你需要重启下Olly。现在,在GetModuleHandleA 上右键,选择“Help on Symbolic Name”。然后Olly会显示一个该函数的备忘单: ![36470945e538d1302017dee5e0c8d13a.png](https://image.cdn.fanlis.xyz/images/2020/09/11/6b0d3f38be1e037ab0b03cfe7efe2fd5.png) ![c89242ee68f0ebfec96b91a545d842a4.png](https://image.cdn.fanlis.xyz/images/2020/09/11/40d7fb1625fe8ac16074508e2fcd1c68.png) 那么,基本上这个函数就是为了获取我们程序内存空间的句柄。在Windows中,如果你想对一个窗口(或者是相当一部分的其他对象)做任何事情,你必须取得它的句柄。这基本上是Windows知道你正在操作的对象的唯一标识符。GetModuleHandle其实比这个稍微复杂点,不过当我们经历了更多知道了更多知识以后再回过头来讨论这个。 关闭帮助窗口,我们看看这个CALL去了哪里。Olly已经试着帮助我们,它用函数名替换掉了GetModuleHandleA的真实地址。让我们看看它驻留的地址是什么。点一下调用GetModuleHandleA的那行代码,再按一下空格键,就会打开一个汇编窗口: ![e9cb792dc10bcad6421f394048f202a6.png](https://image.cdn.fanlis.xyz/images/2020/09/11/c24223eb8f5bfd7867235c54328067ed.png) 该窗口有两个目的:第一它向你显示了正在被计算的(以防Olly帮助性的替换了地址)真实的汇编语言指令,第二它允许我们编辑汇编语言指令。在下一课前我们不会做任何编辑,这次我们只是看看地址:4012D6。有两种方法可以跳转到该地址看看那儿有什么(而不用真的运行程序)。选中“CallGetModuleHandleA”然后按下“Enter”,你也可以按Ctrl+G手动输入地址。我们试试第一种方法,选中401002那行(第三列有相关指令)然后按回车键,你就会来到该CALL要调用的地方: ![89d9de15b3e9329540018016ae2d8d1c.png](https://image.cdn.fanlis.xyz/images/2020/09/11/13d1f91c75922982f2e48468d8b0345c.png) 现在这里比较有趣:它看起来确实不像执行GetModuleHandleA的代码。更像是一些跳转。对此有一个很好的原因说明,不过不幸的是,需要解释一下。 --- # 四、地址跳转表 有件事你需要知道,DLL并不是总是一次性全部载入到内存。Windows载入器负责载入你的程序和所有需要的DLL,它可以修改被载入的DLL在内存中的位置(坦白的说,它甚至能够修改你的程序被载入的位置,这些我们后面再说)。原因是这样的,现在有一个Windows DLL属于最先载入的那种,被映射到地址80000000。好吧,恰好你自己的程序也带有一个DLL且需要载入到地址80000000。两个DLL当然不能被载入到同一个地址,载入器必须将其中一个移到另一个地址。这种情况时常发生,还被叫做重定位。 这里有个问题:在你首次编写一个程序并写了一个调用GetModuleHandleA的指令,编译器会准确的知道正确的DLL在哪,然后它会放一个地址在指令里,有些类似于“Call 80000000”。现在,当你的程序被载入内存时,它仍然会让这个CALL调用80000000(我这里说的有点过于简单了)。不过,如果载入器将这个DLL移到80000E300会怎么样?你的CALL会调用错误的函数! PE文件和此后的Windows文件围绕这个问题提出的解决方法是建立一个跳转表。意思是你的代码在首次编译时,每一个对GetModuleHandleA 的调用都指向你的程序的一个地点,然后这个地点就会立即跳转到一个随意的地址(这是最后的正确的地址)。事实上,所有对DLL函数的调用都采用了同样的技术。它们每一个调用特定的地址,然后立即跳转到一个随意的地址。当载入器载入所有的DLL时,它会遍历“跳转表”,然后在内存中用真实的函数地址替换掉所有的随意地址。下面是所有真实地址被填充后的跳转表的样子: ![0c2f07351f81c677d0bb2a4536a4343f.png](https://image.cdn.fanlis.xyz/images/2020/09/11/4184db7c234e639161b8e062ccadedbe.png) 这个有点复杂,下面我举个例子。我会写一个短程序,使用完全随意的信息(只是为了证明我们的观点)来调用一个Kernel32 DLL中的函数ShowMarioBrosPicture。下面是我的程序(没有特指哪种语言): ```C main(){ call ShowMarioBrosPicture(); call ShowDoYouLikeDialog() exit(); } ShowDoYouLikeDialog(){ If ( user clicks yes ){ call ShowMarioBrosPicture(); Call ShowMessage( "Yes, it's ourfavorite too!") } else{ call showMessage( "You obviouslynever played Super Mario Bros."); } } ``` 这些代码被编译之后,对函数的调用将会被真实的地址替换,就像下面这样(再次声明,这里没有特指某种语言): ``` 401000 call 402000 // Call ChowMarioBrosPicture 401002 call 401006 // Call showDoYouLikeDialog 401004 call ExitProcess 401006 Code for "Do You like It" dialog . . . 40109A if (user clicks yes) 40109C call 402000 // call showMarioBrosPicture 40109E call 4010FE // call show message 4010a1 call ExitProcess 4010a3 if (user clicks no) 4010a5 call 4010FE // call show message 4010a7 call ExitProcess 4010FE code for show message ... 40110A retn ``` 这些代码的后面就有可能是我们的跳转表(本例中,跳转表中只有ShowMarioBrosPicture)。 402000 JMP XXXXXXXX 我们的程序(译者注:这里作者应该是将ourprogram写成了out program,所以我给翻译成我们的,小伙伴们可以自己查阅)并不知道ShowMarioBrosPicture在哪(或者说不知道Kerner32 DLL在哪),我们程序的编译器只是用实际的调用地址填充X(并不是正真的地址,你知道那么意思就行)。 当Windows载入器载入我们的程序时,它首先将二进制文件载入内存,完成跳转表的构建,不过跳转表里没有任何真实的地址。然后开始载入DLL到我们的内存空间,最后开始找出所有函数驻留的地方。一旦它找到了showMarioBrosPicture的地址,它就准备进入跳转表并用函数的真实地址替换掉X。假定showMarioBrosPicture的地址是77CE550A。我们的跳转表代码就会被替换成如下: 402000 JMP 77CE550A 因为Olly能够发现该地址指向的是showMarioBrosPicture,所以它会帮助性的进入跳转表并将跳转表显示如下: 402000 JMP DWORD PTRDS:[<&kernel32.showMarioBrosPicture>] 现在,让我们回到FirstProgram看看跳转表: ![0c2f07351f81c677d0bb2a4536a4343f.png](https://image.cdn.fanlis.xyz/images/2020/09/11/4184db7c234e639161b8e062ccadedbe.png) 在首次编写这个程序时,各种DLL中的函数被调用,但是编译器不知道我们的程序在运行的时候这些函数是在内存的什么地方,所以它要创建一些像下面这样的东西(不是很准确这里): ``` 40124C JMP XXXXX //gdi32.DeleteObject 401252 JMP XXXXX //user32.CreateDialogParamA 401258 JMP XXXXX //user32.DefWindowProcA 40125E JMP XXXXX // user32.DestroyWindow … ``` 在载入器将我们的程序载入之后,再载入所有的DLL并查找所有的函数的地址。然后它会遍历每一个函数,用这些函数当前驻留的真实地址替换,就行前面图片中的那样。如果仔细想想的话,这确实是相当巧妙的处理方法。如果不这样做的话,那么载入器就得遍历整个程序,并对每个DLL中的每个函数的调用都用真实的地址进行替换。那个工作量就大了。使用这种方法,载入器对于每个函数的调用只需要替换一个地方,就是跳转表那样的。 还是看看我们自己的程序吧。重载应用并按下F7。点击选中401002那行指令(和前面做的一样),再按下空格(和前面做的一样): ![e9cb792dc10bcad6421f394048f202a6.png](https://image.cdn.fanlis.xyz/images/2020/09/11/c24223eb8f5bfd7867235c54328067ed.png) 再一次提醒你注意那个地址,4012D6。现在按F7步入那个CALL,注意我们来到了4012D6。如果你向上翻,你会注意到我们来到了跳转表的中间: ![0c2f07351f81c677d0bb2a4536a4343f.png](https://image.cdn.fanlis.xyz/images/2020/09/11/4184db7c234e639161b8e062ccadedbe.png) 现在,再点一下F7,我们就会来到GetModuleHandleA的真正的地址7780B741。有两种方法可以知道我们现在正在模块kernel32中,两者在不同的场合你都可能用到。第一个是Olly的CPU窗口标题: ![73115790ef06db5f7131c95e03038414.png](https://image.cdn.fanlis.xyz/images/2020/09/11/6004c2e4f5950dc93feb92ec79b5244c.png) 你能看到它显示的是“modulekernel32”。第二种方法是到内存映射窗口查看地址: ![ae71bf65c20751132d9c22caf210fd68.png](https://image.cdn.fanlis.xyz/images/2020/09/11/0b9911d3d466948cffcf5275b817a7ff.png) 你会发现我们所在的地址(7780B741)是在kernel32的代码段地址空间中。 现在我们回头看看其他的函数调用。重启应用,按F8直到40100C处。那行代码是对GetCommandLineA的调用。点击选中指令再按下空格键,你就能够看见它指向的地址,是4012D0: ![38019128a53117ffbc0511bb1dd9e6a8.png](https://image.cdn.fanlis.xyz/images/2020/09/11/c24223eb8f5bfd7867235c54328067ed.png) (不好意思,鼠标把地址挡住了,它是4012D0)现在我们来试试手动定位该地址,你会经常用到这种方法的。按Ctrl+G或点那个转到图标file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png,输入我们想要转到的地址: ![4a087708757bf845ba45856496f987c1.png](https://image.cdn.fanlis.xyz/images/2020/09/11/ae8d100d84deaa72bff6a4b66afa2712.png) 你的“GOTO(转到)”窗口可能不太一样,这个问题待会解决。现在点击OK,我们就会跳到跳转表中GetCommandLineA的位置: ![5becf2e1b43fd5ffe72003299169d714.png](https://image.cdn.fanlis.xyz/images/2020/09/11/14b951164961641d0195d939999dfd9e.png) 按下F7我们就来到了kernel32中的GetCommandLineA的开始处。这个函数从7C812FBD开始: ![99632f9eaf90c431ee848acd9298297e.png](https://image.cdn.fanlis.xyz/images/2020/09/11/81cb17b3f440d86c7a42a1566ee68cd9.png) --- # 五、跳入及跳出DLL 当我们围着一个程序转的时候,你不知道什么时候就在DLL中结束了。如果你正在尝试攻克一些保护方案时,通常你是不愿意在DLL中转的,因为Windows DLL中真的没什么东西。关于这方面的一个告诫,如果你正试着逆向的程序本身就带有DLL并且你就是想将它们也进行逆向工程(或者是保护机制确实在DLL中)。这里有几种从DLL回到我们的程序的方法。一个方法是单步通过所有的DLL函数代码直到最后你返回到程序,当然这可能得一会时间(有些情况下像VB程序,就是永远)。第二个选择是,点开“Debug”菜单并选择“Execute till user code(执行直到用户代码)”或者按Alt+F9。意思是执行DLL中的代码直到我们返回到我们自己的程序代码。要注意的是,有时候这不一定好使,因为如果DLL访问了一个在我们的程序空间中的buffer或者变量的话,Olly就会停在那儿,所以你最终可能会按Alt+F9好几次才能回来。 我们来试试这个方法。我们当前应该暂停在7C812FBD,也就是GetCommandLineA的开始处。好,按下Alt+F9。我们会回到程序中对kernel32调用的指令的后面那条指令(往上一行就是那个CALL)。现在我们来试试另外一个回到我们的代码的方法。重启程序,单步步过(F8)直到对GetCommandLineA调用的那个CALL(40100C)。单步步入(F7)那个CALL,并且单步步入那个jmp进入跳转表。现在,我们回到了GetCommandLineA的开始处: ![0e7f13b0cb8fce3599a2f49a48f45397.png](https://image.cdn.fanlis.xyz/images/2020/09/11/81cb17b3f440d86c7a42a1566ee68cd9.png) 现在打开内存映射窗口,滚动到我们的程序的代码段那块(起始地址是400000,写着PE Header): ![728767be32bd2997caecb5c034a93ce1.png](https://image.cdn.fanlis.xyz/images/2020/09/11/5317b914e269220ded4d81c8219dfd60.png) 现在,点击选中401000那行,我们的.text区段在那行。按下F2设一个内存访问断点(或右键选择Breakpoint on access): ![bce786eb1cf89d16f42cff6bf10a3c50.png](https://image.cdn.fanlis.xyz/images/2020/09/11/d012b6e23bfb7f1ecd2a5fddd46aff6d.png) 现在,运行程序。Olly会断在和上面相同的那行,就是401011处,也就是我们对DLL调用CALL之后的那行!!!好,现在删除内存断点,否则你会纳闷,为什么每次你运行程序的时候它都会断在下一行 --- # 六、再议堆栈 堆栈是逆向工程中的非常重要的一部分,如果对它理解的不够深入的话,你永远也不会成为一个伟大的逆向工程师。下面我们针对它做几个实验: 首先,看看寄存器窗口(在重启应用之后),看那个ESP寄存器。该寄存器中的地址指向栈顶。本例中,ESP的值是12FFC4。现在看看下面的堆栈窗口,列表中的顶部地址和ESP中的地址是一样的。 ![f9a33728696c05dc9dd9001af2935adb.png](https://image.cdn.fanlis.xyz/images/2020/09/11/78690146092ba189c57c9f6c3ed7f3f1.png) 现在按F8(或者F7)一次,将0压入堆栈,再看看堆栈窗口: ![de3e442723683f5720c8e13aeaabe2fb.png](https://image.cdn.fanlis.xyz/images/2020/09/11/9df8b85c3aa0b03d39eff599b74a27d3.png) 就像我们上次课提到的那样,该操作将0(null)压入堆栈。现在看看ESP寄存器: ![7f4614a4abc79346c11b4a72fefd9747.png](https://image.cdn.fanlis.xyz/images/2020/09/11/649af579474cfc8f1628aa908d8e8577.png) 已经变成了12FFC0。因为,在向堆栈中压入一个字节后,该字节就变成了新的栈顶。按F8一次,单步步过对GetModuleHandleA的调用,再看看堆栈窗口: ![5c4b5f78f6f50b30372aa72261eb122d.png](https://image.cdn.fanlis.xyz/images/2020/09/11/b5f37c557f18c897006a74c853b76a11.png) 注意我们的堆栈已经向下回退了一位(ESP寄存器也回到了原来的值)。这是因为GetModuleHandleA函数使用了这个被压入堆栈的0,并把它作为参数。然后把它“POP(弹)”出了堆栈,因为这个0已经没用了。就行上一课提到的,这是向函数传递参数的一种方法:将参数压栈,被调用的函数将它们弹出栈,使用它们,然后返回,通常我们需要的信息都在寄存器里(后面会看到)。 接着继续...。如果你按F8两次单步步过对GetCommandLineA的调用,会发现堆栈并没有改变。因为,我们没有向堆栈中压入任何信息以供函数使用。接下来,是一个PUSH 0A的指令。这是准备传递给下一个被调用函数的第一个参数。单步步过,然后你会发现0A出现在了栈顶,ESP寄存器下移了4(当你向堆栈压入一个值时,ESP寄存器会向下移,因为堆栈在内存中是向下“增长”的。译者注:堆栈是从高址向低址增长。)现在再按一次F8,ESP寄存器会再次下移4。因为我们向堆栈中压入了一个4字节的值。如果你看堆栈的顶部,就会发现我们向堆栈中压入了00000000。为什么呢? 我们看看做这个压入操作的那行代码,在401013处: PUSH DWORD PTR DS:[40302c] 这行代码的意思(我保证你知道什么意思,因为你已经学了汇编语言:p)是取地址40302C开始的4字节内容,然后将它们压入堆栈。那么在40302C的是什么呢?好吧,当然是00000000!(开个玩笑)我们来自己看看。右键401013处的指令,选择“Follow in Dump(数据窗口跟随)”->“Memory Address(内存地址)”。然后会在内存数据窗口中显示以40302C开始的内存中的内容: ![95645e6b1382c720757c64237cc3731f.png](https://image.cdn.fanlis.xyz/images/2020/09/11/19a2957099ec2eea98f5864f7d0b5fd7.png) 显然,哪里可没有那么多内容!不过你至少知道0是从哪儿来的。如果你想知道更多的细节比如这块内存是干什么的,这块内存空间被用来存储变量,并且最终会被这些变量填充。不过对于目前来说,所有的变量都被初始化为0。 现在按一下F8,我们遇到了另一个PUSH指令,不过这次是从403028开始。如果你在数据窗口中向上翻,会看到该地址处也是0(在我们上一次课修改的字符串的后面)。这一块正在做的是将内存指针压栈,当前被设置为0,我们的代码将会以变量的形式使用。单步步过上一个PUSH然后单步步入对地址40101C的调用。你应该注意的第一件事是有什么东西被压入堆栈里了:我们的CALL的返回地址,401026。 任何代码在使用CALL指令时,在我们还没有执行这个调用前,下一条将要被执行的指令(译者注:非CALL内部的指令)的地址会被自动的压入堆栈。原因是,我们调用的函数执行完后,它需要知道返回到什么地方。被自动压入堆栈的地址就是返回地址。看那个堆栈窗口的顶部: ![8cf985b3c85e5d6589aae62c9f0ede85.png](https://image.cdn.fanlis.xyz/images/2020/09/11/d35127405a94876fbd70deaef987b4cb.png) 可以看到Olly已经指出了它是一个返回地址,并且它指回到我们的程序(FirstPro),需要被返回的地址是40102C(CALL的下一条指令)。 现在,在函数的结尾,一个RETN指令将会被执行(你肯定知道它是“return”的意思,因为它出现在你的汇编语言书的开头处)。这个返回指令真正的意思是“弹出栈顶的地址,将正在运行的代码指向这个地址”(它主要是用弹出的值替换EIP寄存器——存储当前正在运行的行的地址)。那么现在,被调用的函数在执行完后准确的知道了要返回到哪!事实上,如果你向下滚动一点,就会发现4011A3处的RETN语句会从堆栈中弹出这个地址,然后从该地址开始运行: ![d9cd5ae2d4a582f070e8c70f855f321c.png](https://image.cdn.fanlis.xyz/images/2020/09/11/9e9cd4e7ffefa2881a38148c12941639.png) (RETN语句后面的那个10,意思是给我返回地址,然后再从堆栈中删除10h字节的空间,因为我再也不需要它们了。看看你汇编语言书籍的下一页吧) 这里我们花点时间来启动一句,我保证在逆向工程社区会火的口头禅。我喜欢叫它“Random’s Essential Truths About ReversingData(Random关于逆向数据的必备真言——译者注:大体这个意思吧,就这么翻吧,反正咱们也不会喊)”,或者R.E.T.A.R.D(首字母缩写的听起来还不错)。我正式开启下面这个即将成为传奇的戒律: #*1. You MUST learn assembly language(#1、你必须学习汇编语言).* 如果你还没有的话,在逆向工程领域你不会取得成功。就是那么简单。 ![b364f39b0063ce3d8c8648a0ea66a595.gif](https://image.cdn.fanlis.xyz/images/2020/09/11/5b15c4f48a1aa35d537721386252cfd8.gif) 本次教程我准备最后谈论的是,Olly怎么处理参数和本地变量的显示。如果你双击EIP寄存器,我们就能跳回到代码的当前行(在40101C处),往下可以看到好几行蓝色标记的行,显示的有LOCAL字样(其中一个显示ARG): ![bbf1d0177d150d1d7831773ee2e9eb1c.png](https://image.cdn.fanlis.xyz/images/2020/09/11/8376eef6311d0df43ef59bc95e296067.png) 如果你没有任何编程经验,你可能不太知道本地变量和参数之间有什么不同。对于参数,就像我们早些时候讨论的,是传递给函数的变量,通常通过堆栈传递。本地变量是被调用函数“创建”的用来临时性存储数据的一种变量。下面是一个例子程序,其中有两个不同的概念: ```C main(){ sayHello( "R4ndom"); } sayHello(String name){ int numTimes = 3; String hello = "Hello, "; for( int x = 0; x < numTimes; x++) print( hello + name ); } ``` 程序中,字符串“R4ndom”是传递给sayHello函数的参数。在汇编语言中,这个字符串(至少是这个字符串的地址)会被压入堆栈,以便于sayHello函数引用。一旦控制权转给了sayHello函数,sayHello需要设置一对本地变量(LOCAL VARIABLES),这对变量函数会使用,不过一旦函数执行完毕就不再需要它们了。例子中的本地变量是整形数据numTimes、字符串hello、整形x。不幸的是,为了防止堆栈不够负责,参数和本地变量都存储在堆栈中。堆栈通过ESP寄存器来实现这个,不过寄存器可没有超能力。它通常指向栈顶,不过它是可以被修改的。所以,可以说我们进入了sayHello函数,并且堆栈中有下面的数据: 1. 字符串“R4ndom”的地址 2. 让我们进入函数的那个CALL的返回地址。 如果我们想要创建一个本地变量,我所需要做的的是从ESP寄存器中减去一定的值,这样就会在堆栈中创建一定的空间!假如我们将ESP减去4(会有4个字节大小,或者一个32位的数)。堆栈会像下面这样: 1. 空的32位数 2. 字符串“R4ndom”的地址 3. 让我们进入函数的那个CALL的返回地址。 现在,我们可以在这个地址里放任何数据,比如,我们可以让它存储sayHello函数中的变量numTimes。因为我们的函数使用了三个变量(所有的都是32位长),需要从ESP减去12字节(或十六进制的0xC),然后我们就有了三个可以使用的变量。堆栈就会像下面这样: 1. 指向字符串“hello”的空的32位地址。 2. 变量“x”的空的32位数 3. 变量“numTimes”的空的32位数 4. 字符串“R4ndom”的地址 5. 让我们进入函数的那个CALL的返回地址。 现在,sayHello可以填充、修改以及重用这些地址以用于我们的变量,在第一个位置处有传递给函数的参数(就是字符串“R4ndom”)。当sayHello执行完毕后,它有两种方法来删除这些变量和参数(因为函数执行完毕后不在需要它们),然后将堆栈还原: 1. 它可以将ESP寄存器修改回它被修改之前; 2. 使用后面带数字的RETN指令。 第一种方法,为了让程序能够记住ESP的原始数据,它使用了另一个寄存器——EBP,目的是当我们第一次进入sayHello函数时能够追踪到堆栈指向的原始位置。当函数准备返回时,它从EBP中拷贝ESP的原始值(开始的时候存储在EBP中)到ESP和BAM中。返回地址现在在堆栈的顶部,当RETN指令运行时,它用通过这个返回到我们的主程序中。 第二种方法,你可以告诉CPU堆栈中有多少字节你不再需要了,然后它就会从栈顶删除这些字节。在我们的例子里,我们用RETN 16(十六进制就是0xF),这样就会从栈顶除去16字节(或4个32位数),将返回我主程序的地址留在新的栈顶。具体的返回机制依赖于编译器,不过你两个都会看到。 现在,我们回到我们的FirstProgram.exe: ![bbf1d0177d150d1d7831773ee2e9eb1c.png](https://image.cdn.fanlis.xyz/images/2020/09/11/8376eef6311d0df43ef59bc95e296067.png) 可以看到Olly已经注释出了一个参数和12个本地变量。在我们的程序中这些本地变量是用来追踪类似于图标、我们输入的文本的缓存地址、输入的文本长度等。完成后,就会弹出这些值、将ESP寄存器值改回EBP或RETN一个数字(本例中,三个都有!!!) 我知道堆栈是非常复杂的设计,但是我保证在混乱一段时间以后你会掌握它的窍门。汇编语言的书也会帮很大忙的。 最后修改:2020 年 09 月 12 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏