第五篇了,漏洞分析案例
漏洞利用的灵活程度让这门技术变得似乎没有什么原则可言,只有实践后总结提高才能挥洒自如。
漏洞分析方法
目标:弄清攻击原理、评估潜在利用方式及风险等级。扎实的漏洞利用技术是进行漏洞分析的基础,否则可能将 bug 误判成漏洞,也可以将高危漏洞误判成 DOS 型的中级漏洞。
漏洞来源:挖掘、已公开的漏洞、patch(对比分析打补丁前后的 PE 文件,MS 发布补丁后一周内漏洞还在部分范围存在)
分析方法:动态调试(OllyDbg 等)、静态分析(IDA 等)、指令追踪(对比分析正常运行时记录的指令序列和 poc 触发漏洞之后记录的指令序列)
运动中寻求突破:动态调试技术
调试的原则在于确认,调试的目的在于定位。动态调试通常针对没有源码的程序。
如果说漏洞调试是一门艺术,那么下断点就是这门艺术的精髓,需要深入研究和实践!
断点技巧(OllyDbg)
畸形 RetAddr 断点。将 poc 中溢出后覆盖的函数返回地址修改为一个非法地址,在调试 poc 时能够触发一个非法内存访问错误,使得调试器中断下来。这样的好处是,在调试器中断后,可以从当前栈中找到前一次的函数调用,而它往往就是触发漏洞的函数。这样一来,调试的第一个目的即定位漏洞位置就达到了。见 Yahoo!Messenger 栈溢出漏洞。
条件断点(Shift+F2)。在某个函数入口处下断点,可能会断的太频繁影响正常分析,若不设断点,又很难分析下去。条件断点正好解决这个问题。条件断点是带有条件表达式的普通 INT 3 断点。
例如调试 notepad.exe,要在 CreateFileW() @ kernel.32.dll 处下断点,则用 UNICODE [[esp+4]]=="c:\\test.txt",表示进入 CreateFileW() 后,如果第一个参数 lpFileName==UNICODE("c:\\test.txt") 则断点。等价的命令行:bp CreateFileW UNICODE[[esp+4]]=="c:\\test.txt"。这样,当记事本打开 c:\test.txt 时就会断点暂停。
* 10 表示 unsigned 0x10 (hex) 而 10. 表示 signed 10 (oct) * 表达式 eax 会将 eax 中的内容解释为 unsigned 而 eax. 会解释为 signed * 当带符号数于无符号数比较时,OllyDbg 会将带符号数转化为无符号数,例如 eax<0 恒为假,因为无符号数恒大于等于 0,应该用 eax.<0. * MSG 只能设置在进程消息函数的条件断点内,如 msg==111 表示或消息为 WM_COMMAND(0x111)时为真 * 字符串比较不区分大小写和文本长度,即当 eax 指向的内容为 bRoWn FoXaaa 时 eax=="BROWN FOX” 为真
记录断点(Shift+F4)。也是一种条件断点,每当遇到此类断点或者满足条件时,OllyDbg 会记录已知函数表达式、参数的值。例如可以在一些窗口过程函数上设置记录断点并列出对该函数的所有调用。或者只对接收到的 WM_COMMAND 消息标识符设断,或者对创建文件的函数(CreateFile)设断,并且记录以只读方式打开的文件名等。从记录窗口中浏览几百条消息比按几百次 F9 轻松得多!
在 CreateFileW() 的入口处按 Shift+F4,会弹出设置条件记录断点的对话框:
如上图,意思是在 heap.0x01331309 处(假设这里就是要记录的函数位置)下记录断点,记录内容为 "FilePath=<esp+4 的值>",断点只记录不暂停执行(Pause program: Never)。
消息断点,条件断点的一种(用消息来作表达式),调试 GUI 程序的常用技巧,如分析单击按钮后的处理过程。设置方法有两种:一种用 OllyDbg 的界面导向设置,在 Windows 窗口中右键 Message Breakpoint on ClassProc,但是有时 OllyDbg 获取到的 WinProc 和 ClassProc 不正确,将会无法判断到正确的消息处理函数入口。
另一种方法是设置条件断点,需要在 TranslateMessage() 入口填写条件,然后配合主模块的内存访问断点来断到正确的消息处理函数入口。
GUI 程序创建一个窗口的流程基本如下:
1 注册窗口类(RegisterClass),注册前先填写 RegisterClass 的参数 WNDCLASSEX 结构 2 建立窗口 CreateWindow 3 显示窗口 ShowWindows 4 刷新窗口客户区 UpdateWindow 5 进入消息循环:GetMessage -> DispatchMessage,如果消息是 WM_QUIT 则退出循环
进入消息循环后 GetMessage() 会返回取到的消息,hWnd 参数指定要获取哪个窗口的消息,一般为 NULL,表示获取的是所有本程序所属窗口的消息,wMsgFilterMin 和 wMsgFilterMax 为 0 表示获取所有编号的消息。GetMessage() 从消息队列中取得消息,填写好 MSG 结构并返回。
接下来,TranslateMessage() 将 MSG 结构传给 Windows 进行一些键盘消息的转换,当有键盘按下和放开时,Windows 产生 WM_KEYDOWN / WM_KEYUP 或 WM_SYSKEYDOWN / WM_SYSKEYUP 消息,但这些消息的参数中包含的是按键的扫描码,转换成常用的 ASCII 码要经过查表,很不方便,TranslateMessage 遇到键盘消息则将扫描码换成 ASCII 码并在消息队列中插入 WM_CHAR 或 WM_SYSCHAR 消息,参数就是转换好的 ASCII 码,如此一来,要处理键盘消息的话只要处理 WM_CHAR 消息就好了。遇到别的消息则 TranslateMessage 不做处理。
最后,由 DispatchMessage 将消息发送到窗口对应的过程去处理。窗口过程返回后 DispatchMessage() 才返回,然后开始新一轮消息循环。
可见,每个消息要经历 GetMessage() - TranslateMessage() - DispatchMessage() 三个函数,也就是说只要在这三个函数中任何一个函数下断点,都可以截获相关按钮单击的消息。这是选择 TranslateMessage 作为条件断点的位置,条件用消息来表达。
下面演示如何找到 calc.exe 对于按钮 “1” 的响应代码,按钮 “1” 按下后松开的消息是 WM_LBUTTONUP,TranslateMessage() 原型为:
1 BOOL TranslateMessage( 2 const MSG *lpMsg 3 }; 4 typedef struct { 5 HWND hwnd; // hwnd 不为 NULL 时表示需要处理该消息的窗口句柄,为 NULL 表示是线程消息 6 UINT message; // 消息 ID,应用程序只能使用 message 的低 2 字节(low word),高 2 字节(high word)被系统保留 7 WPARAM wParam; 8 LPARAM lParam; 9 DWORD time; 10 POINT pt; 11 } MSG, *PMSG:
为了能拦截到按钮“1”按下后松开的消息(WM_LBUTTONUP),在 TranslateMessate() 入口设置如下条件断点,命令行输入:
a USER32.TranslateMessage // 跳转到 USER32.TranslateMessage() 入口处
bp TranslateMessage MSG==WM_LBUTTONUP // 设置条件断点
在 calc.exe UI 上用鼠标点下“1”,calc.exe 就会暂停,状态显示 Conditional breakpoint at USER32.TranslateMessage。
在 OllyDbg 调试面片上按下 ALT+F9,回到 calc.exe 主模块空间。下一处函数调用是 USER32.DispatchMessage(),该函数会调用窗口过程函数,为了拦截到窗口过程函数,需要配合使用内存访问断点。首先单步跟进 USER32.DispatchMessage() 的代码,然后打开 Memory 窗口,在 calc.exe 的 text 段上点击 F2 设置内存访问断点,然后直接 F9 继续执行,当 eip 指向 calc.exe 的 .text 时,程序就会暂停(该内存断点的一次性断点,断后就取消)。这时断在了 calc.exe 的主模块空间,所在的指令即是处理按钮“1”的窗口过程处理函数。可以使用 IDA 对该函数进行分析了。
内存断点
OllyDbg 可以设置内存访问断点和内存写入断点,设置方法很简单,在 Dump 窗口中点击要设置的地址,右键选择 Breakpoint - Memory on access / write 即可。对于内存数据也可以设置内存断点,方法类似。
硬件断点
硬件断点使用 4 个调试寄存器(DR0 / DR1 / DR2 / DR3)来设置地址,以及 DR7 设定状态,DR4、DR5 是保留的。可以在代码上设置硬件执行断点,也可以在内存数据上设置硬件访问/写入断点。
设置方法:在要下断点的代码窗口或内存数据窗口右键 - Breakpoint|Hardware on execution/write。
OllyDbg 常用断点速查表 | ||||||
拦截窗口 | bp CreateWindowEx(A/W) | 创建窗口 | 拦截时间 | bp GetLocalTime | 获取本地时间 | |
bp ShowWindow | 显示窗口 | bp GetSystemTime | 获取系统时间 | |||
bp UpdateWindow | 更新窗口 | bp GetFileTime | 获取文件时间 | |||
bp GetWindowText(A/W) | 获取窗口文本 | bp GetTickCount | 系统启动后的毫秒数 | |||
拦截 消息框 |
bp MessageBoxEx(A/W) | 创建消息框 | bp GetCurrentTime | 当前时间(16位) | ||
bp MessageBoxIndirect(A/W) | 创建定制消息框 | bp SetTimer | 创建定时器 | |||
bp IsDialogMessage(A/W) | 消息是否给指定对话框 | bp TimerProc | 定时器超时回调函数 | |||
拦截 对话框 |
bp DialogBox | 创建模态对话框 | GetDlgItemInt | 指定输入框整数值 | ||
bp DialogBoxParam(A/W) | 创建模态对话框 | GetDlgItemText | 指定输入框字符串 | |||
bp DialogBoxIndirect | 创建模态对话框 | GetDlgItemTextA | 指定输入框字符串 | |||
bp DialogBoxIndirectParam(A/W) | 创建模态对话框 | 拦截文件 | bp CreateFile(A/W) | 创建或打开文件 | ||
bp CreateDialog | 创建非模态对话框 | bp OpenFile | 打开文件 | |||
bp CreateDialogParam(A/W) | 创建非模态对话框 | bp ReadFile | 读文件 | |||
bp CreateDialogIndirect | 创建非模态对话框 | bp WriteFile | 写文件 | |||
bp CreateDialogIndirectParam(A/W) | 创建非模态对话框 | bp GetModuleFileName(A/W) | 当前模块路径 | |||
bp GetDlgItemText(A) | 获取对话框文本 | bp GetFileSize | 文件大小 | |||
bp GetDlgItemInt | 获取对话框整数值 | bp SetFilePointer | 设置文件指针 | |||
拦截 注册表 |
bp RegOpenKeyEx(A/W) | 打开子健 | bp FindFirstFileEx(A/W) | 搜索文件 | ||
bp RegQueryValueEx(A/W) | 查找子健 | 拦截驱动器 | bp GetDriveType(A/W) | 获取磁盘驱动器类型 | ||
bp RegSetValue(A/W) | 设置子健 | bp GetLogicalDrives | 获取逻辑驱动器符号 | |||
功能限制 拦截断点 |
bp EnableMenuItem | 禁止或允许菜单项 | bp GetLogicalDriveStrings(A/W) | 根驱动器路径 | ||
bp EnableWindow | 禁止或允许窗口 | 剪贴板 | bp GetClipboardData | 获取剪贴板数据 |
栈回溯
观察和分析漏洞刚刚触发或快要触发时的栈帧,可以快速得到一个函数调用栈,即 Call Stack,通过对这个调用栈可以对漏洞分析的更加透彻,不仅可以看到函数的调用过程,还可以看到每个函数被调用时的参数值。
OllyDbg 中的 Call Stack 窗口(ALT+K)根据选定线程的栈,反向跟踪函数调用顺序及函数参数并显示出来。如果函数创建了标准的堆栈框架(push ebp; mov ebp,esp),则这个任务非常容易完成。现代编译器并不会为栈框架而操心,所以 OllyDbg 另辟蹊径,采用一个变通的方法。例如,跟踪代码到下一个返回处,并计算其中全部的入栈、出栈,及 esp 的修改。如果不成功,则尝试另一个风险更大也更慢的方法:移动栈,搜索所有可能的返回地址,并检查这个地址是否被先前的已分析的命令调用。如果还不行,则会采用启发式搜索。栈移动(Stack Walk)可能会非常慢。OllyDbg 仅在调用栈窗口打开时才会使用。
为了使 OllyDbg 调用栈中的函数(尤其是系统模块函数)地址显示出对应的符号名称,便于调试分析,需要通过 OllyDbg 的插件 StringOD 配置符号文件加载功能:
首先下载 StrongOd 插件(v0.2.6.413 以上版本),并将下载符号库的相关文件(6 个文件:dbgeng.dll、dbghelp.dll、srcsrv.dll、symbolcheck.dll、symsrv.dll、symsrv.yes)复制到 OllyDbg 的安装目录(可以从 WinDbg 目录下复制)。
打开 OllyDbg,选择 Debug | Select path for symbols,设置符号路径,可以准备一个空目录放置微软符号,也可以设成与 WinDbg 所使用的符号目录。
接着设置 StrongOD 选项,选择 Plugins | StringOD | Options,在出现的配置窗口中勾选 Load Symbols 选项。
之后 OllyDbg 就可以像 WinDbg 一样下载符号文件,此时再打开 Call Stack,就可以清楚地看到 Call Stack 中的系统模块函数名称。