深入理解計算機系統Bomb實驗
- 前言
- 準備階段
- 上傳bomb.c檔案
- 生成匯編代碼
- 進入gdb除錯模式
- 獲取主要函式的匯編代碼
- 實驗階段
- Phase1
- 實驗探究
- 輸入字串首地址的保存
- 繼續phase1的研究
- 通關密鑰
- Phase2
- 實驗探究
- 通關密鑰
- Phase3
- 實驗探究
- sscanf陳述句
- swith-case陳述句
- 通關密鑰
- Phase4
- 實驗探究
- 通關密鑰
- Phase5
- 實驗探究
- 通關密鑰
- Phase6
- 實驗探究
- 通關密鑰
- 秘密關卡
- 實驗探究
- 找到隱藏關卡入口
- secret_phase探究
- 小結
前言
最近我在學計算機系統時,做到了一個蠻有趣的實驗游戲——bomb實驗(其實就是一個c程式),這個實驗有六關,每一關需要輸入一個字串(可以稱之為密鑰),每一關只有輸入正確的密鑰才能通過,否則“炸彈“將會爆炸,因此,我們需要通過匯編c代碼找出匯編檔案中藏有的密鑰的資訊,通過這個實驗,我的匯編語言能力獲得了極好的鍛煉,因此紀錄程序以供大家分享并作為紀念,
準備階段
上傳bomb.c檔案
首先,據實驗的要求,我們需要一臺linux機器進行gdb除錯,我們可以使用linux虛擬機或者租一臺linux云主機,在這里我使用的是騰訊云的云主機,在這臺主機上,我新建一個檔案夾,并將實驗所需要的bomb.c檔案進行上傳,
使用工具:xftp

生成匯編代碼
在上傳完成后,我們打開已連接上服務器的xshell或者虛擬機的teminal應用,使用cd命令進入到剛才上傳到的檔案夾中,之后,我們使用objdump -d bomb > bomb_assembly.S命令生成一個名為bomb_assembly.S的匯編代碼檔案

進入gdb除錯模式
使用gdb bomb命令進入bomb.c檔案的除錯模式

獲取主要函式的匯編代碼
首先,我們打開已經生成好的匯編語言檔案(可以在linux的檔案管理器中打開或者通過xftp打開),找到main函式,將其復制下來,單獨保存至一個檔案中,方便查看,
0000000000400da0 <main>:
# ...省略了以上的部分匯編代碼
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi
400e49: e8 c2 fc ff ff callq 400b10 <puts@plt>
400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
400e5b: e8 64 07 00 00 callq 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
400e65: e8 a6 fc ff ff callq 400b10 <puts@plt>
400e6a: e8 2f 06 00 00 callq 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 callq 400f43 <phase_3>
400e77: e8 48 07 00 00 callq 4015c4 <phase_defused>
400e7c: bf 0b 23 40 00 mov $0x40230b,%edi
400e81: e8 8a fc ff ff callq 400b10 <puts@plt>
400e86: e8 13 06 00 00 callq 40149e <read_line>
400e8b: 48 89 c7 mov %rax,%rdi
400e8e: e8 79 01 00 00 callq 40100c <phase_4>
400e93: e8 2c 07 00 00 callq 4015c4 <phase_defused>
400e98: bf d8 23 40 00 mov $0x4023d8,%edi
400e9d: e8 6e fc ff ff callq 400b10 <puts@plt>
400ea2: e8 f7 05 00 00 callq 40149e <read_line>
400ea7: 48 89 c7 mov %rax,%rdi
400eaa: e8 b3 01 00 00 callq 401062 <phase_5>
400eaf: e8 10 07 00 00 callq 4015c4 <phase_defused>
400eb4: bf 1a 23 40 00 mov $0x40231a,%edi
400eb9: e8 52 fc ff ff callq 400b10 <puts@plt>
400ebe: e8 db 05 00 00 callq 40149e <read_line>
400ec3: 48 89 c7 mov %rax,%rdi
400ec6: e8 29 02 00 00 callq 4010f4 <phase_6>
400ecb: e8 f4 06 00 00 callq 4015c4 <phase_defused>
400ed0: b8 00 00 00 00 mov $0x0,%eax
400ed5: 5b pop %rbx
400ed6: c3 retq
400ed7: 90 nop
400ed8: 90 nop
400ed9: 90 nop
400eda: 90 nop
400edb: 90 nop
400edc: 90 nop
400edd: 90 nop
400ede: 90 nop
400edf: 90 nop
# ...省略了以下phase3-phase6的匯編代碼
我們發現,phase1-phase6函式似乎就恰好對應題目中給出的第一關到第六關,于是,在gdb中,我們可以快速地使用disas命令進入這些函式的匯編代碼中一探究竟,
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1> #找到函式名前的函式開始指令的地址
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
進入gdb模式,使用disas 0x400ee0進入phase1的匯編代碼,

重復上述步驟,依次快速復制phase1到phase6的代碼,將代碼分別復制到不同檔案中,
實驗階段
Phase1
實驗探究
打開復制下來的phase1的代碼檔案
關于每一步的解釋以# 的注釋標在代碼后
0x0000000000400ee0 <+0>: sub $0x8,%rsp # 在堆疊中開辟一個8位元組的臨時空間
0x0000000000400ee4 <+4>: mov $0x402400,%esi # 將0x402400的值作為<string not equal>的引數傳入
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax # 測驗該函式回傳值
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23> # 若回傳值為0則跳過炸彈爆炸函式
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb> # 回傳值為1,炸
0x0000000000400ef7 <+23>: add $0x8,%rsp # 恢復堆疊
0x0000000000400efb <+27>: retq
由字面意義可知,<string_no_equal>函式起到了一個比較字串是否相等的作用,于是,猜測該函式具有兩個引數——一個是我們輸入的字串的首地址,另一個是待比較的字串的首地址,
輸入字串首地址的保存
回傳main函式保存的檔案中,查看呼叫phase1函式之前的幾句代碼,發現確實如此,引數暫存器%rdi被<read_line>函式的回傳值賦值,因此,猜測<read_line>函式用于讀取一行字串,將回傳值保存于%rax,而被賦值的%rdi中存盤的就是輸入的字串的首地址,
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
再觀察其他的phase函式呼叫前的陳述句,我們都可以發現類似情況,因此,我們認為這些%rdi暫存器在phase函式的一開始,起到的是存盤輸入字串首地址的作用
400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
繼續phase1的研究
于是,我們可以繼續猜想,在呼叫<string_no_equal>函式之前的%esi暫存器中是否也存盤了一個待比較的字串首地址,
0x0000000000400ee4 <+4>: mov $0x402400,%esi # 將0x402400的值作為<string not equal>的引數傳入
使用,x/s命令將0x402400中存盤的字串匯出,答案如我們所愿,

因此,phase1函式的作用只是單純的讓我們輸入一個字串,再將我們輸入的字串和存盤的字串進行比較而已,
通關密鑰
密鑰即為“Border relations with Canada have never been better.”,輸入即可通關,
使用run命令執行bomb.c程式,再輸入第一關密鑰

Phase2
實驗探究
打開phase2匯編代碼被保存的檔案
0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp # 產生一塊40位元組大小的臨時空間
0x0000000000400f02 <+6>: mov %rsp,%rsi # 將堆疊指標賦值給引數暫存器
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) # 比較1和堆疊頂元素的大小
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb> # 若不相等則炸
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax # 將堆疊中的上一個元素值賦值
0x0000000000400f1a <+30>: add %eax,%eax # 堆疊中的上一個元素值*2后保存
0x0000000000400f1c <+32>: cmp %eax,(%rbx) # 將上一個元素值的2倍與%rbx對應的值(現元素值)進行比較
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx # 將%rbx的+=4
0x0000000000400f29 <+45>: cmp %rbp,%rbx # 比較是否等于尾指標
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx # 將堆疊指標加4的賦值
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp # 將堆疊的尾指標賦值
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
首先函式將堆疊指標傳入引數暫存器,考慮到緊挨的read_six_number函式,猜測堆疊指標作為引數用于保存數字,而另一引數%rdi(上文提到)給出輸入字串地址,因此,函式read_six_number函式用于將輸入的字串轉換為6個數字, 得知,本輪需要輸入6個數字作為密鑰,
接下來從<+14>陳述句中得知,第一個輸入的數字是1,
由
<+41>(偏移量++)
<+57>(尾指標的賦值)
<+45> (偏移量等于尾值)
判斷出,這函式當中存在一個回圈,回圈中%rbx依次保存堆疊中的所有元素的地址,而又由<+27>-<+32>陳述句中可知,堆疊中元素滿足這樣的排列:堆疊中每一元素是它上一元素的兩倍 ,即需輸入一個首項為1,公比為2,項數為6的等比數列,
通關密鑰

Phase3
實驗探究
0x0000000000400f43 <+0>: sub $0x18,%rsp #堆疊指標減24用來存放3個臨時變數(看大小決定個數)
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx #%rcx=堆疊指標+12(引數)
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx #%rdx=堆疊指標+8(引數)
0x0000000000400f51 <+14>: mov $0x4025cf,%esi #某個引數的傳遞
0x0000000000400f56 <+19>: mov $0x0,%eax #對回傳值賦值0,為sscanf陳述句做準備
0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>#按格式讀入輸入
0x0000000000400f60 <+29>: cmp $0x1,%eax #將回傳值與1進行比較
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39> #若回傳值大于1(說明scanf的引數大于1),jump39
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb> #否則炸
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) #比較第一個數字與7的大小
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> #若>7,跳轉106,炸;并且是無符號數的比較,
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax #把第一個數字的值給%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) #這是屬于跳轉表的形式,
0x0000000000400f7c <+57>: mov $0xcf,%eax #以下就是把某一個值放到%eax中在做<+123>的程序,就是switch-case陳述句
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: callq 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax #都是在拿rsp+12的地址對應的值與eax進行比較
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134> #若等就會結束,成功;不等,就會炸
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq
sscanf陳述句
在該匯編語言中,使用了sscanf格式化輸入,sscanf的陳述句同read_six_number函式類似,只是具有了更靈活的形式,引數有需要規定的輸入形式 ,本陳述句中以%esi引數暫存器傳入,通過該引數,函式可以將輸入的合法字串轉換為規定的數字或者字串,
使用x/s 命令查看,得知是“%d %d”,即需要輸入兩個整數,

而另外兩個引數,分別是堆疊的+12地址,堆疊的+8地址,這兩個引數用作保存轉換的數字,回傳值是輸入成功的值的個數,這里是兩個%d,所以若正常按格式輸入兩個數字,回傳值應大于1,據<+32>得,若不大于1,則炸彈爆炸,
swith-case陳述句
接著是一個典型的swith-case陳述句,首先在<+44>中,將第一個數字與7進行無符號的小于比較,這是在規定輸入的第一個數字必須是0-6之間的第一個數(包含0,6),然后,是一個經典的跳轉表形式,

通過<+46>陳述句,第一個數字成為了跳轉表的引數<+50>,<+57><+64>等陳述句,分別對應的輸入第一個數字為0-6情況的不同跳轉,
在跳轉后,將某個值(每個跳轉對應的值均不同)存入%eax暫存器中,接著統一跳轉至<+123>,0-6對應的不同的%eax的結果與第二個數字進行比較,若相等,方可通過,
| 第一個數字 | 對應的case陳述句下取出的值 |
|---|---|
| 0 | 0xcf=207 |
| 1 | 0x137=311 |
| 2 | 0x2c3=707 |
| 3 | 0x100=256 |
| 4 | 0x185=389 |
| 5 | 0xce=206 |
| 6 | 0x2aa |
通關密鑰
因此,本局關卡需要輸入兩個數字,第一個數字必須是0-6中的一個,而第二個數字通過swith-case陳述句對應0-6,需輸入不同數字,
通關實體 (以第一個數字0為例)

Phase4
實驗探究
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx //要用的引數,放入引數暫存器中給scanf存
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx //要用的引數
0x000000000040101a <+14>: mov $0x4025cf,%esi //這個也是“%d %d”
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax //不是正常的兩個引數就炸
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp) //這個值和14
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46> //低于或者相等
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax //回傳0才是正確做法
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp) //再比較第二個輸入值和0的關系
0x0000000000401056 <+74>: je 0x40105d <phase_4+81> //需等于0,否則炸
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
該函式簡單明了,同Phase3,同樣使用了一個sscanf陳述句,同樣是"%d %d"格式輸入,因此,密鑰仍為兩個數字,其次,0x8(%rsp)作為第一個數字,應該滿足<+34>陳述句,即低于或者小于14,最后,func4的回傳值必須是0,而func4的引數在<+46>-<+56>中給出,
于是,我們通過disas 命令獲取func4的匯編代碼,(這里不再示例gdb的使用)
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax # result=14
0x0000000000400fd4 <+6>: sub %esi,%eax # result-=0,不變
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx # %ecx邏輯右移31位,補0,取最高位之意
0x0000000000400fdb <+13>: add %ecx,%eax # 拿自己的最高位加上result;(負數加1正數加0)14+0=0
0x0000000000400fdd <+15>: sar %eax # 算術右移,單運算元是只移動一位的意思 7
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx # 7+0=%ecx
0x0000000000400fe2 <+20>: cmp %edi,%ecx # 比較第一個輸入值和%ecx的關系
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax # 給出0
0x0000000000400ff7 <+41>: cmp %edi,%ecx # 再比較一次
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> # 大于等于
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
根據注釋,我們可以得出:當回傳值為0時,第一個數字需為7, 再回到函式phase4中來,我們看到有<+69>陳述句,該陳述句規定了第二個數字需為0這一輸入 ,
通關密鑰

Phase5
實驗探究
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp //開辟一個32位元組的空間
0x0000000000401067 <+5>: mov %rdi,%rbx //rdi是輸入字串陣列地址
0x000000000040106a <+8>: mov %fs:0x28,%rax // 堆疊溢位保護
0x0000000000401073 <+17>: mov %rax,0x18(%rsp) //把回傳值存盤到堆疊臨時記憶體中
0x0000000000401078 <+22>: xor %eax,%eax //異或自己,置零
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax //字串長度與6比較
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> //等于的話跳轉,否則炸
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx //將字符依次賦值
0x000000000040108f <+45>: mov %cl,(%rsp) //%rcx的最低位元組(依次的元素的值)給堆疊頂記憶體存盤
0x0000000000401092 <+48>: mov (%rsp),%rdx //將這個字符賦值給%rdx
0x0000000000401096 <+52>: and $0xf,%edx //使得%edx高位的值被0覆寫掉,只剩0-15
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx // 將(0x4024b0+%rdx)對應記憶體的值給了%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) //再將%edx的低位保存在堆疊中
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>//似乎是一個回圈
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) //把0的值改寫到這個字串對應的結尾字串,所以最后有'\0'做結尾
0x00000000004010b3 <+81>: mov $0x40245e,%esi //這個是需要比較的字串地址
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi //這個是輸入字串地址
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax //是0就是兩字串相等
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1) //對齊作用
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax //回傳值后32位置0
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax //看是否被改寫,否則出現大問題,這可不是炸的問題了
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
首先,根據<+29>陳述句,<string_length>函式的回傳值,即輸入字串的長度必須等于6, 緊接著,跳轉至<+112>陳述句將%eax置0,然后才回傳<+41>陳述句繼續進行,<+41>陳述句將字符依次賦值給%ecx ,我們注意到,%rax在<+66>處++,并且在<+74>處跳轉回去形成一個回圈,而在這個回圈中%rax是每次都改變的,因此每一次回圈都使得%ecx獲得到的是下一個的新字符,因此,稱之為依次,
之后通過一系列的操作:%ecx->堆疊頂->%rdx,將這個元素的值賦給%rdx,接著對%rdx使用0xf掩碼,使其只剩低4位有效,(注意,這使得后面可以免去輸入低位ASCLL碼的麻煩)
我們接著看到<+55>陳述句,這是關鍵;這一陳述句將不同的%rbx值作為偏移量,對0x4024b0的地址進行偏移,從而獲得不同的字符放入%edx暫存器中,然后再將%edx暫存器中的值放如堆疊中保存,在回圈陳述句退出后(回圈執行6次,我們可以得知應輸入6個字符),將第7個字符設為/0標志字串的結尾,然后是一個簡單字串比較函式(看保存在堆疊中的字串與0x40245e作為首地址的字串是否相等——<+81>中給出引數),
所以,Phase5的接題關鍵是,輸入的字符作為偏移量,可以剛好使得0x4024b0作為首地址偏移所對應得新的字符與0x40245e對應得字符依次相等
因此,我們分別使用x/s 0x4024b0以及x/s 0x40245e命令查看兩個字串,


得到,偏移量應該為 9 15 14 5 6 7 ,才能依次對應“f l y e r s”,
由于前面使用了0xf作為掩碼,所以可以使用字串“9?>567”代替低ascll碼得輸入,
通關密鑰

Phase6
實驗探究
Section1 準備作業
0x00000000004010f4 <+0>: push %r14
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp //開辟80位元組的臨時空間
0x0000000000401100 <+12>: mov %rsp,%r13 //將堆疊指標保存到被呼叫者暫存器r13
0x0000000000401103 <+15>: mov %rsp,%rsi //將堆疊指標傳入引數,用于接受那6個數字
0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov %rsp,%r14 //r14同樣用來保存堆疊指標
0x000000000040110e <+26>: mov $0x0,%r12d
由上述代碼可知,需要輸入6個數字作為密鑰,
Section2
0x0000000000401114 <+32>: mov %r13,%rbp //將堆疊指標的值賦給%rbp保存(這里,每一次回圈都會+4%r13)
0x0000000000401117 <+35>: mov 0x0(%r13),%eax //將堆疊指標指向的數字(%eax也是32位的)賦給%eax
0x000000000040111b <+39>: sub $0x1,%eax //數字的值--
0x000000000040111e <+42>: cmp $0x5,%eax //減完以后和5進行比較
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> //低于或者相等才可(也就是說,每一個數字都是要低于等于5的才行)(也不能是負數)
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>//否則炸
0x0000000000401128 <+52>: add $0x1,%r12d //0+1
0x000000000040112c <+56>: cmp $0x6,%r12d //比較和6比較大小,因此猜測是在一個回圈中
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>//這是跳出外層回圈
0x0000000000401132 <+62>: mov %r12d,%ebx //將這個會變化的值(第一次是1)賦值給一個被呼叫者暫存器
0x0000000000401135 <+65>: movslq %ebx,%rax //有符號數的低位元組到高位元組賦值,%rax被改變
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax //將這個值對應的元素(每一次回圈給一個)賦值給%eax
0x000000000040113b <+71>: cmp %eax,0x0(%rbp) //將這些數字與(%rbp進行比較)%rbp在外面其實一直在被遞增(所以比較的始終是這個元素和它的上一個元素)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81> //不等于才是對的
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb>
0x0000000000401145 <+81>: add $0x1,%ebx //再將這個計數器值加1
0x0000000000401148 <+84>: cmp $0x5,%ebx //將這個值與5進行比較
0x000000000040114b <+87>: jle 0x401135 <phase_6+65> //這是一個嵌套回圈
0x000000000040114d <+89>: add $0x4,%r13 //將%r13+4
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
在這一部分,由注釋可以得出,這一部分是一個嵌套回圈,該嵌套回圈有兩層,外層回圈作用是,確定輸入這幾個數字均在1-6之間(包含1,6)(見<+45>),內層回圈作用是,確定這幾個數字互不相等(見<+71>),
Section3
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi //將指標+0x18地址對應的值賦值給%rsi引數
0x0000000000401158 <+100>: mov %r14,%rax //將堆疊指標的值傳遞給回傳值暫存器
0x000000000040115b <+103>: mov $0x7,%ecx //將7賦值給第二個引數
0x0000000000401160 <+108>: mov %ecx,%edx //將第二個引數賦值給第三個引數 發現,第一個回圈后%ecx不受影響,這是一個定值
0x0000000000401162 <+110>: sub (%rax),%edx //讓7-%rax指向的數字
0x0000000000401164 <+112>: mov %edx,(%rax) //將這個結果賦值給這個數字
0x0000000000401166 <+114>: add $0x4,%rax //讓它指向第二個數字
0x000000000040116a <+118>: cmp %rsi,%rax //比較%rsi尾指標地址是否不同,這應該是最后一個數字,說明這是一個回圈
---Type <return> to continue, or q <return> to quit---
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
該部分的作用即,將輸入的數字分別轉換為7-該數字,如1變為6…,其中堆疊指標對應的是堆疊頂元素,堆疊指標+8對應的是堆疊中的第二個元素,即堆疊中的每一個元素之間的地址間隔8個位元組,
Section4
0x000000000040116f <+123>: mov $0x0,%esi //賦值0給%esi
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx //將某個值給取出來賦值給%rdx==6304480
0x000000000040117a <+134>: add $0x1,%eax //將這個值+1
0x000000000040117d <+137>: cmp %ecx,%eax //這里是比較%ecx(每一個數字)和%eax(第一次是2)
0x000000000040117f <+139>: jne 0x401176 <phase_6+130> //若不等,跳轉回到130,這是一個回圈
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>
0x0000000000401183 <+143>: mov $0x6032d0,%edx
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) //將這個值存起來
0x000000000040118d <+153>: add $0x4,%rsi //將計數器++
0x0000000000401191 <+157>: cmp $0x18,%rsi //計數器退出條件
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183>
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx //這一看又是一個回圈,目的,將不同的數字給依次取出
0x000000000040119a <+166>: cmp $0x1,%ecx //比較這些數字和1的大小關系
0x000000000040119d <+169>: jle 0x401183 <phase_6+143> //如果是小于等于1就直接到143
0x000000000040119f <+171>: mov $0x1,%eax //繼續執行 將1賦值給%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx //將這個值賦值給%edx(復原)
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
這一部分起到了關鍵作用,首先將堆疊中元素取出<+163>(這里同樣是依次取出),置于%ecx,接著是一個判斷陳述句<+143>,我們首先考慮堆疊頂元素等于1(這是經過了section3后的,原值是6)的情況——這時,堆疊頂元素被覆寫為0x6032d0 <+148>,
那么,我們繼續考慮堆疊中元素大于1的情況,此時均會跳轉至<+130>處,在<+130>到<+141>之間是一個回圈,若堆疊頂元素是2,則只執行一次<+130>陳述句后退出,若是3,則執行兩次,依此類推,
而<+130>陳述句,實際上是對%rdx+8這一地址取值后,賦值給%rbx自己(可以看成是一個鏈表:p=p->next)
因此,不同的值對應的不同結果如下,堆疊中最后會按照原來對應的數字來保存不同的地址

Section5
在這里,我們為方便敘述,我們將堆疊中存的地址稱為地址元素,
0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx //給定開始地址元素
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax //這是下一元素的堆疊地址
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi //這個是末尾元素的堆疊地址
0x00000000004011ba <+198>: mov %rbx,%rcx //開始地址元素的賦值
0x00000000004011bd <+201>: mov (%rax),%rdx //將堆疊中的下一個地址元素賦值給%rdx(中轉站)
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) //這是將堆疊中的下一個地址元素賦值給(上一地址元素+8)對應的記憶體中
0x00000000004011c4 <+208>: add $0x8,%rax //將%rax+8(下一堆疊地址)
0x00000000004011c8 <+212>: cmp %rsi,%rax //退出條件:下一堆疊地址等于末尾元素堆疊地址(所以只回圈五次)此時%rdx為第五個地址元素
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>
0x00000000004011cd <+217>: mov %rdx,%rcx //中轉站中的值賦值給%rcx(這里是把第二個地址元素賦給(原來的開始元素),副本間的賦值)
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
該部分的作用:把堆疊中所有的下一個地址元素賦值給,其上一個地址元素+8對應的記憶體中,見<+204>,但是,堆疊本身存的地址元素并沒有被改變,而且,這些地址元素指向的值確實也沒有被改變,因為改變的是地址元素+8對應的記憶體值,而我們不是地址元素對應的記憶體值,(注:地址元素所對應的值的大小只占8位元組的大小)
Section6
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) //將0值賦給(最后一個地址元素+8)對應記憶體中
0x00000000004011da <+230>: mov $0x5,%ebp
0x00000000004011df <+235>: mov 0x8(%rbx),%rax //這里是(現地址元素+8對應的記憶體值)對應的記憶體(注意,即下一個地址元素)
0x00000000004011e3 <+239>: mov (%rax),%eax //將這個地址元素再解參考得到(就是下一個地址元素指向的值!!注意,這個值是沒有被改變的!)
0x00000000004011e5 <+241>: cmp %eax,(%rbx) //將這個值與上一個元素地址對應的記憶體(均沒有被改變!)進行比較
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> 上一個元素地址對應的值需要大于等于下一個的
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx //將%rbx++
0x00000000004011f2 <+254>: sub $0x1,%ebp //計數器,五次回圈,比較只需要五次就可以比完
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> //若不等于則回去
---Type <return> to continue, or q <return> to quit---
0x00000000004011f7 <+259>: add $0x50,%rsp
0x00000000004011fb <+263>: pop %rbx
0x00000000004011fc <+264>: pop %rbp
0x00000000004011fd <+265>: pop %r12
0x00000000004011ff <+267>: pop %r13
0x0000000000401201 <+269>: pop %r14
0x0000000000401203 <+271>: retq
在這部分,我們使用一個回圈將堆疊中所有的地址元素對應的值,和它的下一個地址元素(見<+235><+239>)對應的值進行比較(見<+241>),而比較的目的是,讓下一個元素地址對應的值均大于現地址元素對應的值,
因此,我們使用x/g 陳述句,對于0x6032d0-0x603320對應的值查詢,
| 地址 | 值 |
|---|---|
| 0x6032d0 | 332 |
| 0x6032e0 | 168 |
| 0x6032f0 | 924 |
| 0x603300 | 691 |
| 0x603310 | 477 |
| 0x603320 | 443 |
再對應上一張表,我們可以得出使得對應元素值從大到小數字串是"4 3 2 1 6 5"
通關密鑰

秘密關卡
實驗探究
我們注意到,每一個函式后面都存在一個phase_defused函式,而當我們使用disas命令匯編這一代碼時,卻意外的發現位于代碼<+108>陳述句下的secret_phase關卡,
0x00000000004015c4 <+0>: sub $0x78,%rsp
0x00000000004015c8 <+4>: mov %fs:0x28,%rax
0x00000000004015d1 <+13>: mov %rax,0x68(%rsp) #將phase函式的回傳值放入堆疊中保存
0x00000000004015d6 <+18>: xor %eax,%eax # 置0
0x00000000004015d8 <+20>: cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings>
0x00000000004015df <+27>: jne 0x40163f <phase_defused+123>
0x00000000004015e1 <+29>: lea 0x10(%rsp),%r8 # sscanf引數,下同
0x00000000004015e6 <+34>: lea 0xc(%rsp),%rcx
0x00000000004015eb <+39>: lea 0x8(%rsp),%rdx
0x00000000004015f0 <+44>: mov $0x402619,%esi # “%d %d %s”
0x00000000004015f5 <+49>: mov $0x603870,%edi
0x00000000004015fa <+54>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x00000000004015ff <+59>: cmp $0x3,%eax # 引數有3
0x0000000000401602 <+62>: jne 0x401635 <phase_defused+113>
0x0000000000401604 <+64>: mov $0x402622,%esi
0x0000000000401609 <+69>: lea 0x10(%rsp),%rdi # %rdi中存盤的是字串
0x000000000040160e <+74>: callq 0x401338 <strings_not_equal> # 比較兩字串
0x0000000000401613 <+79>: test %eax,%eax
0x0000000000401615 <+81>: jne 0x401635 <phase_defused+113>
0x0000000000401617 <+83>: mov $0x4024f8,%edi # 輸出函式提示
0x000000000040161c <+88>: callq 0x400b10 <puts@plt>
0x0000000000401621 <+93>: mov $0x402520,%edi
0x0000000000401626 <+98>: callq 0x400b10 <puts@plt>
0x000000000040162b <+103>: mov $0x0,%eax # 將回傳值置0
---Type <return> to continue, or q <return> to quit---
0x0000000000401630 <+108>: callq 0x401242 <secret_phase>
0x0000000000401635 <+113>: mov $0x402558,%edi
0x000000000040163a <+118>: callq 0x400b10 <puts@plt>
0x000000000040163f <+123>: mov 0x68(%rsp),%rax
0x0000000000401644 <+128>: xor %fs:0x28,%rax
0x000000000040164d <+137>: je 0x401654 <phase_defused+144>
0x000000000040164f <+139>: callq 0x400b30 <__stack_chk_fail@plt>
0x0000000000401654 <+144>: add $0x78,%rsp
0x0000000000401658 <+148>: retq
找到隱藏關卡入口
- 首先,在<+20><+27>陳述句中,使用了一個 0x202181(%rip) 的值,這個值從匯編代碼自帶的提示中可以猜想得到,是一個當前已經輸入的關卡數,即字串數,而該比較陳述句的含義是,只有在已完成關卡6的情況下才能進入到secret_phase,否則,將會直接跳到<+123>陳述句,失去進入<+108>秘密關卡函式的機會,
- 其次,是一個sscanf陳述句,然而,sscanf語言的引數相比于前面的引數要多了一個,通過x/s陳述句查看,輸入格式為"%d %d %s",那么照理而言除了三個地址作為引數外,不應該有0x603870作為引數,
- 于是,我們想到sscanf陳述句與scanf陳述句的不同之處,sscanf陳述句可以有一個字串引數,用于指定sscanf所需輸入的字串的源,(默認源是標準輸入)
- 而這個字串應當由我們輸入觸發隱藏關卡才對,為什么會由匯編語言給出?這就說明應該是我們前面的輸入,保存到了這個地方,使得隱藏函式的以觸發,
于是,我們嘗試使用gdb進行端點的除錯,查看0x603870處的這個字串的值,我們在phase6的最后一條陳述句中設定斷點,

接著,我們開始按照剛剛的答案一步步運行程式,到了斷點處,程式將自動停止,

接著,我們使用x/s命令查看0x603870處字串的值

我們可以通過之前6關輸入的字串得知,**第4關的字串就是觸發隱藏關卡的入口,**但是,在第四關處,我們還應該輸入某個字串在"7 0"之后,于是,我們開始對這個字串進行尋找,
我們發現,<+74>陳述句對兩個字串進行了比較,而其中一個字串就是sscanf輸入地址引數所指向的字串,因此,我們了解到需要將與其作比較字串作為輸入,
使用x/s命令查看,我們知道可以通過在第4關輸入"7 0 DrEvil"作為通關密鑰的同時,開啟隱藏關卡,

secret_phase探究
0x0000000000401242 <+0>: push %rbx
0x0000000000401243 <+1>: callq 0x40149e <read_line>
0x0000000000401248 <+6>: mov $0xa,%edx # 按十進制轉換
0x000000000040124d <+11>: mov $0x0,%esi # 將字串要保存到的地址設定為空地址
0x0000000000401252 <+16>: mov %rax,%rdi # 要轉換的字串是標準輸入進來的
0x0000000000401255 <+19>: callq 0x400bd0 <strtol@plt>
0x000000000040125a <+24>: mov %rax,%rbx # 保存該轉換后的數字
0x000000000040125d <+27>: lea -0x1(%rax),%eax # 將該數字-=1
0x0000000000401260 <+30>: cmp $0x3e8,%eax # 將已經減過1的數字與0x3e8進行比較
0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42> # 若低于或者等于則跳轉
0x0000000000401267 <+37>: callq 0x40143a <explode_bomb>
0x000000000040126c <+42>: mov %ebx,%esi # 將原數字賦值給該引數
0x000000000040126e <+44>: mov $0x6030f0,%edi
0x0000000000401273 <+49>: callq 0x401204 <fun7>
0x0000000000401278 <+54>: cmp $0x2,%eax # 若回傳值為2,那么該函式拆彈成功
0x000000000040127b <+57>: je 0x401282 <secret_phase+64>
0x000000000040127d <+59>: callq 0x40143a <explode_bomb>
0x0000000000401282 <+64>: mov $0x402438,%edi
0x0000000000401287 <+69>: callq 0x400b10 <puts@plt>
0x000000000040128c <+74>: callq 0x4015c4 <phase_defused>
0x0000000000401291 <+79>: pop %rbx
0x0000000000401292 <+80>: retq
- 根據C庫函式strtol的定義,我們得知這是一個轉換字串為數字的字串,所以,我們需要輸入一個數字,
- 根據<+27><+30>,該數字需要低于0x3e8,
- 在進入到func7函式后,若回傳值為2,則跳轉到<+64>列印出拆彈成功的字串,
因此,關鍵在于func7函式,注意,初次呼叫func7時,我們的引數分別是輸入的數字(%esi),以及0x6030f0(%rdi),
fun7函式
0x0000000000401204 <+0>: sub $0x8,%rsp
0x0000000000401208 <+4>: test %rdi,%rdi # 測驗rdi
0x000000000040120b <+7>: je 0x401238 <fun7+52> # 若為%rdi為0則跳轉,回傳0xffffffff,無法獲得回傳值為2的正確結果
0x000000000040120d <+9>: mov (%rdi),%edx # 獲取該地址對應的值
0x000000000040120f <+11>: cmp %esi,%edx # 與輸入數字進行比較,
0x0000000000401211 <+13>: jle 0x401220 <fun7+28>
0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi
0x0000000000401217 <+19>: callq 0x401204 <fun7>
0x000000000040121c <+24>: add %eax,%eax
0x000000000040121e <+26>: jmp 0x40123d <fun7+57>
0x0000000000401220 <+28>: mov $0x0,%eax
0x0000000000401225 <+33>: cmp %esi,%edx
0x0000000000401227 <+35>: je 0x40123d <fun7+57>
0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi
0x000000000040122d <+41>: callq 0x401204 <fun7>
0x0000000000401232 <+46>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401236 <+50>: jmp 0x40123d <fun7+57>
0x0000000000401238 <+52>: mov $0xffffffff,%eax
0x000000000040123d <+57>: add $0x8,%rsp
0x0000000000401241 <+61>: retq
這一部分的代碼比較短,也比較好讀,下面呼叫了兩次fun7說明這也是一個遞回程式,而且觀察得到%rsi的值在整個遞回的程序中沒有變化過,起到的只是一個比較的作用,
一開始還檢測了一下%rdi是否為0,后面設定遞回引數的時候用mov 0x8(%rdi),%rdi,自身加上一個偏移量的間接尋址代替自身,基本可以確定%rdi是一個指標,%rdi+0x8和%rdi+0x10同樣也是一個指標,看到這里基本已經猜出這個資料結構就是二叉樹了,之后的尋找答案也就不難了,順著左右兒子找一下就得到答案了:
答案是0x16,也就是22,
小結
這一次的bomb實驗,包含了計算機系統中第三章匯編語言的幾乎所有知識點,通過本次的練習,我的匯編語言能力獲得了很好的鍛煉,對于一些重要知識點(如跳轉表,回圈)的知識點,掌握的更加牢靠,而本實驗中包含的許多有趣實用的匯編語言技巧(如一些精巧的中間變數的使用、靈活的jump跳轉指令的運用)使我更加注意編程技巧的學習,匯編語言的學習無疑是一件重中之重的學習任務,學習之途任重而道遠,今發此文,與諸君共勉,
——ECNU 楊政 (轉載請宣告)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275148.html
標籤:其他
下一篇:只有懦夫才會畏懼選擇!
