【CSAPP】Bomb Lab實驗筆記


bomblab這節搞的是二進制拆彈,可以通俗理解為利用反匯編知識找出程序的六個解鎖密碼.
早就聽聞BOMBLAB的大名,再加上我一直覺得反匯編是個很艱難的工作,開工前我做好了打BOSS心理准備.實際上手后發現比想象的要簡單.
我覺得這多虧了作者沒有搞代碼優化,讓我能比較輕易的還原出源代碼,甚至一眼看出所用的數據結構.但凡它搞一點兒代碼混淆,都會把這次實驗變成一次苦痛之旅.

前置小技巧

1.gdb調試匯編
我試了一番后覺得用以下幾條指令在全屏模式下調試是體驗最好的

gdb -tui ./bomb #帶文字用戶界面
layout asm      #大板塊的匯編視圖
layout regs     #大板塊的寄存器視圖

效果預覽

2.函數參數與寄存器的關系

位數\參數數量 1 2 3 4 5 6
64 %rdi %rsi %rdx %rcx %r8 %r9
32 %edi %esi %edx %ecx %r8d %r9d
16 %di %si %dx %cx %r8w %r9w
8 %dil %sil %dl %cl %r8b %r9b

3.有什么問題不妨先翻到文章末尾看看參考鏈接

phase_1

執行objdump -S --no-show-raw-insn ./bomb > dump.txt,將bomb可執行文件反匯編結果放入到dump.txt中,並去除掉冗余的機器碼數據

觀察main的反匯編,在調用phase_1之前有一步mov %rax,%rdi,故猜測rdi中為輸入字符串的地址

  0X400e32:	callq  0X40149e <read_line>
  0X400e37:	mov    %rax,%rdi
  0X400e3a:	callq  0X400ee0 <phase_1>
  0X400e3f:	callq  0X4015c4 <phase_defused>

啟動gdb ./bomb,輸入b phase_1在phase_1處加斷點,用"testmsg"做測試信息

(gdb) b phase_1
Breakpoint 1 at 0x400ee0
(gdb) r
Starting program: /home/kangyu/Desktop/bomb/bomb 
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
testmsg

info r查看寄存器狀態

(gdb) info r
rax            0x603780	6305664
rbx            0x0	0
rcx            0x7	7
rdx            0x1	1
rsi            0x603780	6305664
rdi            0x603780	6305664
rbp            0x402210	0x402210 <__libc_csu_init>
rsp            0x7fffffffde38	0x7fffffffde38

記下rdi對應值,x/s 0x603780查看對應字符串

(gdb) x/s 0x603780
0x603780 <input_strings>:	"testmsg"

則確認猜想

再來看phase_1的反匯編代碼

0X0000000000400ee0 <phase_1>:
  0X400ee0:	sub    $0x8,%rsp
  0X400ee4:	mov    $0x402400,%esi
  0X400ee9:	callq  0X401338 <strings_not_equal>
  0X400eee:	test   %eax,%eax
  0X400ef0:	je     0X400ef7 <phase_1+0x17>
  0X400ef2:	callq  0X40143a <explode_bomb>
  0X400ef7:	add    $0x8,%rsp
  0X400efb:	retq   

大膽猜測strings_not_equal是比較兩個字符串是否相等的,且一個參數是rdi,另一個參數是esi.只要這倆字符串一樣,phase_1就解開了
在strings_not_equal前加個斷點看看esi對應字符串的值

(gdb) b *0x400ee9
Breakpoint 2 at 0x400ee9
(gdb) c
Continuing.

Breakpoint 2, 0x0000000000400ee9 in phase_1 ()
(gdb) x/s 0x402400
0x402400:	"Border relations with Canada have never been better."

記下上面這個字符串,重啟一遍bomb測試之

Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?

成功押中.

phase_2

為了方便研究,我把跳轉指令的目標地址替換成了對應標簽

0X0000000000400efc <phase_2>:
  0X400efc:	push   %rbp
  0X400efd:	push   %rbx
  0X400efe:	sub    $0x28,%rsp
  0X400f02:	mov    %rsp,%rsi
  0X400f05:	callq  0X40145c <read_six_numbers>
  0X400f0a:	cmpl   $0x1,(%rsp)
  0X400f0e:	je     .L3
  0X400f10:	callq  0X40143a <explode_bomb>
  0X400f15:	jmp    .L3
.L1
  0X400f17:	mov    -0x4(%rbx),%eax
  0X400f1a:	add    %eax,%eax
  0X400f1c:	cmp    %eax,(%rbx)
  0X400f1e:	je     .L2
  0X400f20:	callq  0X40143a <explode_bomb>
.L2
  0X400f25:	add    $0x4,%rbx
  0X400f29:	cmp    %rbp,%rbx
  0X400f2c:	jne    .L1
  0X400f2e:	jmp    .L4
.L3
  0X400f30:	lea    0x4(%rsp),%rbx
  0X400f35:	lea    0x18(%rsp),%rbp
  0X400f3a:	jmp    .L1
.L4
  0X400f3c:	add    $0x28,%rsp
  0X400f40:	pop    %rbx
  0X400f41:	pop    %rbp
  0X400f42:	retq   

看到里面有個read_six_numbers,我們直接猜這次是要輸入六位數字做密碼,在read_six_numbers后一行打個斷點,分別用"123456","12345","1234567"做密碼嘗試,結果全在運行到斷點前炸了
於是分析read_six_numbers代碼

0X000000000040145c <read_six_numbers>:
  0X40145c:	sub    $0x18,%rsp
  0X401460:	mov    %rsi,%rdx
  0X401463:	lea    0x4(%rsi),%rcx
  0X401467:	lea    0x14(%rsi),%rax
  0X40146b:	mov    %rax,0x8(%rsp)
  0X401470:	lea    0x10(%rsi),%rax
  0X401474:	mov    %rax,(%rsp)
  0X401478:	lea    0xc(%rsi),%r9
  0X40147c:	lea    0x8(%rsi),%r8
  0X401480:	mov    $0x4025c3,%esi
  0X401485:	mov    $0x0,%eax
  0X40148a:	callq  0X400bf0 <__isoc99_sscanf@plt>
  0X40148f:	cmp    $0x5,%eax
  0X401492:	jg     .L1
  0X401494:	callq  0X40143a <explode_bomb>
.L1
  0X401499:	add    $0x18,%rsp
  0X40149d:	retq   

看到里面有個__isoc99_sscanf,這不就是標准的庫函數嘛.它的格式是

int sscanf(const char *str, const char *format, ...)

sscanf的第二個參數對應rsi,觀察代碼,調用sscanf前剛好對esi進行了一次賦值操作,查看對應地址

(gdb) x/s 0x4025c3
0x4025c3:	"%d %d %d %d %d %d"

原來read_six_numbers的六個數間是有空格的啊!
在read_six_numbers末尾加個斷點,用"1 2 3 4 5 6"測試之.嗯,這次沒炸.
存進去的值總要有地方放,又需要方便的讀取,我們自然想到了按順序放到棧里.我在read_six_numbers返回后的下一行加斷點,用特殊值"1 1 4 5 1 4"驗證之

Breakpoint 2, 0x0000000000400f30 in phase_2 ()
(gdb) info r
rax            0x6	6
rbx            0x0	0
rcx            0x0	0
rdx            0x7fffffffde14	140737488346644
rsi            0x0	0
rdi            0x7fffffffd770	140737488344944
rbp            0x402210	0x402210 <__libc_csu_init>
rsp            0x7fffffffde00	0x7fffffffde00
......

(gdb) x 0x7fffffffde00
0x7fffffffde00:	"\001"
(gdb) x 0x7fffffffde04
0x7fffffffde04:	"\001"
(gdb) x 0x7fffffffde08
0x7fffffffde08:	"\004"
(gdb) x 0x7fffffffde0c
0x7fffffffde0c:	"\005"
(gdb) x 0x7fffffffde10
0x7fffffffde10:	"\001"
(gdb) x 0x7fffffffde14
0x7fffffffde14:	"\004"
(gdb) x 0x7fffffffde18
0x7fffffffde18:	"1\024@"

完美命中.
這里多說一句lea 0x4(%rsp),%rbxmov 0x4(%rsp),%rbx的效果是不同的,拿偽代碼舉例就是

rbx=rsp+4  //lea 0x4(%rsp),%rbx
rbx=*(rsp+4) //mov 0x4(%rsp),%rbx

所以用C++對phase_2手工逆向就是

int sp[7]; //注意是int型,元素長度為4byte
sscanf(input,"%d %d %d %d %d %d",sp);
if(!(sp[0]==1)){
	BOOM();
}
bx=sp+1;
bp=sp+7;
do{
	ax=*(bx-1);
	ax+=ax;
	if(!(ax==*bx)){
		BOOM();
	}
	bx+=1;
	if(bx==bp){
		break;
	}
}while(1);
return;

把這個代碼推導一下得出密碼"1 2 4 8 16 32",測試之,OK

Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2.  Keep going!

phase_3

先來貼代碼

0X0000000000400f43 <phase_3>:
  0X400f43:	sub    $0x18,%rsp
  0X400f47:	lea    0xc(%rsp),%rcx
  0X400f4c:	lea    0x8(%rsp),%rdx
  0X400f51:	mov    $0x4025cf,%esi
  0X400f56:	mov    $0x0,%eax
  0X400f5b:	callq  0X400bf0 <__isoc99_sscanf@plt>
  0X400f60:	cmp    $0x1,%eax
  0X400f63:	jg     .L1
  0X400f65:	callq  0X40143a <explode_bomb>
.L1
  0X400f6a:	cmpl   $0x7,0x8(%rsp)
  0X400f6f:	ja     .L2
  0X400f71:	mov    0x8(%rsp),%eax
  0X400f75:	jmpq   *0x402470(,%rax,8)
  0X400f7c:	mov    $0xcf,%eax
  0X400f81:	jmp    .L3
  0X400f83:	mov    $0x2c3,%eax
  0X400f88:	jmp    .L3
  0X400f8a:	mov    $0x100,%eax
  0X400f8f:	jmp    .L3
  0X400f91:	mov    $0x185,%eax
  0X400f96:	jmp    .L3
  0X400f98:	mov    $0xce,%eax
  0X400f9d:	jmp    .L3
  0X400f9f:	mov    $0x2aa,%eax
  0X400fa4:	jmp    .L3
  0X400fa6:	mov    $0x147,%eax
  0X400fab:	jmp    .L3
.L2
  0X400fad:	callq  0X40143a <explode_bomb>
  0X400fb2:	mov    $0x0,%eax
  0X400fb7:	jmp    .L3
  0X400fb9:	mov    $0x137,%eax
.L3
  0X400fbe:	cmp    0xc(%rsp),%eax
  0X400fc2:	je     .L4
  0X400fc4:	callq  0X40143a <explode_bomb>
.L4
  0X400fc9:	add    $0x18,%rsp
  0X400fcd:	retq   

發現直接調用了sscanf,查看一下格式字符串的內容

(gdb) x/s 0x4025cf
0x4025cf:	"%d %d"

故本次輸入為兩個數字
代碼里還出現了JA,這是無符號大於時跳轉
后面用到了jmpq,它是64位系統專用的jump,可以粗略的理解為

jmpq d(a,b,c)=jmp a+b*c+d

注意題目里的jmpq后面是*0x402470,查看內存得知它的值為400f7c

現在來手工逆向

int sp[2]; //注意是int型,元素長度為4byte
ax=sscanf(input,"%d %d",sp+2,sp+3);
if(!(ax>0)){
	BOOM();
}
if(!(*(sp+2)>7)){
	BOOM();
}
ax=*(sp+2);
jmpq 0x400f7c+ax*8;
/*
  0X400f7c:	mov    $0xcf,%eax
  0X400f81:	jmp    .L3
  0X400f83:	mov    $0x2c3,%eax
  0X400f88:	jmp    .L3
  ......
*/
if(!(*(sp+3)==ax)){
	BOOM();
}
return;

根據輸入參數的不同,jmpq跳躍的位置也不同,不過ax只能在[0,7]上取值,挨個算出來就行.一算發現ax=0時就可以,此時(sp+2)=0,(sp+3)=0XCF=207,測試之,OK

That's number 2.  Keep going!
0 207
Halfway there!

phase_4

反匯編的代碼,這次涉及到了一個子函數func4

0X000000000040100c <phase_4>:
  0X40100c:	sub    $0x18,%rsp
  0X401010:	lea    0xc(%rsp),%rcx
  0X401015:	lea    0x8(%rsp),%rdx
  0X40101a:	mov    $0x4025cf,%esi
  0X40101f:	mov    $0x0,%eax
  0X401024:	callq  0X400bf0 <__isoc99_sscanf@plt>
  0X401029:	cmp    $0x2,%eax
  0X40102c:	jne    <explode_bomb>
  0X40102e:	cmpl   $0xe,0x8(%rsp)
  0X401033:	jbe    .L1
  0X401035:	callq  <explode_bomb>
.L1
  0X40103a:	mov    $0xe,%edx
  0X40103f:	mov    $0x0,%esi
  0X401044:	mov    0x8(%rsp),%edi
  0X401048:	callq  0X400fce <func4>
  0X40104d:	test   %eax,%eax
  0X40104f:	jne    <explode_bomb>
  0X401051:	cmpl   $0x0,0xc(%rsp)
  0X401056:	je     .L2
  0X401058:	callq  <explode_bomb>
.L2
  0X40105d:	add    $0x18,%rsp
  0X401061:	retq   

0000000000400fce <func4>:
  0X400fce:	sub    $0x8,%rsp
  0X400fd2:	mov    %edx,%eax
  0X400fd4:	sub    %esi,%eax
  0X400fd6:	mov    %eax,%ecx
  0X400fd8:	shr    $0x1f,%ecx
  0X400fdb:	add    %ecx,%eax
  0X400fdd:	sar    %eax
  0X400fdf:	lea    (%rax,%rsi,1),%ecx
  0X400fe2:	cmp    %edi,%ecx
  0X400fe4:	jle    .L1
  0X400fe6:	lea    -0x1(%rcx),%edx
  0X400fe9:	callq  0X400fce <func4>
  0X400fee:	add    %eax,%eax
  0X400ff0:	jmp    .L2
.L1
  0X400ff2:	mov    $0x0,%eax
  0X400ff7:	cmp    %edi,%ecx
  0X400ff9:	jge    .L2
  0X400ffb:	lea    0x1(%rcx),%esi
  0X400ffe:	callq  0X400fce <func4>
  0X401003:	lea    0x1(%rax,%rax,1),%eax
.L2
  0X401007:	add    $0x8,%rsp
  0X40100b:	retq  

查看格式字符串的值,是兩個整數

(gdb) x/s 0x4025cf
0x4025cf:	"%d %d"

等你把代碼費勁的扒完一遍后,你會發現test ax,ax意思不是ax==ax嘛,它結果不是恆為1嘛,這不是說func4壓根沒造成任何影響.這一大片匯編代碼可以簡化為:

ax=sscanf(input,"%d %d",sp+2,sp+3);
if(!(ax=2)){
	BOOM();
}
if(!(*(sp+2)<=0xe)){
	BOOM();
}
func4(); //毫無作用的占位代碼
if(!(*(sp+3)==0x0)){
	BOOM();
}
return;

所以只要第一個數小於等於0xe,第二個數等於0就能過
我的答案是0 0

Halfway there!
0 0
So you got that one.  Try this one.

后來我一看別人的答案發現不對,這個結果是我巧合做對了. test a,b的實際作用是a&b,所以test ax,ax即是判斷ax==0,這就得把匯編代碼重新翻譯一遍了.

void BOOM()
{
    printf("BOOM!!!");
    exit(0);
}
int ax, cx, dx, si, di;
void func4()
{
    ax = dx;
    ax -= si;
    cx = ax;
    cx >>= 31;
    ax += cx;
    ax >>= 1;
    cx = ax + si;
    if (cx <= di)
        goto L1;
    dx = cx - 1;
    func4();
    ax += ax;
    goto L2;
L1:
    ax = 0;
    if (cx >= di)
        goto L2;
    si = cx + 1;
    func4();
    ax = ax + ax + 1;
L2:
    return;
}
int main()
{
    int arg1, arg2;
    if (scanf("%d %d", &arg1, &arg2) != 2){
        BOOM();
    }
    if (!(arg1 <= 0xe)){
        BOOM();
    }
    dx = 0xe;
    si = 0x0;
    di = arg1;
    func4();
    if (ax != 0){
        BOOM();
    }
    if (arg2 != 0x0){
        BOOM();
    }
    printf("OK");
    return 0;
}

分析分析新翻譯的匯編代碼,對func4有影響的只有用戶輸入的第一個數(我在上面翻譯成了arg1),因為arg1<=0xe,那我們直接寫個程序窮舉之

void f(){
    for(int arg1=0;arg1<=0xe;arg1++){
        dx = 0xe;
        si = 0x0;
        di = arg1;
        func4();
        if ((ax & ax) == 0){
            printf("%d is ok\n",arg1);
        }
    }
}

輸出結果為

0 is ok
1 is ok
3 is ok
7 is ok

所以第一個數可以取得值為0,1,3,7,第二個數只能取0

phase_5

0X0000000000401062 <phase_5>:
  0X401062:	push   %rbx
  0X401063:	sub    $0x20,%rsp
  0X401067:	mov    %rdi,%rbx
  0X40106a:	mov    %fs:0x28,%rax
  0X401073:	mov    %rax,0x18(%rsp)
  0X401078:	xor    %eax,%eax
  0X40107a:	callq  0X40131b <string_length>
  0X40107f:	cmp    $0x6,%eax
  0X401082:	je     .L2
  0X401084:	callq  0X40143a <explode_bomb>
  0X401089:	jmp    .L2
.L1
  0X40108b:	movzbl (%rbx,%rax,1),%ecx
  0X40108f:	mov    %cl,(%rsp)
  0X401092:	mov    (%rsp),%rdx
  0X401096:	and    $0xf,%edx
  0X401099:	movzbl 0x4024b0(%rdx),%edx
  0X4010a0:	mov    %dl,0x10(%rsp,%rax,1)
  0X4010a4:	add    $0x1,%rax
  0X4010a8:	cmp    $0x6,%rax
  0X4010ac:	jne    .L1
  0X4010ae:	movb   $0x0,0x16(%rsp)
  0X4010b3:	mov    $0x40245e,%esi
  0X4010b8:	lea    0x10(%rsp),%rdi
  0X4010bd:	callq  0X401338 <strings_not_equal>
  0X4010c2:	test   %eax,%eax
  0X4010c4:	je     .L3
  0X4010c6:	callq  0X40143a <explode_bomb>
  0X4010cb:	nopl   0x0(%rax,%rax,1)
  0X4010d0:	jmp    .L3
.L2
  0X4010d2:	mov    $0x0,%eax
  0X4010d7:	jmp    .L1
.L3
  0X4010d9:	mov    0x18(%rsp),%rax
  0X4010de:	xor    %fs:0x28,%rax
  0X4010e7:	je     .L4
  0X4010e9:	callq  0X400b30 <__stack_chk_fail@plt>
.L4
  0X4010ee:	add    $0x20,%rsp
  0X4010f2:	pop    %rbx
  0X4010f3:	retq   

觀察string_length附近的幾行,我們猜測這幾行功能是判斷輸入字符串長度是否為6,不是的化就炸了.分別用長度為5,6,7的字符串做測試,在explode_bomb下面打個斷點,結果只有6沒炸,猜想正確.
再往后跳到L2,把ax置0,再跳入L1
注意到L1着一塊形成了一個循環,只有在ax==6時才能跳出,且每次判斷前都有一個ax++操作,所以我們猜測ax是L1的循環變量
具體研究L1,這里用到了movzbl,注意mov()為取數值,lea()為取地址.mov的其中一個參數為bx,往會看注意到phase_4開始時令bx=di,即bx此時指向輸入字符串input,而整個循環中bx未改變過,所以movzbl (%rbx,%rax,1),%ecx的作用相當於cx=input[ax]
下面的mov %cl,(%rsp)佐證了我們的猜想,因為cl為cx的低八位,正好是一個字節.
一直到下一個movzbl這一行的功能可以概括為input[ax]的低四位,進而令dx=0x4024b0+input[ax]&0xf.觀察一下0x4024b0的值

(gdb) x/s 0x4024b0
0x4024b0 <array.3449>:  "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

注意前16個字符為無規律的小寫字符串,而input[ax]&0xf恰好只有16種取值,相當於把它倆做了個對應.再往后這個對應出來的字符串會被放到sp[ax+10]里.這照應了開頭的sub $0x20,%rsp為運行棧分配空間.
等到L1結束后會調用strings_not_equal,很明顯兩個參數是待比較的字符串,一個是我們剛求得sp,另一個是0x40245e,觀察其值

(gdb) x/s 0x40245e
0x40245e:       "flyers"

所以我們的目的很明確了:要把輸入字符串經過L1里的循環邏輯轉換成"flyers".循環的過程上面分析了,又到了寫暴力程序的時候了

#include <stdio.h>
char sa[20]="maduiersnfotvbyl";
char sb[20]="flyers";
char ans[20];
int main()
{
    for(int i=0;i<6;i++){
        for(int ch='a';ch<='z';ch++){
            if(sa[ch&0xf]==sb[i]){
                ans[i]=ch;
                break;
            }
        }
    }
    printf("%s",ans);
    return 0;
}

運行結果為"ionefg",測試,通過

So you got that one.  Try this one.
ionefg
Good work!  On to the next...

phase_6

0X00000000004010f4 <phase_6>:
  0X4010f4:	push   %r14
  0X4010f6:	push   %r13
  0X4010f8:	push   %r12
  0X4010fa:	push   %rbp
  0X4010fb:	push   %rbx
  0X4010fc:	sub    $0x50,%rsp
  0X401100:	mov    %rsp,%r13
  0X401103:	mov    %rsp,%rsi
  0X401106:	callq  0X40145c <read_six_numbers>
  0X40110b:	mov    %rsp,%r14
  0X40110e:	mov    $0x0,%r12d

#第一部分
.L0
  0X401114:	mov    %r13,%rbp
  0X401117:	mov    0x0(%r13),%eax
  0X40111b:	sub    $0x1,%eax
  0X40111e:	cmp    $0x5,%eax
  0X401121:	jbe    .L1
  0X401123:	callq  0X40143a <explode_bomb>
.L1
  0X401128:	add    $0x1,%r12d
  0X40112c:	cmp    $0x6,%r12d
  0X401130:	je     .L4
  0X401132:	mov    %r12d,%ebx
.L2
  0X401135:	movslq %ebx,%rax
  0X401138:	mov    (%rsp,%rax,4),%eax
  0X40113b:	cmp    %eax,0x0(%rbp)
  0X40113e:	jne    .L3
  0X401140:	callq  0X40143a <explode_bomb>
.L3
  0X401145:	add    $0x1,%ebx
  0X401148:	cmp    $0x5,%ebx
  0X40114b:	jle    .L2
  0X40114d:	add    $0x4,%r13
  0X401151:	jmp    .L0

#第二部分

.L4  
  0X401153:	lea    0x18(%rsp),%rsi
  0X401158:	mov    %r14,%rax
  0X40115b:	mov    $0x7,%ecx
.L5
  0X401160:	mov    %ecx,%edx
  0X401162:	sub    (%rax),%edx
  0X401164:	mov    %edx,(%rax)
  0X401166:	add    $0x4,%rax
  0X40116a:	cmp    %rsi,%rax
  0X40116d:	jne    .L5

  0X40116f:	mov    $0x0,%esi
  0X401174:	jmp    .L9

#第三部分
.L6
  0X401176:	mov    0x8(%rdx),%rdx
  0X40117a:	add    $0x1,%eax
  0X40117d:	cmp    %ecx,%eax
  0X40117f:	jne    .L6

  0X401181:	jmp    .L8
.L7  
  0X401183:	mov    $0x6032d0,%edx
.L8
  0X401188:	mov    %rdx,0x20(%rsp,%rsi,2)
  0X40118d:	add    $0x4,%rsi
  0X401191:	cmp    $0x18,%rsi
  0X401195:	je     .L10
.L9
  0X401197:	mov    (%rsp,%rsi,1),%ecx
  0X40119a:	cmp    $0x1,%ecx
  0X40119d:	jle    .L7

  0X40119f:	mov    $0x1,%eax
  0X4011a4:	mov    $0x6032d0,%edx
  0X4011a9:	jmp    .L6

#第四部分
.L10
  0X4011ab:	mov    0x20(%rsp),%rbx
  0X4011b0:	lea    0x28(%rsp),%rax
  0X4011b5:	lea    0x50(%rsp),%rsi
  0X4011ba:	mov    %rbx,%rcx
.L11
  0X4011bd:	mov    (%rax),%rdx
  0X4011c0:	mov    %rdx,0x8(%rcx)
  0X4011c4:	add    $0x8,%rax
  0X4011c8:	cmp    %rsi,%rax
  0X4011cb:	je     .L12
  0X4011cd:	mov    %rdx,%rcx
  0X4011d0:	jmp    .L11

#第五部分
.L12
  0X4011d2:	movq   $0x0,0x8(%rdx)
  0X4011da:	mov    $0x5,%ebp
.L13
  0X4011df:	mov    0x8(%rbx),%rax
  0X4011e3:	mov    (%rax),%eax
  0X4011e5:	cmp    %eax,(%rbx)
  0X4011e7:	jge    .L14
  0X4011e9:	callq  0X40143a <explode_bomb>
.L14
  0X4011ee:	mov    0x8(%rbx),%rbx
  0X4011f2:	sub    $0x1,%ebp
  0X4011f5:	jne    .L13


  0X4011f7:	add    $0x50,%rsp
  0X4011fb:	pop    %rbx
  0X4011fc:	pop    %rbp
  0X4011fd:	pop    %r12
  0X4011ff:	pop    %r13
  0X401201:	pop    %r14
  0X401203:	retq   

這一階段的代碼看起來特長,但我們可以根據有無嵌套拆成五個部分,然后逐個攻破
第一部分檢查每個數是否都小於等於6,且各不相同,逆向得

void f1(int a[]){
    for(int i=0;i<6;i++){
        if(a[i]>6) BOOM();
        for(int j=i+1;j<6;j++){
            if(a[i]==a[j]) BOOM();
        }
    }
}

第二部分令每個元素x等於7-x

void f2(int a[]){
    for(int i=0;i<6;i++)
        a[i]=7-a[i];
}

第三部分我看出來它做了一個鏈式結構,但一開始並沒有想過它還用了數據結構,導致陷入迷惑了.然后一看別人博文里提到了鏈表立馬反應過來了.

我們先來檢查一下0x6032d0附近的內存

(gdb) x/40x 0x6032d0
0x6032d0 <node1>:	0x0000014c	0x00000001	0x006032e0	0x00000000
0x6032e0 <node2>:	0x000000a8	0x00000002	0x006032f0	0x00000000
0x6032f0 <node3>:	0x0000039c	0x00000003	0x00603300	0x00000000
0x603300 <node4>:	0x000002b3	0x00000004	0x00603310	0x00000000
0x603310 <node5>:	0x000001dd	0x00000005	0x00603320	0x00000000
0x603320 <node6>:	0x000001bb	0x00000006	0x00000000	0x00000000

通過變量名和數據組織形式我們可以看出來這是一個鏈表,對應的結構體為

struct node{
	int data;
	int id;
	node *next;
	int other;
};

對第三部分代碼進行逆向得

while(1){
	cx=*(sp+si*1);  // cx=sp+0,sp+0x4,sp+0x8,sp+0xc,....
	if(cx<=1) dx=0x6032d0; //dx=表頭地址
	else {
		ax=1;
		dx=0x6032d0;
		//遍歷鏈表,使得dx=第sp[si/4]項的地址
		do{	
			dx=*(dx+0x8);
			ax+=1;
		while(ax!=cx);
	}	
	*(sp+si*2+0x20)=dx;//從sp+0x20起,每8字節記錄一個鏈表項地址
	si+=4;
	if(si==0x18) goto L10; //sp+0x18,即&sp[6]
}

檢查一下運行棧的內存布局,切合了我們的推導

(gdb) x/20x 0x7fffffffddc0
0x7fffffffddc0:	0x00000006	0x00000005	0x00000004	0x00000003
0x7fffffffddd0:	0x00000002	0x00000001	0x00000000	0x00000000
0x7fffffffdde0:	0x00603320	0x00000000	0x00603310	0x00000000
0x7fffffffddf0:	0x00603300	0x00000000	0x006032f0	0x00000000
0x7fffffffde00:	0x006032e0	0x00000000	0x006032d0	0x00000000

第四部分將鏈表進行了重排

bx=*(sp+0x20)=sp[8]
ax=sp+0x28
si=sp+0x50
cx=bx
while(1)
{	
	//遍歷上一步在運行棧中記錄好的地址
	//用這個順序重新排列鏈表項
	dx=*ax
	*(cx+0x8)=dx
	ax+=0x8
	if(ax==si) goto L12
	cx=dx
}

第五部分遍歷鏈表,如果排序結果是按value遞減的,那么就是合法的密碼

//此時dx指向重排后的鏈表尾項,bx指向鏈表首項
*(dx+0x8)=0;
bp=0x5;
do{
ax=*(bx+0x8);	//ax=bx->next
ax=*ax;			//ax=ax->value
if(bx->value<ax) BOOM(); 
bx=bx->next;
bp--;
}while(bp!=0);

觀察0x6032d0附近的內存布局,我們可以推出合法的鏈表項順序應為3->4->5->6->1->2,因為第二步操作的存在,每個數x應該取7-x,即4 3 2 1 6 5

secret_phase

根據網友的提醒bomb里還有一個隱藏關卡,通過分析phase_defused可以找到入口

0X00000000004015c4 <phase_defused>:
  0X4015c4:	sub    $0x78,%rsp
  0X4015c8:	mov    %fs:0x28,%rax
  0X4015d1:	mov    %rax,0x68(%rsp)
  0X4015d6:	xor    %eax,%eax
  0X4015d8:	cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
  0X4015df:	jne    0X40163f <phase_defused+0x7b>
  0X4015e1:	lea    0x10(%rsp),%r8
  0X4015e6:	lea    0xc(%rsp),%rcx
  0X4015eb:	lea    0x8(%rsp),%rdx
  0X4015f0:	mov    $0x402619,%esi
  0X4015f5:	mov    $0x603870,%edi
  0X4015fa:	callq  0X400bf0 <__isoc99_sscanf@plt>
  0X4015ff:	cmp    $0x3,%eax
  0X401602:	jne    0X401635 <phase_defused+0x71>
  0X401604:	mov    $0x402622,%esi
  0X401609:	lea    0x10(%rsp),%rdi
  0X40160e:	callq  0X401338 <strings_not_equal>
  0X401613:	test   %eax,%eax
  0X401615:	jne    0X401635 <phase_defused+0x71>
  0X401617:	mov    $0x4024f8,%edi
  0X40161c:	callq  0X400b10 <puts@plt>
  0X401621:	mov    $0x402520,%edi
  0X401626:	callq  0X400b10 <puts@plt>
  0X40162b:	mov    $0x0,%eax
  0X401630:	callq  0X401242 <secret_phase>
  0X401635:	mov    $0x402558,%edi
  0X40163a:	callq  0X400b10 <puts@plt>
  0X40163f:	mov    0x68(%rsp),%rax
  0X401644:	xor    %fs:0x28,%rax
  0X40164d:	je     0X401654 <phase_defused+0x90>
  0X40164f:	callq  0X400b30 <__stack_chk_fail@plt>
  0X401654:	add    $0x78,%rsp

我們注意到這里面多了secret_phase,因此秘密關卡進入條件就在這里.分別分析sscanf和 strings_not_equal的輸入參數

(gdb) x/s 0x603870
0x603870 <input_strings+240>:   "0 0"
(gdb) x/s 0x402619
0x402619:       "%d %d %s"
(gdb) x/s 0x402622
0x402622:       "DrEvil"

可知只有在輸入"0 0 DrEvil"才可進入隱藏關卡,觀察我們的輸入數據,只有在第四階段時輸入了0 0,故在它后面加上個"DrEvil"即可進入隱藏關

Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
0 0 DrEvil
ionefg
4 3 2 1 6 5
0 DrEvilPhase 1 defused. How about the next one?
That's number 2.  Keep going!
Halfway there!
So you got that one.  Try this one.
Good work!  On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...

進入之后我們先研究secret_phase的代碼

0X0000000000401242 <secret_phase>:
  0X401242:	push   %rbx
  0X401243:	callq  0X40149e <read_line>
  0X401248:	mov    $0xa,%edx
  0X40124d:	mov    $0x0,%esi
  0X401252:	mov    %rax,%rdi
  0X401255:	callq  0X400bd0 <strtol@plt>
  0X40125a:	mov    %rax,%rbx
  0X40125d:	lea    -0x1(%rax),%eax
  0X401260:	cmp    $0x3e8,%eax
  0X401265:	jbe    .L1
  0X401267:	callq  0X40143a <explode_bomb>
  .L1
  0X40126c:	mov    %ebx,%esi
  0X40126e:	mov    $0x6030f0,%edi
  0X401273:	callq  0X401204 <fun7>
  0X401278:	cmp    $0x2,%eax
  0X40127b:	je     .L2
  0X40127d:	callq  0X40143a <explode_bomb>
  .L2
  0X401282:	mov    $0x402438,%edi
  0X401287:	callq  0X400b10 <puts@plt>
  0X40128c:	callq  0X4015c4 <phase_defused>
  0X401291:	pop    %rbx
  0X401292:	retq   

逆向得

//輸入長度小於10的字符串,轉換為long,放入ax中
ax=input;
bx=ax;
ax=ax-1;
if(ax>0x3e8) BOOM();
si=bx;
di=0x6030f0;
ax=fun7()
if(ax!=2) BOOM(); //返回值為2時通過

這里研究一下0x6030f0對應的內存區域是啥

(gdb) x/120x 0x6030f0
0x6030f0 <n1>:	0x00000024	0x00000000	0x00603110	0x00000000
0x603100 <n1+16>:	0x00603130	0x00000000	0x00000000	0x00000000
0x603110 <n21>:	0x00000008	0x00000000	0x00603190	0x00000000
0x603120 <n21+16>:	0x00603150	0x00000000	0x00000000	0x00000000
0x603130 <n22>:	0x00000032	0x00000000	0x00603170	0x00000000
0x603140 <n22+16>:	0x006031b0	0x00000000	0x00000000	0x00000000
0x603150 <n32>:	0x00000016	0x00000000	0x00603270	0x00000000
0x603160 <n32+16>:	0x00603230	0x00000000	0x00000000	0x00000000
0x603170 <n33>:	0x0000002d	0x00000000	0x006031d0	0x00000000
0x603180 <n33+16>:	0x00603290	0x00000000	0x00000000	0x00000000
0x603190 <n31>:	0x00000006	0x00000000	0x006031f0	0x00000000
0x6031a0 <n31+16>:	0x00603250	0x00000000	0x00000000	0x00000000
0x6031b0 <n34>:	0x0000006b	0x00000000	0x00603210	0x00000000
0x6031c0 <n34+16>:	0x006032b0	0x00000000	0x00000000	0x00000000
0x6031d0 <n45>:	0x00000028	0x00000000	0x00000000	0x00000000
0x6031e0 <n45+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x6031f0 <n41>:	0x00000001	0x00000000	0x00000000	0x00000000
0x603200 <n41+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x603210 <n47>:	0x00000063	0x00000000	0x00000000	0x00000000
0x603220 <n47+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x603230 <n44>:	0x00000023	0x00000000	0x00000000	0x00000000
0x603240 <n44+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x603250 <n42>:	0x00000007	0x00000000	0x00000000	0x00000000
0x603260 <n42+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x603270 <n43>:	0x00000014	0x00000000	0x00000000	0x00000000
0x603280 <n43+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x603290 <n46>:	0x0000002f	0x00000000	0x00000000	0x00000000
0x6032a0 <n46+16>:	0x00000000	0x00000000	0x00000000	0x00000000
0x6032b0 <n48>:	0x000003e9	0x00000000	0x00000000	0x00000000
0x6032c0 <n48+16>:	0x00000000	0x00000000	0x00000000	0x00000000

一看這組織形式,不就是個二叉樹嘛,對應的結構體:

struct node{
	long data
	node *left
	node *right
	long other
}

接下來研究fun7的代碼

0X0000000000401204 <fun7>:
  0X401204:	sub    $0x8,%rsp
  0X401208:	test   %rdi,%rdi
  0X40120b:	je     .L2
  0X40120d:	mov    (%rdi),%edx
  0X40120f:	cmp    %esi,%edx
  0X401211:	jle    .L1
  0X401213:	mov    0x8(%rdi),%rdi
  0X401217:	callq  0X401204 <fun7>
  0X40121c:	add    %eax,%eax
  0X40121e:	jmp    .L3
.L1
  0X401220:	mov    $0x0,%eax
  0X401225:	cmp    %esi,%edx
  0X401227:	je     .L3
  0X401229:	mov    0x10(%rdi),%rdi
  0X40122d:	callq  0X401204 <fun7>
  0X401232:	lea    0x1(%rax,%rax,1),%eax
  0X401236:	jmp    .L3
.L2
  0X401238:	mov    $0xffffffff,%eax
.L3
  0X40123d:	add    $0x8,%rsp
  0X401241:	retq   
//調用時di指向根節點,si存放輸入值input
fun7:
sp-=0x8;
//遍歷到葉子節點
if(di==0) {
	ax=bigint;
	return;
}
dx=*di; //取節點對應的data
if(dx<=si) { // data<=input,訪問右子節點
	ax=0;
	if(dx==si) { //data==input
		return; 
	}
	di=*(di+0x10); //di=di->right
	call fun7;
	ax=ax+ax+1;
	return ;
}
else {
	di=*(di+0x8); //di=di->left
	call fun7;
	ax+=ax;
	return;
}

很明顯,這不僅是個二叉樹,還是個四層的二叉排序樹.
我們要返回2,逆推就是1*2 => 0*2+1 => 0*2 => 0,即按照LRL的順序進行訪問,對比着前面的內存分布圖可得出約束條件

0x24>x
0x8<=x
0x16>x
0x14==x

所以輸入值應為0x14,即20

Curses, you've found the secret phase!
But finding it and solving it are quite different...
20
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!

ALL STAGE CLEAR!

參考資料:

寄存器操作數大小及參數數量 by alike_meng
GDB 單步調試匯編 by 張雅宸
學 Win32 匯編[28] - 跳轉指令: JMP、JECXZ等 by 萬一的 Delphi 博客
gdb中x的用法 by 我打打江南走過過


免責聲明!

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



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