定位堆相关问题:ollydbg2大小差一

By Zing - 2014-08-24

原文链接(翻译:Zing)

译文

介绍

昨天下午,我正惬意地写代码,但是后来代码却不能运行。和往常一样,我打开调试器查找到底哪里发生了bug。但这次却有一点奇怪,为了更好的向你说明,我写了一些内联x86汇编代码,并且在推测可能引发bug的汇编代码头放了int3指令。用OllyDbg2加载之后,直接F9快速到达故意放置的int3指令处。几次单步跟踪之后,“嘭!”,我的调试器崩溃了。这种情况曾经发生过。然后,我重新载入了二进制文件,希望再次触发这个bug:同样的步骤导致了再一次崩溃。好的,我找到OllyDbg2中可以重现的崩溃问题。

我喜欢这样的事情发生(还记得我在OllyDbg/IDA发现的崩溃吗:PDB Ain’t PDD
对于我来说总会是一次不错的练习:

  • 定位程序的bug:在实用的,较大的程序通常并不繁琐。
  • 逆向分析bug相关的代码,以了解发生了什么(有时候我有源代码,有时我不喜欢这样)

在这篇文章中,我将展示定位bug方法,使用GFlags,PageHeap.aspx)和Windbg。然后,逆向分析导致bug的代码来理解bug发生的原因,最后了解如何编写一个简洁的触发器。

崩溃

我做的第一件事情就是启动Windbg监视OllyDbg来调试二进制文件的过程。OllyDbg启动后,我重复了之前的步骤引发bug,下面是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
...

就像我们刚才说的,当OllyDbg尝试释放内存堆块的时候,堆分配器传递的信息被触发。

和崩溃有关的主要问题有两个:

  • 堆块在哪里分配
  • 在哪里发生的越界写入内存

在没有合适的工具时,这使我们的bug调试起来并不繁琐。如果你想了解更多的关于高效调试堆问题的信息,你可以阅读堆这一章Advanced Windows Debugging(cheers Ivan)

查明堆问题:引入全页堆

简而言之,全页堆选项对于诊断堆问题很有用,这里列出两个原因:

  • 每一个堆块分配之后它会保存
  • 它会在堆尾分配一个防护页(因此当写错误发生时,我们可能得到一个写权限异常)

开启全页堆选项会使给堆分配器的工作带来细微改变(比如给每个堆块加上了更多的元数据)。
如果你想了解更多信息,分别在使用和不使用page heap进行内存分配,并且分析比较所分配的内存的不同。下面是开启全页堆之后堆块的样子:

在ollydbg中打开它是很繁琐的。我们只启动gflag.exe(在windbg目录下)你可以勾上你想要使用的选项。

现在,你可以在windbg中重启目标程序了,重现这个bug,下面是我获得的信息:

(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 ecx
address 0f00ed30 found in
_DPH_HEAP_ROOT @ 4f11000
in busy allocation
( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
f6f1b2c: f00ed30 2d0 - f00e000 2000

6e858e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
76f90d96 ntdll!RtlDebugAllocateHeap+0x00000030
76f4af0d ntdll!RtlpAllocateHeap+0x000000c4
76ef3cfe ntdll!RtlAllocateHeap+0x0000023a
75d84e55 KERNELBASE!GlobalAlloc+0x0000006e
00403bef ollydbg!Memalloc+0x00000033
004ce5ec ollydbg!Findfreehardbreakslot+0x0000205c
004cf1df ollydbg!Getsourceline+0x0000007f
00479e1b ollydbg!Getactivetab+0x0000241b
0047b341 ollydbg!Setcpu+0x000006e1
004570f4 ollydbg!Checkfordebugevent+0x00003f38
0040fc51 ollydbg!Setstatus+0x00006441
004ef9ef ollydbg!Pluginshowoptions+0x0001214f

手工输入这条长长的命令后,我们获得了很多相关的信息:

  • 这个堆块大小为0x2d。从0xf00ed30到0xf00efff
  • 误写现在有意义了:应用程序尝试在堆块外写4字节(我猜测是无符号数组大小差一问题)
  • 在ollyydbgMemalloc中分配内存(称为ollydbg!Getsourceline,PDB相关)我们将会在后面学习。
  • 误写发生在0x4ce769

在ollydbg里面查看

比较幸运的是,这个bug涉及到的流程逆向分析很简单,Hexrays用起来也非常得心应手。下面是产生bug的函数(最有趣的部分)的c代码:

ollydbgbuggy@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)
  {
    // doing stuff to count all the char, and all the lines in the file
    // ...
  }

  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] )
        {
            // Consume a line, increment stuff until finding a '\r' or '\n' sequence
            // ..
        }
      }
      ++nb_lines2;
    }
    // BOOM!
    u->mem1_ov[3 * nb_lines2] = nbchar;
    // ...
  }
}

让我解释一下流程:

  • 当OllyDbg2给你的二进制文件找到一个PDB数据库时,这段代码被OllyDbg2调用,但恰恰是在这个数据库中,它发现了你的程序的源代码的路径。当你调试的时候拥有这些信息是很有用的,OllyDbg2能够告诉你你现在在C代码的哪一行。
  • 在第10行:"u->Sourcefile"是一个字符串指针指向你的源代码的路径(在PDB数据库中找到)。这段代码在读整个文件,告诉你文件大小,文件内容的指针现在指向内存。
  • 从第12行到18行:我们有一个loop循环计算源代码的行数。
  • 在第20行:堆块的分配,分配12*(nb_line+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行:出现了bug。我们可以设法跳出循环"nb_lines2=nb_lines+1",这意味着第41行将会尝试在缓冲区外写一个单元。在我们的例子中,如果有"nb_lines2=60",并且堆缓冲区从0xf00ed30开始,这意味着我们尝试在(0xf00ed30+6034)=0xf00f000进行写操作。这就是之前看到的。

现在我们可以完全的解释这个bug了。如果你想做一些动态的分析来跟踪重要的流程,我已经下了一些断点,你可以参考:

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], 0xb73

eax=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=????????

Repro@home

1.最新版本的OllyDbg2在这里下载,提取文件

2.从odb2-oob-write-heap下载三个文件,将他们放到和ollydbg.exe同一个目录下

3.启动Windbg并且打开最新版本的OllyDbg2

4.设置断点(或者不设),F5启动

5.在OllyDbg2中打开触发器

6.文件加载完毕之后F9跑起来

7.嘭!!注意你可能得不到一个可见的崩溃(记住,这是在没有开启全页堆的情况下使我们的bug调试不繁琐的原因)。在调试器中四处戳戳:重启二进制文件或者关闭OllyDbg2来获取堆分配器的信息应该足够了。

有趣的事实

你可以仅仅用二进制文件和PDB数据库触发这个bug。技巧是篡改PDB,它保存你的源代码的路径。这样,OllyDbg将会加载PDB数据库,它会读取同样的数据库,就像它是程序的源代码一样。太棒了~

总结

这些崩溃总是学习新东西的好地方。调试/重现不会花费你太多的时间,并且你可以在真实的例子中提高逆向分析的技术,所以调试它吧~~

顺便一提,我怀疑这个bug可以利用但是我没有尝试利用它,但是你如果成功了我会非常开心地去阅读你的write-up。如果它可以被利用,你需要发布PDB文件,源代码文件(我猜这比使用PDB能获得更多的控制),用来攻击的二进制文件。
如果你太懒了不想调试崩溃,把他们发送给我,我可以看看!
oh,差点忘了:我们仍然在寻找活跃的贡献者写帅气的文章。

From Z1ng'Blog