C和C++安全编码(原书第2版)
上QQ阅读APP看书,第一时间看更新

3.8 atexit()和on_exit()函数

atexit()是C标准定义的一个通用工具函数。atexit()可以注册无参函数,并在程序正常结束后调用该函数。C要求实现支持至少32个函数的注册。SunOS上的on_exit()函数具有类似的功能。libc4、libc5和glibc也提供了这样的函数[Bouchareine 2005]。

在例3.11展示的程序中,第8行(main()中)使用atexit()注册test()函数。该程序在退出前将全局变量glob赋值为字符串“Exiting.\n”(第9行)。test()函数在程序退出后得以执行,并且打印出该字符串。

例3.11 使用atexit()的程序


01  char *glob;
02
03  void test(void) {
04    printf("%s", glob);
05  }
06
07  int main(void) {
08    atexit(test);
09    glob = "Exiting.\n";
10  }

atexit()通过向一个退出时将被调用的已有函数的数组中添加指定的函数完成工作。当exit()被调用时,数组中的每一个函数都以“后进先出”(Last-in,First-out,LIFO)的顺序被调用。由于atexit()和exit()都要访问该数组,因此它被分配为一个全局性的符号(在BSD操作系统中是__atexit,在Linux操作系统中则是__exit_funcs)。

例3.12中使用gdb调试atexit程序的会话过程,展示了atexit数组的位置和结构。在该调试会话中,在main()中调用atexit()之前设了一个断点,然后运行程序。接下来执行atexit(),注册test()函数。在test()函数注册后,显示了在__exit_funcs位置处的内存。每一个函数都保存在由4个双字(doubleword)构成的结构中。每一个结构的最后一个双字保存着函数的实际地址。通过检查这些地址的内存得知,已经注册了3个函数:

_dl_fini()、__libc_csu_fini()以及我们编写的test()。可以通过对__exit_funcs结构采用任意内存写或缓冲区溢出手段将程序的控制权转移到任意的代码。请注意,即使受攻击的程序不显式调用atexit()注册_dl_fini()和__libc_csu_fini()函数,它们也会存在。

例3.12 使用gdb的atexit程序的调试会话


(gdb) b main 
Breakpoint 1 at 0x80483f6: file atexit.c, line 6. 
(gdb) r 
Starting program: /home/rcs/book/dtors/atexit 
Breakpoint 1, main (argc=1, argv=0xbfffe744) at atexit.c:6 
6 atexit(test); 
(gdb) next 
7 glob = "Exiting.\n"; 
(gdb) x/12x __exit_funcs 
0x42130ee0 <init>:    0x00000000 0x00000003 0x00000004 0x4000c660 
0x42130ef0 <init+16>: 0x00000000 0x00000000 0x00000004 0x0804844c 
0x42130f00 <init+32>: 0x00000000 0x00000000 0x00000004 0x080483c8 
(gdb) x/4x 0x4000c660 
0x4000c660 <_dl_fini>: 0x57e58955 0x5ce85356 0x81000054 0x0091c1c3 
(gdb) x/3x 0x0804844c 
0x804844c <__libc_csu_fini>: 0x53e58955 0x9510b850 x102d0804 
(gdb) x/8x 0x080483c8 
0x80483c8 <test>: 0x83e58955 0xec8308ec 0x2035ff08 0x68080496