Canary
-
当函数存在缓冲区溢出漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode执行.
-
当启用栈保护后,函数执行后会先往栈里插入Cookie信息,当函数返回时会验证Cookie信息是否合法,如果不合法就停止程序运行.
-
攻击者在覆盖返回地址时也会将Cookie信息覆盖掉,当函数返回时会因为Cookie检查失败而阻止shellcode执行.
-
同Windows下的GS检查
gcc编译程序开启或关闭命令如下:
1
2
3
4
5
6
7
8
9
10
11
|
# 默认情况下不开启栈溢出保护
gcc luoPwn.c -o luoPwn
# 启用栈保护, 只为局部变量中含char数组的函数插入保护代码
gcc -fstack-protector luoPwn.c -o luoPwn
# 启用栈保护,为所有函数插入保护代码
gcc -fstack-protector-all luoPwn.c -o luoPwn
# 禁用栈保护
gcc -fno-stack-protector luoPwn.c -o luoPwn
|
开启栈保护,程序示例代码如下:
FORTIFY
Fortify技术是gcc在编译源码时判断程序的哪些buffer会存在可能的溢出,在buffer大小已知的情况下,gcc会把strcpy、memcpy、memset等函数自动替换成相应的__strcpy_chk
、__memcpy_chk
、__memset_chk
等函数,达到防止缓冲区溢出的作用。
对格式化字符串有以下两个限制:
- 限制%n对地址写入
- 当使用位置参数时,必须连续使用.如只使用%3$p会报错,需使用
%1$p,$2$p,%3%p
gcc编译程序开启或关闭命令如下:
1
2
3
4
5
6
7
8
9
10
11
|
# 默认情况下不开启FORTIFY检查
gcc luoPwn.c -o luoPwn
# 仅在编译时进行检查
gcc -D_FORTIFY_SOURCE=1 -O2 luoPwn.c -o luoPwn
# 程序编译和执行均检查 使用-O优化,才会开启FORTIFY检查
gcc -D_FORTIFY_SOURCE=2 -O2 luoPwn.c -o luoPwn
# 关闭FORTIFY检查
gcc -D_FORTIFY_SOURCE=0 -O2 luoPwn.c -o luoPwn
|
NX
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功到达栈上执行shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令.
gcc编译程序开启或关闭命令如下:
1
2
3
4
5
6
7
8
|
# 默认情况下,开启NX
gcc luoPwn.c -o luoPwn
# 开启NX
gcc -z noexecstack luoPwn.c -o luoPwn
# 关闭NX
gcc -z execstack luoPwn.c -o luoPwn
|
PIE
PIE(Position Independent Executable)
是一种编译技术,旨在使可执行文件位置无关.
- 开启PIE:生成程序代码段和数据段重定位信息,允许程序在内存的任意位置加载.
- 关闭PIE:程序加载基址固定,但程序堆、栈地址跟随Linux ASLR变化.
gcc编译程序开启或关闭命令如下:
1
2
3
4
5
6
7
8
|
# Kali默认情况下,开启PIE
gcc luoPwn.c -o luoPwn
# 开启PIE
gcc -fPIE luoPwn.c -o luoPwn
# 关闭PIE
gcc -no-pie luoPwn.c -o luoPwn
|
ASLR(Address Space Layout Randomization)
是一种Linux系统安全技术,用于随机化进程的地址空间布局,以防止攻击者利用已知的内存地址进行攻击.通过以下方式进行工作:
-
共享库地址随机化:共享库(如libc)的加载地址会被随机化.
-
栈地址随机化:每次程序运行时,栈的基地址会被随机化.
-
堆地址随机化:堆的基地址会被随机化.
-
内存映射区域随机化:通过mmap()分配的内存区域的基地址会被随机化.
Linux ASLR开启和关闭的命令如下:
1
2
3
4
5
6
7
8
|
# 关闭Linux ASLR
echo 0 > /proc/sys/kernel/randomize_va_space
# 仅栈地址、共享库地址和内存映射区域随机化
echo 1 > /proc/sys/kernel/randomize_va_space
# 栈地址、共享库地址、内存映射区域随机化和堆地址随机化
echo 2 > /proc/sys/kernel/randomize_va_space
|
当关闭PIE,ASLR级别为0、1、和2
时对应的地址变化如下:
ASLR |
程序加载基址 |
共享库加载基址 |
栈地址 |
堆地址 |
内存映射区域基地址 |
0 |
固定 |
固定 |
固定 |
固定 |
固定 |
1 |
固定 |
随机 |
随机 |
固定 |
随机 |
2 |
固定 |
随机 |
随机 |
随机 |
随机 |
当开启PIE,ASLR级别为0、1、和2
时对应的地址变化如下:
ASLR |
程序加载基址 |
共享库加载基址 |
栈地址 |
堆地址 |
内存映射区域基地址 |
0 |
固定 |
固定 |
固定 |
固定 |
固定 |
1 |
随机 |
随机 |
随机 |
固定,但根据程序加载地址变化而变化 |
随机 |
2 |
随机 |
随机 |
随机 |
随机 |
随机 |
测试代码如下:
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
void* get_base_address(char* name) {
FILE *maps = fopen("/proc/self/maps", "r");
if (maps == NULL) {
perror("fopen");
exit(1);
}
char line[256];
void *base_address = NULL;
while (fgets(line, sizeof(line), maps)) {
// printf("Line: %s", line);
if (strstr(line, name)) {
sscanf(line, "%lx", (unsigned long*)&base_address);
break;
}
}
fclose(maps);
return base_address;
}
int main() {
// 获取程序的基地址
void *base = get_base_address("luoPwn");
if (base != NULL) {
printf("程序的基地址: %p\n", base);
} else {
printf("无法获取程序的基地址\n");
}
// 获取共享库libc的加载地址
void *libc_addr = get_base_address("libc.so.6");
if (libc_addr != NULL) {
printf("共享库libc加载地址: %p\n", libc_addr);
} else {
printf("无法获取共享库libc加载地址\n");
}
// 获取栈地址
int stack_var;
printf("栈地址: %p\n", (void *)&stack_var);
// 获取堆地址
void *heap_var = malloc(10);
if (heap_var == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
printf("堆地址: %p\n", heap_var);
printf("堆地址相对程序加载基址的偏移: %p\n", (char*)heap_var - (char*)base );
// 使用mmap分配内存映射并获取基地址
void *mmap_addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mmap_addr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("内存映射分配地址: %p\n", mmap_addr);
// 释放堆内存和内存映射
free(heap_var);
munmap(mmap_addr, 4096);
return 0;
}
|
RELRO
RELRO(RELocation Read Only),通过将重定位表(如GOT表)设置为只读,防止攻击者修改这些表中的指针,从而劫持程序执行流程.
Partial RELRO
:将.got段映射为只读,但.got.plt还是可以写.
Full RELRO
:将.got.plt合并到.got段中,所以.got.plt将不复存在.
gcc编译程序开启或关闭命令如下:
1
2
3
4
5
6
7
8
9
10
11
|
# 默认情况下,Partial RELRO
gcc luoPwn.c -o luoPwn
# Partial RELRO
gcc -z lazy luoPwn.c -o luoPwn
# Full RELRO
gcc -z now luoPwn.c -o luoPwn
# 关闭
gcc -z norelro luoPwn.c -o luoPwn
|
参考链接
Linux和Windows保护机制
Linux保护机制和绕过方式
关于Linux下ASLR与PIE的一些理解
栈溢出漏洞的利用和缓解