《逆向工程核心原理》第一~二章知识整理
对应《逆向工程核心原理》第一章到第二章内容
逆向工程
逆向工程(软件反向工程),是指从可运行的程序系统出发,运用解密、反汇编、系统分析、程序算法理解等多种计算机技术,对软件的结构、流程、代码等进行逆向拆解和分析,从而推导出软件产品的源代码、设计原理、结构、算法、处理过程、运行方法及相关文档等。
用途:
- 逆向分析已经编译好的软件,然后使用高级语言重现;
- 分析 病毒 ,提取出特征码,开发杀毒程序;
- 高级代码审计,在汇编层面调试 审计程序 ;
- 游戏 外挂 ,反外挂,破解软件;
- 分析挖掘 嵌入式 设备中的漏洞。
逆向分析法
静态分析法
- 不执行代码
- 观察代码文件外部特征,获取文件类型(exe,dli,doc,zip)大小,PE 头信息,Import/Export API,内部字符串,是否运行时解压缩,注册信息,调试信息,数字证书
- 使用反汇编工具查看内部代码,分析代码结构
动态分析法
- 执行程序文件,通过调试分析代码流,获得内存状态
- 在观察文件,注册表,网络等同时分析软件程序的行为
- 常用调试器分析程序的内部结构与动作原理
源代码、十六进制代码、汇编代码
源代码编译为 二进制 可执行文件,
二进制代码文件转换为 十六进制 代码文件方便查看,
十六进制用 调试器 反汇编 得到汇编代码
逆向分析"Hello World"
源代码
#include "windows.h"
#include "tchar.h"
int _tmain(int argc, TCHAR* argv[])
{
MessageBoxW(NULL,
L"Hello World!",
L"www.reversecore.com",
MB_OK);
return 0;
}
编译器是 Visual C++
把源代码编译得到 exe 可执行文件
初步调试
用 OllyDbg 打开上面编译得到的 exe 文件,下面利用这个 exe 文件介绍 OllyDbg 的用法
窗口
- 代码窗口:
-
- 默认用于显示反汇编代码。
- 显示各种注释、标签。
- 分析代码时显示循环、跳转位置等信息。
- 寄存器窗口:
-
- 实时显示CPU寄存器的值。
- 可用于修改特定的寄存器。
- 数据窗口:
-
- 以Hex、ASCII、Unicode值的形式显示进程的内存地址。
- 也可在此修改内存地址。
- 栈窗口:
-
- 实时显示ESP(栈指针)寄存器指向的进程栈内存。
- 并允许修改。
快捷键
指令 | 快捷键 | 含义 |
Restart | Ctrl+F2 | 重新开始调试(终止正在调试的进程后再次运行) |
Step Into | F7 | 执行一句 OP code(操作码),若遇到调用命令(CALL),将进入函数代码内部 |
Step Over | F8 | 执行一句 OP code,若遇到 CALL,仅执行函数自身,不跟随进入 |
Execute till Return | Ctrl+F9 | 一直在函数代码内部运行,直到遇到 RETN 命令,跳出函数 |
Go to | Ctrl+G | 移动到指定地址,用来查看代码或内存,运行时不可用 |
Execute till Cursor | F4 | 执行到光标位置,即直接转到要调试的地址 |
Comment | ; | 添加注释 |
Label | : | 添加标签 |
Set/Reset BreakPoint | F2 | 设置或取消断点(BP) |
Run | F9 | 运行(若设置了断点,则执行至断点处) |
Show the current EIP | * | 显示当前 EIP(命令指针)位置 |
Show the previous Cursor | - | 显示上一个光标的位置 |
Preview CALL/JMP address | Enter | 若光标处有 CALL/JMP 等指令,则跟踪并显示相关地址(运行时不可用,简单查看函数内容时非常有用) |
汇编基础指令
CALL XXXX | 调用 XXXX 地址处的函数 |
JMP XXXX | 跳转到 XXXX 地址处 |
PUSH XXXX | 保存 XXXX 到栈 |
RETN | 跳转到栈中保持的地址 |
入口点
调试器停止的地点即 exe 文件执行的起始地址(4011A0),它是一段 EP (可执行文件的入口点)代码
- EP 是执行应用程序时最先执行的代码的起始位置,它依赖于 CPU
在寄存器窗口可以找到 EP
一开始调试代码(或重新运行调试器时),代码窗口的调试流(即黑色高亮处)都是运行在 EP 上
四个列的含义
- 地址:进程的虚拟内存地址(VA)
- 指令:IA32(或 x86)CPU 指令
- 反汇编代码:将 OP code 转换为便于查看的汇编指令
-
- 本例红框内即:先调用(CALL)40270C 地址处的函数,再跳转至(JMP)40104F 地址处
- 注释:调试器添加的注释(根据选项不同,显示的注释略有不同)
跟踪 40270C 函数
以下涉及 ollydbg 快捷键的使用,需要弄明白:
在某地址处->表示调试流运行到当前地址,表现为该地址是黑色高亮的.
在非 CALL 指令处,按下 F7,F8 快捷键,都是让程序执行一句操作码,表现为往下读一位
弄清楚操作与对应的表现,可借助代码区域下的解释区域辅助理解
我们的目标是在 main()函数中找出调用 MessageBox()函数的代码
在 EP 代码的 4011A0 地址处(黑色高亮处)使用 F7 快捷键进入 40270C 函数
- 最右侧注释的红字部分:
-
- 代码中调用的 API 函数名称,只需要看名称即可
- 这些函数不是在源代码中调用的函数,也不是要查找的 main 函数
- 它们是 Visual C++位保证程序正常运行自动添加的启动函数
- 不需要着重关注
- 4027A1 地址处有一条 RETN 指令
-
- 一般在被调用函数的最后一句(长括号的尾巴部分)
- 用于返回到函数调用者的下一条指令
- 如何让程序到 4027A1 地址
-
-
- 在刚刚的 40270C 地址处(黑色高亮在 40270C),按 F7 快捷键,会执行一句 OP code 操作码(往下读一行)
-
-
-
- 多次按 F7(或 F8) 快捷键,直到程序(黑色高亮)到 4027A1 地址(此处就可以看到 RETN 指令了)
-
-
- 在 4027A1 地址处(黑色高亮在 4027A1)上按 F7(或 F8) 快捷键,就可以发现程序跳转到了 4011A5 地址处
跟踪 40104F 跳转语句
本段开始,省略上段重复的详细提示
在上一段最后,我们到了 4011A5 地址处
在 4011A5 地址处按 F7,跳转到 40104F 地址处
查找 main() 函数
从 40104F 地址处开始逐条分析个函数调用指令,就能够找到要查找的 main()函数
在 40104F 地址处多次按 F7,直至移动到 401056 地址处,调用 CALL402524 指令,就可以进入 402524 函数了
很难把 402524 函数称为 main()函数,因为在它的代码中未发现调用 MessageBox() API 的代码
按 Ctrl+F9 调试转到 402568 地址处的 RETN 指令,
按 F7(或 F8)执行 RETN 指令,跳出 402524 函数,返回 40105B 地址处,
- 我们刚刚是在上一条 40105B 地址处,通过命令 CALL 402524 进入的 402524,然后在 402524 函数里跳转到函数结束的位置 402568,从而离开 402524 函数,开始进入本条 40105B
- 也就是,我们遇到指令 CALL,就可以按 F7 进入函数查看一下,如果不是我们的目标函数,就按 Ctrl+F9 跳到该函数结束的位置,再按 F7 跳出该函数,进入下一条操作码
这样重复不断地进入函数,跳出函数,我们的操作码不断往下读取
到了 4010E4 这里,注释是红字部分,这是 Win32 API 的代码,直接按 F8 跳过即可,就无需多此一举进入看是不是我们需要找的 main()函数
一路读取下来,到了 401144 时,我们还是按 F7 进入函数
发现了这个函数内部出现了调用 MessageBox() API 的代码,该函数参数为"www.reversecore.com"与"Hello World!"两个字符串,就是我们上面源码的内容,所以我们断定 401000 函数就是我们在查找的 main()函数
进一步调试
地址跳转
按 Ctrl+G 打开窗口,输入地址把光标定位到某地址处
这里让光标定位到 40104F 位置后,按 F4 即可让调试流运行到该处
设置断点
F2 可以设置某地址为硬件断点(BP),调试运行到断点将会暂停
设置断点后,地址会变红
按 ALT+B,可以打开断点窗口,列出已设置的断点,双击可跳转对应的位置
注释
按 ; 可以在指定地址添加注释,
鼠标右键选择 Search for-User defined comment 就可以看到输入的所有注释
标签
按 : (即 shift+;)即可输入标签
标签后,由于 40104F 是 Jump from 4011A5,所以当我们到 4011A5 可以看到已经打上了标签
鼠标右键选择 Search for-User defined labels 就可以看到设置的标签
快速查找指定函数
代码执行法
- 适用于被调试的代码量不大,程序功能明确时
- 从 40104F 开始,多次按 F8,直到操作码不再往下读取时
同时会发现弹出了一个窗口
弹出这个对话框时调用的函数即为 main()函数
按 Ctrl+F2 重新载入文件调试,然后直接跳转到 401144,并按 F4 让调试流到 401144,
按 F7 进入被调用的函数,可以发现该函数就是要找的 main()函数
跟前面一样,我们进入了 main()函数内部
- 401002 与 401007 处有 PUSH 语句,把消息对话框的标题与显示字符串(Title,Text)保存到栈中,并作为参数传递给 MessageBoxW()函数
- Win32 应用程序中,API 函数的参数是通过栈传递的,VC++中默认字符串是使用 Unicode 码表示的,处理字符串的 API 函数也全部变更为 Unicode 系列函数
字符串检索法
OllyDbg 初次载入待调试的程序时,都会先经历一个预分析过程,此过程会查看进程内存,程序中引用的字符串和调用的 API 都会被摘录出来,整理到另外一个列表中,这样的列表对调试是相当有用的,使用 All referenced text strings 命令会弹出一个窗口,其中列出了程序代码引用的字符串
鼠标右键菜单-Search for-All referenced text strings
地址 401007 处有一条 PUSH 004092A0 命令,该命令引用的 004092A0 处即是“Hello World!”,双击“Hello World!”即可跳转到该地址
在数据 dump 窗口(左下角的窗口)中,按 Ctrl+G,查看位于内存 4092A0 的字符串
下面的灰色部分即是“Hello World!”字符串,它是以 Unicode 码形式表示的,字符串的后面被填补上了 NULL 值
- VC++中,static 字符串会被默认保存为 Unicode 码形式,static 字符串是指在程序内部被硬编码的字符串
- 在 HelloWorld.exe 进程中,409XXX 地址空间被用来保存程序使用的数据,它与代码区域地址(401XXX)是彼此分开的
API 检索法(1):在调用代码中设置断点
- Windows 编程中,相向显示器显示内容,需要调用 Win32API 向 OS 请求显示输出。通过程序的功能大概推测运行时调用的 Win32API,从而进行查找
HelloWorld.exe 在运行时弹出一个消息窗口,可推断出该程序调用了 user32.MessageBoxW() API
鼠标右键 search for-All intermodular calls
API 检索法(2):在 API 代码中设置断点
- OllyDbg 不能为所有可执行文件都列出 API 函数调用列表,如果使用压缩器/保护器工具对可执行文件进行压缩或保护之后,文件结构就会改变,Ollydbg 就无法列出 API 调用列表了(甚至连调试都会变得困难)
-
- 压缩器:压缩可执行文件的代码、数据、资源等,压缩后的文件本身是一个可执行文件
- 保护器:具有压缩功能,和反调试、反模拟、反转储功能,能有效保护进程
- 这种情况下,DLL 代码库被加载到进程内存后,可以直接向 DLL 代码库添加断点,API 是操作系统对用户应用程序提供的一系列函数,它们实现于 C:\\Windows\system32 文件夹中的*.dll 文件(kernel32.dll、user32.dll、gdi32.dll 等)内部。就是说,我们编写的程序执行某种操作时(如 I/O 操作)必须使用 OS 提供的 API 向 OS 提出请求,然后与被调用 API 对应的系统 DLL 文件就会被加载到程序的进程内存
按 Alt+M,打开内存映射窗口
从上框可以看出,USER32 库被加载到了内存
鼠标右键 search for-Name in all modules 可以列出被加载的 DLL 文件中提供的所有 API,
单击 Name 栏目按名称排序,通过键盘敲出 MessageBoxW 后,光标会自动定位到 MessageBoxW 上
USER32 模块中有一个 Export 类型的 MessageBoxW 函数(不同系统环境下函数地址不同),它实现于 USER32.dll 库中
双击该函数就会显示其代码,观察 MessageBoxW 函数的地址空间可以发现,它与 HelloWorld.exe 使用的地址空间完全不同,
在函数的起始地址上按 F2 键,设置好断点后按 F9 继续执行
会发现程序运行到该处停止了,说明 HelloWorld.exe 应用程序中调用了 MessageBoxW()API
此时寄存器窗口中 ESP 的值为 19FB04(不同系统环境下地址可能不同),它是进程栈的地址
右下角的栈窗口有详细的信息
19FB04 处对应一个返回地址 401014,main()函数调用完 MessageBoxW 函数后,程序执行流将返回到该地址处。按 Ctrl+F9 使程序运行到 MessageBoxW 函数的 RETN 命令处,然后按 F7 键也可以返回到 401014 地址处
修改字符串
- 修改的对象可以是文件、内存、代码、数据
前面我们已经查找到了调用 MessageBoxW 的部分和"Hello World!"字符串的地址,已经成功了一半
按 Ctrl+F2 重新调试,使调试流运行到 main 函数的起始地址 401000,并在 401000 地址按 F2 设置断点,再按 F9 执行程序
直接修改字符串缓冲区
"Hello World!"保存在地址 4092A0 处的一段缓冲区,只要修改这段内容,就可以修改对应的字符串
在数据窗口按 Ctrl+G ,在弹出的窗口中输入 4092A0 进入字符串缓冲区,用鼠标选中 4092A0 地址处的字符串,按 Ctrl+E 打开编辑窗口
- Unicode 编码中用 2 个字节表示 1 个罗马字母
- 若新字符串长度大于原有字符串,执行覆盖操作时可能损坏字符串后面的数据,从而导致数据损坏,程序内存引用错误
在 UNICODE 栏中输入"Hello Reversing"字符串,注意 Unicode 字符串必须以 NULL 结束,它占据两个字节,但NULL 不直接在 UNICODE 文本框里添加,而是在 HEX 项目中添加
所以在窗口中,先取消"Keep size",再在 UNICODE 栏输入"Hello Reversing"
接着在 HEX 栏补足两个字节的 0
(注:这里用更长的字符串替代,只是为了演示)
修改后,虽然指令保持不变,但字符串已经被取代了,用作 MessageBoxW()函数的参数,并且参数的地址仍未 4092A0,只是该地址空间中的内容(字符串)发生了改变
按 F9 运行程序后,成功了
如果要把这种修改保存到可执行文件,在数据窗口中,选中更改后的"Hello Reversing"字符串,鼠标右键-Copy to executable file,打开 Hex 窗口
在这个窗口里右键-Save file ,就可以打开保存窗口了
再次打开,发现字符串已经修改好了
在其他内存区域新建字符串并传递给消息函数
如果用字符串"Hello Reversing World!!!"替代"Hello World!",就不能用上面的方法了,
按 Ctrl+F2 重启调试,按 F9 运行,由于之前 main 函数设置了断点 401000,所以调试流自动转到 main()函数,
401007 地址有一条 PUSH 004092A0 命令,它把 4092A0 地址处的"Hello World!"字符串以参数形式传递给 MessageBoxW()函数
- 向 MessageBoxW()函数传递字符串参数时,传递的是字符串所在区域的首地址,如果改变字符串地址,消息框就会显示变更后的字符串。在内存的某个区域新建一个长字符串,并把新字符串的首地址传递给 MessageBoxW()函数,可以认为传递的是完全不同的字符串地址
新字符串可以在数据窗口最下面由 NULL 填充的区域上创建
- 应用程序被加载到内存时有一个最小的内存分配大小,一般为 1000,即使程序运行只占 100,也会分配 1000,剩下的由 NULL 填充
不妨选取 409F50 地址新建缓冲区,UNICODE 输入"Hello Reversing World!!!",然后 HEX 栏记得补充上 2 字节的 0
下面我们就把 409F50 地址作为参数传递给 MessageBowW()函数。在代码窗口中使用汇编命令修改代码,将光标置于 401007 地址,按空格键打开 Assemble 窗口
修改地址为 40AC40,即新字符串的首地址,
再按 F9 运行程序
成功了,PS:建议先按照上面的地址进行修改,使用其他空地址可能会出现修改错误的情况
- 若把修改后的代码重新保存为程序文件,会发现程序无法正常运行,这是 409F50 这一地址引起的。可执行文件被加载到内存并以进程形式运行时,文件并非原封不动地被载入内存,而是要遵循一定规则进行。这一过程中,通常进程的内存是存在的,但是相应的文件偏移并不存在。上面示例中,与内存 409F50 对应的内存偏移就不存在,所以修改后的程序无法正常运行
总结
前两章主要是关于 OllyDbg 的初步使用,
主要是通过一个简单的程序,学会 OllyDbg 快捷键的使用,学会一些调试的操作,初步明白一些常用操作的含义,初步学会修改字符串这一简单操作