1. Hello World!程序
|
|
不论用那种语言编写的程序,编译后都会生成二进制可执行文件.
2. 调试HelloWorld.exe程序
提示
- 分析时可采用 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函数.
4027A1地址处有一个RETN指令,它用于返回函数调用者的下一条指令,一般是被调用函数的最后一句.在4027A1地址处的RETN指令上执行SETP over(F8)或Execute till Return(Ctrl+F9)命令.程序会跳转到4011A5地址处.
2.3 跟踪 40104F 跳转语句
执行4011A5 地址处的跳转命令JMP 0040104F,跳转到40104F.
2.4 查找main()函数
在40104F地址开始,每执行一次Step Into(F7)命令就下移1行代码,移动到401056地址处的CALL 402524函数调用命令时,执行Step Into(F7)命令,进入402524函数.
我们很难把402524函数称为Main()函数,因为在它的代码中没发现调用MessageBox()API的代码.执行Execute till Return(Ctrl+F9)指令,跳出402524函数,返回到40105B地址处.
同样,在40104F地址处执行Step Into(F7)命令调试,遇到函数调用就进入函数查看代码,确认是否是main()函数.若不是main()函数,则使用(Ctrl+F9)命令跳出相关函数,继续以相同方式调试.
4010E4地址处的CALL Kernel32.GetCommandLineW指令是调用Win32 API的代码.
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”
执行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都会被摘录出来,调整到另外一个列表中.
地址401007处有一个PUSH 004092A0命令,该命令引用004092A0处即是字符串”helloworld!”双击,光标定位到main()函数中调用MessageBoxW()函数的代码处,在Dump窗口中使用Go to(Ctrl+G)命令,进一步查看内存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.
可以查看调用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快捷键打开编辑窗口 .
在Dump窗口中,选中更改后的字符串,通过鼠标右键,在弹出的菜单中选中Copy to executable file 菜单,然后选中保存即可.
- 在其他内存区域新建字符串并传递给消息函数
401007地址处有一条PUSH 00409A0命令,它把409A0地址处的”Hello world!”字符串以参数形式传递给MessageBoxW()函数.
我们修改字符串地址为4092A0,下面用Dump窗口查看该部分,相应内存区域由NULL填充(NULL padding)结束.
这就是程序中未使用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)不是用户编写的代码,在调试程序中,我们不需要仔细分析启动函数.