逆向工程核心原理读书笔记——逆向分析Hello World程序

PunkLu 2020年02月08日 366次浏览
逆向工程核心原理读书笔记——逆向分析Hello World程序

代码逆向工程

逆向分析法

分析可执行文件时使用的方法大致分为两种:静态分析法和动态分析法。

  1. 静态分析法

    静态分析法是在不执行代码文件的情形下,对代码进行静态分析的一种方法。静态分析时并不执行代码,而是观察代码文件的外部特征,获取文件的类型(EXE、DLI、DOC、ZIP等)、大小、PE头信息、IMPORT/Export API、内部字符串、是否运行时解压缩、注册信息、调试信息、数字证书等多种信息。此外,使用反汇编工具查看内部代码、分析代码结构也属于静态分析的范畴。

  2. 动态分析法

    动态分析法是在程序文件的执行过程中对代码进行动态分析的一种方法,它通过调试来分析代码流,获得内存的状态等。通过动态分析法,可以在观察文件、注册表(Registry)、网络等的同时分析软件程序的行为。还可以使用调试器(Debugger)分析程序的内部结构与动作原理。

逆向分析Hello World

编写Hello World程序

首先使用Microsoft Visual C编写以下C代码。

#include "windows.h"
#include "tchar.h"


int _tmain(int argc,TCHAR *argv[]){
	MessageBox(NULL,
		"Hello world!",
		"www.punklu.tech",
		MB_OK);
	return 0;
}

经过Complile、Build、Run之后会得到一个编译后可执行的exe文件。运行这个可执行文件后,会弹窗显式Hello World!。

使用OllyDbg分析

使用OllyDbg逆向分析工具打开之前编译好的exe可执行文件。

OllyDbg布局如下所示:

OllyDbg布局

入口点

使用OllyDbg打开可执行文件后,OllyDbg会自动跳转到可执行文件的起始地址,它是一段EP(EntryPoint,入口点)代码。

在我的电脑上的入口点如下图所示:

入口点

EP:

​ EP是Windows可执行文件(EXE,DLL,SYS等)的代码入口点,是执行应用程序时最先执行的代码的起始位置,它依赖于CPU。

OllyDbg基本指令

在开始调试前,需要先了解下OllyDbg的基本指令,以便开始调试。

指令快捷键含义
RestartCtrl + F2重新开始调试(终止正在调试的进程后再次运行)
Step InfoF7执行一句Op code(操作码),若遇到调用命令(CALL),将进入函数代码内部
Step OverF8执行一句Op code(操作码),若遇到调用命令(CALL),仅执行函数自身,不跟随进入
Execute till ReturnCtrl + F9一直在函数代码内部运行,直到遇到RETN命令,跳出函数

查找main函数

调试的目标是找出C++代码中调用的MessageBox的tmain函数。

从EP处开始向下运行,使用F7逐步执行(遇到调用进入调用内部),这个过程需要持续数次,每当进入一个调用内部发现不是要寻找的代码块时,使用CTRL + F9跳转到RETN命令处,再按F7即可回到进入这个调用的地方。直到看到一个被调用(Call)的代码块里调用了MessageBox() API的代码。在我的环境里是执行到address为00401194的代码行时,调用了main函数,main函数里又调用了MessageBox函数。如下图:

调用main函数的代码行:

调用main函数

调用MessageBox的main函数代码块:

MessageBox

调用MessageBox函数所在代码行的前两行是将声明的字符串压入栈供MessageBox使用的代码。Win32应用程序中,API函数的参数是通过栈传递的。VC++中默认字符串是使用Unicode码表示的,并且,处理字符串的API函数也全部变更为Unicode系列函数。

进一步熟悉调试器

调试器操作命令

指令快捷键含义
Go toCtrl + G移动到指定地址,用来查看代码或内存,运行时不可用
Execute till CursorF4执行到光标位置,即直接转到要调试的地址
Comment;添加注释
User-defined comment 鼠标右键菜单Search for User-defined comment,查找自己之前添加的注释
Label:添加标签
User-defined label 鼠标右键菜单Search for User-defined label,查找自己之前添加的标签
Set/Reset BreakPointF2设置或取消断点(BP)
RunF9运行(若设置了断点,则执行至断点处)
Show the current EIP*显示当前EIP(命令指针)位置
Show the previous Cursor-显示上一个光标的位置
Preview CALL/JMP addressEnter若光标处有CALL/JMP等指令,则跟踪并显示相关地址(运行时不可用,简单查看函数内容时非常有用)

设置使用断点

  1. Goto命令

    假设要设置断点的地址为上例中的开始地址为00401005的main函数,执行Goto(Ctrl + G)命令,在打开的对话框中输入这个地址并确认,鼠标光标会自动定位到00401005地址处,执行Execute till cursor(F4)命令,让调试流运行到该处,然后从40104F处开始调试代码就变得非常方便了。

  2. 设置断点

    让光标跳转到想要设置断点的代码行,然后使用快捷键F2即可在改行设置断点。设置好断点后,使用快捷键F9即可快速执行到断点所在的地方。

    可在OllyDbg菜单栏中依次选择View-Breakpoints选项(ALT+B快捷键),查看已经设置好的断点。在断点列表中双击某个断点会直接跳转到相应位置。

  3. 注释

    按键盘上的";"键可以在指定位置处添加注释,还可以通过查找命令鼠标右键菜单Search for User-defined comment找到它。

  4. 标签

    可以使用标签在指定地址添加特定名称。按":"键输入标签。添加标签后,代码变得非常直观。还可以通过查找命令鼠标右键菜单Search for User-defined label找到它。

快速查找指定代码

在上例中,main()函数并不直接位于可执行文件的EP位置上,出现在EP位置上的是开发工具(VISUAL C++)生成的启动函数。需要查看的main()函数距离EP代码很远,如果可以快速查找到main()函数,会为调试带来极大帮助。

代码执行法

在上例中,需要查找的是main()函数中调用的MessageBox函数的代码。在调试器中调试Hello World时,main函数的MessageBox函数在某个时刻就会被调用执行,弹出消息对话框,显示“Hello World!”这条信息。在这种程序功能非常明确时,逐条执行指令来查找需要查找的位置。代码执行法仅适用于被调试的代码量不大、且程序功能明确的情况。倘若被调试的代码量很大并且比较复杂时,此种方法就不再适用了。

字符串检索法

鼠标右键菜单 - search for - All -referenced text strings

OlluDbg初次载入待调试的程序时,都会先经历一个预分析过程。此过程中会查看进程内存,程序中引用的字符串和调用的API都会被摘录出来,整理到另外一个列表中,使用上面的方法即可查看这个列表。如下图所示,可以清晰的看到在C++程序中声明的www.punklu.tech及Hello World!字符串。

查找指定字符串

选中其中一行,并双击即可直接跳转到该代码行。

在代码行上再次双击,即可获得该行的详细汇编代码,如下图所示,可知是把Dump区地址为0042202c的数据压入了栈中供之后MessageBox调用。

将鼠标移到OllyDbg左下角的Dump数据区点击下选中,然后使用Goto快捷键Ctrl+G,在弹出的框中输入0042202c后回车即可跳转向相应的数据。如下图所示:

dump数据区数据

API检索法

在调用代码中设置断点

鼠标右键菜单 - search for - All intermodular calls

Windows编程中,若想向显示器显示内容,则需要使用Win32 API向OS请求显示输出。换言之,应用程序向显示器画面输出内容时,需要在程序内部调用Win32 API。认真观察一个程序的功能后,能够大致推测出它在运行时调用的Win32 API,若能进一步查找到调用的Win32 API,会为程序调试带来极大便利。以HelloWorld.exe为例,它在运行时会弹出一个消息窗口,由此可以推断出该程序调用了user32.MessageBox() API。

在OllyDbg的预分析中,不仅可以分析出程序中使用的字符串,还可以摘录出程序运行时调用的API函数列表。若只想查看程序代码中调用了哪些API函数,可以直接使用All intermodular calls命令。

HelloWorld程序进行查看可以发现MessageBox函数,如下图所示:

API1

双击它,即可快速跳转到该行代码所在处。

在API代码中设置断点

鼠标右键菜单 - Search for - Name in all calls

OllyDbg并不能为所有可执行文件都列出API函数调用列表。使用压缩器/保护器工具对可执行文件进行压缩或保护之后,文件结构就会改变,此时OllyDbg就无法列出API调用列表了(甚至连调试都会变得十分困难)。

压缩器(Run time Packer,运行时压缩器)

​ 压缩器是一个实用压缩工具,能够压缩可执行文件的代码、数据、资源等,与普通压缩不同,它压缩后的文件本身就是一个可执行文件。

保护器

​ 保护器不仅具有压缩功能,还添加了反调试、反模拟、反转储等功能,能够有效保护进程。若想仔细分析保护器,分析者需要具有高级逆向知识。

这种情况下,DLL代码库被加载到进程内存后,可以直接向DLL代码库添加断点。API是操作系统对用户应用程序提供的一系列函数,它们实现于c:\Windows\system32文件夹中的*.dll文件(如kernel32.dll、user32.dll、gdi32.dll、advapi32.dll、ws2_32.dll等)内部。简言之,我们编写的应用程序执行某种操作时(如各种I/O操作),必须使用OS提供的API向OS提出请求,然后与被调用API对应的系统DLL文件就会被加载到应用程序的进程内存。

在OllyDbg菜单栏中依次选择View-Memory菜单(快捷键Alt+M),打开内存映射窗口。如下图所示,可以看到内存中已经加载了USER32库。

MemoryMap

使用OllyDbg中的Name in all modules命令可以列出被加载的DLL文件中提供的所有API。右键-Search for - Name in all modules命令打开All names窗口,单击Name栏目按名称排序,通过键盘敲出MessageBox后,光标会自动定位到MessageBox上。如下图所示:

API列表中的MessageBox

USER32模块中有一个Export类型的MessageBox函数(不同系统环境下函数地址不同)。双击MessageBox函数后就会显示其代码,它实现于USER32.dll库中。如下图所示:

User32中的MessageBox

在该入口行上使用F2设置断点,然后按F9继续运行。会发现在该断点处停止。因为此时已经执行到了调用MessageBox函数的地方。如下图所示:

MessageBox调试

此时右上角寄存器窗口中ESP的值为12FF30,它是进程栈的地址。在右下角的栈窗口中能够看到更详细的信息。此时ESP寄存器中的地址指向的栈里的数据代表的是执行完MessageBox函数后,程序执行流将返回到的地址处,这里就是main函数中调用MessageBox的代码行的下一行的地址。

动态修改Hello World

打补丁

代码逆向分析中,“打补丁”操作是不可或缺的重要主题。利用“打补丁”技术不仅可以修复已有程序中的Bug,还可以向程序中添加新功能。“打补丁”的对象可以是文件、内存,还可以是程序的代码、数据等。本示例中,使用“打补丁”技术把HelloWorld程序消息窗口显示的“Hello world!”字符串更改为其他字符串。

按Ctrl+F2快捷键重新调试,并使调试流运行到main函数的起始地址处(在我的环境里是00401005处),在此位置设置断点。

修改字符串的两种方法

直接修改字符串缓冲区

首先Ctrl+F2重新运行程序。

HelloWorld内存地址

双击上图中的这一行定义“Hello World!”字符串的代码,可以发现,MessageBox函数的字符串参数“Hello World!”保存在42201C处,如下图所示。

HelloWorld内存地址2

将光标移至OllyDbg左下角的数据窗口。使用快捷键Ctrl+G弹出跳转框,输入42201C确认后,可以看到会跳转到“Hello World!”字符串所在的地址处。如下图所示:

HelloWorld数据

将“Hello World!”字符串全部光标选中,然后使用快捷键Ctrl+E打开数据编辑窗口,并将原本的“Hello World!”修改为“Hello punklu!”。如下图所示:

修改HelloWorld

然后F9继续运行程序,会发现弹出框的数据已经变成了更改的“Hello punklu!”。如下图:

修改HelloWorld后

但是这种直接修改字符串缓冲区来修改的方法虽然非常简单,但是缺点是它对新字符串的长度有所限制,新字符串的长度不应比原字符串长。因为原字符串后一般会存在某些有意义的数据,使用更长的字符串覆盖原字符串时,数据可能会遭到破坏,这是非常危险的,实际操作中不建议这样做。

保存更改到可执行文件

​ 上面的调试中,通过修改字符串缓冲区更改了程序显示的消息内容,但是这种更改只是暂时的,终止调试后,程序中的原字符串仍然没有改变。如果想把这种更改永久保存下来,就要把更改后的程序另保存为一个可执行文件。

​ 在左下角的Dump窗口中,选中更改后的“Hello punklu!”字符串,单击鼠标右键,在弹出的菜单中选中Copy to executable file菜单,然后会出现一个新的包含修改后的字符串的窗口,在这个窗口上右键,选择Save file菜单,随便起一个名字,并选择一个路径保存,打开新保存的exe文件,会发现弹框显示的内容已经变成了“Hello punklu!”,说明已经将更改保存到了可执行文件中。

新建并传递给消息函数

如果要用一个特别长的字符串替换原字符串“Hello World!”,出于安全角度考虑,上述方法就不适用了。此时可以换一种方法。

如同方法一中所说,在00401031代码处,将数据区中“Hello World!”字符串所在的42201C行以参数形式传递给MessageBox函数。如果把42201C修改成另一个存放着修改后的数据的数据区的地址,也可以实现修改字符串。

这个想法相当不错,但是要考虑:应该在内存的哪块区域创建新字符串?这个问题需要掌握PE文件格式与虚拟地址(Virtual Address)结构的相关知识。此处任选一块区域即可。

在方法一中修改的字符串地址为42201C,再次查看该部分,向下拖动滑动条,相应内存区域由NULL填充(NULL padding)结束。如下图所示:

NULLPADDING

这就是程序中未使用的NULL填充区域。应用程序被加载到内存时有一个最小的内存分配大小,一般为1000。即使程序运行时只占用100内存,它被加载到内存时仍然会分到1000左右的内存,这些内存一部分被程序占用,其余部分为空闲区域,全部被填充为NULL。

在我的环境里,这一块空闲区域的开始地址为00422F8C,使用Ctrl+E向其中写入新字符串“Hello Reversing punklu!”即可。然后把其地址00422F8C作为参数传递给MessageBox函数。将main函数中定义”Hello World!“字符串的地方由PUSH 42201C改为PUSH 422F8C后确认。然后F9控制程序继续执行,会发现此时的弹框内容已经变为了"Hello Reversing punklu!",如下图所示:

HelloReversingPunklu

用户可以在Assemble窗口中输入任何想输入的汇编指令,输入当时就能在代码中体现出来,也可以被执行,这种”在运行过程中动态修改进程代码“的方式正是调试最强大的功能之一。

若把修改后的代码重新保存为程序文件,可以发现程序无法正常运行,这是由409F50这一地址引起的。可执行文件被加载到内存并以进程形式运行时,文件并非原封不动的被载入内存,而是要遵循一定规则进行。这一过程中,通常进程的内存是存在的,但是相应的文件偏移(offset)并不存在。上面示例中,与内存422F8C对应的文件偏移就不存在,所以修改后的程序无法正常运行。

OllyDbg常用命令

指令快捷键说明
Step InfoF7执行一条OP CODE(操作码),遇到CALL命令时,进入函数代码内部
Step OverF8执行一条OP CODE(操作码),遇到CALL命令时,不进入函数代码内部
RestartCtrl+F2再次从头调试(终止调试中的进程,重新载入调试程序)
Go toCtrl + G跳转到指定地址(查看代码时使用,非运行时命令)
RunF9运行(遇到断点时暂停)
Execute till returnCtrl + F9执行函数代码内的命令,直到遇到RETN命令,用于跳出函数体
Execute till cursorF4执行到光标所在位置(直接转到要调试的位置)
Comment;添加注释
User-defined comment鼠标右键菜单Search for - User-defined comment查看用户输入的注释目录
Label:添加标签
User-defined label鼠标右键菜单Search for - User-defined label查看用户输入的标签目录
BreakpointF2设置或取消断点
All referenced text strings鼠标右键菜单Search for - All referenced text strings查看代码中引用的字符串
All intermodular calls鼠标右键菜单Search for - All intermodular calls查看代码中调用的所有API函数
Name in all modules鼠标右键菜单Search for - Name in all modules查看所有API函数
Edit dataCtrl+E编辑数据
AssembleSpace编写汇编代码
Copy to executable file鼠标右键菜单Copy to executable file创建文本副本(修改的项目被保留)

Assembly(汇编语言)基础指令:

术语说明
VA(Virtual Address)进程的虚拟地址
Op Code(Operation code)CPU指令
PE(Portable Executable)Windows可执行文件(EXE、DLL、SYS等)

扩展知识:

启动函数(Stub code)不是用户编写的代码,而是编译器任意添加的代码。编译程序时。不同编译器会根据自身特点添加不同启动函数,特别是EP代码区域中存在着许多启动函数,它们也被称为启动代码(Startup code)。调试程序时,不需要仔细分析这些启动函数。

PE(Portable Executable)是Windows操作系统下的可执行文件的格式,主要包含了对文件规格的描述。如果不了解PE文件结构的相关知识,将无法进行高级调试。