没几天,你的程序出炉了,加以时日,回归测试也完成了,你兴冲冲地想,啊哈,终于可以上线给东家挣钱啦!
  有一天,你的程序 core dump 了,光荣地被谋杀在战场的某个小小小角落。你战战兢兢地打开了 GDB,妄图勘察现场,习惯性地按下 bt,看到了下面的一幕:

#0  0x0000000000000078 in ?? ()
#1  0x000014473a087d0a in ?? ()
#2  0x00007f66db85b351 in ?? ()
#3  0x0000000000000025 in ?? ()

  WTF! 栈乱掉了,满眼的疑问号:你的小宝贝她从哪里来?要到哪里去?当时在搞什么?在被谁搞?
  怎么样?现在知道栈是多么伟大多么重要的一个东西了吧。它只在程序运行时候被创建,保存了函数的参数、局部变量和调用关系,以至于在进程异常死亡时 OS 把他留下来,存到磁盘上。现如今,它被糟践成这副德性,怪得了谁呢?要么是你的编译器抽抽了,要么就是你脏心烂肺内存写越界了。不信,请 call 一下 foo:

1
2
3
4
void foo() {
    int a[1];
    memset(a, 0, 1024);
}

  能恢复吗?抱歉,IA32 下,调用链比较规整,根据情况 hack 一下 core 文件,还有一线生机。但是,在 IA64 里,rbp 可能已做它用,无力回天了,反正我耗费一个周末研究 ELF 和 DWARF 最终放弃了。
  还有其他招儿吗?有,但要看运气,我就属于那种运气好的:

(gdb) x/8a $rsp
0x7f66d4534a10:	0x3a087d0a47140002	0x0
0x7f66d4534a20:	0x0	0x0
0x7f66d4534a30:	0x523846d0	0x43eae5 <check_server_status(uint32_t)+613>
0x7f66d4534a40:	0xebfd50	0x7f66d4534af0
(gdb) disas 0x43eae0,+6
Dump of assembler code from 0x43eae0 to 0x43eae6:
   0x000000000043eae0 <check_server_status(uint32_t)+608>:	callq  0x4a0f40 <is_alive(uint64_t)>
   0x000000000043eae5 <check_server_status(uint32_t)+613>:	test   %al,%al
End of assembler dump.

  运气好的话,is_alive() 就是程序生前调用的最内层函数了。看是不是它做错了什么吧:

1
2
3
4
5
6
7
8
9
10
11
12
bool is_alive(uint64_t server_id)
{
    ...
    int fd = socket(...);
    set_nonblock(fd);
    fd_set set;
    FD_ZERO(&set);
    FD_SET(fd, &set);
    connect(fd, ...);
    select(fd + 1, NULL, &set, NULL, &timeout);
    ...
}

  is_alive 实现了一个超时时间可控的 connect。如果你现在已经明白怎么回事了,请务必留下您的大名,我可是花了两周零五分钟才看出来的。
  对,就是万恶的 select,FD_SET 可能越界,因为 fd_set 中只有 FD_SETSIZE 个 bit 来标识 fd。FD_SETSIZE 是一个宏,定义在 /usr/include/sys/select.h。查看该文件,FD_SET 并未对 fd 做参数检查,因此当 fd 大于 1024 时,FD_SET 就写了它不该写的地方了,写到谁,有没有影响,有多大影响,什么时候触发,程序会不会 core 掉,什么时候 core 掉,这些都要看造化了。比如这次是写到栈里的 return address 了,导致程序跑飞。另外一个 core dump 里面,某个类的 this 从 0x1e41500 变成了 0x11e41500,当时定位不出写越界,只好认为世界末日前宇宙射线爆发导致硬件异常,聊以自慰。
  吐个槽,Linux Kernel 就不能增加一个带超时的 connect 调用?glibc 里面,FD_SETSIZE 只用在 fd_set 里的位域定义和 FD_ZERO,FD_SET 没有做任何检查;Kernel 里面的 select 实现,申请内核态 fd_set 的时候,完全依据 fd 的大小,并没有大小的限制(如果我没有漏掉某些逻辑的话)。
  总结:
  如果出现 stack corruption,几乎一定是栈的缓冲区写越界了;基本不可能是用户态的堆内存越界,因为除了主线程,每个线程的栈空间两侧又有一个 page 的虚拟空间做 gap,这些 gap,不可读,不可写,不可执行。主线程的 stack 是按需向下生长的,在 IA64 环境下,也不可能和 mmap 区域无缝相邻。当然,跳着写堆内存,或者偏移量算错就另说了。
  这篇文章中 FD_SET 写越界只篡改了一个 bit,而且被篡改的那个 bit 可能本来就是 1,非常难重现,而且 stack 的破坏程度较轻,通过查看栈内容,可以大致知道函数的调用关系。如果是类似上文 foo() 中的 memset 越界,栈可能真的面目全非了,但这时候栈内容通常是有规律可循的。
  根据栈内容判断函数调用关系,需要知道 call 指令的语义,栈中保存的是 caller 中 call 指令的下一条指令,这条指令的前一条才是 callee。

That’s All.

Tags: ,.
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Home

Be the first to comment on this entry.

You must be logged in to post a comment.