对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