一个用来帮助解析内存overrun的工具libmcheckb.so
通过解决long run bug4862,在BSE的指导下学习了一个简易但相对有效的内存完整性检查工具libmcheckb.so的使用。
在这里重新整理和总结,分享给大家。
什么是memory overrun,over run会带来什么后果?
Memory overrun,就是指在c/c++环境下,越过数据类型的界限写入的行为。例如bug 4862中,memory overrun形成的条件就是:
malloc一段内存p,假设size是M,之后使用memcpy拷贝一段大于M的字符串给p,这时编译器帮不了你、库函数memcpy帮不了你,神一样的越界写入就这么发生了。
那么它会带来什么后果呢?答案是不知道。因为越过M之后是什么,就不清楚了,可以是另一段倒霉的buffer(要是display用的buffer的话,可能你还有幸在画面上看到星星),
可能是个变量,可能是任何东西,这时候程序的行为就不受我们控制了,所以它这是C/C++程序员心中永远的痛,我这个帖子也不能包治这个疑难杂症。
这个工具什么来路,为什么能够帮助我们分析内存overrun的问题?
这个工具是1989年由Mike Haertel编成,此仁兄就是大名鼎鼎的grep的作者。它的原理是:
“劫持”你的可执行程序,强迫你的可执行程序放弃调用libc中的内存管理函数,转而执行它所提供的内存管理函数。(典型的病毒行为)它在你分配的内存区域前面埋伏了24byte的无人区、同时在你分配的内存区域后埋伏了1byte的地雷,供那些倒霉的野指针踩。
踩了之后会怎么样呢?他会大声的招呼你,把出事地点所在的内存page、其后的内存page都打印出来供你研究。当然,这里讲的触雷可能还不太贴切,因为这颗雷只会在你主动去free你申请的缓冲区的时候才会爆炸。
如何使用?
接下来我们进入正题,准备环境。
-
将libmcheckb.so传入目标系统的某个目录下,例如我们系统中的/opt/sony/lib/下。
-
修改启动脚本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
-
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;
//4byte,free内存时的tid。
-
unsigned long msec;
//4byte,free这段内存时的系统时间。
-
void * block;
//4byte,真正的内存起始地址。
-
unsigned long magic;
//4byte,header尾部的magic word,是一个常量0xfedabeeb。
};
经过这段解说,大家可以归纳如下:
在以下情况下,libmcheckb会报告头部被破坏。
-
Magic2被破坏。也就是header的头4个byte。
-
Block被破坏。也就是header的第17到第20个byte。
-
Magic被破坏。也就是header的第21到第24个byte。
在以下情况下,libmcheckb会报告尾部被破坏。
-
size被破坏。也就是header的第5到第8个byte。
-
内存尾部的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,而我在第20个byte之后就找到结尾magic byte了呢?一个可能性就是size本身被写坏了!在深入研究size,发现低位0x14正是十进制20,那么高位是什么呢?正是我们测试写入的字符串“a”。
至此,libmcheckb.so的使命就结束了。这时,我们就应该展开想象,看看哪里会写入“a”。搜一搜,找一找,在找人聊一聊,也许你就离发现问题不远了。
浙公网安备 33010602011771号