引言:主要任務是「拆炸彈」。所謂炸彈,其實就是一個二進位制的可執行檔案,要求輸入六個字串,每個字串對應一個phase。如果字串輸入錯誤,系統就會提示BOOM!!!
解決這次實驗需要將二進位制檔案反組合,通過觀察理解組合語言描述的程式行為來猜測符合條件的字串。可以看出該可執行程式要求從命令列或者檔案以 行 為單位讀入字串,每行字串對應一個phase的輸入。如果phase執行完畢,會呼叫phase_defused 函數表明該 phase 成功搞定。實驗共有6個 phase,難度是逐級提升,考點也不盡相同。首先執行命令objdump -d bomb > bomb.txt
得到反組合程式碼。
考察點:字串的傳遞方式
檢視bomb.txt
檔案的反組合程式碼,如下所示,首先棧頂指標向下移動了8個位元組,在64位元機器下就是一格,然後將0x402400
傳遞給了esi
暫存器(儲存函數引數的暫存器),在0x400ee9
處呼叫了string_not_equal
函數,呼叫返回後如果eax
暫存器的值為0的話,我們就會跳轉到phase_1 + 0x17 = 400ef7
的位置,否則的話呼叫explode_bomb
函數就失敗了,顯然,我們需要讓其判斷相等,利用gdb
檢視0x402400
處的字串,
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
我們按 s 單步執行時也可以看到這個字串
我們gdb bomb
時,將上面的字串輸入,可以看到第一關就過了,Border relations with Canada have never been better.
考察點:組合程式碼中陣列的表示
還是首先檢視組合程式碼
0000000000400efc <phase_2>:
400efc: 55 push %rbp # 儲存rbp
400efd: 53 push %rbx # 儲存rbx
400efe: 48 83 ec 28 sub $0x28,%rsp # 擴大棧空間,擴大0x28即40個位元組
400f02: 48 89 e6 mov %rsp,%rsi # 儲存棧頂元素到rsi暫存器
# 對應的C語言格式組合程式碼
rsi = rsp;
callq read_six_number;
if (*rsp == 1)
goto 400f30;
else
callq explode_bomb;
goto 400f30;
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> # 讀入六個數位
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) # 比較rsp必須為1
400f0e: 74 20 je 400f30 <phase_2+0x34> # 如果m[rsp] = 1則跳轉到0x400f30
400f10: e8 25 05 00 00 callq 40143a <explode_bomb> # 顯然不能執行這條指令
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax # 下面是一段迴圈 eax = M[rbx - 4]
# 400f17 - 400f25:
eax = *(rbx-4); # 每次取出M[rbx - 4]的值給eax
eax += eax; # eax每次都會變為 *(ebx - 4)的二倍
if (eax == *rbx) # rbx為存放的第二個元素的值 即上一個元素的二倍必須等於下一個元素的值
goto 400f25;
else
callq explode_bomb;
400f1a: 01 c0 add %eax,%eax # eax = eax + eax
400f1c: 39 03 cmp %eax,(%rbx) # if (eax == m[rbx]) goto 0x400f25
400f1e: 74 05 je 400f25 <phase_2+0x29> # 跳過下面的bomb 顯然我們需要讓eax = m[rbx]
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx # rbx = rbx + 4
# 400f25 - 400f2e:
rbx += 4; # 下一個元素,下一次的 rbx - 4就相當於這一次的rbx了
# 不難看出,下面是一個以rbx為搜尋指標,以rbp為結尾訊號的迴圈
if (rbx != rbp) # 只要rbx還沒有到rbp rbx其實就相當於for迴圈的i rbp為6
goto 400f17; # 迴圈
else
goto 400f3c;
400f29: 48 39 eb cmp %rbp,%rbx # if (rbx-rbp!=0) goto 0x400f17,回到迴圈開始
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40> # rbx==rbp的話就會到這裡 0x400f3c
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx # rbx = rsp + 4 lea指令傳遞的是暫存器的內容
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp # rbp = rsp + 24
400f3a: eb db jmp 400f17 <phase_2+0x1b> # 接著迴圈
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
# 六個數分別存放到 rsp rsp+0x4 rsp+0x8 rsp+0xc rsp+0x
# read_six_numbers程式碼 需要我們輸入6個數位然後進行比較這裡還有如果數位不滿足6個的健壯性判斷 注意rdi和rsi暫存器已經被用來儲存read_six_numbers的兩個引數了,
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp # 6個數, 4 * 6 = 24 = 0x18
401460: 48 89 f2 mov %rsi,%rdx # 在上面的函數中我們將rsp儲存到了rsi中
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx # rsp + 4的地址,存放輸入的第二個數
401467: 48 8d 46 14 lea 0x14(%rsi),%rax # 用rax暫存輸入的第六個數(rsp + 0x14)
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) # rsp + 8 = rax = rsi(之前的rsp) + 0x14
401470: 48 8d 46 10 lea 0x10(%rsi),%rax # 存放第五個數,存放到了rax暫存器中
401474: 48 89 04 24 mov %rax,(%rsp) # rsp = rsi(之前的rsp) + 0x10(多的引數存到記憶體)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 # 存放第四個數 這時候六個暫存器已經用完了
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 # 存放第三個數
401480: be c3 25 40 00 mov $0x4025c3,%esi # 給rsi 賦值為 0x4025c3
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> # 呼叫 sscanf 函數讀取輸入
40148f: 83 f8 05 cmp $0x5,%eax # 比較上面函數的返回值 如果大於5,說明讀取的合法
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb> # 否則執行炸彈bomb
401499: 48 83 c4 18 add $0x18,%rsp # 恢復堆疊
40149d: c3 retq
這次組合程式碼比較長了,分析的結果都寫在註釋裡了,下面通過gdb
動態偵錯一下,首先b phase_2
然後run
,可以看到四個暫存器均儲存了我們的輸入
檢視暫存器的內容i reg
或者p $eax
,接著檢視記憶體中該地址的內容,/s表示以字元形式顯示。可以看到我們輸入的內容都是以字串格式先儲存的,然後通過sscanf
格式化輸出為了6個整數
執行到呼叫read_six_numbers
函數之前,我們可以看到該函數的第一個引數傳遞給了rdi
,即我們輸入的字串,然後將rsi
暫存器置為0,注意這裡的反組合第一個運算元是目的運算元,rsi
儲存的是提升堆疊後的rsp
的值,用來儲存陣列的起始地址。
使用 f 可以檢視當前棧資訊,利用 bt
指令可以檢視函數呼叫棧之間的關係
一步步執行下去,直到上面不是很懂的mov $0x4025c3, %esi
指令,可以看到該地址的內容如下,其實就是作為sscanf
函數的引數,rdi
暫存器的內容始終都沒有被修改,這裡也可以看出端倪,輸入的字串儲存在rdi
中,此時作為sscanf
函數的第一個引數
呼叫下面這個函數將rax
暫存器的值設定為6,從而可以下面可以cmp $0x5, %eax
使eax
的值大於5,直接跳轉回phase_2
函數。回到phase_2
函數,之後執行的就是一個迴圈判斷了,判斷存進去的數是否滿足後一個數是前一個數的二倍,rbx
儲存的地址是從2開始的,格式化後的6個數位儲存到了從ebp
開始的連續的記憶體空間,檢視可見下圖
這裡的組合程式碼比較好理解,第一行rbx = rsp + 4
,第二行rbp = rsp + 0x18
,儲存迴圈結束位置(6個數,每個數4位元組,共16+8=24位元組),將M[rbx - 4]賦值給eax
,此時eax
儲存的即為輸入的第一個數 1,然後add eax, eax
是eax
儲存的數變為原來的二倍,接著比較eax
儲存的值與當前記憶體中M[rbx]
是否相等,相等的話接下來讓rbx + 4
(這裡的+4其實就對應陣列元素的+1),看是否滿足迴圈終止條件,不滿足就跳到上面phase_2+27
繼續執行。
動態執行完後就可以看到過掉了
考察點:switch語句,索引表的組合表示
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp ; 首先提升堆疊
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx ; rcx = rsp + 0xc
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ; rdx = rsp + 0x8 儲存的是引數
400f51: be cf 25 40 00 mov $0x4025cf,%esi ; 猜測這裡與上面一樣 是sscanf用到的引數 %%
400f56: b8 00 00 00 00 mov $0x0,%eax ; 用作返回值
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> ;這個函數與上面的一樣,先輸入字串再格式化
400f60: 83 f8 01 cmp $0x1,%eax ; 上面的函數返回值
400f63: 7f 05 jg 400f6a <phase_3+0x27> ; 0x400f6a 跳過爆炸的函數,返回值需要大於1
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) # rsp + 8儲存第一個引數,rsp+c儲存第二個
400f6f: 77 3c ja 400fad <phase_3+0x6a>;0x400fad 會爆炸,所以rsp+8<7,用ja當a[0]<0是也no
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax ; eax = rsp + 8 < 7 將第一個數存到eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) ; 跳轉到 M[0x402470+rax*8] 處其實就是400fb9
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax ; 如果 a[0]=1時會跳轉到這裡 eax=0x137
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax ; 比較第二個引數與eax的值是否相同 相同的話就過了
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq
下面利用gdb
動態偵錯,在進入sscanf
函數之前,檢視0x4025cf
處儲存的要傳入sscanf
的字串,所以可以知道sscanf
這次要讀取的是兩個整數,不用跟進去猜測sscanf
的作用就知道,它將輸入的標準字串格式化為了兩個整數,
一步一步執行下去,知道進入sscanf
函數之前,可以看到與該函數有關的資訊如下所示:
退出sscanf
函數後,可以檢視該函數將格式化後的數位儲存到了哪裡,其中0x7fffffffe3d0
為棧指標rsp
的地址,rsp + 4
儲存返回地址,rsp + 8
儲存輸入的第一個數,rsp + c
存放輸入的第二個數,
讀取堆疊的資料 --兩種方式 入棧(edx為棧頂,ebx為棧底)
1、base加偏移 棧底為高地址
讀第一個壓入的資料:mov esi,dword ptr ds:[ebx-4]
讀第四個壓入的資料:mov esi,dword ptr ds:[ebx-0x10]
2.top加偏移 棧頂為低地址
讀第二個壓入的資料:mov edi,dword ptr ds:[edx+8]
讀第三個壓入的資料:mov edi,dword ptr ds:[edx+4]
rsp和rbp
暫存器不用我們指定內容,是由編譯器確定的,接下來是比較rsp + 8 和 0x7
的大小,需要滿足rsp + 8 < 0x7
,即第一個引數小於7, 注意這裡的ja
指令可以同時處理輸入的a[0] > 7
和a[0] < 0
的情況,之後會做一個無條件的jmp *0x402470(,%rax,8)
,根據rax
的值去找對應的語句,猜測是一個以rax
為索引的索引表,類比switch
語句,對於我們輸入的每一對數,都會根據第一個數的值去確定第二個數的值。檢視以地址0x402470
為基址的索引表的資訊如下所示,我們輸入的是1,所以取0x400fb9
的地址尋找
當我們跳轉到指定地址後可以看到(這裡輸入的第一個引數為1),將eax
賦值為0x137
,然後比較我們輸入的第二個數與這個數是否相等,即我們可以輸入的有1 311或者其他六種其他的數。
對應的C形式的虛擬碼就如下所示:
void phase_3(char* output)
{
int x, y;
if(sscanf(output, "%d %d", &x, &y) <= 1)
explode_bomb();
if(x > 7)
explode_bomb();
int num;
switch(x) {
case 0:
num = 207;
break;
case 1:
num = 311;
break;
case 2:
num = 707;
break;
case 3:
num = 256;
break;
case 4:
num = 389;
break;
case 5:
num = 206;
break;
case 6:
num = 682;
break;
case 7:
num = 327;
}
if (num != y)
explode_bomb();
return;
}
考察點:遞迴函數的引數及返回值
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> # 同樣呼叫了sscanf這個函數
401029: 83 f8 02 cmp $0x2,%eax # 如果上面函數的返回值與2不相等的話就bomb了
40102c: 75 07 jne 401035 <phase_4+0x29> # 跳到 0x401035 即bomb函數
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) # 這裡需要滿足 M[rsp+8] <= 0xe 這樣才能跳過bomb
401033: 76 05 jbe 40103a <phase_4+0x2e> # jbe是小於等於
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx # edx = 0xe 下面三行應該都是 func4函數的引數
40103f: be 00 00 00 00 mov $0x0,%esi # esi = 0x0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi # edi = a[0](我們輸入的第一個引數的值)
401048: e8 81 ff ff ff callq 400fce <func4> # 這裡又呼叫了一個函數
40104d: 85 c0 test %eax,%eax # 判斷 eax 是否為0,即func4函數的返回值是否為0
40104f: 75 07 jne 401058 <phase_4+0x4c> # 如果不為0的話跳轉到 bomb,所以需要使eax為0
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) # 比較輸入的第二個數和0是否相等 不相等會bomb
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq
; func4函數
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp # 棧空間擴大8個位元組 這裡的 0x1就代表地址空間可以多儲存一個位元組
400fd2: 89 d0 mov %edx,%eax # eax作為sscanf的返回值一直沒有修改 edx為第三個引數0xe
400fd4: 29 f0 sub %esi,%eax # eax = eax - esi = 0xe - 0 = 0xe
400fd6: 89 c1 mov %eax,%ecx # ecx = eax = 0xe
400fd8: c1 e9 1f shr $0x1f,%ecx # shr邏輯右移指令 ecx = ecx >> 0x1f = 0
400fdb: 01 c8 add %ecx,%eax # eax = eax + ecx = 0xe
400fdd: d1 f8 sar %eax # sar 算術右移指令 省略了一個運算元 gdb中顯示為 1 1110>> 1 =111=7
到這裡就可以看出端倪了:eax = (eax + eax >> 0x1f) >> 1 其中 eax = edx - esi = 0xe
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx # ecx = rax + rsi * 1 = 7 + 0 = 7
400fe2: 39 f9 cmp %edi,%ecx # 將我們輸入的第一個引數與 7 比較
400fe4: 7e 0c jle 400ff2 <func4+0x24> # 如果7 <= a[0] 跳轉到 0x400ff2 執行
400fe6: 8d 51 ff lea -0x1(%rcx),%edx # 否則 a[0] < 7 edx = rcx - 1 = 6
400fe9: e8 e0 ff ff ff callq 400fce <func4> # 遞迴呼叫 func4 函數
400fee: 01 c0 add %eax,%eax # 2 * func()
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax # eax = 0
400ff7: 39 f9 cmp %edi,%ecx # if(ecx(7) >= edi(a[0])) goto 401007; else func4();
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff callq 400fce <func4> # 這裡如果 a[0] > 7的話也會進行遞迴 a[0] = 7就是邊界條件
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax # eax = rax + rax + 1 遞迴呼叫
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq
遞迴函數其實就是
int func(int x, int a, int b) (edi esi edx)
{
int c = b - a;(c儲存在 ecx b在edx裡)
c = (c + c >> 31) >> 1; 這裡c又儲存到了 eax 裡
int d = c + a; (rax + rsi(用來傳遞第二個引數)) d 儲存在 ecx
if (d <= x) goto 0x400ff2
{
if (d >= x) return 0;
; 遞迴呼叫 注意第二個引數變了 lea 0x1(%rcx),%esi esi = d + 1
return 2 * func4(x, d + 1, b) + 1
}
goto 0x400fe6 lea -0x1(%rcx),%edx b = b - 1
return 2 * func(x, a, b - 1); 只有第三個引數變了 別的都沒變
}
由上面的分析可知,輸入的第二個數一定為0,第一個數作為func4
函數的第一個引數進行了運算,需要滿足func4
函數的返回值為0。下面利用gdb
動態偵錯,可以看到地址0x4025cf
處儲存的是% %
,所以我們要輸入的引數個數是兩個
由上面組合的分析可知,我們輸入的第一個引數需要小於等於14,第二個引數一定為0。執行到呼叫func4
函數時介面如下,可以看到func4
函數有四個引數,第一個引數就是我們輸入的第一個數。分析可知,func4
函數是一個遞迴函數,遞迴終止條件為 a[0] >=7
,且下面還有一個判斷如果a[0] > 7
也會遞迴呼叫函數func4
,所以我們令第一個引數為7即可,如第二張圖。
仔細分析後可以得知,func4
函數這個遞迴函數的程式碼如下所示
int func4(int x, int a, int b)
{
int num = b - a;
num = (num + num >> 31) / 2; // 31就是 0x1f
int c = num + a;
if (c <= x) {
if (c >= x) return 0;
return 2 * func4(x, num+1, b) + 1;
}
return 2 * func4(x, a, num-1);
}
考察點:字元陣列,迴圈,ASCII,與運算
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp # 開闢 32 位元組的棧空間
401067: 48 89 fb mov %rdi,%rbx # rbx = rsi
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax # %fs:0x28儲存的是 輸入的值
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) # rsp + 0x18 = rax(存放的是我們輸入的引數)
401078: 31 c0 xor %eax,%eax # eax = 0
40107a: e8 9c 02 00 00 callq 40131b <string_length> # 獲取輸入的字串長度(包括空格)
40107f: 83 f8 06 cmp $0x6,%eax # 如果輸入的字串長度不為6 會爆炸
401082: 74 4e je 4010d2 <phase_5+0x70> # 跳轉到 0x4010d2
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
; 下面這段指令的含義:遍歷輸入字串的每一個字元,然後逐次將每個字元與0xf與操作,得到的值做為0x4024b0處字串的下標
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx # movzbl零擴充套件指令 move zero byte to double word
; rax此時為0 ecx = rax + rbx (零擴充套件後再傳送) 一般用於使用小位元組變數給大位元組變數賦值
40108f: 88 0c 24 mov %cl,(%rsp) # M[rsp] = cl(ecx的低8位元) = 0x31(1的ASCII碼)
401092: 48 8b 14 24 mov (%rsp),%rdx # rdx = M[rsp] = 0x31
401096: 83 e2 0f and $0xf,%edx # edx = edx & 1111 = 110001 & 1111 = 1
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx # edx = M[rdx + 0x4024b0] 根據上面與的結果去記憶體尋找
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) # 將edx的第八位存到後面指定的記憶體地址
4010a4: 48 83 c0 01 add $0x1,%rax # rax = rax + 1
4010a8: 48 83 f8 06 cmp $0x6,%rax # if(rax!=6) goto 40108b; else goto 4010ae 需要執行6次
4010ac: 75 dd jne 40108b <phase_5+0x29> # 回到上面繼續迴圈
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) # 6次迴圈結束後 執行到這裡 M[rsp+0x16] = 0
4010b3: be 5e 24 40 00 mov $0x40245e,%esi # 函數的引數
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax # eax = 0
4010d7: eb b2 jmp 40108b <phase_5+0x29> # 又跳轉到上面了
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq
開啟gdb
進行動態偵錯,首先看到我們輸入的長度為6的字串如下所示
前面的指令都很簡單,我們直接看movzbl
這條指令,是一個帶零擴充套件的資料傳送指令,在gdb
中檢視該指令是如下形式,明確給出了byte
型別,此時rax = 0
,rbx = 0x6038c0(我們輸入的字串的地址)
,執行完這條指令後rcx = 0x31 = 49(1的ASCII碼)
,
可以看到0x6038c0
處儲存的內容如下,儲存的是我們輸入的123456
的ASCII碼,
這裡還發現了python
中對變數做and
運算時的一些有意思的點,python
中所有變數的位元運算都是通過強制轉換成bool
實現的,嚴格遵循短路邏輯,只有and
,如果每個表示式都不為假,返回第二個,只有or
,從左往右有一個不為假就返回這個值。
下一條指令mov %cl,(%rsp)
是將ecx
暫存器的低八位賦值給M[rsp]
,存放到棧指標指向的地址,0x7f
開頭的往往就是棧所在地址
中間經過一些處理後此時rdx = 49 & 0xf = 1
,然後又是一條零擴充套件指令movzbl 0x4024b0(%rdx),%edx
,根據上面相與的結果取記憶體中尋找對應的值賦值給edx
,可以看到這裡是0x61
,可以看到記憶體中儲存的字串為下面的maduiersnfotvbyl
接下來gdb
中的指令更容易理解,mov byte ptr [rsp + rax + 0x10], dl
,將0x61
儲存到rsp+0x10
開始的記憶體地址(即儲存變化後的字元到一個棧中新開闢的字元陣列裡),rax
此時仍為0,先檢視未執行前,該地址儲存的數為:0x10
,執行之後就變成了了0x61
,
下面首先rax = rax + 1
,然後判斷rax != 6
的話回到上面迴圈之前的操作,可知這裡是一個6次的迴圈,下一次迴圈,rbx
儲存的還是我們輸入的字串的地址,但是rax
就變成1了,取到的字元由之前的0x31
變為了0x32
,直到遍歷完6個長度的字串
總結一下,這一段迴圈的意義是遍歷輸入的每一個字元,將每一個字元的ASCII碼與0xf
相與,與後的結果作為索引去指定記憶體地址0x4024b0
處找對應的字元儲存起來。迴圈結束後,我們再往下看,下面就是傳遞引數,然後呼叫了strings_not_equal
這個函數
該函數的第一個引數為我們輸入的字串的每一個字元與上0xf
後作為索引去記憶體中找到的maduiersnfotvbyl
這個字串的子串,第二個引數為記憶體中儲存的正確結果flyers
,顯然我們需要讓這兩個字串相等,這樣這個函數才會返回0,才會跳過下面的explode_bomb
下面要做的就很清晰了,找到所有與上0xf
後的索引為flyers
在0x4024b0
為起始地址的索引表中的位置即可,索引依次為9 15 14 5 6 7
,我們需要找到與上0xf
後為以上索引的字元,x & 1111 = 1001 x = 1001001或者111001或者1111001
,可以看出後四位即為索引,我們先嚐試第一個1001001(73)
,對應的輸入為IONEFG
,第二種,對應的輸入為9?>567
,第三種輸入y(112+15=127)
不是可列印字元。
因此,關鍵步驟用C語言來寫就是
const char g_str[16] = "maduiersnfotvbyl";
void phase_5(char* input)
{
char str[7];
if (string_length(input) != 6) {
explode_bomb();
}
// x & 0xf = 9 15 14 5 6 7
// I O N E F G 或 9 ? > 5 6 7
for (int i = 0; i != 6; i++) {
str[i] = g_str[input[i] & 0xf];
}
str[7] = '\0';
if(string_not_equal(str, "flyers") != 0) {
explode_bomb();
}
}
至此,第五關也就過了
考察點:多重回圈,連結串列,結構體,
eax
比較數值時只會比較低32位元,冗長的組合
00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
; 傳入引數 為read_six_numbers做準備
4010fc: 48 83 ec 50 sub $0x50,%rsp # 提供80位元組的棧空間
401100: 49 89 e5 mov %rsp,%r13 # r13 = rsp
401103: 48 89 e6 mov %rsp,%rsi # rsi = rsp 第二個引數
401106: e8 51 03 00 00 callq 40145c <read_six_numbers> # 讀取六個數位,這個函數在 p2 見過
40110b: 49 89 e6 mov %rsp,%r14 # r14 = rsp 此時rsp根進去之前其實還是一樣的 所以r14=r13
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d # r12d = 0
401114: 4c 89 ed mov %r13,%rbp # rbp = r13 = rsp
401117: 41 8b 45 00 mov 0x0(%r13),%eax # eax = M[r13] = M[rsp] M[rsp]=0x200000001 因為eax為32位元暫存器,只能儲存下來 0x200000001 的低4位元組 即 00000001 所以此時 eax = 0x1
40111b: 83 e8 01 sub $0x1,%eax # eax = eax - 1
40111e: 83 f8 05 cmp $0x5,%eax # eax需要 < 5
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d # r12d += 1 每次迴圈加1
40112c: 41 83 fc 06 cmp $0x6,%r12d # 迴圈終止條件 r12d = 6
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx # rbx = r12d 迴圈變數暫存到rbx中
401135: 48 63 c3 movslq %ebx,%rax # 符號位擴充套件,l->q 字到雙字, rax = ebx(符號位擴充套件) 正數用0
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51> # *rbp 不能等於 eax
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41> # ebx <= 5 繼續迴圈
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>
; 上面是一個迴圈
phase_6(rdi) ; 我們輸入的字串傳入到 rdi 中
{
r13 = rsp;
rsi = rsp;
read_six_numbers(rdi, rsi); rdi 為我們輸入的字串, rsi為 %%%%%%
r14 = rsp;
for (r12 = 0 r12 != 6 r12++)
{
rbp = r13;
eax = *r13; 去記憶體中找
eax -= 1;
if (eax > 5)
explode_bomb();
for (ebx = r12 + 1 ebx <= 5 ebx++)
{
rax = ebx; 符號位擴充套件 e->r ebx為正數用0填充高位 為負數用1填充高位
eax = *(rsp + rax * 44);
if (*rbp == eax)
explode_bomb();
}
r13 += 4;
}
}
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax # rax != rsi 的話繼續迴圈
40116d: 75 f1 jne 401160 <phase_6+0x6c>
; 這裡也是一個迴圈 單獨寫在這裡
rsi = rsp + 0x18;
rax = r14;
ecx = 0x7;
for (rax = r14 rax != rsi rax += 4)
{
edx = ecx;
edx = edx - *rax;
*rax = edx;
}
40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx # ebx = 0x6032d0
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) # *(rsp + rsi*2) = rdx
40118d: 48 83 c6 04 add $0x4,%rsi # rsi += 4
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7> # rsi = 0x18的話 就跳到下面了
; 因為這是最外層開始的迴圈 所以也可以通過這條語句跳轉到的地址確定本次迴圈的層數,即最內層迴圈的語句在哪裡結束
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
40119a: 83 f9 01 cmp $0x1,%ecx # ecx <= 1
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>
; 小tips 怎麼看回圈到哪裡結束呢 找下面最遠的跳到上面的指令往往就是最內層迴圈
for (esi = 0 rsi != 0x18 rsi += 4)
{
ecx = *(rsp + rsi);
if (ecx <= 1)
{
edx = 0x6032d0;
*(rsp + rsi * 2 + 0x20) = rdx; 這句話兩個分支 都會跳轉到那裡執行
}
else
{
edx = 0x6032d0;
for (eax = 1 eax != ecx eax++)
{
rdx = *(rdx + 8);
}
*(rsp + rsi * 2 + 0x20) = rdx;
}
}
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
; 又是一個迴圈
rbx = *(rsp + 0x20);
rsi = rsp + 0x50;
rcx = rbx;
for (rax = rsp + 0x28 rax != rsi rax += 8)
{
rdx = *rax;
*(rcx + 0x8) = rdx;
rcx = rdx;
}
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx) # *rbx 需要大於 eax
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq
*(rdx + 0x8) = 0;
for (ebp = 0x5 ebp != 0x1 ebp -= 1)
{
rax = *(rbx + 0x8);
eax = *rax;
if (*rbx < eax)
explode_bomb();
rbx = *(rbx + 0x8);
}
程式碼太長,這裡我考慮直接用gdb
分析,前面入棧的六個暫存器的值如下圖所示
前面的指令沒什麼好說的,注意此時r13
和rsi
中儲存的都是棧指標rsp
的內容,偵錯到呼叫read_six
之前,這個函數需要兩個引數,第一個是我們輸入的字串,儲存在暫存器rdi
中,第二個返回值的6個int
型元素陣列的首地址,儲存在暫存器rsi
中,
這個函數內部同樣呼叫了sscanf
,在次就不再詳細展開
從該函數退出之後,mov r14, rsp
將rsp
的值又賦給了r14
,注意rsp
進入read_six
函數之後又回來棧是被平衡了的,所以此時 r13 = r14 = rsp
,下一步是mov r12d, 0
,很有意思,r12d
是r12
暫存器的??,接著將r13
中儲存的rsp
地址又傳給了rbp
,將M[rsp]
傳給了eax
,這裡要注意,M[rsp] = 0x200000001
,但是eax
暫存器是32位元暫存器,只能儲存低四個位元組,即0x00000001
,之後eax -= 1 變成了0
,
整理一下組合程式碼,其對應的C風格如下,分成了以空行間隔的五段程式碼,
phase_6(rdi) // 我們輸入的字串傳入到 rdi 中
{
r13 = rsp;
rsi = rsp;
read_six_numbers(rdi, rsi); // rdi 為我們輸入的字串, rsi為 %%%%%%
r14 = rsp;
// 總結一下 這個迴圈的含義:輸入6個1-6的數,且不能重複
for (r12 = 0; r12 != 6; r12++) // r12d猜測應該是 r12 的低32位元
{
rbp = r13; // 這裡 rbp = r13 =rsp rsp 儲存的就是我們輸入的字串格式化後的數位
eax = *r13; // 去記憶體中找 第一次 eax = 1 第二次eax= 2
eax -= 1; // eax -= 1 = 0 這裡限制了輸入的數位必須為 1-6
if (eax > 5)
explode_bomb();
for (ebx = r12 + 1; ebx <= 5; ebx++) // 初始 ebx = r12+1 = 1
{
rax = ebx; // 符號位擴充套件 e->r ebx為正數用0填充高位 為負數用1填充高位 rax = ebx 這裡是整數 rax = 0x1
eax = *(rsp + rax * 44); // eax = *(rsp + i * 4) 依次遍歷 1 2 3 4 5 6 初始rax=1 所以eax = 2
// 之後 *rbp 是不變的,始終是1 但是 eax 會依次遍歷所有 2 3 4 5 6 都不會相等 所以最終ebx = 5 eax=6退出迴圈
if (*rbp == eax) // 2 != 1(*rbp)
explode_bomb();
}
r13 += 4; // r13 = rsp + 4 相當於下一次迴圈 rbp + 4 取下一個數判斷是否有與它相同的數
}
// 下面這段迴圈的含義:將 a[i] 變為 7 - a[i] 儲存到原先a[i]所在的位置 即 esp + 4*i
rsi = rsp + 0x18; // 剛好是我們輸入的6個字元的下一個位置 24個位元組 其實是我們輸入的字元陣列的 '\0'
rax = r14; // rax = r14 = rsp
ecx = 0x7;
for (rax = r14; rax != rsi; rax += 4) // 遍歷所有字元陣列
{
edx = ecx; // edx = 7
edx = edx - *rax; // edx = 7 - a[i] 同樣也是 1-6 的數
*rax = edx; // *rax = rdx 存回記憶體
}
// 下面含義:
for (esi = 0; rsi != 0x18; rsi += 4) // 遍歷所有字元陣列 0x18很明顯 遍歷7次 剛好到'\0'結束迴圈
{
ecx = *(rsp + rsi); // 取出對應的字元陣列的值 輸入的是123456 經過上面變換後成了 654321
if (ecx <= 1) // 只有 輸入的為 6 時才會執行
{
edx = 0x6032d0; // 此地址處是一個結構體
// 下面的含義:將edx儲存的結構體資訊 儲存到rsp + rsi * 2 + 0x20 處的地址 就是 rsp + 8*i + 0x20
//*(rsp + rsi * 2 + 0x20) = rdx; // 這句話 無論進入哪個分支都會跳轉到那裡執行 可以寫到外面
}
else // 只要 輸入的 不為6
{
edx = 0x6032d0; // 與上面一樣
for (eax = 1; eax != ecx; eax++) // 第一個 ecx = 6 迴圈6次 最終7 - 1儲存到了 node6
{ // 迴圈一次 對應node1 迴圈兩次 對應node2 即 node{7-a[i]}
rdx = *(rdx + 8); // 從 6032d0(node1) -> 6032e0(node2)
}
//*(rsp + rsi * 2 + 0x20) = rdx;
}
*(rsp + rsi * 2 + 0x20) = rdx; // 第一次 rcx=7-1=6 rdx指向node6 rsp+0x20 = node6
}
// 這段好像沒什麼用
rbx = *(rsp + 0x20); // 距離棧指標最近的 node 對應輸入的第一個數 node的編號即為 7-a[i] node6
rsi = rsp + 0x50; // node 的結束地址
rcx = rbx; // 儲存輸入
for (rax = rsp + 0x28; rax != rsi; rax += 8) // rax 從 第二個node 開始遍歷 node5
{
// 典型的交換操作
rdx = *rax; // 暫存遍歷到的node rdx = node5 rdx = node4
*(rcx + 0x8) = rdx; // node5 = node6 node4=node5
rcx = rdx; // node6 = node5
}
// 分析: node[7-input[i]]->data >= node[7-input[i+1]]->data
*(rdx + 0x8) = 0; // 此時 rdx 儲存最後一個node
for (ebp = 0x5; ebp != 0x1; ebp -= 1) // 迴圈5次
{
rax = *(rbx + 0x8); // rbx 仍指向距離棧指標最近的node rax = node
eax = *rax; // 取出node的值(注意eax,取得是低32位元) 只看低32位元 node的大小順序為 3 4 5 6 1 2
if (*rbx < eax) // 如果第一個node的值小於下一個 就會爆炸 所以需要保證輸入的數對應的node是降序排列在棧中的
// 即 node6 node5 .. node1 只看低32位元 node的大小順序為 3 4 5 6 1 2 所以我們輸入的應該為 4 3 2 1 6 5 (7-a)
explode_bomb();
rbx = *(rbx + 0x8);
}
}
首先確認我們輸入的資料的位置,可以看到在rsp rsp + 4
處依次存放著格式化後的數位 1 2 ..,然後根據gdb
看上面的分析即可
到第三段程式碼時,可以看到程式會將edx
設定為0x6032d0
,檢視該地址處資訊可知,猜測這裡應該是一個結構體node1
,在gdb
中也明確地告訴了我們
執行完mov rdx, qword ptr [rdx + 8]
這條指令後,rdx
儲存的內容由 node1
變成了node2
,接著迴圈又會變成node3
、node4
一直到node
,然後執行qword ptr [rsp + rsi*2 + 0x20], rdx
指令,將node6
儲存到了棧上我們輸入的字串的上面。同樣,迴圈6次,找到每一個變化後的 7- a[i] 對應的node,並儲存到棧的對應位置。
迴圈結束後,可以看到棧的情況(右側的數位即為node->data
):
檢視六個node
結構體的資訊
經過分析,得知最後一段程式碼的作用是將結構體的data
按非升序排列,每一個node的data如上圖所示,注意我們比較時用的是eax
來儲存結構體node->data
,只能儲存低32位元,所以按node->data
的低32位元排序,可以得到降序排列為node 3,4,5,6,1,2
,而7-input[i]
剛好與node
的編號是一一對應的,所以我們的輸入為4 3 2 1 6 5
。 終於完成了