對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