
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()退出后外壳代码仍然能够驻留在内存中。