深入理解计算机系统-bomblab详解
目錄
- Phase 1
- Phase 2
- Phase 3
- Phase 4
- Phase 5
- Phase 6
- Secret Phase
- 幾條野路子
下載實驗用的文件們戳這里
bomblab的背景很有趣。Dr. Evil把“二進制炸彈”裝在了教室的機子里。想要拆掉炸彈,你必須反編譯“炸彈”,通過其中的匯編指令推測出可以拆掉炸彈的phrase。
好啦,我們看一看下載的文件夾里都有什么。bomb就是要我們去拆的“炸彈”;bomb.c是炸彈的源代碼,但是最為關鍵的部分被刪掉了,只保留了骨架。gdbnotes-x86-64是個重要的文件,里面有這個實驗里用到的各種工具的使用方法。剩下的就是實驗的各種說明了~
Phase 1
我們來看一看phase_1的反匯編是什么樣子。一種方法是直接在終端里輸入"objdump -d bomb > bomb.txt", 直接把objdump的輸出重定向到bomb.txt里,之后就可以直接在記事本里面看反匯編了;或者,先在終端里輸入"gdb bomb", 進到gdb里面;再輸入"disas phase_1", 就能看到phase_1的反匯編了。我們這里用第一種方法,最終在記事本里看到的結果是下面這個樣子:
0000000000400ee0 <phase_1>:400ee0: 48 83 ec 08 sub $0x8,%rsp400ee4: be 00 24 40 00 mov $0x402400,%esi400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>400eee: 85 c0 test %eax,%eax400ef0: 74 05 je 400ef7 <phase_1+0x17>400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>400ef7: 48 83 c4 08 add $0x8,%rsp400efb: c3 retq那我們就先來捋順這段匯編的邏輯吧。這段代碼先把一個值,更準確地說,是字符串的地址(0x402400)放到%esi里,之后調用一個叫"strings_not_equal"的函數;最后判斷這個函數的返回值:等于零,通過;不等于零,調用"explode_bomb"把炸彈炸掉(或許你會問%edi在哪里?其實%edi就是我們輸入的字符串的地址)。顯然,這短短的一段匯編里,最重要的就是對strings_not_equal函數的調用。至于這個函數是干什么的,猜也能猜得出來:判斷兩個字符串是不是不相等:相等,返回零;否則返回非零(仔細看strings_not_equal的實現,實際上不相等時返回1)(當然你也可以自己翻到0x401338,看一看這個推測是否正確。)。
現在讓我們考慮一下strings_not_equal這個函數的兩個參數。%rdi中的值,就是我們輸入的字符串的地址;%rsi中的值是后面傳進去的0x402400.那么0x402400指向的是什么字符串呢?我們打開gdb看一看。在終端中輸入:
輸出是什么呢?
0x402400: "Border relations with Canada have never been better."只要我們的輸入和上面這個字符串相同就行了。打開終端試一試~
(P.S. 這個字符串是2016年初更新的。它的出處是,2001年時任美國總統的喬治·布什,為歡迎加拿大總理訪美所作的講話。原句為"Border relations between Canada and Mexico have never been better. " 5年后,布什簽署法案,授權在美墨邊境修建隔離墻。2015年9月,特朗普宣布競選美國總統,而其競選承諾中有一條就是“在美墨邊境修墻”。從這個細節,我們似乎可以一瞥作者對美墨邊境相關政策的態度。)
Phase 2
前面那個Phase就當熱身啦,接下來的幾個Phase才是重頭戲。還是剛才的辦法,我們把phase_2的匯編也拿出來:
0000000000400efc <phase_2>:400efc: 55 push %rbp400efd: 53 push %rbx400efe: 48 83 ec 28 sub $0x28,%rsp400f02: 48 89 e6 mov %rsp,%rsi400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>... ...這段匯編先“讀入“六個數(0x400f05:read_six_numbers)。從哪里“讀入”呢?當然不是stdin。我們注意到,和上面那個Phase一樣,%rdi并沒有在調用這個函數之前出現。也就是說,phase_2函數的第一個參數,被原封不動地傳到了read_six_numbers的第一個參數。那么%rsi,也就是第二個參數,代表的是什么呢?注意看這兩行匯編:
400efe: 48 83 ec 28 sub $0x28,%rsp400f02: 48 89 e6 mov %rsp,%rsi知道了嗎?%rsi里放的是地址!結合read_six_numbers第一個參數的含義(恰好是我們輸入的字符串)大膽猜想,我們可以知道—— 1. read_six_numbers從我們自己輸進去的字符串里"read"; 2. %rsi在源代碼里應該是個指針,這個指針指向一個數組的開頭,而這個數組就是放read_six_numbers從字符串里抽出的六個數用的。不過我們還是來看一看read_six_numbers具體是怎么實現的:
000000000040145c <read_six_numbers>:40145c: 48 83 ec 18 sub $0x18,%rsp401460: 48 89 f2 mov %rsi,%rdx401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx401467: 48 8d 46 14 lea 0x14(%rsi),%rax40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)401470: 48 8d 46 10 lea 0x10(%rsi),%rax401474: 48 89 04 24 mov %rax,(%rsp)401478: 4c 8d 4e 0c lea 0xc(%rsi),%r940147c: 4c 8d 46 08 lea 0x8(%rsi),%r8401480: be c3 25 40 00 mov $0x4025c3,%esi401485: b8 00 00 00 00 mov $0x0,%eax40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>40148f: 83 f8 05 cmp $0x5,%eax401492: 7f 05 jg 401499 <read_six_numbers+0x3d>401494: e8 a1 ff ff ff callq 40143a <explode_bomb>401499: 48 83 c4 18 add $0x18,%rsp40149d: c3 retq%rsi是什么?我們猜是數組首個元素的地址。0x4(%rsi)是什么?我們猜是數組第二個元素的地址。那么0x14(%rsi)、0x10(%rsi)等等呢?同理。以上這些都被read_six_numbers做成了sscanf的末幾個參數(不是"scanf"!多了一個"s"!!!)(注意到了嗎?有幾個地址,因為寄存器放不下,被送到棧里面了)。sscanf的第一個參數,從匯編來看,是read_six_numbers的第一個參數,也就是我們輸入的字符串;而第二個參數是存儲在0x4025c3的字符串。和上面一樣,我們來看一看0x4025c3里有什么。
0x4025c3: "%d %d %d %d %d %d"覺得熟悉嗎?事實上,sscanf的作用與scanf類似,只不過scanf從sdtin中讀取數據,sscanf從它的第一個參數中讀取數據。read_six_numbers就是借助sscanf,把我們輸入的字符串中的數抽出來,放到一個數組里。我們的猜測是對的。
現在,我們知道了phase_2的要求是輸入6個特定的數,數與數之間用空格隔開。那么這六個數又是什么呢?我們接著往下看。
我們已經知道了,%rsp里放的就是數組第一個元素的地址。所以說,很顯然,這段匯編在判斷我們輸入的六個數,第一個數是不是等于一。判斷了之后就跳到0x400f30:
... ...400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp400f3a: eb db jmp 400f17 <phase_2+0x1b>... ...嗯,這三行匯編把第二個數(int是四個字節)的地址放進%rbx里,把最后一個數下一個字節((18)16=(24)10(18)_{16} = (24)_{10}(18)16?=(24)10?)的地址放進%rbp里(是判斷循環終止條件用的),之后跳到0x400f17.
... ...400f17: 8b 43 fc mov -0x4(%rbx),%eax400f1a: 01 c0 add %eax,%eax400f1c: 39 03 cmp %eax,(%rbx)400f1e: 74 05 je 400f25 <phase_2+0x29>400f20: e8 15 05 00 00 callq 40143a <explode_bomb>400f25: 48 83 c3 04 add $0x4,%rbx400f29: 48 39 eb cmp %rbp,%rbx400f2c: 75 e9 jne 400f17 <phase_2+0x1b>... ...從0x400f17這里,程序就開始循環處理我們輸進去的六個數了。首先,程序把%rbx指向的數的前一個放到%eax里,之后讓它翻倍:
400f17: 8b 43 fc mov -0x4(%rbx),%eax400f1a: 01 c0 add %eax,%eax翻倍之后再檢查%eax里的值是不是和%rbx當前指向的數相同:
400f1c: 39 03 cmp %eax,(%rbx)400f1e: 74 05 je 400f25 <phase_2+0x29>400f20: e8 15 05 00 00 callq 40143a <explode_bomb>如果相同呢,就跳到0x400f25;如果不相同,就調用explode_bomb引爆炸彈。看來,這好像是個等比數列!1為首項,2為公比!
我們最后再來看一看0x400f25那里的匯編是什么。
和我們想得一樣,這里程序把%rbx里的指針后移四字節,再判斷指針是否到達了數組末尾。
所以,要過這個phase,只要輸入以一為首項,二為公比的等比數列前六項就行了。
Phase 3
這個phase考的是switch語句的匯編表示。
好啦,先看題吧。phase_3開頭和上面那個read_six_numbers很像,也調用了sscanf,從我們輸入的字符串里提取數字。只不過這次只有兩個數,第一個放在0x8(%rsp)那里,第二個放在0xc(%rsp)那里。
仔細看匯編。我們發現,0x8(%rsp)和0xc(%rsp)這兩個值,只在兩個地方出現過。對于0x8(%rsp), 這個地方是:
而對于0xc(%rsp), 這個地方是:
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax400fc2: 74 05 je 400fc9 <phase_3+0x86>400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>在0x400f75這一行,代碼究竟要跳轉到哪里呢?應該是0x402470這個地址中儲存的值(星號解引用,和指針一樣),和8乘%rax里的值加在一起組成的地址。%rax里的值我們知道,就是我們輸入的第一個數;那么0x402470這個地址中的值又是什么呢?還是像前面那樣,我們在gdb中輸入x /wx 0x402470,看一看輸出:
(gdb) x /wx 0x402470 0x402470: 0x00400f7c0x400f7c顯然是個地址。翻回到bomb的匯編,我們發現這個地址就是0x400f75的下一行;而這行恰巧對應switch的第一個case。其實,從0x402470這個地址開始,儲存著7個指向不同case的地址。而從匯編來看,這7個case處理輸入輸出的邏輯是一致的,比如第一個case,對應"case 0:",也就是我們輸入的首個值為零的情況:
400f7c: b8 cf 00 00 00 mov $0xcf,%eax400f81: eb 3b jmp 400fbe <phase_3+0x7b>這兩行匯編的邏輯,想必不難理解。先把一個數放到%eax里,之后跳到0x400fbe。當然0x400fbe那里有什么,上面已經提到過了——程序在那里處理我們輸入的第二個數!
這樣看來,程序的邏輯就清楚了。我們需要先輸入兩個數,第一個指示應該用哪個case;第二個用來和這個case放到%eax里的數比較。如果相等,這個phase就過掉了;不相等就引爆炸彈。
所以,這個phase的答案也不是唯一的啦。每一個case對應不同的值,只要我們輸入的兩個值像匯編里的值那樣對應好就行。比如我們輸入的第一個數是0(小于七的非負數就好),查過第一個case之后(0x400f7c)我們就知道第二個數應該是0xcf,也就是十進制的207.之后把"0 207"輸進去就行了~
Phase 4
加油加油!phase已經做掉一半啦~
嗯,關于這個phase,我只能說我先看到了bomb.c里的一段注釋:
我的數學水平……嗯,一言難盡。還是來看匯編吧。phase_4和上面phase_3的開頭是類似的,也是讓我們輸入兩個數,并且要求第一個數不大于14,第二個數等于零(作者大概是想重用上面的字符串?還是另有原因呢……)。之后程序調用func4。func4第一個參數就是我們輸入的第一個數,第二個、第三個參數都是常數,是0和14(%edx里的那個)。func4返回之后,程序判斷func4的返回值,等于零就通過,否則引爆炸彈。
好了,我們現在來看一看func4里到底發生了什么。
當然這段匯編很亂……我最后把它翻譯成了C語言代碼:
int func4(int edi, int esi, int edx) {int eax = edx;eax -= esi;int ecx = eax;ecx >>= 31;eax += ecx;eax >>= 1;ecx = eax + esi;if (ecx > edi){edx = ecx - 1;eax = func4(edi, esi, edx);eax *= 2;return eax;}else{eax = 0;if(ecx < edi){esi = ecx + 1;eax = func4(edi, esi, edx);eax = eax * 2 + 1;return eax;}elsereturn eax;} }其實改一改就更好理解了:
int func4(int edi, int esi, int edx) {int eax = edx - esi;if (eax < 0)eax -= 1;eax /= 2;int ecx = eax + esi;if (ecx > edi)return func4(edi, esi, ecx - 1) * 2;else if (ecx < edi)return func4(edi, ecx + 1, edx) * 2 + 1;elsereturn 0; }或許這個函數有它的現實意義(我很想知道!)。不過現在我們只要讓它返回零……這個簡單。既然edx是14,esi是0,那么開頭三行代碼執行完之后ecx就是7. 什么情況下func4會返回零呢?當然是ecx等于edi的情況。這樣我們就可以確定,edi是7!
(實際上滿足題意的數不僅僅是7. 寫循環把0到14的值都試一遍就知道了。不過我覺得作者的本意可能是讓我們先推出func4的數學表達式……)
Phase 5
phase 5呢,讓我們先輸入一個長為六的字符串,之后程序就進到循環里操作這些字符串了:
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx40108f: 88 0c 24 mov %cl,(%rsp)401092: 48 8b 14 24 mov (%rsp),%rdx401096: 83 e2 0f and $0xf,%edx401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) 4010a4: 48 83 c0 01 add $0x1,%rax4010a8: 48 83 f8 06 cmp $0x6,%rax4010ac: 75 dd jne 40108b <phase_5+0x29>%rbx的地址就是我們輸入的字符串的地址,%rax里是字符的索引。每輪循環,程序先把字符串的一個字符放到%edx里((%rsp)其實就是個中介),之后用0xf按位與。之后再把按位與的結果當成索引,從0x4024b0那里的字符串拿出在對應位置的字節,放到一個數組里(就是0x10(%rsp))。當然這個數組也相當于一個字符串。我們猜測,循環結束之后,程序很有可能會把這個這個字符串和某個特定的字符串比較。不過我們看一看0x4024b0那里的字符串是什么。
(gdb) x /s 0x4024b0 0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"嗯,有點亂,對不對?現在我們再來看循環結束之后程序又做了什么。
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)4010b3: be 5e 24 40 00 mov $0x40245e,%esi4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>4010c2: 85 c0 test %eax,%eax4010c4: 74 13 je 4010d9 <phase_5+0x77>4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>看到了沒有?在這里,程序調用了strings_not_equal,而它的第一個參數是前面新生成的字符串,第二個參數是0x40245e那里的一個字符串。現在我們來看一看0x40245e那里是什么:
(gdb) x /s 0x40245e 0x40245e: "flyers"再翻回到前面那個有點亂的的字符串,這里的六個字母前面都有對不對?所以,我們首先要逐一找到flyers里的六個字母在前面那個字符串里對應的索引;之后從可打印的字符中找到六個,讓它們和0xf按位與的結果,恰好等于那些索引就行了。
好啦,我們很容易找到這六個字母對應的索引是“9 15 14 5 6 7”,而它們對應的二進制值是“1001 1111 1110 101 110 111“。0xf對應的二進制值是“1111”。什么樣的字符是滿足要求的呢?我們知道,可打印字符ascii碼的范圍是33-126,也就是說,只要我們在這個范圍內,找到二進制表示下末尾幾位和上面的值相同的數就可以了(所以說,這個phase答案也是不唯一的)。其實也不用太費心地去找,只要在上面那些二進制索引之前,都添上兩個一就行(三位的先在前面添上0補成四位)。所以我們最后得到的字符串是“9/>567“。試試吧~
Phase 6
phase_6好長好長,而且循環好多好多!不過沒關系,我們分塊來看這些不知所云的匯編。
phase_6也是要求我們輸入6個數,而且調用了前面的read_six_numbers. 我們知道,read_six_numbers這個函數會把讀入的六個數放到第二個參數指向的一塊內存里。在這里,第二個參數是%rsp的值;而在調用read_six_numbers之前,%rsp的值已經被同時拷到%r13里了。所以,%r13實際上包含了指向我們輸入的六個數的地址。這個下面會用到。
在非常耐心地盯了很長時間匯編之后,我們發現第一個循環從0x401114開始,到0x401151結束。仔細捋里面的代碼,我們發現其實從0x401135到0x40114b又是一個循環。那么這兩個循環是干什么用的呢?我們先把這段匯編貼出來:
我們已經知道,%r13里是我們輸入的第一個數的地址(放在循環里來說,就是本輪迭代中檢查到的那個數);所以前幾行匯編的意思不難理解,就是檢查一下我們輸入的數是不是比六大。檢查了之后呢,就把當前數的索引值加一,再放到%ebx里。重頭戲在兩條虛線之間的嵌套循環那里:先把%ebx里的索引放到%rax里;之后算出上面查過的數的地址,通過地址找到這個數,放到%eax里;之后把%eax和0x0(%rbp)中的中進行比較(當然你很可能會覺得%rbp出現得非常突兀——其實0x0(%rbp)指向的也是我們在上面檢查過的那個數)。如果相等,就引爆炸彈。我們再來觀察后面控制循環的指令。直到%ebx等于五,也就是查到了我們輸入的最后一個數,循環才終止。之后程序把%r13加上4(移到下一個數),開始新一輪循環。
這樣描述是不是有些抽象?沒關系,我們把這段匯編翻譯成C語言:
所以說,這段匯編的實際作用是確定我們輸入的六個數是不是全部小于等于六,并且是不是互不相等。這樣的要求和數組索引的要求好像!那么這組數究竟是不是真正的索引呢?我們接著往下看。
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi401158: 4c 89 f0 mov %r14,%rax40115b: b9 07 00 00 00 mov $0x7,%ecx-------------------------------------------------------------401160: 89 ca mov %ecx,%edx401162: 2b 10 sub (%rax),%edx401164: 89 10 mov %edx,(%rax) 401166: 48 83 c0 04 add $0x4,%rax40116a: 48 39 f0 cmp %rsi,%rax40116d: 75 f1 jne 401160 <phase_6+0x6c> ------------------------------------------------------------- 40116f: be 00 00 00 00 mov $0x0,%esi401174: eb 21 jmp 401197 <phase_6+0xa3>這段匯編里也有一段循環。循環初始,%ecx的值是7,%rax指向我們輸入的六個數;之后的操作就是用7減去每一個我們輸入的數……(401162,401164;循環的控制語句是401166到40116d)如果這六個數真是索引的話,Dr. Evil您可真會玩兒……
緊挨著這段匯編的又是一段循環;注意這段循環是從中間的401197開始執行的:
401197把剛才洗過的數放到%ecx里,之后把它和一比較。如果這個數小于等于一,就跳到401183. 否則,就把兩個數分別放到兩個寄存器里,之后跳到401176. 順著這兩個分支分別看過去,一個神奇的數引起了我們的注意:0x6032d0. 這種無厘頭的魔數,出現在這里,超級像地址(“你確信嗎?”),但是我們現在還沒有證據。不過我們接著往下看大于一程序會往哪里走(等于一太特殊了,看大于一的一般情況就夠了)。40119f那里,程序把兩個數分別放到兩個寄存器里,就朝著401176去了;401176的指令,證明了我們的猜想:確實,0x6032d0是個地址。但是為什么是 “mov 0x8(%rdx),%rdx” 呢?為什么偏移量偏偏就是8呢?我們接著往下看。40117a,程序把%eax加上了一,之后,40117d判斷%ecx是否和%eax相等。不相等,就回到401176?看來,0x8(%rdx)可能也是個地址呢……因為之前放到%rdx里的值又被當成地址引用了一次……那么,如果兩個值相等呢?程序會跳到401188,把%rdx里的地址放到內存里的某個位置,看起來像個數組。到底是什么樣的地址值得如此大存特存呢?我們還是打開gdb查一查:
(gdb) x /wx 0x6032d0 0x6032d0 <node1>: 0x0000014c嗯,gdb提示0x6032d0屬于一個叫node1的變量。這下清楚了……敢情phase 6這兒有個鏈表……
我們繼續看其他的地址:
這架勢……單向鏈表實錘……
看起來nodeX開頭四字節是節點存儲的值,最后八字節是下個節點的地址,中間四字節相當于索引值。
看來node結構在源文件中應該是這樣定義的:
之后弄了不少全局變量:
node node6 = { 6, 0x1bb, NULL }; node node5 = { 5, 0x1dd, &node6 }; node node4 = { 4, 0x2b3, &node5 }; node node3 = { 3, 0x39c, &node4 }; node node2 = { 2, 0xa8, &node3 }; node node1 = { 1, 0x14c, &node2 };這樣,你應該就清楚了前面那個嵌套循環的具體作用——根據我們輸入的索引,找到對應node變量的地址,并且把這個地址放到一個數組里。如果寫成C語言就是這樣的:
node* addresses[6] = { 0 }; //存node變量地址的數組 for (int i = 0; i < 6; ++i) {int index = six_numbers[i];addresses[i] = &node1;while (addresses[i]->index != index)addresses[i] = addresses[i]->next; }沒辦法,C語言里可可愛愛的幾行,放在匯編里就是不知所云的一大片。
現在我們可以說是搞定了整個phase 6最難的一部分!勝利在望!
接下來的四行,是折騰地址用的。0x20(%rsp)里放的相當于是addresses[0]的地址;0x28(%rsp)是addresses[1]的地址;這樣一來,%rbx里放的就是addresses[0];%rax里放的就是(addresses + 1)。
下面又是一個循環。不過比起之前我們分析的那些,下面這幾個就是小巫見大巫了。
4011bd: 48 8b 10 mov (%rax),%rdx4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)4011c4: 48 83 c0 08 add $0x8,%rax 4011c8: 48 39 f0 cmp %rsi,%rax4011cb: 74 05 je 4011d2 <phase_6+0xde>4011cd: 48 89 d1 mov %rdx,%rcx4011d0: eb eb jmp 4011bd <phase_6+0xc9>4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)這邊循環折騰寄存器確實很亂……先從%rax指向的位置拿出某個node變量的地址,取道%rdx放到0x8(%rcx)里。那么這個0x8(%rcx)相當于什么呢?我們可以看出,%rcx里放的總是addresses數組里%rax指向元素的上一個,是某個node變量的地址;那么0x8(%rcx)就是nodeX.next. 原本nodeX.next里放的應該是node(X + 1)的地址對不對?假設我們輸入的索引是" X Y Z…", 那么一輪循環執行完之后,nodeX.next里放的就是nodeY的地址了。這個循環是重排節點用的。
我們終于來到了最后一個循環(眼睛覺得不行的話,就休息一下吧……馬上就結束了)。這個循環是最后判斷我們輸入的索引順序對不對的。
%rbx指向我們輸入的第一個索引對應的節點。4011df把nodeX.next放到%rax里,再通過這個地址拿出%rax指向的節點的key值,把它和%rbx指向節點的key比較,如果前者大與等于后者就通過。
所以這個循環要求我們降序排列節點中的key值。翻回到前面看一看各個索引對應的key值,排好序就行了。別忘了輸入之前每個值都要用7減哦。(所以最后結果是"4 3 2 1 6 5")
Secret Phase
Dr. Evil提示我們,可能有什么東西被忽略了:
/* Wow, they got it! But isn't something... missing? Perhaps* something they overlooked? Mua ha ha ha ha! */翻到匯編一看,還真是。phase_6之后還有一個secret_phase. 我們先來搜一下這個函數是在哪里調用的:
嗯,是在phase_defused里面。現在讓我們來研究一下什么情況下會觸發secret_phase:
phase_defused調用sscanf處理在0x603870那里的一個字符串。之后把sscanf的第五個參數(輸出參數)和0x402622那里的一個字符串比較,相等就可以觸發phase_defused。那么我們先來看一看0x603870那里有什么。
我們先在合適的位置設好斷點,之后按順序輸入之前的字符串。之后程序會在調用sscanf之前停下。輸出0x603870那里的字符串,我們發現它和我們在phase_4中輸入的內容是一致的。再看一看sscanf的格式說明符,我們發現只要我們再在phase_4的兩個數后面加上一個合適的字符串,就能觸發secret_phase了。那么這個字符串是什么呢?自己看一看0x402622那里的字符串~ 于是我們順利地來到了secret_phase~
secret_phase調用了strtol,把我們輸入的字符串轉換成數(具體用法自行百度)。里面還有一行是檢查這個數的大小的,不能大于1001(好有童話色彩的魔數!)。查完就調用fun7(沒少敲"c"!!!真的是fun!),fun7返回之后檢查返回值,等于二就通過。
那我們就看一看fun7吧。fun7的第一個參數是0x6030f0(又是魔數!),第二個參數是strtol的返回值。和func4類似,fun7有令人頭疼的遞歸結構:
根據之前的經驗,我們猜測%rdi里放著的應該也是個指向結構的指針,因為%rdi指向的值拿出來還是可以當成地址引用。所以經過大膽的聯想與想象,我們猜測fun7原本應該是這樣寫的:
int fun7(node* rdi, int rsi) {if (rdi == NULL)return -1;int key = rdi->key;if (rsi < key){rdi = /* 不知道怎么寫…… */;return 2 * fun7(rdi, rsi) + 1;}else if (rsi == key)return 0;elsereturn 2 * fun7(rdi->next /* 是嗎…… */ , rsi); }確實,基本結構是這樣。不過如果你假設這里的數據結構還是上面的單向鏈表的話,你會發現"mov 0x10(%rdi),%rdi"這行匯編用C語言是描述不出來的……因為如果假設%rdi指向某個節點,那么0x10(%rdi)將指向下個節點的key值,這就相當于直接把一個int值當成了地址……
所以,我們去看一看那個魔數(0x6030f0)附近到底有什么……
好吧。這是棵二叉搜索樹(相信你一眼就看出來了~)。每個變量前四字節(前八字節也說不定)是節點的key,從第九字節到第十六字節是指向左子結點的指針,從第十七字節開始的八字節是指向右子結點的指針,最后八字節用途不明(可能是對齊用的?)。畫出來長這樣:
所以fun7應該是這樣寫的:
int fun7(node* current, int input) {if (current == NULL)return -1;int key = current->key;if (input > key)return 2 * fun7(current->right, input) + 1;else if (input == key)return 0;elsereturn 2 * fun7(current->left, input); }要讓fun7返回2,我們應該怎么設計input的值呢?看fun7的C代碼我們可以發現,我們的輸入只能是樹中的某個節點值,否則返回值肯定是負的。另外,我們也可以發現,最后一行,返回(2 * fun7()), 是虛的,只能把原來的值翻倍,而不能從0造出新的值。所以要想湊出2,只能從前面的(2 * fun7() + 1)入手。
我們從樹頂的0x24開始,自頂向下地搭一棵遞歸樹。最初調用的fun7()要返回2對不對。此時它面臨向左向右兩種選擇。如果向右呢,就要求之后調用的fun7()要返回0.5——當然這是不可能的,所以只好向左走啦——要求之后調用的fun7()返回1.
好了,現在我們來到了遞歸樹的第二層。這一層,%eax里的值應該是1才行。向左還是向右呢?如果向左走的話,那么要湊出返回值1,那么之后調用的fun7()又要返回0.5了對不對。所以這里要向右走,這時之后調用的fun7()只要返回0就可以了。
什么樣的情況下fun7()會返回0呢?很顯然,input等于key的時候。所以,8的右子結點值0x16就是答案。
不過,這個答案是唯一的嗎?
事實上,從0x16那個節點,還可以再向左走一步到0x14,返回值也滿足要求。
所以,secret_phase的答案有兩個,20和22.
幾條野路子
如果你時間緊任務重,只想快速刷掉這些phase,可以試試下面的方法。
我們知道,gdb可以查看寄存器或者某個地址的值。既然查看可以,那么修改也應該可以……
其實gdb里的set命令就是干這個用的。語法是"set [something] = [value]",something那里填的東西和匯編里訪問寄存器、內存的語法類似,不過要注意所有百分號都要換成’$’,而立即數之前的’$'去掉.
我們用phase 1演示一下。先在合適的位置打上斷點:
沒錯,0x400eee就是strings_not_equal執行完之后的下一行。之后隨便輸些什么東西:
(gdb) r Starting program: /media/snowingfield/Data2/csapp-lab/bomb/bomb2 Welcome to my fiendish little bomb. You have 6 phases with which to blow yourself up. Have a nice day! 北京歡迎你,為你開天辟地輸完之后敲回車。因為我們設了斷點,所以程序會在判斷%eax的值之前停下來。當然我們知道現在%eax里的值肯定不是0:
Breakpoint 1, 0x0000000000400eee in phase_1 () (gdb) p $eax $1 = 1之后就可以用set命令了:
(gdb) set $eax = 0 (gdb) p $eax $2 = 0 (gdb) c Continuing. Phase 1 defused. How about the next one?成功地騙過了bomb呢。
當然我們也可以在explode_bomb上做些手腳。我們先看一看explode_bomb的匯編:
000000000040143a <explode_bomb>: 40143a: 48 83 ec 08 sub $0x8,%rsp 40143e: bf a3 25 40 00 mov $0x4025a3,%edi 401443: e8 c8 f6 ff ff callq 400b10 <puts@plt> 401448: bf ac 25 40 00 mov $0x4025ac,%edi 40144d: e8 be f6 ff ff callq 400b10 <puts@plt> 401452: bf 08 00 00 00 mov $0x8,%edi 401457: e8 c4 f7 ff ff callq 400c20 <exit@plt>explode_bomb是不會返回的,因為它最后調用了exit。如果我們把explode_bomb里的內容全部抹掉,并且直接修改字節碼讓它正常返回,炸彈不就不會爆炸了嗎?所以我們用十六進制文本編輯器打開bomb文件,找到explode_bomb的位置(就是在編輯器里偏移量為143a的位置),并且把里面的內容全部用"90"(nop指令)替代,再把最后一字節改成"C3"(ret指令):
之后我們只要隨便輸進一些東西就能過掉phase了。
直接把火藥去掉了呢。
secret_phase不知道怎么進?沒關系,我們通過改字節碼的方式直接在main調用它。
觀察發現,main后面足足有9字節的nop指令!簡直就是為了方便我們設計的好嗎!觀察一下其他位置的call指令,我們發現call指令占五字節,第一個字節都是"e8",后面跟上四字節的偏移量。不過在開始添指令之前,為了保證main正常返回,我們先把main的最后三個指令向后挪5字節:
之后算call指令的偏移量 = 0x401242(secret_phase的地址)- 0x400ed1(e8后面那個字節的地址) - 4(e8后跟偏移量的長度,單位是字節) = 0x36d.(第七章有這個公式) 之后我們把"e8 6d 03 00 00"(偏移量按小尾數擺)填到那個5字節的空位里:
保存運行就行了。
突然覺得Dr. Evil應該給他的bomb加個殼的。[暗中觀察][狗頭][狗頭]
總結
以上是生活随笔為你收集整理的深入理解计算机系统-bomblab详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件性能测试方案模板,性能测试方案模板
- 下一篇: Windows Server 2003