PWN保护机制详解

管理员

对pwn过程中遇到的保护机制做一下详解与归纳。

Stack Canaries

放一篇写的好的:PWN之Canary学习 - sarace - 博客园 (cnblogs.com)

简介

stack canaries取名自地下煤矿的金丝雀,能比矿工更快发现煤气泄露,有预警的作用。这个概念应用在栈保护上则是在初始化一个栈帧时在栈底设置一个随机的canary值 ,栈帧销毁前测试该值是否“死掉”,即是否被改变,若被改变则说明栈溢出发生,程序走另一个流程结束,以免漏洞利用成功。

主要分为三类:terminator, random, random XOR ,具体实现有 StackGuard,StackShied, ProPoliced 等。

  • terminator canaries: 考虑到很多栈溢出都是由于字符串操作不当所产生的,而这些字符串以NULL \x00结尾, 被\x00截断,所有terminator将低位设置为\x00,防止泄露,也可以防止伪造,截断字符还包括CR(0x0d),LF(0x0a), EOF(0xff)。其实就是将最后部分的最高位置为00,\x00ab1245

  • random canaries : 为了防止canaries被攻击者猜到,通常会在程序初始化的时候随机生成canary,保存在安全的地方。

  • random canaries XOR:其实就是比random canaries多了一个XOR操作,无论canaries还是XOR的数据被篡改,都会被检测到。xor eax,DWORD PTR gs:0x14

实现原理

Linux下,存在fs寄存器,用于保存线程局部存储TLS,TLS主要是为了避免多个线程访问同一全局变量或静态变量所导致的冲突。64位使用fs寄存器,偏移在0x28。32位使用gs寄存,偏移在0x14。该位置存储stack_guard,即保留和canary,最后和栈中的canary进行比较,检测溢出。

具体过程是使用_dl_random来生成stack_chk_guard,然后使用THREAD_SET_STACK_GUARD来设置stack_guard ,canary的最低位设置为\x00。如果_dl_random==NULL,那么canary为定值。

如果程序没有定义THREAD_SET_STACK_GUARD宏,那么就会直接使用_stack_chk_guard,它是一个全局变量,放在.bss段中。

TLS结构体

x86 32位

mov    eax,gs:0x14
mov    DWORD PTR [ebp-0xc],eax

mov    eax,DWORD PTR [ebp-0xc]
xor    eax,DWORD PTR gs:0x14
je     0x80492b2 <vuln+103> # 正常函数返回
call   0x8049380 <__stack_chk_fail_local> # 调用出错处理函数
 High  
        Address |                 |  
                +-----------------+
                | args            |
                +-----------------+
                | return address  |
                +-----------------+
                | old ebp         |
      ebp =>    +-----------------+
                | ebx             |
    ebp-4 =>    +-----------------+
                | unknown         |
    ebp-8 =>    +-----------------+
                | canary value    |
   ebp-12 =>    +-----------------+
                | 局部变量         |
        Low     |                 |
        Address

64位

mov    rax,QWORD PTR fs:0x28
mov    QWORD PTR [rbp-0x8],rax

mov    rax,QWORD PTR [rbp-0x8]
xor    rax,QWORD PTR fs:0x28
je     0x401232 <vuln+102> # 正常函数返回
call   0x401040 <__stack_chk_fail@plt> # 调用出错处理函数
 High
        Address |                 |
                +-----------------+
                | args            |
                +-----------------+
                | return address  |
                +-----------------+
                | old ebp         |
      rbp =>    +-----------------+
                | canary value    |
    rbp-8 =>    +-----------------+
                | 局部变量         |
        Low     |                 |
        Address

实验

canary.c smash:粉碎,破碎,打破

#include<tdio.h>
int main(){
  char buf[10];
  scanf("%s",buf);
}
gcc -fno-stack-protector canary.c -o canary_no.out
gcc -fstack-protector canary.c -o canary_pro.out

绕过方式

  • 泄露内存中的canary,如通过格式化字符串漏洞打印出来

  • one-by-one爆破,但是一般是多线程的程序,产生新线程后canary不变才行。最高位为00。

  • 劫持_stack_chk_fail函数,canary验证失败会进行该函数,__stack_chk_fail 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。

  • 覆盖线程局部存储TLS中的canary,溢出尺寸比较大可以用。同时修改栈上的canary和TLS中的canary.

No-eXecute(NX)

简介

No-eXecute(NX)表示不可执行,其原理是将数据所在的内存页标识为不可执行。

在Linux中,程序载入内存后,将.text节标记为可执行,.data .bss等标记为不可执行,堆栈等均不可知性,传统的修改GOT表的方式不再可行。但是无法阻止代码重用攻击ret2libc

实现

通过编译选项,使用strcmp比较,在_handle_option函数设置link_info结构体的execstack和noexecstack为true和false。

在bfd_elf_size_dynamic_sections函数中,根据link_info来设置elf_stack_flags = PF_R | PF_W | PF_X

开启了NX就只有两个,没有PF_X。

在_bfd_elf_map_sections_to_segments函数中,设置stuct elf_segment_map结构体中的p_flags=elf_stack_flags。就完成了编译设置。

在装载时,调用elf_load_binary函数,根据上面的p_flags来设置executable_stack=EXSTACK_ENABLE_X

或EXSTACK_DISABLE_X

将executable_stack传入setup_arg_pages中,通过vm_flags设置进程的虚拟内存空间vma。

当程序计数器指向了不可知性的内存页时,就会触发页错误。

实验

nx.c

#include<unistd.h>
void vuln_func(){
    char buf[128];
    read(STDIN_FILENO,buf,256);
}
int main(int argc , char*argv[]){
    vuln_func();
    write(STDOUT_FILENO,"Hello world!\n",13);
}

ASLR和PIE

简介

大多数攻击都需要知道程序的内存布局,引入内存布局的随机化可以增加漏洞利用的难度,地址空间布局随机化ASLR(address space layout randomization)

ASLR /proc/sys/kernel/randomize_va_space有三种情况:

ASLR Executable PLT Heap Stack Shared Libraries
0 不变 不变 不变 不变 不变
1 不变 不变 不变
2 不变 不变
2+pie

PIE 位置无关可执行文件,在应用层的编译器上实现,通过将程序编译为位置无关代码PIC,使程序加载到任意位置,就像是一个特殊的共享库。PIE会一定程度上影响性能。

实验:

#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>
int main(){
    int stack;
    int *heap=malloc(sizeof(int));
    void *handle = dlopen("libc.so.6",RTLD_NOW | RTLD_GLOBAL);

    printf("executable:%p\n",&main);
    printf("system@plt:%p\n",&system);
    printf("heap: %p\n",heap);
    printf("stack: %p\n",&stack);
    printf("libc: %p\n",handle);
    free(heap);
    return 0;
}
cat /proc/sys/kernel/randomize_va_space
echo 0/1/2 > /proc/sys/kernel/randomize_va_space

ASLR=2,且开启PIE

FORTIFY_SOURCE

简介

缓冲区溢出常常发生在程序调用了一些危险函数的时候,如memcpy,当源字符串的长度大于目的缓冲区时,就会发生缓冲区溢出。

FORTIFY_SOURCE本质上一种检查和替换机制,对GCC和glibc的一个安全补丁。

检查危险函数,并替换为安全函数,不会对程序的性能产生大的影响。目前支持memcpy, memmove, memset, strcpy, strncpy, strcat, strncat,sprintf, vsprintf, snprintf, vsnprintf, gets等。

实现

缓冲区溢出检查 ,以安全函数_strcpy_chk()为例,可以看到该函数判断源数据长度是否大于目的缓冲区,是就调用_chk_fail()否则正常调用memcpy执行。

格式化字符串检查 ,以安全函数 _printf_chk()为例,针对%n和%N$两种格式化字符串。

实验

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc, char*argv[]){
    char buf1[10],buf2[10],*s;
    int num;

    memcpy(buf1,argv[1],10);           //safe
    strcpy(buf2,"AAAABBBBC");
    printf("%s %s\n",buf1,buf2);

    memcpy(buf1,argv[2],atoi(argv[3])); //unknown
    strcpy(buf2,argv[1]);
    printf("%s %s\n",buf1,buf2);

    //memcpy(buf1,argv[1],11);         //unsafe
    //strcpy(buf2,"AAAABBBBCC");

    s=fgets(buf1,11,stdin);            //fmt unknown
    printf(buf1,&num);
}

使用gdb-pwndbg,反编译main

使用选项0 1 2 分别生成fortify0 1 2

gdb-pwndbg fortify1,可以看到替换成了安全函数,但是printf并没有被替换。

Dump of assembler code for function main:
   0x0000000000001175 <+0>:     push   r12
   0x0000000000001177 <+2>:     push   rbp
   0x0000000000001178 <+3>:     push   rbx
   0x0000000000001179 <+4>:     sub    rsp,0x20
   0x000000000000117d <+8>:     mov    rbx,rsi
   0x0000000000001180 <+11>:    mov    rax,QWORD PTR [rsi+0x8]
   0x0000000000001184 <+15>:    mov    rdx,QWORD PTR [rax]
   0x0000000000001187 <+18>:    mov    QWORD PTR [rsp+0x16],rdx
   0x000000000000118c <+23>:    movzx  eax,WORD PTR [rax+0x8]
   0x0000000000001190 <+27>:    mov    WORD PTR [rsp+0x1e],ax
   0x0000000000001195 <+32>:    movabs rax,0x4242424241414141
   0x000000000000119f <+42>:    mov    QWORD PTR [rsp+0xc],rax
   0x00000000000011a4 <+47>:    mov    WORD PTR [rsp+0x14],0x43
   0x00000000000011ab <+54>:    lea    r12,[rsp+0xc]
   0x00000000000011b0 <+59>:    lea    rbp,[rsp+0x16]
   0x00000000000011b5 <+64>:    mov    rdx,r12
   0x00000000000011b8 <+67>:    mov    rsi,rbp
   0x00000000000011bb <+70>:    lea    rdi,[rip+0xe42]        # 0x2004
   0x00000000000011c2 <+77>:    mov    eax,0x0
   0x00000000000011c7 <+82>:    call   0x1030 <printf@plt>
   0x00000000000011cc <+87>:    mov    rdi,QWORD PTR [rbx+0x18]
   0x00000000000011d0 <+91>:    mov    edx,0xa
   0x00000000000011d5 <+96>:    mov    esi,0x0
   0x00000000000011da <+101>:   call   0x1050 <strtol@plt>
   0x00000000000011df <+106>:   movsxd rdx,eax
   0x00000000000011e2 <+109>:   mov    rsi,QWORD PTR [rbx+0x10]
   0x00000000000011e6 <+113>:   mov    ecx,0xa
   0x00000000000011eb <+118>:   mov    rdi,rbp
   0x00000000000011ee <+121>:   call   0x1040 <__memcpy_chk@plt>
   0x00000000000011f3 <+126>:   mov    rsi,QWORD PTR [rbx+0x8]
   0x00000000000011f7 <+130>:   mov    edx,0xa
   0x00000000000011fc <+135>:   mov    rdi,r12
   0x00000000000011ff <+138>:   call   0x1070 <__strcpy_chk@plt>
   0x0000000000001204 <+143>:   mov    rdx,r12
   0x0000000000001207 <+146>:   mov    rsi,rbp
   0x000000000000120a <+149>:   lea    rdi,[rip+0xdf3]        # 0x2004
   0x0000000000001211 <+156>:   mov    eax,0x0
   0x0000000000001216 <+161>:   call   0x1030 <printf@plt>
   0x000000000000121b <+166>:   mov    rsi,QWORD PTR [rbx+0x8]
   0x000000000000121f <+170>:   mov    ecx,0xa
   0x0000000000001224 <+175>:   mov    edx,0xb
   0x0000000000001229 <+180>:   mov    rdi,rbp
   0x000000000000122c <+183>:   call   0x1040 <__memcpy_chk@plt>
   0x0000000000001231 <+188>:   mov    edx,0xa
   0x0000000000001236 <+193>:   lea    rsi,[rip+0xdce]        # 0x200b
   0x000000000000123d <+200>:   mov    rdi,r12
   0x0000000000001240 <+203>:   call   0x1070 <__strcpy_chk@plt>
   0x0000000000001245 <+208>:   mov    rcx,QWORD PTR [rip+0x2e04]        # 0x4050 <stdin@GLIBC_2.2.5>
   0x000000000000124c <+215>:   mov    edx,0xb
   0x0000000000001251 <+220>:   mov    esi,0xa
   0x0000000000001256 <+225>:   mov    rdi,rbp
   0x0000000000001259 <+228>:   call   0x1060 <__fgets_chk@plt>
   0x000000000000125e <+233>:   lea    rsi,[rsp+0x8]
   0x0000000000001263 <+238>:   mov    rdi,rbp
   0x0000000000001266 <+241>:   mov    eax,0x0
   0x000000000000126b <+246>:   call   0x1030 <printf@plt>
   0x0000000000001270 <+251>:   mov    eax,0x0
   0x0000000000001275 <+256>:   add    rsp,0x20
   0x0000000000001279 <+260>:   pop    rbx
   0x000000000000127a <+261>:   pop    rbp
   0x000000000000127b <+262>:   pop    r12
   0x000000000000127d <+264>:   ret    
End of assembler dump.

gdb-pwndbg fortify2 disas main,可以看到printf也被替换成安全函数了。

Dump of assembler code for function main:
   0x0000000000001175 <+0>:     push   r12
   0x0000000000001177 <+2>:     push   rbp
   0x0000000000001178 <+3>:     push   rbx
   0x0000000000001179 <+4>:     sub    rsp,0x20
   0x000000000000117d <+8>:     mov    rbx,rsi
   0x0000000000001180 <+11>:    mov    rax,QWORD PTR [rsi+0x8]
   0x0000000000001184 <+15>:    mov    rdx,QWORD PTR [rax]
   0x0000000000001187 <+18>:    mov    QWORD PTR [rsp+0x16],rdx
   0x000000000000118c <+23>:    movzx  eax,WORD PTR [rax+0x8]
   0x0000000000001190 <+27>:    mov    WORD PTR [rsp+0x1e],ax
   0x0000000000001195 <+32>:    movabs rax,0x4242424241414141
   0x000000000000119f <+42>:    mov    QWORD PTR [rsp+0xc],rax
   0x00000000000011a4 <+47>:    mov    WORD PTR [rsp+0x14],0x43
   0x00000000000011ab <+54>:    lea    r12,[rsp+0xc]
   0x00000000000011b0 <+59>:    lea    rbp,[rsp+0x16]
   0x00000000000011b5 <+64>:    mov    rcx,r12
   0x00000000000011b8 <+67>:    mov    rdx,rbp
   0x00000000000011bb <+70>:    lea    rsi,[rip+0xe42]        # 0x2004
   0x00000000000011c2 <+77>:    mov    edi,0x1
   0x00000000000011c7 <+82>:    mov    eax,0x0
   0x00000000000011cc <+87>:    call   0x1070 <__printf_chk@plt>
   0x00000000000011d1 <+92>:    mov    rdi,QWORD PTR [rbx+0x18]
   0x00000000000011d5 <+96>:    mov    edx,0xa
   0x00000000000011da <+101>:   mov    esi,0x0
   0x00000000000011df <+106>:   call   0x1040 <strtol@plt>
   0x00000000000011e4 <+111>:   movsxd rdx,eax
   0x00000000000011e7 <+114>:   mov    rsi,QWORD PTR [rbx+0x10]
   0x00000000000011eb <+118>:   mov    ecx,0xa
   0x00000000000011f0 <+123>:   mov    rdi,rbp
   0x00000000000011f3 <+126>:   call   0x1030 <__memcpy_chk@plt>
   0x00000000000011f8 <+131>:   mov    rsi,QWORD PTR [rbx+0x8]
   0x00000000000011fc <+135>:   mov    edx,0xa
   0x0000000000001201 <+140>:   mov    rdi,r12
   0x0000000000001204 <+143>:   call   0x1060 <__strcpy_chk@plt>
   0x0000000000001209 <+148>:   mov    rcx,r12
   0x000000000000120c <+151>:   mov    rdx,rbp
   0x000000000000120f <+154>:   lea    rsi,[rip+0xdee]        # 0x2004
   0x0000000000001216 <+161>:   mov    edi,0x1
   0x000000000000121b <+166>:   mov    eax,0x0
   0x0000000000001220 <+171>:   call   0x1070 <__printf_chk@plt>
   0x0000000000001225 <+176>:   mov    rsi,QWORD PTR [rbx+0x8]
   0x0000000000001229 <+180>:   mov    ecx,0xa
   0x000000000000122e <+185>:   mov    edx,0xb
   0x0000000000001233 <+190>:   mov    rdi,rbp
   0x0000000000001236 <+193>:   call   0x1030 <__memcpy_chk@plt>
   0x000000000000123b <+198>:   mov    edx,0xa
   0x0000000000001240 <+203>:   lea    rsi,[rip+0xdc4]        # 0x200b
   0x0000000000001247 <+210>:   mov    rdi,r12
   0x000000000000124a <+213>:   call   0x1060 <__strcpy_chk@plt>
   0x000000000000124f <+218>:   mov    rcx,QWORD PTR [rip+0x2dfa]        # 0x4050 <stdin@GLIBC_2.2.5>
   0x0000000000001256 <+225>:   mov    edx,0xb
   0x000000000000125b <+230>:   mov    esi,0xa
   0x0000000000001260 <+235>:   mov    rdi,rbp
   0x0000000000001263 <+238>:   call   0x1050 <__fgets_chk@plt>
   0x0000000000001268 <+243>:   lea    rdx,[rsp+0x8]
   0x000000000000126d <+248>:   mov    rsi,rbp
   0x0000000000001270 <+251>:   mov    edi,0x1
   0x0000000000001275 <+256>:   mov    eax,0x0
   0x000000000000127a <+261>:   call   0x1070 <__printf_chk@plt>
   0x000000000000127f <+266>:   mov    eax,0x0
   0x0000000000001284 <+271>:   add    rsp,0x20
   0x0000000000001288 <+275>:   pop    rbx
   0x0000000000001289 <+276>:   pop    rbp
   0x000000000000128a <+277>:   pop    r12
   0x000000000000128c <+279>:   ret    
End of assembler dump.

fortify1测试结果,在strcpy中出现溢出,被检测到了。但是任然可以使用格式化字符串漏洞。

使用fortify2实验,%n和%N$ 被检测到了。而且%N$需要从%1$x后开始连续可用,下图中仅打印出一个。

RELRO

简介

在启用延时绑定时,符号的解析只发生在第一次使用的时候,该过程是通过PLT表进行的,解析完成后,相应的GOT表条目才会修改为正确的函数地址。因此,在延迟绑定的情况下,.got.plt必须是可写的。攻击者就可以通过篡改地址劫持程序。

RELRO(Relocation Read-Only)机制的提出就是为了解决延时绑定的安全问题。将符号重定向表设置为只读,或者在程序启动时就解析绑定所有的动态符号,从而避免GOT被篡改。RELRO有两种形式:

  • Partial RELRO : 一些段(.dynamic , .got等在初始化后将会被标记为只读),默认开启。

  • Full RELRO: 除了Partial RELRO,延时绑定被禁止,所有的导入符号将在开始时被解析,.got.plt段会被完全初始化为目标函数的最终地址,并被mprotect标记为只读,但是.got.plt会被合并到.got,也就看不到这段了。会对性能造成影响。

实验

relro.c 意思就是输入一个16进制地址,然后向该地址写入4141414141414141

#include<stdio.h>
#include<stdlib.h>
int main(int argc,char*argv[]){
    printf("hello");
    printf("%s",argv[1]);
    printf("sdsd");
    size_t * p=(size_t*)strtol(argv[1],NULL,16);
    p[0]=0x41414141;
    printf("RELRO: %x\n",(unsigned int )*p);
    return 0;
}

实验过程失败了,按照书来的出现一个问题

动态重定位表中,main始终是R_X86_64_GLOB_DAT, 书上应该是和printf相同的才对。

结论:norelro,可以修改.got和.got.plt

​ partial可以修改.got.plt

full一个都不能修改

不能修改情况如下,当然可能有其他的原因

实现

有延时绑定时,call会先跳到printf@plt,然后jmp到.got.plt项,再跳归来进行符号绑定,完成后.got.plt修改为真正的函数地址。

没有延时绑定时,所有解析工作在程序加载时完成,执行call指令跳转到对应的.plt.got项,然后jmp到对应的.got项,已经保存了解析好的函数地址。

编译选项总结

stack canaries

-fstack-protector        对alloca系列函数和内部缓冲区大于8字节的函数启用保护
-fstack-protector-strong 增加对包含局部数组定和地址引用的函数的保护
-fstack-protector-all    对所有函数启用保护
-fstack-protector-explicit 对包含stack_protect属性的函数启用保护
-fno-stack-protector       禁用保护

nx

-z execstack
-z no execstack

ASLR

-ldl

PIE

-fpic   为共享库生成位置无关代码
-pie    生成动态链接的位置无关可执行文件,通常需要同时指定-fpie
-no-pie 不生成动态链接的位置无关可执行文件
-fpie   类似于-fpic,但生成的位置无关代码只能用于可执行文件
-fno-pie 不生成位置无关代码

FORTIFY_SOURCE

-D_FORTIFY_SOURCE=1   开启缓冲区溢出攻击检查
-D_FORTIFY_SOURCE=2   开启缓冲区溢出以及格式化字符串攻击检查

RELRO

-z norelro              禁用relro
-z lazy                 开启Partial RELRO
-z now                  FULL PARTIAL