定位堆相关问题:OllyDbg2的off-by-one漏洞分析
引言
昨天下午,我在编写代码时遇到了问题,代码无法正常工作。于是,我启动了调试器来查看底层发生了什么。奇怪的是,我在x86内联汇编中故意插入了一个int3
指令,当文件加载到OllyDbg2后,我按下F9快速到达int3
位置。单步执行后,程序突然崩溃了。这种情况虽然不常见,但确实发生了。于是我重新启动二进制文件,尝试复现这个漏洞:同样的操作,再次崩溃。这次我确认了OllyDbg2中存在一个可复现的崩溃漏洞。
我喜欢遇到这种情况(还记得之前在OllyDbg/IDA中发现的崩溃漏洞吗:PDB Ain't PDD),因为这对我来说是一个很好的练习机会:
- 定位应用程序中的漏洞:对于大型应用程序来说通常并不简单。
- 逆向工程涉及漏洞的代码,以理解为什么会发生(有时我有源代码,有时没有,比如这次)。
在本文中,我将展示如何使用GFlags、PageHeap和WinDbg定位漏洞,然后逆向工程漏洞代码,理解其成因,并编写一个干净的触发程序。
目录
- 引言
- 崩溃现象
- 定位堆问题:介绍完整PageHeap
- 深入OllyDbg2内部
- 家庭复现
- 有趣的事实
- 结论
崩溃现象
我首先启动了WinDbg来调试OllyDbg2,以调试我的二进制文件(是的,套娃调试)。当OllyDbg2启动后,我按照之前的步骤复现漏洞,WinDbg显示了以下信息:
HEAP[ollydbg.exe]: Heap block at 00987AB0 modified at 00987D88 past requested size of 2d0
(a60.12ac): Break instruction exception - code 80000003 (first chance)
eax=00987ab0 ebx=00987d88 ecx=76f30b42 edx=001898a5 esi=00987ab0 edi=000002d0
eip=76f90574 esp=00189aec ebp=00189aec iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200202
ntdll!RtlpBreakPointHeap+0x23:
76f90574 cc int 3
堆分配器显示了一条调试信息,告诉我们进程在其堆缓冲区之外进行了写入。但问题是,这条消息和断点并不是在错误写入时触发的,而是在后续调用分配器时触发的。此时,分配器会检查块是否正常,如果发现异常,就会输出消息并中断。堆栈跟踪也证实了这一点:
0:000> k
ChildEBP RetAddr
00189aec 76f757c2 ntdll!RtlpBreakPointHeap+0x23
00189b04 76f52a8a ntdll!RtlpCheckBusyBlockTail+0x171
00189b24 76f915cf ntdll!RtlpValidateHeapEntry+0x116
00189b6c 76f4ac29 ntdll!RtlDebugFreeHeap+0x9a
00189c60 76ef34a2 ntdll!RtlpFreeHeap+0x5d
00189c80 75d8537d ntdll!RtlFreeHeap+0x142
00189cc8 00403cfc KERNELBASE!GlobalFree+0x27
00189cd4 004cefc0 ollydbg!Memfree+0x3c
...
正如我们上面所说,堆分配器的消息可能是在OllyDbg2尝试释放内存块时触发的。问题的关键在于我们不知道:
- 堆块是在哪里分配的。
- 错误的写入发生在哪里。
这使得我们的漏洞在没有合适工具的情况下难以调试。如果你想了解更多关于高效调试堆问题的信息,强烈建议阅读《Advanced Windows Debugging》中的堆章节(感谢Ivan)。
定位堆问题:介绍完整PageHeap
简而言之,完整PageHeap选项对于诊断堆问题非常强大,至少有以下两个原因:
- 它会保存每个堆块的分配位置。
- 它会在块的末尾分配一个保护页(因此当错误写入发生时,可能会触发写入访问异常)。
为了实现这一点,该选项稍微改变了分配器的工作方式(它为每个堆块添加了更多的元数据等)。如果你想了解更多信息,可以尝试在有/无PageHeap的情况下分配内存并比较分配的内存。以下是启用完整PageHeap时堆块的样子:
要为ollydbg.exe启用它,非常简单。我们只需启动gflags.exe二进制文件(位于Windbg的目录中),然后勾选要启用的功能。
现在,你只需在WinDbg中重新启动目标程序,复现漏洞,以下是我现在得到的结果:
(f48.1140): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.eax=000000b4 ebx=0f919abc ecx=0f00ed30 edx=00000b73 esi=00188694 edi=005d203c
eip=004ce769 esp=00187d60 ebp=00187d80 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
ollydbg!Findfreehardbreakslot+0x21d9:
004ce769 891481 mov dword ptr [ecx+eax*4],edx ds:002b:0f00f000=????????
太棒了,现在我们知道问题出在哪里了。让我们获取更多关于堆块的信息:
0:000> !heap -p -a ecxaddress 0f00ed30 found in_DPH_HEAP_ROOT @ 4f11000in busy allocation( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)f6f1b2c: f00ed30 2d0 - f00e000 20006e858e89 verifier!AVrfDebugPageHeapAllocate+0x0000022976f90d96 ntdll!RtlDebugAllocateHeap+0x0000003076f4af0d ntdll!RtlpAllocateHeap+0x000000c476ef3cfe ntdll!RtlAllocateHeap+0x0000023a75d84e55 KERNELBASE!GlobalAlloc+0x0000006e00403bef ollydbg!Memalloc+0x00000033004ce5ec ollydbg!Findfreehardbreakslot+0x0000205c004cf1df ollydbg!Getsourceline+0x0000007f00479e1b ollydbg!Getactivetab+0x0000241b0047b341 ollydbg!Setcpu+0x000006e1004570f4 ollydbg!Checkfordebugevent+0x00003f380040fc51 ollydbg!Setstatus+0x00006441004ef9ef ollydbg!Pluginshowoptions+0x0001214f
通过这个非常有用的命令,我们得到了很多相关信息:
- 这个块的大小为0x2d0字节。因此,从0xf00ed30到0xf00efff。
- 错误的写入现在有了意义:应用程序尝试在其堆缓冲区之外写入4字节(我猜是无符号数组的off-by-one)。
- 内存是在ollydbg!Memalloc中分配的(由ollydbg!Getsourceline调用,与PDB相关?)。我们将在后文中研究这个例程。
- 错误的写入发生在地址0x4ce769。
深入OllyDbg2内部
我们有点幸运,涉及此漏洞的例程相对简单,Hexrays工作得非常顺利。以下是漏洞函数的C代码(至少是重要的部分):
//ollydbg!buggy @ 0x004CE424
signed int buggy(struct_a1 *u)
{int file_size;unsigned int nbchar;unsigned __int8 *file_content;int nb_lines;int idx;// ...file_content = (unsigned __int8 *)Readfile(&u->sourcefile, 0, &file_size);// ...nbchar = 0;nb_lines = 0;while(nbchar < file_size){// 计算文件中的字符数和行数// ...}u->mem1_ov = (unsigned int *)Memalloc(12 * (nb_lines + 1), 3);u->mem2 = Memalloc(8 * (nb_lines + 1), 3);if ( u->mem1_ov && u->mem2 ){nbchar = 0;nb_lines2 = 0;while ( nbchar < file_size && file_content[nbchar] ){u->mem1_ov[3 * nb_lines2] = nbchar;u->mem1_ov[3 * nb_lines2 + 1] = -1;if ( nbchar < file_size ){while ( file_content[nbchar] ){// 消耗一行,递增直到找到'\r'或'\n'序列// ..}}++nb_lines2;}// BOOM!u->mem1_ov[3 * nb_lines2] = nbchar;// ...}
}
让我解释一下这个例程的作用:
这个例程在OllyDbg2找到二进制文件的PDB数据库时被调用,更准确地说,是在数据库中找到应用程序源代码的路径时。在调试时,这些信息非常有用,OllyDbg2可以告诉你当前位于C代码的哪一行。
- 第10行:"u->Sourcefile"是一个指向源代码路径的字符串指针(在PDB数据库中找到)。该例程只是读取整个文件,给出其大小,以及存储在内存中的文件内容指针。
- 第12到18行:一个循环,计算源代码中的总行数。
- 第20行:我们分配了我们的块。它分配了12*(nb_lines + 1)字节。我们之前在WinDbg中看到块的大小是0x2d0:这意味着我们正好有((0x2d0 / 12) - 1) = 59行源代码:
D:\TODO\crashes\odb2-OOB-write-heap>wc -l OOB-write-heap-OllyDbg2h-trigger.c
59 OOB-write-heap-OllyDbg2h-trigger.c
很好。
- 第24到39行:一个与之前类似的循环。基本上是再次计算行数,并用一些信息初始化我们刚分配的内存。
- 第41行:我们的漏洞。不知何故,我们可能在循环结束时得到"nb_lines2 = nb_lines + 1"。这意味着第41行将尝试在缓冲区之外写入一个单元格。在我们的例子中,如果"nb_lines2 = 60",而我们的堆缓冲区从0xf00ed30开始,这意味着我们将尝试写入(0xf00ed30+60*4)=0xf00f000。这正是我们之前看到的。
此时,我们已经完全解释了漏洞。如果你想进行动态分析以跟踪重要的例程,我设置了几个断点,如下:
bp 004CF1BF ".printf \"[Getsourceline] %mu\\n[Getsourceline] struct: 0x%x\", poi(esp + 4), eax ; .if(eax != 0){ .if(poi(eax + 0x218) == 0){ .printf \" field: 0x%x\\n\", poi(eax + 0x218); gc }; } .else { .printf \"\\n\\n\" ; gc; };"
bp 004CE5DD ".printf \"[buggy] Nbline: 0x%x \\n\", eax ; gc"
bp 004CE5E7 ".printf \"[buggy] Nbbytes to alloc: 0x%x \\n\", poi(esp) ; gc"
bp 004CE742 ".printf \"[buggy] NbChar: 0x%x / 0x%x - Idx: 0x%x\\n\", eax, poi(ebp - 1C), poi(ebp - 8) ; gc"
bp 004CE769 ".printf \"[buggy] mov [0x%x + 0x%x], 0x%x\\n\", ecx, eax * 4, edx"
在我的环境中,它给出了如下输出:
[Getsourceline] f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c
[Getsourceline] struct: 0x0
[...]
[Getsourceline] oob-write-heap-ollydbg2h-trigger.c
[Getsourceline] struct: 0xaf00238 field: 0x0
[buggy] Nbline: 0x3b
[buggy] Nbbytes to alloc: 0x2d0
[buggy] NbChar: 0x0 / 0xb73 - Idx: 0x0
[buggy] NbChar: 0x4 / 0xb73 - Idx: 0x1
[buggy] NbChar: 0x5a / 0xb73 - Idx: 0x2
[buggy] NbChar: 0xa4 / 0xb73 - Idx: 0x3
[buggy] NbChar: 0xee / 0xb73 - Idx: 0x4
[...]
[buggy] NbChar: 0xb73 / 0xb73 - Idx: 0x3c
[buggy] mov [0xb031d30 + 0x2d0], 0xb73eax=000000b4 ebx=12dfed04 ecx=0b031d30 edx=00000b73 esi=00188694 edi=005d203c
eip=004ce769 esp=00187d60 ebp=00187d80 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246
ollydbg!Findfreehardbreakslot+0x21d9:
004ce769 891481 mov dword ptr [ecx+eax*4],edx ds:002b:0b032000=????????
家庭复现
- 下载最新版本的OllyDbg2这里,解压文件。
- 从odb2-oob-write-heap下载三个文件,将它们放在与ollydbg.exe相同的目录中。
- 启动WinDbg并打开最新版本的OllyDbg2。
- 设置断点(或不设置),按F5启动。
- 在OllyDbg2中打开触发器。
- 当二进制文件完全加载后,按F9。
- BOOM 😃。注意,你可能看不到明显的崩溃(记住,这就是为什么在没有完整PageHeap的情况下调试这个漏洞不简单)。尝试用调试器四处查看:重新启动二进制文件或关闭OllyDbg2应该足以在调试器中看到堆分配器的消息。
有趣的事实
你甚至可以只用二进制文件和PDB数据库触发漏洞。诀窍是篡改PDB,更准确地说,篡改它保存源代码路径的部分。这样,当OllyDbg2加载PDB数据库时,它会将同一个数据库当作应用程序的源代码读取。太棒了。
结论
这类崩溃总是学习新事物的机会。要么它很容易调试/复现,你不会浪费太多时间,要么它不容易,你会在真实的例子中提高调试/逆向工程技能。所以去做吧!
顺便说一句,我怀疑这个漏洞是可利用的,我甚至没有尝试利用它;但如果你成功了,我非常乐意阅读你的分析文章!但如果我们假设它是可利用的,你仍然需要向受害者分发PDB文件、源文件(我认为它会比PDB给你更多的控制权)和二进制文件。所以没什么大不了的。
如果你懒得调试你的崩溃,可以发给我,我可能会看一看!
哦,我差点忘了:我们仍在寻找有动力的贡献者来写很酷的文章,传播知识。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码