第2章 逆向分析Hello World! 程序

1. Hello World!程序

1
2
3
4
5
6
7
8
9
10
11
12
#include "windows.h"
#include "tchar.h"
int _tmain(int argc, TCHAR *argv[])
{
MessageBox(NULL,
L"Hello World!",
L"www.reversecore.com",
MB_OK);
return 0;
}

不论用那种语言编写的程序,编译后都会生成二进制可执行文件.

2. 调试HelloWorld.exe程序

OllyDbg基本界面

提示

  • 分析时可采用 OllyDbg 的 Win32 专业调试工具.
  • 达到一定水平建议使用 Hex -Rays 公司的 IDA Pro.

    代码窗口 : 默认用于显示反汇编代码,适用于显示各种注释,标签,分析代码时显示循环,跳转位置等信息.
    寄存器窗口 : 实时显示CPU寄存器的值,可用于修改特定的寄存器.
    栈窗口 : 实时显示ESP寄存器指向的进程栈内存,并允许修改.

2.1 入口点

调试器停止的地点即为HelloWorld.exe执行的起始地址(4011A0),它是一段EP(EntryPoint入口点)代码.
地址-指令-反汇编代码-注释]

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

2.2 跟踪 40270C 函数

OllyDbg基本指令(适用于代码窗)

指令 快捷键 含义
Restart Ctrl+F2 重新开始调试
Step Into F7 执行一句OP code(操作码),若遇到命令(CALL),将进入函数代码内部
Step Over F8 执行一句OP code(操作码),若遇到命令(CALL),仅执行函数自身,不跟随进入
Execute till Return(执行到返回) Ctrl+F9 一直在函数代码内部运行,直到遇到RETN命令,跳出函数

在 EP 代码的40010A0地址处使用Step Into(F7)指令,进入40270C函数.
40270C函数

4027A1地址处有一个RETN指令,它用于返回函数调用者的下一条指令,一般是被调用函数的最后一句.在4027A1地址处的RETN指令上执行SETP over(F8)或Execute till Return(Ctrl+F9)命令.程序会跳转到4011A5地址处.
4011A5函数

2.3 跟踪 40104F 跳转语句

执行4011A5 地址处的跳转命令JMP 0040104F,跳转到40104F.
40104F处的部分代码

2.4 查找main()函数

在40104F地址开始,每执行一次Step Into(F7)命令就下移1行代码,移动到401056地址处的CALL 402524函数调用命令时,执行Step Into(F7)命令,进入402524函数.
调用40104F

402524函数

我们很难把402524函数称为Main()函数,因为在它的代码中没发现调用MessageBox()API的代码.执行Execute till Return(Ctrl+F9)指令,跳出402524函数,返回到40105B地址处.

同样,在40104F地址处执行Step Into(F7)命令调试,遇到函数调用就进入函数查看代码,确认是否是main()函数.若不是main()函数,则使用(Ctrl+F9)命令跳出相关函数,继续以相同方式调试.
调用API

4010E4地址处的CALL Kernel32.GetCommandLineW指令是调用Win32 API的代码.
main()函数

401000函数内部出现了调用MessageBoxW()API的代码,该API的函数参数为与源码内容一致,由此可以判断401000函数就是我们需要找的main()函数.

3. 进一步熟悉调试器

3.1 调试器指令

指令 快捷键 含义
Go to Ctrl+G 移动到指定地址,用来查看代码或内存,运行时不可用
Execute till Cursor F4 执行到光标位置,即直接转到要调试的地址
Comment ; 添加注释
User-defined comment 鼠标右键菜单Search for User-defined comment
Set/Reset BreakPoint F2 设置或取消断点(BP)
Run F9 运行(若设置了断点,则执行到断点处)
Show the previous Cursor - 显示上一个光标的位置
Preview CALL/JMP address Enter 若光标处有CALL/JMP等指令,则跟踪并显示相关地址(运行时不可用,简单查看函数内容时非常好用)

3.2 “大本营”

当每次重新运行调试器时,调试器会返回到EP处,并从此处新开始调试.

3.3 设置”大本营”的四种方法

  • GoTo命令

设置地址40104F.执行Go to(Ctrl+G)命令,在文本框中输入”40104F”
Go to(Ctrl+G)命令

执行Execute till cursor(F4)命令,让调试运行到该处,然后从40104F处开始调试代码.

  • 设置断点

调试代码,还可以设置BP断点,让调试直接流转到”大本营”.设置断点后,程序运行到断点处将会暂停.

在OllyDbg菜单栏中依次选择View-Breakpoints(快捷键ALT+B),打开Breakpoints对话框,列出代码中设置的断点.
断点

  • 注释

键盘上的”;”键可以在指定位置添加注释,还可以通过命令找到它.

在鼠标右键中依次选择Search for User defined comment,这样就能看到用户输入的所有注释.
用户的注释

红色显示的文字即是光标当前所在的位置,当注释位置与光标位置重合时,将仅以红色方式显示.

  • 标签

我们能通过标签功能在指定的地址添加特定名称.移动光标到40104F地址处,按”:”键输入标签.
标签

4. 快速查找指定代码的四种方法

调试代码时,main()函数并不直接位于可执行代码的EP位置上,出现的是开发工具生成的启动函数.

4.1 代码执行法

按F8键逐行执行命令,在某个时刻弹出信息对话框,显示”hello world!”信息.按Ctrl+F2再次载入待调试的可执行文件并重新调试,不断按F8键,某个时刻一定会弹出信息对话框.弹出信息对话框调用时的函数即为main()函数.

提示
Win32 应用程序中,API函数的参数是通过栈传递的.VC++中默认字符串是使用Unicode码表示,并且,处理字符串的API函数也全部变更为Unicode系列函数.

4.2 字符串检索法

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

OllDbg初次载入待调试程序有预分析过程,此过程中会查看进程内存,程序中引用的字符串和调用的API都会被摘录出来,调整到另外一个列表中.
All referenced text strings

地址401007处有一个PUSH 004092A0命令,该命令引用004092A0处即是字符串”helloworld!”双击,光标定位到main()函数中调用MessageBoxW()函数的代码处,在Dump窗口中使用Go to(Ctrl+G)命令,进一步查看内存4092A0地址处的字符串.
4092A0地址处的字符串

提示:
VC++中,static字符串会被默认保存为Unicode码格式.需要注意的是4092A0地址,它与我们之前代码区地址(401XXX)不同,在helloworld.exe进程中,409XXX地址空间被用来保存程序中的数据, 代码与数据所在的区域是彼此分开的.

4.3 API索引法(1) : 在调用代码中设置断点

鼠标右键菜单 - Search for - All intermodular calls
应用程序想向显示屏输出内容时,需要在程序内部调用Win 32 API.helloworld.exe,它在运行时会弹出一个信息窗口,由此可以判断该程序调用了user32.MessageBox()API.
Intermodular calls

可以查看调用MessageBoxW()的代码,该函数位于40100E地址处,它是user.MessageBoxW()API.

4.4 API索引法(2) : 在API代码中设置断点

鼠标右键菜单 Search for - Name in all calls
如果使用了压缩器/保护器工具对可执行文件进行压缩或保护,因为文件结构的改变.此时用该方法显得十分困难.

DLL代码库会被加载到进程内存中,然后我们可以直接向DLL代码库添加断点,API是操作系统对用户应用程序提供一系列函数,它们位于C:\windows\system32文件夹中的*.dll文件(如kernel32.dll,user32.dll,gdi32.dll,advapi32.dll,ws2_32.dll等)内部.因为编写的应用程序执行某些操作时,必须使用os提供的API向os提出请求,然后与被调用API对用的系统DLL文件就会被加载到应用程序的进程内存.

菜单栏->View-Memory菜单(快捷键ALT+M),打开内存映射窗口.
内存映射窗口

然后通过查找命令将光标定位到MessageBoxW上.

5. 使用”打补丁”方式修改”hello world!”字符串

5.1 “打补丁”

使调试流运行到main函数的起始地址处(401000),在401000地址处按F2设置断点,再按F9执行程序.

5.2 修改字符串的两种方法

直接修改字符缓冲区(buffer).
在其他内存区域生成新字符串并传递给消息函数.

  • 直接修改字符串缓冲区
    在Dump窗口中Ctrl+G快捷键Go to 命令,在窗口中输入4092A0字符串缓冲区.使用鼠标选中4092A0地址的内容,按Ctrl+E快捷键打开编辑窗口 .
    helloworld字符串

在Dump窗口中,选中更改后的字符串,通过鼠标右键,在弹出的菜单中选中Copy to executable file 菜单,然后选中保存即可.

  • 在其他内存区域新建字符串并传递给消息函数
    main()函数

401007地址处有一条PUSH 00409A0命令,它把409A0地址处的”Hello world!”字符串以参数形式传递给MessageBoxW()函数.

我们修改字符串地址为4092A0,下面用Dump窗口查看该部分,相应内存区域由NULL填充(NULL padding)结束.
内存中NULL填充区域

这就是程序中未使用NULL填充区域.

提示
应用程序被加载到内存时有一个最小的内存分配大小,一般为1000.即使程序运行时只占用了100内存,它被加载到内存时依然会分到1000左右的内存,这些内存一部分被程序占用,其余部分分为空余区域,全部被填充为NULL.
从空白区域写入新的字符串

因为新建了缓冲区,接下来把新的缓冲区地址(409F50)作为参数传递给MessageBoxW()函数.为此,我们在代码窗口使用汇编命令修改代码.用空格键打开Assemble窗口.
原字符串地址

在打开的Assemble窗口中输入”PUSH 409F50”指令,那么将把409F50作为新字符串的首地址!
新字符串地址

提示
可执行文件被加载到内存并以进程形式运行时,文件并非原封不动地载入内存,而要遵守一定规则进行.在这一过程中,通常进程的内存地址是存在的,但是相应的文件偏移(offset)并不存在.

6. 小结

  • OllDbg常用命令
指令 快捷键 说明
Step Into F7 执行一条OP code(操作码),遇到CALL命令时,进入函数代码内部.
Step Over F8 执行一条OP code(操作码),遇到CALL命令时,不进入函数代码内部,仅执行函数本身.
Restart Ctrl+F2 再次从头调试(终止调试中的过程,重新载入调试程序)
Go to Ctrl+G 跳转到指定地址(查看代码时使用,非运行时命令)
Run F9 运行(运动断点会暂停)
Execute till return Ctrl+F9 执行函数代码内的命令,直到遇到RETN命令,用于跳出函数整体.
Execute till cursor F4 执行到光标所在的位置(直接调到要指定的位置)
Comment ; 添加注释
User-defined comment 鼠标右键菜单Search for - User-defined comment 查看用户的注释目录
Label : 添加标签
User -defined label 鼠标右键菜单Search for - Usr-defined label 查看用户输入的标签目录
Breakpoint F2 设置或取消断点
All referenced text strings 鼠标右键菜单Search for -All referenced text strings 查看代码中引用的字符串
All intermodular calls 鼠标右键菜单Search for -All intermodular calls 查看代码中调用的所有API函数
Name is all modules 鼠标右键菜单Search for -Name is all modules 查看所有的API函数
Edit data Ctrl+E 编辑数据
Assemble Space 编写汇编代码
Copy to executable file 鼠标右键菜单Copy to executable file 创建文本副本(修改的项目被保留)
  • Assemble(汇编语言)基础指令
指令 说明
CALL XXXX 调用XXXX地址处的函数
JMP XXXX 跳转到XXXX地址处
PUSH XXXX 保存XXXX到栈
RETN 跳转到栈中保持的地址
  • 修改(Path)进程数据与代码的方法
术语 说明
VA(Virtual Address) 进程的虚拟地址
OP code(Operation code) CPU指令(字节码byte code)
PE(Portable Executable) Window可执行文件(EXE,DLL,SYS等)

疑问

  • 快捷键F4与F9最大的不同在于F4可以看做为断点+运行的组合.
  • 启动函数(Stub code)不是用户编写的代码,在调试程序中,我们不需要仔细分析启动函数.