Linux程序保护机制

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的一些理解

栈溢出漏洞的利用和缓解


相关内容

0%