以下使用的练习示例是《Windows程序设计》第五版第一章的HelloMsg.exe;以及《加密与解密》第三版附带的RebPE.exe;
1 字节顺序
字节存储顺序
和CPU有关;微处理器的存放顺序有正序(Big-Endian),逆序(Little-Endian);Intel逆序,Power-PC正序;
正序:高位字节存入低地址,低位字节存入高地址;
逆序:反之;
十进制7869,十六进制是1EBD;
看一下此数在emu8086中的存放;Intel逆序;低位字节存入低地址,高位字节存入高地址;
1E是高位字节,BD是低位字节;
1E存入01001单元,BD存入01000单元,逆序;
2 调试符号
Windows调试基础 - 符号
当应用程序被链接以后,代码被逐一地翻译为一个个的地址。
使用vs或者windbg等微软的调试工具进行调试的时候,可以方便地使用变量名来查看内存、可以使用函数名称来下断点、可以指定某个文件的某一行来下断点。
这一切背后,是符号在指导调试器工作,pdb或者dbg文件。
(.NET自己有元数据,符号不需要元数据已有的信息)。
程序运行的时候,计算机只需要逐条执行指令即可。而与源代码对应的关系是完全不需要知道的。这就给调试带来了困难。
无论什么编译都有自己的一套用于对应代码和可执行程序。各种编译器都有自己保存类似这种对应关系的办法,有的直接嵌入可执行文件,有的则是独立出来的。
而微软的编译器则是独立产生了这种文件,它就被成为符号文件。
符号文件一般都是pdb文件。
pdb文件,根据微软官方的解释,包含有: 全局变量;局部变量;函数名及入口点;FPO记录;源代码行号。
3 调试寄存器机制
利用调试寄存器机制
Win操作系统提供了两种层次的进程控制和修改机制:
跨进程内存存取机制;
Debug API 监控目标进程运行信息;
这两种是运行在“操作系统”层次之上的。
自386以后,Intel公司已经在其CPU内部集成了Dr0-Dr7一共8个调试寄存器;并且对EFLAGS标志寄存器的功能也进行了扩展,使其也具有一部分调试的能力。
调试信息通知机制。
作为接收方,不能直接接收DrX调试寄存器发出来的中断/异常信息,Windows已经将这个调试信息包装到了Debug API 体系中,每当DrX调试信息被触发时,ExceptionRecord.ExceptionCode部分都被设置成EXCEPTION_SINGLE_STEP,只需要在Debug API 循环中接受这个消息就可以达到目的。
自Win2000起,CreateProcess后,没有办法在目标进程的入口点地址处中断,常见的解决办法有两种。
1 利用Single Step机制
2 利用ntdll.ntcontinue作为跳板
1
OllyDbg,简称OD;www.ollydbg.de;
结合了动态调试和静态分析;
调试Ring 3级程序的首选工具;
可识别大量被C和Windows频繁使用的函数,并能将其参数注释出;
开放插件接口,功能变得越来越强;
当前默认窗口是CPU窗口,调试的大部分操作在这个窗口中进行;它包含5个面板窗口:反汇编面板,寄存器面板,信息面板,数据面板,堆栈面板;
2
OllyDbg的配置
配置在菜单Options里,有界面选项、调试选项,配置保存在ollydbg.ini文件里;
Options-Appearance-Directories,设置UDD文件和插件的路径;
UDD文件是OllyDbg的工程文件,保存当前调试的一些状态,断点、注释等;
将插件复制到目录,主菜单会出现“Plugin”菜单项;
颜色:可根据喜好设置;
调试设置:一般按默认;异常(Exceptions),可以设置让OllyDbg忽略或不忽略哪些异常;可以全选;
加载符号文件
可以让OllyDbg以函数名显示DLL中的函数。
例如MFC42.DLL是以序号输出函数,在OllyDbg显示的是序号,如果加载MFC42.DLL调试符号,则以函数名显示相关输出函数;
加载程序
OllyDbg可以两种方式加载目标调试程序:通过CreateProcess创建进程;利用DebugActiveProcess函数将调试器捆绑到一个正在运行的进程上;
绿色一排按钮用于打开各种窗口;
L:打开日志窗口;
E:打开可执行模块窗口;
M:打开内存映射窗口;
W:打开窗口列表;指被反汇编的程序所包含的窗口;
T:打开线程窗口;查看被反汇编的程序所包含的线程;
C:打开CPU窗口;默认打开;
R:打开搜索结果窗口;
...:打开运行跟踪窗口;
K:打开调用堆栈窗口;
OllyDbg常见问题
1 跟踪程序时乱码
这是因为OllyDbg将一段代码当成数据;没有进行反汇编识别;右键快捷菜单,执行 Analysis/Analyse code (分析/分析代码);
不行,则执行菜单 Analysis/Remove analysis from module (分析/从模块中删除分析);或在UDD目录中删除相应的.udd文件;
2 快速回到当前领空
如果查看代码翻页到其他地方,想快速回到当前CPU所在的指令上,双击寄存器面板中的EIP或单击
3 修改EIP
将光标移到需要的地址;执行右键菜单 New origin here (此处新建EIP);
4 在反汇编窗口键入汇编代码,输入 push E000,提示 未知标识符
不能识别E是字母还是数字;输入 push 0E000;
启动函数
在编写Win32 应用程序时,都必须在源码里实现一个WinMain函数;但Windows程序执行并不是从WinMain函数开始;首先被执行的是启动函数相关代码,这段代码是编译器生成的。
对于VC++程序,它调用的是C/C++运行时启动函数,该函数负责对C/C++运行库进行初始化。
对一个程序跟踪调试后;退出时,此处调试情况默认保存在安装目录下,xxxx.udd文件中;
时间长了安装目录会乱;按图2,设置新的UDD目录;
装载程序;
在CPU窗口右击,选择 转到-表达式;
输入欲运行代码到的地址;假设要运行到004011B4;点击 跟随表达式;
光标定位到004011B4;
打F4,(F4是运行到光标,Execute till cursor),程序运行到004011B4停住;
装载程序;
在CPU窗口右击,选择 转到-表达式;
输入欲运行代码到的地址;假设要运行到00413033;点击 跟随表达式;
光标定位到00413033;
打F4,(F4是运行到光标,Execute till cursor),程序运行到75E305A0停住;
加载程序;
在反汇编代码窗口右击;选择 搜索 - 名称;
名称窗口列出此程序调用的API函数;双击要下断点的API函数;
要跟踪程序的C代码如下;
#include
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
MessageBox (NULL, TEXT ("Hello, Windows 98!"), TEXT ("HelloMsg"), 0) ;
return 0 ;
}
装载程序;先练习跟踪MessageBox函数;
打Ctrl+G,弹出对话框;输入MessageBox;
点击 跟随表达式 按钮;提示要选定一个匹配项;系统dll中以MessageBox打头的函数有多个;
后缀A是ASCII版本;后缀W是宽字符版本;
此处选定MessageBoxA;
确定;自动定位到75231F70处;看CPU窗口第四列,此处是USER32.MessageBoxA函数的地址;
打F2在此行下一个断点;可以点击B按钮,查看已经对此程序下的断点;有2个,一个是前次下的;
现在只跟踪MessageBoxA,禁用另一个断点;在断点上右击弹出菜单即可禁用;
打F9运行程序;中断在75231F70处;
OllyDbg
F7,单步步进,遇到Call跟进;
F8,单步步过,遇到Call跳过;
加载HelloMsg.exe;
停留在004011C0;这是一条Call语句;Call的地址是004027BC;
打F7,代码运行到地址004027BC处;
重新加载程序;停留在004011C0;
打F8,程序运行到下一条,地址004011C5处;没有跟进Call;
加载程序;停留在004011C0;
打F7单步步进,跳到004027BC;
执行数次F7,程序运行到004027C1;
现在想从此call中返回;打Ctrl+F9,执行到返回(Execute till return);OllyDbg会停在遇到的第一个返回命令(RET、RETF、IRET);在此是 00402851;
Ollydbg可以对代码段设置断点;
加载程序;
按 Alt+M 打开内存模板;对代码段,此处是.text区块,按F2键,设置内存访问断点;
运行程序;中断在图2;
运行程序;按前述进入到系统地址空间,76F805A0;
看右侧寄存器窗口,此地址处是系统dll,KERNEL32.dll;
打F7往前走几步;运行到76F805A8;
Windows程序的内存布局中,7XXXXXXX通常是系统地址空间;0040XXXX通常是应用程序地址;
现在想返回应用程序地址空间;打Alt+F9;
程序返回到00402443;
Win32 API 的GetDlgItemTextA函数获取窗口上控件的文本;跟踪此函数有可能获取到密码;
这是根据《加密与解密》第三版第2章所做的练习;使用的示例程序是原书附带的TraceMe.exe;
操作细节只有2个地方和原书不一样;
一个是ollydbg,右击eax寄存器查看数据窗口时,非汉化版本的菜单是 Follow in Dump ,汉化版的菜单是 在转储跟随 ;
一个是1.x版本,改变一条指令为nop空指令,是手动输入nop;2.x版本已经在右键菜单中集成此功能,无需手动输入nop;
ollydbg版本不同,生成的反汇编注释略有差别;
1 装载程序;
2 对 GetDlgItemTextA 函数设置断点;
3 在程序窗口输入用户名pediy,随便输入个密码;点 check 按钮;
4 程序中断在 GetDlgItemTextA 函数地址处;此处是 752468C0;
5 打Alt+F9回到调用函数的地方;004011B6;
先禁用 GetDlgItemTextA 断点;
6 为了方便反复跟踪;在004011AE处下一个断点;
API 函数基本采用__stdcall调用约定:函数入口参数按从右到左的顺序入栈,由被调用者清理栈中参数,返回值放在eax寄存器中;调用API前的push指令,这些push指令将参数放进堆栈以传给API调用;
C代码中的子程序采用C调用约定:函数入口参数按从右到左的顺序入栈,由调用者清理栈中的参数;
GetDlgItemTextA函数,第一个参数是对话框句柄,第二个参数是控件标识(id),第三个参数是文本缓冲区指针,第四个参数是最大字符数;
7 push 51 ...... push esi,这四句是把调用GetDlgItemTextA需要的参数压入堆栈;
8 打F8,使程序运行到004011B0停住;在寄存器窗口右击EAX,在菜单选择 在转储跟随;
9,10 EAX内容为 0019F790,数据窗口定位到0019F790;此时数据窗口没有什么有价值的;ASCII是些乱码;
11 打F8,执行完CALL EDI一句;即调用了GetDlgItemTextA函数;获取了用户在对话框输入的内容;
此时用户输入的用户名,字符串pediy,出现在数据窗口;
12 004011E5 到 004011F5,这段是序列号的判断核心,最后一句跳转语句不跳转,即可注册成功;
13 使程序运行到004011F5停住;
14 此时可以在寄存器窗口,Z标志,右击,选择菜单 切换,改变Z标志的值为1或0,来判断;
15,16 或者;右击004011F5,选择如图15菜单;把此句跳转指令改为NOP,空指令;
17 改为空指令后,判断语句失效;随便输入的密码注册成功;
TraceMe先进行到此;密码尚未破解;下回再搞;