一个用来帮助解析内存overrun的工具libmcheckb.so

通过解决long run bug4862,在BSE的指导下学习了一个简易但相对有效的内存完整性检查工具libmcheckb.so的使用。

在这里重新整理和总结,分享给大家。

什么是memory overrunover run会带来什么后果?

Memory overrun,就是指在c/c++环境下,越过数据类型的界限写入的行为。例如bug 4862中,memory overrun形成的条件就是:

malloc一段内存p,假设sizeM,之后使用memcpy拷贝一段大于M的字符串给p,这时编译器帮不了你、库函数memcpy帮不了你,神一样的越界写入就这么发生了。

那么它会带来什么后果呢?答案是不知道。因为越过M之后是什么,就不清楚了,可以是另一段倒霉的buffer(要是display用的buffer的话,可能你还有幸在画面上看到星星),

可能是个变量,可能是任何东西,这时候程序的行为就不受我们控制了,所以它这是C/C++程序员心中永远的痛,我这个帖子也不能包治这个疑难杂症。

这个工具什么来路,为什么能够帮助我们分析内存overrun的问题?

这个工具是1989年由Mike Haertel编成,此仁兄就是大名鼎鼎的grep的作者。它的原理是:

劫持”你的可执行程序,强迫你的可执行程序放弃调用libc中的内存管理函数,转而执行它所提供的内存管理函数。(典型的病毒行为)它在你分配的内存区域前面埋伏了24byte的无人区、同时在你分配的内存区域后埋伏了1byte的地雷,供那些倒霉的野指针踩。

踩了之后会怎么样呢?他会大声的招呼你,把出事地点所在的内存page、其后的内存page都打印出来供你研究。当然,这里讲的触雷可能还不太贴切,因为这颗雷只会在你主动去free你申请的缓冲区的时候才会爆炸。

如何使用?

接下来我们进入正题,准备环境。

  1. libmcheckb.so传入目标系统的某个目录下,例如我们系统中的/opt/sony/lib/下。

  2. 修改启动脚本rc.local,添加如下2行,然后重启。

+if [ ! -f /opt/sony/var/imstart ]; then

+ LD_PRELOAD="/opt/sony/lib/libmcheckb.so" im.elf bootall & fi

if [ ! -f /opt/sony/var/imstart ]; then

    1. im.elf bootall &

这个时候,你就要反复执行疑似会产生内存overrun的动作,为什么要反复执行呢?因为有可能这个overrun的行为会有累积效应,在反复的累加了多少次之后才会踩到别人,

亦或者即便是进入我们的“雷区”,我们的雷区也不是严丝合缝,在某些byte被踩时,这个库也不会报告(这个事情不知道Mike老哥知道不知道)

如何分析?

意外发生了,系统向你报告libmcheckb引发crash了。此时不要慌,看一看系统向我们说了什么,实际上这个程序向我们报告的东西足够丰富,你不需要借助gdb或者emparse,靠眼睛就能搞定.但你要实在说你搞不定,那就去把生成的core文件拷出来,或者是把exception发生时的终端log保存起来,用emparse解析一下。但这里我要声明:crash的地点肯定不是overrun发生的地点。它只是一个无辜的受害者,我这个工具只能帮助你找到受害者身上的“血迹”(比如bug 4862我们找到了一个clip名,还有三个圆点),通过这些线索,大家去展开联想。所以这只是个开始,绝不是终点。

闲话少说,看看系统向我们报告了什么:

我以malloc一段20byte的内存,之后人为向内存的起始地址的-18byte偏移量写入2byte(“a”,字符串,一个0x61和一个0x00)为例。

要注意:系统在不稳定的时候,crash的原因千奇百怪,我们要善于去伪存真,把那些不是我们想要的crash放在一边。只有像下面这些以mcheck XXX开头的crash才是我们想要的。


mcheck: Header: 0x64b06b20

mcheck: memory clobbered past end of allocated block

mcheck: size 0x610014

mcheck: backtrace

mcheck: tid (Curr: 639 - Affected: 639)

mcheck: time (Curr: 4288532966 - Affected: 4288532966)

-------------------------------------------------------

0x64b06a80 : 0x00000000 0x00000000 0x00000000 0x000000d7

0x64b06a90 : 0x64b06a18 0x0000002d 0x9a6ad473 0x00000004

0x64b06aa0 : 0x0000027f 0xff9dd1dd 0x64b06a98 0xfedabeeb

0x64b06ab0 : 0x64b10a98 0x959595d7 0x95959595 0x00000011

0x64b06ac0 : 0x64b05d48 0x64b06eb8 0x00000010 0x0000002c

0x64b06ad0 : 0x9a6ad43b 0x00000008 0x0000027f 0xff9dd1df

0x64b06ae0 : 0x64b06ad0 0xfedabeeb 0x0031326c 0x64b10a98

0x64b06af0 : 0x959595d7 0x0000002d 0x9a6ad413 0x00000004

0x64b06b00 : 0x0000027f 0xff9db4a7 0x64b06af8 0xfedabeeb

0x64b06b10 : 0x00000002 0x959595d7 0x959595d7 0x0000003d

0x64b06b20 :0x9a6ad5cb 0x00610014 0x0000027f 0xff9dd1e6

0x64b06b30 : 0x64b06b20 0xfedabeeb 0x93939393 0x93939393

0x64b06b40 : 0x93939393 0x93939393 0x93939393 0x959595d7

0x64b06b50 : 0x00000038 0x00000035 0x9a6ad5b3 0x00000010

0x64b06b60 : 0x0000027f 0xff9dd1df 0x64b06b58 0xfedabeeb


  • mcheck: Header: 0x64b06b20

告诉你,这次free的内存之前被加入的“无人区”地址。这个“无人区”共24字节。这个地址你可以在后续的内存页中找到。

  • mcheck: memory clobbered past end of allocated block

告诉你内存尾部被破坏了。可能出现的种类还可能有:

    • memory clobbered before allocated block

内存头部被破坏了。

    • block freed twice

内存被释放了两次。

  • mcheck: size 0x610014

告诉你这段内存申请的size

  • mcheck: backtrace

无用。

  • mcheck: tid (Curr: 639 - Affected: 639)

free这段内存的tid

  • mcheck: time (Curr: 4288532966 - Affected: 4288532966)

Free这段内存的时间。


之后,找到这里提到的header地址,认真研究它的内容,因为问题就出现在这里。在进入实质的分析内存内容之前,我先给大家解说一下这个内存header的结构:它是一个24byte的结构体,如下:

struct header {

    • unsigned long magic2;

//4byte,最前部的哨兵,4byte,这个数值是变化的,是一个常量0xfedabeeb与真实的内存起始地址异或的产物。他一旦被写坏了,会触发“内存头部被破坏”的报告。

    • size_t size;

//4byte,内存malloc时的size,这个值被写坏了会导致“内存尾部被破坏”的报告,注意,我说的没错,是尾部。因为这个程序就是靠他找到内存尾部预埋的Magic byte 0xd7,如果它的值不对,那么找到的尾部magic byte就不是我们想要的,自然就要报告尾部被写坏了。

    • void * backtrace[BACKTRACE_DEPTH];

// 0byte无用。注意,它是一个0元素的数组,编译器在这里没有给它分配空间。

    • pid_t tid;

//4bytefree内存时的tid

    • unsigned long msec;

//4bytefree这段内存时的系统时间。

    • void * block;

//4byte,真正的内存起始地址。

    • unsigned long magic;

//4byteheader尾部的magic word,是一个常量0xfedabeeb

};



经过这段解说,大家可以归纳如下:

在以下情况下,libmcheckb会报告头部被破坏。

  1. Magic2被破坏。也就是header的头4byte

  2. Block被破坏。也就是header的第17到第20byte

  3. Magic被破坏。也就是header的第21到第24byte

在以下情况下,libmcheckb会报告尾部被破坏。

  1. size被破坏。也就是header的第5到第8byte

  2. 内存尾部的magic byte被破坏。也就是block + size +1被破坏。

大家可以看到,header的第9到第16字节是盲点,即便被写坏了也检查不出来。


接下来,我就具体看一下这个header:


0x64b06b20 :0x9a6ad5cb 0x00610014 0x0000027f 0xff9dd1e6

0x64b06b30 : 0x64b06b20 0xfedabeeb 0x93939393 0x93939393

0x64b06b40 : 0x93939393 0x93939393 0x93939393 0x959595d7


起始地址

偏移量0~3

偏移量4~7

偏移量8~11

偏移量12~15

0x64b06b20

0x9a6ad5cb

0x00610014

0x0000027f

0xff9dd1e6


Magic2

Size

Tid

Msec

0x64b06b30

0x64b06b20

0xfedabeeb

0x93939393

0x93939393


Block

Magic

真正的缓冲区内容


0x64b06b40

0x93939393

0x93939393

0x93939393

0x959595d7





这里是破绽!!


通常,看到这里,我还要补充一下,这个程序会在malloc的时候将内存全部填充0x93,在Free时,将内存全部填充0x95,当我们研究这段内存时,要能在0x64b0b4f处找到破绽,这里出现了我们的结尾magic byte 0xd7,距离header的尾部如此之近,只差20byte,那么问题出现了,为什么size告诉我这段内存大小是0x610014,而我在第20byte之后就找到结尾magic byte了呢?一个可能性就是size本身被写坏了!在深入研究size,发现低位0x14正是十进制20,那么高位是什么呢?正是我们测试写入的字符串“a”

至此,libmcheckb.so的使命就结束了。这时,我们就应该展开想象,看看哪里会写入“a”。搜一搜,找一找,在找人聊一聊,也许你就离发现问题不远了。


posted @ 2016-07-17 16:16  neu_feng  阅读(679)  评论(0)    收藏  举报