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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM