Linux中的栈缓冲区溢出

总结Linux中的栈缓冲区溢出漏洞的缓解方法

前言

缓冲区溢出漏洞是一种广泛存在且危害严重的漏洞,发现至今,关于此类漏洞的利用与防御技术不断发展.其中,栈缓冲区溢出漏洞的缓解机制主要有不可执行技术(NX,on executable)、地址随机化技术(ASLR,address space layout randomization)和栈保护机制(SSP,stack smashing protector),相对应地也存在一些绕过手段,例如return-to-libcROP技术可以绕过NX;基于信息泄露的攻击使ASLR失效等.然而针对SSP防御机制,目前却没有有效的绕过技术

Stack Canaries

Canaries(金丝雀),取名自地下煤矿的金丝雀,因为它能比矿工更早地发现煤气泄漏,有预警的作用。这是一种对抗栈溢出的技术,又称之SSP栈保护机制,或者Stack Cookies,目前有4种具体的实现技术:StackGuard、StackShield、ProPolice以及XOR技术。

Canary的值是栈上的一个随机数,在程序启动时随机生成并保存在比函数返回地址更低的位置。由于栈溢出是从低地址向高地址进行覆盖,因此攻击者要想控制函数的返回指针,就一定要先覆盖到Canary。程序只需要在函数返回前检查Canary是否被篡改,就可以达到保护栈的目的。

那么这个随机值是如何产生的?

Canaries通常可分为3类:terminator、random和random XOR,具体的实现有StackGuard、StackShield、ProPoliced等。

例子

我们通过一个例子来了解金丝雀机制是如何工作的。

1
2
3
4
5
6
7
8
//test.c
#include <stdio.h>

int main (void)
{
char arr [10];
sacnf("%s",arr);
}
1
gcc -Wstack-protector -g ./test.c

通过objdump -S ./a.out查看反汇编。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>

int main (void)
{
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 83 ec 20 sub $0x20,%rsp
1175: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
117c: 00 00
117e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1182: 31 c0 xor %eax,%eax
char arry [10];
scanf("%s",arry);
1184: 48 8d 45 ee lea -0x12(%rbp),%rax
1188: 48 89 c6 mov %rax,%rsi
118b: 48 8d 05 72 0e 00 00 lea 0xe72(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1192: 48 89 c7 mov %rax,%rdi
1195: b8 00 00 00 00 mov $0x0,%eax
119a: e8 d1 fe ff ff call 1070 <__isoc99_scanf@plt>

return 0;
119f: b8 00 00 00 00 mov $0x0,%eax
}
11a4: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11a8: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11af: 00 00
11b1: 74 05 je 11b8 <main+0x4f>
11b3: e8 a8 fe ff ff call 1060 <__stack_chk_fail@plt>
11b8: c9 leave
11b9: c3 ret

Disassembly of section .fini:

00000000000011bc <_fini>:
11bc: f3 0f 1e fa endbr64
11c0: 48 83 ec 08 sub $0x8,%rsp
11c4: 48 83 c4 08 add $0x8,%rsp
11c8: c3 ret

在开启了栈保护之后,我们如果输入超过10字节的内容会发生什么?

1
*** stack smashing detected ***: terminated

可以看到有东西阻止了错误。如果没有使用保护机制的话就会直接发生段错误

分析二进制代码

现在我们查看main函数的二进制代码,可以看到mov %fs:0x28,%rax这样一句话。在Linux中,fs寄存器被用于存放线程局部存储(Thread Local Storage, TLS),TLS主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。TLS为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度看,就好像一个全局变量被克隆成了多份副本,每一份副本都可以被一个线程独立地改变。在glibc的实现里,TLS结构体tcbhead_t的定义如下所示,偏移0x28的地方正是stack_guard。

Ⅰ. Glibc中

1.内核提供的随机数生成器_dl_random产生随机数

2.函数_dl_setup_stack_chk_guard根据系统是32位还是64位将随机数处理成4byte或8byte的canary,并赋给变量__stack_chk_guard

3.宏THREAD_SET_STACK_GUARD通过处理将变量__stack_chk_guard放入结构体tcbhead_t的成员stack_guard中(即fs:28h)

Ⅱ. GCC中

4.选择canary的插入位置, 以及引入与canary有关的汇编代码

Ⅲ. 程序运行中

5.程序函数开头从 fs:28h 中取出8字节的值插入到栈中,同时清空rax中的副本

6.函数结束时,程序会再次从 fs:28h 中将canary的值取出与栈上的canary进行比较

7.如果canary不同则跳到函数 ___stack_chk_fail 直接终止程序,否则继续执行程序

gcc中的安全选项

1
2
3
4
5
6
-Wstack-protector # 对于alloca系列函数和内部缓冲区大于8字节的函数开启保护
-fstack-protector-strong # 增强对包含局部数组定义和地址引用的函数的保护
-fstack-protector-all # 对所有函数开启保护

-fstack-protector-explicit # 对于包含stack_protect属性的函数开启保护
-fon-fstack-protector # 关闭金丝雀保护,为默认情况

控制流保护

有时我们在反汇编的时候会看到endbr64这样一条指令,这条指令就是控制流保护指令,防止攻击者对于控制流的劫持。需要注意的是,endbr64指令通常是由编译器在生成代码时自动插入的,而不是由程序员直接编写的。这是因为它是与控制流保护相关的低级指令,通常不需要直接操作。

参考资料


Linux中的栈缓冲区溢出
https://ysc2.github.io/ysc2.github.io/2023/12/31/Linux中的常见安全机制/
作者
Ysc
发布于
2023年12月31日
许可协议