这里将之前学习《逆向工程核心原理》的笔记重新实现整理一遍,代码重新编写实现,以方便以后查阅。
环境主要是Win7 32位系统,使用VS2010进行编程:
#include "windows.h"
int main()
{
MessageBox(NULL, L"Hello World!", L"blog.csdn.net/ski_12", MB_OK);
return 0;
}
MessageBox()函数用于弹框显示第二个参数的字符,其中第三个参数为框的标题内容。
点击工具栏下拉框的“Release”,然后点击“生成helloworld”即可编译生成exe文件:
运行helloworld.exe文件后会弹框显示如图内容:
接着明确一下逆向调试的任务:查找出helloworld.exe文件的main()函数。
使用Ollydbg打开helloworld.exe文件:
一开始调试器停止的地方为helloworld.exe程序执行的起始地址即EP(Entry Point入口点),其中左上角为代码窗口,右上角为寄存器窗口,左下角为数据窗口,右下角为栈窗口。
注:EP是Windows可执行文件的代码入口点,是执行应用程序时最先执行的代码的起始位置,依赖于CPU。
初次学习,先按部就班地逐一调试。按F7运行进入00D112B4的call指令所指的函数地址查看:
发现其中右侧一栏(Ollydbg的注释)含有几个红色标记的函数名称,其为代码中调用的API函数名称,是VC++启动函数,并非是需要查找的main()函数。继续下拉看到RETN指令,并没有发现main()函数,直接Ctrl+F9运行到该RETN指令,再F7跳出该函数。
注意:启动函数是编译器任意添加的代码,不是用户编写的代码。
继续F7执行00D112B9的jmp指令,跳转到指定的地址:
可以看到存在好几条的call指令,逐一按F7进行调试,遇到call指令F7进去查看是否是main()函数,若无则直接Ctrl+F9运行到该RETN指令,再F7跳出该函数。重复该操作直至遇到main()函数。其实也可以从右侧的注释区域显示的函数名称来排除是否为main()函数,若不是则直接按F8执行call指令但不进入该函数内部执行。调试到后面发现main字样:
可以猜测该函数为main()函数,F7执行call指令进入函数内部进行确认:
可以看到,右侧注释一栏显示了MessageBox()函数及其四个参数的值,包括之前编写代码时写入的字符串。这里call指令调用MessageBox()函数,其之前的4个push指令分别将该函数的4个参数以逆序的形式存入栈中。
至此,已经找到了main()函数。
从EP或调试点出发,逐一按F8运行指令,当F8运行某一条指令弹框如下图时,即可确认是main()函数。仅适用于代码量少、功能明确的程序。
查看与分析的字符串列表。右键>Search for>All referenced text strings,找到MessageBox()函数输出的字符串字样,双击该项即可跳转到该地址。Ollydbg初次载入程序时会先进行一个预分析过程,会先查看进程内存,将程序中引用的字符串和调用的API摘录出来,整理到另外一个列表中。
可以在Dump窗口中查看该地址存储的数据,注意,是以00F01002地址存储的00F020F4为地址:
编写的Windows程序要弹框显示内容,则需要用Win32 API向OS请求实现输出,本次程序的API为user32.MessageBoxW() API。即查看预分析的API列表。右键>Search for>All intermodular calls,然后直接输入“MessageBox”字符即可查找到该API,接着点击即可进入main()函数。
调试器并不能列出所有可执行文件的API函数调用列表,使用压缩器、保护器等工具对可执行文件进行压缩或保护后,可执行文件的文件结构就会发生改变,从而导致调试器无法列出API调用列表。这种情况下,DLL代码库被加载到进程内存后,可以直接向DLL代码库添加断点。编写的程序执行某种操作时,必须使用OS提供的API向OS提出请求,然后与被调用API对应的系统DLL文件就会被加载到应用程序的进程内存。依据这个,当无法列出API调用列表时,可以查看所有被加载的DLL文件中提供的所有API,再到其中找出相关的API即可。
右键>Search for>Name in all calls,输入“MessageBoxW”,然后在USER32模块中的Export类型找到相应的API:
注:压缩器指运行时压缩器,可用于压缩可执行文件的代码、数据、资源等,压缩后的文件本身也是一个可执行文件;保护器具有压缩、反调试、反模拟、反转储等功能,可以有效地保护进程。
双击进入相应的地址,F2设置断点,然后F9运行程序,程序停止在断点处:
该段代码为MessageBox() API的代码段,通常在此时程序已经将各参数值保存在栈中,此时观察栈窗口:
可以看到MessageBox()函数地址以及各个参数的值都逆序保存在栈中。也就是说,这里是调用的MessageBox()函数的内部,现在只需要直接访问返回main()函数的地址或者在其内部按Ctrl+F9运行到RETN指令,再F7跳出该函数即返回至main()函数。
若每次调试都是从EP出发则会导致效率低下,调试到一定步骤时通常会设置调试点以便于下次直接从调试点出发继续调试以提高调试效率。
以main()函数地址01091000为调试点为例,键入Enter,输入01091000后回车来到该地址处,再按F4执行到光标指定的位置即可:
通过F2设置断点,然后直接F9运行到断点处停止,接着可以继续调试,可以通过View>Breakpoints查看断点信息:
“;”键可以在指定地址添加注释,也可以通过查找命令找到它。在改地址添加“SKI12”的注释,可以看到右侧的注释被修改掉了,将光标移至别处,再右键>Search for>User defined comment:
调试时只需双击注释地址那一项的到达目标地址,再F4即可。
可以通过标签提供的功能在指定地址添加特定名称。“:”键可以在指定地址添加标签:
然后在该指令前一条指令,即调用main()函数的call指令中可以看到标签名:
同样可以检索标签,右键>Search for>User defined labels:
调试时只需双击标签地址那一项到达目标地址,再F4即可。
明确目标为修改弹出的框的内容“Hello World!”为其他字符串如“Goodbye My Friend!”。
在main()函数起始位置处设置断点,F9运行到断点后,接着F7运行到将第二个参数的字符串存入栈的push指令即注释中存入“Hello World!”字符串的命令,此时观察栈窗口可看到保存“Hello World!”字符串的内存地址,在Dump窗口中Ctrl+g转到该地址可查看到保存在其中的内容:
选上包含“Hello World!”字符串的一块区域,Ctrl+e编辑内容:
修改之后:
注意:Unicode字符串必须以NULL结束,其占据两个字节。添加时不能直接在Unicode项上添加而是在HEX项中直接添加。
直接F9运行查看效果:
正常显示。然而这种方式并不能永久保存,下次运行程序时还是显示原来的字符串。
要想保存更改到可执行文件,则需要将更改后的程序另存为一个可执行文件。选中更改后的字符串>右键>Copy to executable file,弹出如图窗口:
对上述选中的内容右键>Save file,即可保存成新的可执行文件。
需要注意的是,修改后的字符串一般不应该比原字符串的长度长,因为紧跟在原字符串后面的内存区域中可能保存着一些重要数据,所以一般而言,这种方法仅适用于更改字符串长度比原字符串长度短。
Ctrl+F2重新开始调试,在地址为01371007的push“Hello World!”字符串进栈的指令中,是向MessageBoxW()函数传递字符串所在区域的首地址,可以看到保存该字符串的地址为01372120:
现在只需要在未使用的NULL填充区域编写新的内容,然后再修改上述的地址即可。
在Dump窗口查看保存“Hello World!”字符串下面的区域是否有大片的NULL区域:
确实有一大片未使用的NULL区域。随意选01372700地址开始编写新的字符串,步骤和之前一样:
之后修改内存地址即可,将光标移到上述push指令即地址01371007处,按空格键打开Assemble窗口来编辑汇编指令,将地址修改为01372700地址即可:
直接F9运行查看效果:
要想永久保存就要和前面一样另存为一个可执行文件即可。
注意:若把修改后的代码重新保存为程序文件会无法正常运行。原因在于文件被加载到内存并以进程形式运行时,通常进程的内存是存在的,而文件偏移则不存在,因而01372700对应的文件偏移并不存在,从而导致出错。