CSAPP Bomb Lab記錄


記錄關於CSAPP 二進制炸彈實驗過程

(CSAPP配套教學網站Bomb Lab自學版本,實驗地址:http://csapp.cs.cmu.edu/2e/labs.html

 (個人體驗:對x86匯編尋址模式要有清晰的了解,如mov指令涉及的是計算出的地址所指向的存儲單元的值,而lea指令保留的是計算出來的地址,數字是否加$表示常數的問題等;

      實驗中涉及的跳表的存儲方式、鏈表的處理等是C語言的匯編語言實現方式,處理起來較為復雜,但可對這些方式的對象底層實現方式有一個較為清晰的了解;

      涉及指針操作時,尤其需要注意哪些指令是用於計算地址,哪些指令是使用某個地址所對應的空間的值;

      雖然自學版本不涉及打分與扣分,但仍需要較大的耐心和精力去完成,have fun~)

1.實驗准備

  閱讀隨實驗准備的Writeup和README文件,對實驗的內容有大致的了解。

  README:二進制炸彈是一個包含有6個“炸彈”的linux環境中可運行的C程序,實驗過程中需要借助調試工具和技巧獲得目標字符串,從而拆除炸彈。該文件也介紹了如何將實驗動態部署從而獲得打分等功能的過程,這里不做討論;

  Writeup:提示了一些可能應用的到的工具。也提供了關於實驗的一些有效信息。

  (1)實驗使用兩種方式讀取輸入,直接在運行過程中讀取輸入或是從指定文件中讀取輸入(每個字符串占據一行),后者格式為./bomb file.txt(./  表示在當前目錄下,bomb為可執行文件, file.txt為記錄目標字符串的文件);

  (2)可以幫助實驗的工具如gdb,linux中的指令objdump -t(輸出程序符號表)/-d(輸出程序的反匯編),strings(輸出程序中的可輸出字符串)。在使用上述工具的一個重要發現在於除了明確提出的6個字符串處理函數(phase1-6)外,還存在另外的兩個名為fun7和secert_phase的函數,猜想可能有額外的字符串需要進行破解;

  另外,簡單的使用objdump -d bomb命令對程序查看,可以知道主干部分(main)主要是進行初始化操作、讀取方式選擇、對目標字符的處理和判斷這幾個部分,與字符串讀取相關的函數有read_line等, 字符串讀取后調用單獨的函數進行驗證,故實驗過程可以直接匯聚精力在字符串處理的函數部分,其它邏輯可依自己的興趣查看。

 

2.實驗過程

  程序對字符串的處理主要流程如下圖所示

  

  調用read_line函數讀取字符串,將%eax中值入棧(注意這里的%eax是在函數調用之后的值,所以存儲的是read_line的返回值,具體過程參見read_line函數定義),這里的入棧是函數調用傳入參數的過程,之后通過特定的函數對字符串進行驗證。

  •   phase_1

   

  將函數參數存放在%eax中,並將%eax和0x80497c0入棧,調用strings_not_equal函數,猜想0x80497c0為目標字符串存放的地址,使用gdb進行斷點調試,得到的結果如下圖

  

  從而得到了第一個字符串。 

  •   phase_2

  分析主要集中在phase_2調用的函數上面。

  read_six_number:將六個連續棧上空間地址入棧,從低地址到高地址為%ebp-24至%ebp-4,這里的%ebp是對應phase_2函數的%ebp,而不是read_six_number的%ebp,后者在函數被調用時更新。之后將0x8049b1b和輸入字符串的地址依次入棧,調用sscanf函數;

  sscanf:用於格式化字符串的讀取,典型的參數形式為——待讀取字符串的地址,讀取字符串的格式,存儲讀取內容的地址。按照函數入棧時從右自左的順序,猜想程序將輸入字符串的地址作為讀取的源地址,0x8049b1b存儲的應為讀取字符串的格式,其余的六個地址應為存儲讀取出來的數據的地址。輸出0x8049b1b處的字符串,結果如下,驗證了猜想。

  

  read_six_number是將輸入的字符串(6個數字)讀取到棧的相應位置,准確的說是按輸入順序存放在%ebp-24至%ebp-4之間連續的6個存儲單元中(注意函數參數的入棧順序,且%ebp相對於phase_2函數而言),再進行后續的驗證工作。

  phase_2驗證部分:

  檢驗第一個數的值是否為1

  

  前三條語句使%ebx值為1,%esi值為%ebp-24,%eax值為2,imul指令使得%eax為2,其中一個乘法因子地址為%ebp-24+1*4-4即為%ebp-24,指向的值為前面驗證過的1

  

  之后為一個循環語句,%ebx值自增1,當%ebx不大於5時,重復上述過程,即

  %ebx=%ebx+1;

  %eax=%ebx+1,

  %eax=%eax*前一個驗證過的數字的值,將%eax與當前待驗證的值相比較

  故第一個值為1,第二個值應為(1+1)*1=2,第三個值為(2+1)*2=6,第四個值為(3+1)*6=24,第五個值為(4+1)*24=120,第六個值為(5+1)*120=720.

  •   phase_3

  phase_3也調用了sscanf函數,其參數按高地址到低地址為%ebp-4,%ebp-5,%ebp-12(相對於phase_3函數的%ebp),0x80497de,%edx(存放從輸入讀取的字符串)

  

  類似於phase_2中sscanf的用法,此時讀取格式如圖所示,在讀取數字和字符的基礎上,對相應字符串進行驗證。

  

  需要注意的是函數的參數是按從左至右的順序入棧的,所以對於輸入的字符串,%ebp-4存儲的是輸入的第三個數字,%ebp-5存儲字符,%ebp-12存儲的是輸入的第一個數字,即需要注意順序。

  

  對后續驗證過程進行分析,首先對第一個數字(%ebp-12處)進行檢驗,驗證其值是否大於7,並根據其值進行跳轉,在這一步暫時無法發現具體的值是多少,繼續向后看。

  

  后續存在多處類似圖中所示的處理,即對%bl進行操作,再將第三個數字(%ebp-4處)檢驗,最后跳轉至0x8048c8f處,並在該處對中間的字符(%ebp-5)進行驗證

  

  

   猜想應該有多種可能的值相對應可以選擇,使得對應的一組字符串相匹配。(注,后來發現,形如jmp *xx(,%eax,4)的寫法是switch語句中跳表所用的結構,為間接跳轉

  左圖為0x80497e8處的跳轉地址表的具體值.

  這里求解使用的是case 0的情況,即%eax=0,從而有%bl=0x71,%ebp-4=0x309,%ebp-5=0x71,得到指定字符串為0 q 777.

  

  •   phase_4  

  同樣調用sscanf函數,傳入參數為%ebp-4,0x8049808,%edx,即讀取一個整數。

  

  

  首先將讀取的值與0比較,若小於0則炸彈爆炸,再將讀取的這個值作為參數,調用函數func4.

  

  且只有函數返回值為0x37時,才能解開phase_3.

  

  func4的處理邏輯描述:

  func4應為遞歸函數的形式。產生的返回值隨k的增加為斐波那契數列。

int func4(int k)
{
  if(k<=1) return 1; return func4(k-1)+func4(k-2); }

  由上述分析可知,func4返回值為0x37(即55)的輸入為9.

  •   phase_5

  phase_5首先調用string_length函數得到輸入字符串的值,並與6比較(再次提醒,在函數調用之后出現的%eax很可能存儲函數的返回值,假如返回值存在的話),相等時繼續進行判定。

  

  下圖進行一些賦值操作,令%edx=0(異或),%ecx=%ebp-8,%esi=0x804b220。

  

  接下來是一個循環語句,進行字符串的驗證操作。尋址模式(%edx,%ebx,1)表示計算%edx+%ebx*1,並將結果作為地址訪問對應的存儲單元的值。(%ebx存儲的是指向輸入字符串第一個字符的地址)

  

  處理的邏輯是:

  (1)將第n個字符存放在%al中(n=0,1,2,3,4,5),並截取低4位(and操作)符號拓展存放到%eax中

  (2)將%eax作為偏移量,將0x804b220(即%esi)+%eax所指向的存儲單元的值存放在%al中

  (3)將上述處理后的值存放到%ebp-8+n處

  之后將0x804980b和%ebp-8入棧,並比較兩者是否相等,則0x804980b處存放的應該為處理后的字符串。

  

  具體的情況如下

  經處理后的字符應為giants,對應的字符分別為0x67,0x69,0x61,0x6e,0x74,0x73

  圖示為0x804b220起始的連續16個字符序列

  則前文中對應的偏移量為0xf,0x0,0x5,0xb,0xd,0x1.則輸入字符串保證低四位與對應的偏移量相同即可。

  這里選用的是0x6f,0x70,0x65,0x6b,0x6d,0x61,即為opekma(答案不唯一)。

  •   phase_6(接觸時感覺處理有點復雜,涉及多重循環,后來經人提醒,處理過程還涉及鏈表操作)

  首先進行賦值操作,%edx=%ebp+8(即輸入字符串起始地址,也是phase_6傳入的參數)處存儲的值,%eax=%ebp-24,並將%eax與%edx入棧,調用read_six_numbers函數,其功能前面已有介紹。

  

  再對讀取出來的數字進行相應的處理,后續為一個較大的循環過程,這里直接描述其邏輯處理過程,不再截圖。

  (1)%eax=%ebp-24,%eax=%eax+%edi*4處存儲的值(%edi初始值為0)

  (2)%eax--,並將其值與5比較,當%eax-1的值不大於5時,繼續進行驗證

  (3)%ebx=%edi+1,若%ebx>5,則跳至(7)

  (4)%eax=%edi*4,並將%eax種的值存放在%ebp-0x38處,令%esi=%ebp-24

  (5)%edx=%ebp-0x38處存儲的值(這里注意有連續的兩條指令一個是lea,另一個是mov),令%eax=%edx+%esi*1處存儲的值,並與%esi+%ebx*4處存儲的值相比較

  (6)兩者不相等時,將%ebx++,當%ebx不大於5時,跳至(5),否則順序執行

  (7)%edi++,若%edi不大於5,則跳至(1)

  上述循環完成的功能是:將每個數讀取出來,並驗證其不大於6,同時,保證任意兩個數字不相等

  

  之后是一個類似於上述描述的循環:

  (1)%ecx=%ebp-24,%eax=%ebp-48,並將%eax的值存放在%ebp-60處

  (2)%esi=%ebp-52處存儲的值,%ebx=1,%eax=4*%edi(%edi初值為0),%edx=%eax

  (3)比較%ebx和%eax+%ecx處存儲的值,若前者大於等於后者,則跳轉至(6),否者順序執行

  (4)將%edx+%ecx處存儲的值賦值給%eax,%esi=%ebp-44,

  (5)%ebx++,若%ebx<%eax,則跳轉至(4)

  (6)將%ebp-52處存儲的值賦值給%edx,%esi的值賦值給%edx+4*%edi處的存儲單元,%edi++,若%edi小於等於5,則跳轉至(2)執行,否則順序執行

   (在這段程序中,出現了%eiz寄存器,最終在Stack Overflow中找到了對應的解釋,原答案。自己對最高票回答的理解,即在指令的執行過程中,為了保證指令的正常執行如流水線操作等,可能需要加入必要的延時來避免競爭,一般是可以在兩條指令中間插入合適數量的nop保證正常運行,但是處理器處理一條長指令比對應的多條短指令如nop要更有效率,所以有時會在程序中插入這種奇怪的lea指令,占據7個字節,作為替代,比執行7條nop指令要更為快,同時也保證程序正常執行。這里%eiz為一個值為0的偽寄存器,通過lea    0x0(%esi,%eiz,1),%esi這種指令達到類似於nop指令的效果。)

   乍一看感覺一點頭緒都沒有...

  經他人提示,程序處理中涉及鏈表。猜想下列程序過程涉及的是鏈表操作(因為%esi中存儲的總是地址,且可以通過%esi+8訪問新的地址),%esi初始值(即%ebp-52處的值)為鏈表的頭節點地址,%esi+8為該節點的指針域,存放的是下一個節點的地址,理論上是可以成立的。

  

  按照上述猜想,上述循環的功能為根據輸入的每個數字的大小,將每個數字對應的節點(1或小於1對應頭節點,6對應第6個節點,前面已經證明輸入數字不大於6,且兩兩不相等)的地址按順序存放在%ebp-48至%ebp-28的存儲空間。

  

  相應的,以下處理將每個節點的指針域按照上述排列的順序所存儲的地址做出了修改。每次取存放在%ebp-48+(i-1)*4處的地址所對應的節點,將其指針域修改為%ebp-48+i*4處的地址。

  

  緊接着上述邏輯之后的代碼,功能似乎是將尾節點的指針域賦值為NULL,使得上述猜想更有說服力。

  

 

  按照上述猜想,下列代碼的功能為:

  (1)將%esi賦值為頭節點地址,%edi=0;

  (2)%edx=節點指針域的值,則(%esi)表示節點數據域的值,(%edx)為后繼節點數據域的值(數據域應為雙字四字節即一個寄存器的大小)

  (3)將當前節點數據域與后繼節點數據域比較,當前者大於等於后者時繼續進行后續節點的比較,否則炸彈爆炸

    上述代碼的功能為:檢驗經過重新排序的鏈表應為單調遞減鏈表

  

 

  此時對原始鏈表的情況進行分析,原始鏈表的起始地址存放在%ebp-52處(未經過排序操作前)。

  gdb可以直接輸出鏈表節點中存儲的值,再由上述分析鏈表應重新排列為遞減鏈表。

  所以輸入的序列值可以為4 2 6 3 1 5

   以上即為所有6個字符串炸彈的解答過程,主線任務已經完成...

  然而,從實驗准備階段我們已經知道存在secert_phase的支線任務...

 

  •   secert_phase

   從反匯編的結果我們可以看出存在一個函數secert_phase,而在前面的所有過程中包括phase1-6和main函數中都沒有包含對其的調用。在main函數所調用的各種函數中,只有initialize_bomb、read_line、phase_defused函數是自定的函數,查看這幾個函數的定義,發現secert_phase函數在phase_defused中被調用。

  對phase_defused的處理方式如下圖所示,main函數調用phase_i進行驗證(如不滿足條件,調用bomb函數),之后馬上調用phase_defused,之前以為其功能為拆除炸彈后進行相應的交互提醒,后來發現這一功能由后面的printf實現,如圖示0x80496e0處即存儲一個提示字符串。

  

  

   對phase_defused進行分析:

  將0x804b480處的值與6比較,從反匯編中可以看出該處存放的應該為輸入的字符串的個數,若輸入字符串不為6,則直接跳至phase_defused的末尾,即secert_phase需要在前六個字符串解除后完成。。

  

  

  之后調用了sscanf函數,其中各個參數的值在gdb中可以顯示出來

  

  第一個參數指明了讀取的格式,而第二個參數,筆者的第一反應是前面phase4的輸入9.

  即在phase_defused函數除了讀取輸入9外,還可以讀入額外加入的字符串,並對字符串進行處理。沒有字符串時,sscanf返回值不為2,會直接跳至phase_defused函數末尾,不影響主干部分執行。

  

  之后,phase_defused函數對讀取的字符串進行了比較,用來比較的是0x8049d09處存儲的字符,如下圖所示。當比較相匹配時,就會調用secert_phase函數。

  

  

  

  對secert_phase函數解析:

  首先調用__strtol_internal函數,原型為long int  __strtol_internal(const char *__nptr, char **__endptr, int __base, int __group);最后一個參數為0時,功能與strtol相同。

  

  其中,第一個參數為目標字符串,中間參數可用作錯誤返回值(題中衛NULL),第三個參數為采用的進制base(指第一參數采用的進制)。返回值為目標字符串對應的整數值。題中的%eax為read_line函數的返回值,即為輸入的字符串的起始地址。設函數的返回值為n。這里輸入數字(以字符串表示)采用的是0xa即十進制。

  

  令%ebx=n,%eax=n-1,首先應滿足無符號比較中%eax≤0x3e8。

  

  之后調用fun7函數,只有fun7返回值為7時,才能避免炸彈爆炸。這里傳入的參數為n和常數0x804b320.

  

  

  fun7為一個遞歸函數,處理過程如下:

  

int fun7(int *p,int n)
{
  if(!p) return 0; if(n<*p) return 2*fun7(*(p+4),n); else { if(n==*p) return 0; else return 2*fun7(*(p+8),n)+1; } }

  在0x804b320處存儲的值的情況如下圖:

  

  

  對fun7返回值為7進行猜想,可能為7=2*3+1,3=2*1+1,1=2*0+1的情況。

  故第一層:p=0x804b320,n>*p(即36),*(p+8)=0x804b308,函數返回值為2*3+1=7,

   第二層:p=0x804b308,n>*p(即50),*(p+8)=0x804b2d8,函數返回值為2*1+1=3,

  

   第三層:p=0x804b2d8,n>*p(即107),*(p+8)=0x804b278,函數返回值為2*0+1=1

  

   第四層:p=0x804b278,n=*p(即為0x3e9,前面已限定n≤0x3e9)時,函數返回值為0。

  

  從而使得遞歸最終的返回值為7,此時,n=0x3e9的十進制表示,而輸入字符串為1001時,會由__strtol_internal轉化為10進制數。

  所以輸入為1001.

 

3.實驗結果  

  得到最終的結果:

  

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM