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

3.6 .dtors区

任意内存写攻击的另外一个目标是覆写由GCC生成的可执行文件的.dtors区中的函数指针[Rivas 2001]。GNU C允许程序员利用__attribute__关键字后跟一个包含于双括号中的属性修饰符来声明函数的属性[FSF 2004]。属性修饰符包括constructor和destructor。constructor属性指示函数在main()之前被调用,destructor属性则表示函数将在main()执行完成后或exit()被调用后进行调用。

例3.7所示的程序展示了constructor和destructor属性的用法。该程序包含3个函数:main()、create()和destroy()。第4行声明的create()函数是一个构造函数,第5行声明的destroy()函数则是一个析构函数。这两个函数都没有被main()调用,main()只是打印了两个函数的地址然后就退出了。例3.8展示了示例程序的执行结果。显然,create()首先执行,然后是main(),最后才是destroy()。

例3.7 具有constructor和destructor属性的程序


01  #include <stdio.h>
02  #include <stdlib.h>
03
04  static void create(void) __attribute__ ((constructor));
05  static void destroy(void) __attribute__ ((destructor));
06
07  int main(void) {
08    printf("create: %p.\n", create);
09    printf("destroy: %p.\n", destroy);
10    exit(EXIT_SUCCESS);
11  }
12
13  void create(void) {
14    puts("create called.\n");
15  }
16
17  void destroy(void) {
18    puts("destroy called.");
19  }

例3.8 示例程序的输出


% ./dtors
create called.
create: 0x80483a0.
destroy: 0x80483b8.
destroy called.

构造函数和析构函数分别存储于生成的ELF可执行映像的.ctors和.dtors区中。这两个区都具有如下的布局形式:


0xffffffff { 
函数地址 } 0x00000000 

.ctors和.dtors区映射到进程地址空间后,默认属性为可写。漏洞利用程序从未利用过构造函数,因为它们都在main()函数之前执行。结果,攻击者的兴趣都集中到了析构函数和.dtors区上。

如例3.9所示,可以使用objdump命令检查可执行映像中.dtors区中的内容,我们可以看到头、尾标签,以及destroy()函数的地址(采用小端格式)。

例3.9 .dtors区的内容


1  % objdump -s -j .dtors dtors 
2 
3  dtors:     file format elf32-i386 
4 
5  Contents of section .dtors: 
6  804959c ffffffff b8830408 00000000 

攻击者可以通过覆写.dtors区中的函数指针的地址从而将程序控制权转移到任意的代码。如果攻击者能够读取到目标二进制文件,那么通过分析ELF映像,很容易就能确定要覆写的确切位置。

有趣的是,即使没有指定任何析构函数,.dtors区仍然存在。在这种情况下,该区中只含有头、尾标签而中间没有函数地址。不过,仍然可以通过将尾标签0x00000000覆写为攻击者提供的外壳代码的地址,从而将控制转移过去。如果外壳代码返回,则进程将会继续调用接下来的函数直到遇到尾标签或发生错误为止。

对于攻击者而言,覆写.dtors区的好处在于该区总是存在并且会映射到内存中 [1]。当然,dtors仅存在于用GCC编译和链接的程序中。有时候,很难找到合适的外壳代码注入点,使得在main()退出后外壳代码仍然能够驻留在内存中。

[1] .dtors 区不会被一个二进制strip(1) 删除。