开始写bomblab了,听说这是所有lab里最有趣的lab。反思一下写这篇博文既想当作实验报告,又想当成一个教程,边做边写总会出现一些纰漏,故前半部分记录了做lab的全过程,后半部分再做全局的总结和思考。

前置知识

实验一览

Phase 1

反汇编出phase_1的汇编代码:

1
(gdb) disas phase_1
1
2
3
4
5
6
7
8
9
10
11
12
Dump of assembler code for function phase_1:
0x08048b60 <+0>: sub $0x1c,%esp
0x08048b63 <+3>: movl $0x804a284,0x4(%esp)
0x08048b6b <+11>: mov 0x20(%esp),%eax
0x08048b6f <+15>: mov %eax,(%esp)
0x08048b72 <+18>: call 0x80490a4 <strings_not_equal>
0x08048b77 <+23>: test %eax,%eax
0x08048b79 <+25>: je 0x8048b80 <phase_1+32>
0x08048b7b <+27>: call 0x80491b6 <explode_bomb>
0x08048b80 <+32>: add $0x1c,%esp
0x08048b83 <+35>: ret
End of assembler dump.
  • sub $0x1c, %esp:在栈上为该函数分配 28 字节的空间。
  • movl $0x804a284,0x4(%esp):将 0x804a284 这个地址值(在 .rodata 节)写入从 %esp 偏移量为 4 的位置,即第一个函数参数的位置。
  • mov 0x20(%esp),%eax:将从 %esp 偏移量为 32 的位置,即第二个函数参数的位置处的值加载到寄存器 %eax 中。
  • mov %eax,(%esp):将 %eax 中的值存储到栈的顶部。
  • call 80490a4 <strings_not_equal>:调用名为 strings_not_equal 的函数,该函数的地址为 0x80490a4。
  • test %eax,%eax:将 %eax 寄存器与自身进行 AND 操作并设置标志寄存器。这是为了检查函数返回值是否为 0(两个字符串相等)。
  • je 8048b80 <phase_1+0x20>:如果上一条指令设置了零标志,则跳转到地址 0x8048b80,即函数的结尾。
  • call 80491b6 <explode_bomb>:如果字符串不相等,则调用名为 explode_bomb 的函数,该函数会使程序停止运行并输出错误信息。
  • add $0x1c,%esp:将栈顶向上移动 28 个字节,即释放为该函数分配的栈空间。
  • ret:从该函数返回。

参数是从右往左入栈的,第二个参数的长度为28,第一个参数为4。故第一个参数的地址为esp+4,第二个参数的地址为esp-32。

可以合理推测我们输入的字符串通过函数phase_1的第二个参数传入,将其放入eax寄存器。

将其与strings_not_equal函数中设定的答案对比,若相等则拆除炸弹成功。故我们需要使用gdb调试strings_not_equal函数,找到设定的答案。

我们在strings_not_equal函数处打上断点,运行程序并输入input1(test):

1
2
3
4
5
6
7
8
9
10
11
(gdb) b strings_not_equal
Breakpoint 1 at 0x80490a4
(gdb) r
Starting program: /home/wujean/bomb216/bomb
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
test

Breakpoint 1, 0x080490a4 in strings_not_equal ()

我们为了查看ebx和esi的值,需要执行程序:

1
2
(gdb) si 6
0x080490bb in strings_not_equal ()

image-20230417111505401

2-5行是被调用者保存寄存器 保存的过程。

1
2
0x080490b3 <+15>:    mov    0x14(%esp),%ebx
0x080490b7 <+19>: mov 0x18(%esp),%esi

这两行代码读取函数的两个参数,我们使用gdb获取存储在这两个寄存器中参数的值。

1
2
3
4
(gdb) p/s (char*)$ebx
$1 = 0x804c3e0 <input_strings> "test"
(gdb) p/s (char*)$esi
$2 = 0x804a284 "Houses will begat jobs, jobs will begat houses."

这样一看就很清楚了,我们输入的test为函数的第二个参数,而第一个参数为一个定值:Houses will begat jobs, jobs will begat houses.

这便是我们需要比对的字符串,将其作为第一个输入,第一个炸弹成功拆除。

1
2
3
4
5
wujean@DESKTOP-8K47HOB:~/bomb216$ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Houses will begat jobs, jobs will begat houses.
Phase 1 defused. How about the next one?

Phase 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) disas phase_2
Dump of assembler code for function phase_2:
0x08048b84 <+0>: push %ebx
0x08048b85 <+1>: sub $0x38,%esp
0x08048b88 <+4>: lea 0x18(%esp),%eax
0x08048b8c <+8>: mov %eax,0x4(%esp)
0x08048b90 <+12>: mov 0x40(%esp),%eax
0x08048b94 <+16>: mov %eax,(%esp)
0x08048b97 <+19>: call 0x80492eb <read_six_numbers>
0x08048b9c <+24>: cmpl $0x0,0x18(%esp)
0x08048ba1 <+29>: jns 0x8048ba8 <phase_2+36>
0x08048ba3 <+31>: call 0x80491b6 <explode_bomb>
0x08048ba8 <+36>: mov $0x1,%ebx
0x08048bad <+41>: mov %ebx,%eax
0x08048baf <+43>: add 0x14(%esp,%ebx,4),%eax
0x08048bb3 <+47>: cmp %eax,0x18(%esp,%ebx,4)
0x08048bb7 <+51>: je 0x8048bbe <phase_2+58>
0x08048bb9 <+53>: call 0x80491b6 <explode_bomb>
0x08048bbe <+58>: add $0x1,%ebx
0x08048bc1 <+61>: cmp $0x6,%ebx
0x08048bc4 <+64>: jne 0x8048bad <phase_2+41>
0x08048bc6 <+66>: add $0x38,%esp
0x08048bc9 <+69>: pop %ebx
0x08048bca <+70>: ret
End of assembler dump.

拆了第一个炸弹后发现需要格外注意其中调用的函数,第二个炸弹中有两个函数:read_six_numbersexplode_bomb

read_six_numbers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) disas read_six_numbers
Dump of assembler code for function read_six_numbers:
0x080492eb <+0>: sub $0x2c,%esp
0x080492ee <+3>: mov 0x34(%esp),%eax
0x080492f2 <+7>: lea 0x14(%eax),%edx
0x080492f5 <+10>: mov %edx,0x1c(%esp)
0x080492f9 <+14>: lea 0x10(%eax),%edx
0x080492fc <+17>: mov %edx,0x18(%esp)
0x08049300 <+21>: lea 0xc(%eax),%edx
0x08049303 <+24>: mov %edx,0x14(%esp)
0x08049307 <+28>: lea 0x8(%eax),%edx
0x0804930a <+31>: mov %edx,0x10(%esp)
0x0804930e <+35>: lea 0x4(%eax),%edx
0x08049311 <+38>: mov %edx,0xc(%esp)
0x08049315 <+42>: mov %eax,0x8(%esp)
0x08049319 <+46>: movl $0x804a4b7,0x4(%esp)
0x08049321 <+54>: mov 0x30(%esp),%eax
0x08049325 <+58>: mov %eax,(%esp)
0x08049328 <+61>: call 0x8048870 <__isoc99_sscanf@plt>
0x0804932d <+66>: cmp $0x5,%eax
0x08049330 <+69>: jg 0x8049337 <read_six_numbers+76>
0x08049332 <+71>: call 0x80491b6 <explode_bomb>
0x08049337 <+76>: add $0x2c,%esp
0x0804933a <+79>: ret
End of assembler dump.

可以由函数名以及前几行代码的操作看出,本函数读取了六个参数,若读取的参数不足6个则直接调用explode_bomb,炸弹爆炸。

回到phase_2函数中,与第一问,答案存储在内存中的某个地址中不同,本题主要为分析汇编代码得出六个数的规律。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) disas phase_2
Dump of assembler code for function phase_2:
0x08048b84 <+0>: push %ebx
0x08048b85 <+1>: sub $0x38,%esp
0x08048b88 <+4>: lea 0x18(%esp),%eax
0x08048b8c <+8>: mov %eax,0x4(%esp) //预留六个int类型的空间,将其地址作为参数传入read_six_numbers
0x08048b90 <+12>: mov 0x40(%esp),%eax
0x08048b94 <+16>: mov %eax,(%esp)
0x08048b97 <+19>: call 0x80492eb <read_six_numbers> //输入六个数字(检验是否为6个)
0x08048b9c <+24>: cmpl $0x0,0x18(%esp) //a[0]>0
0x08048ba1 <+29>: jns 0x8048ba8 <phase_2+36>
0x08048ba3 <+31>: call 0x80491b6 <explode_bomb> //否则爆炸
->0x08048ba8 <+36>: mov $0x1,%ebx --------------------------------
0x08048bad <+41>: mov %ebx,%eax int i=1,j=1;i<6;i++,j++
0x08048baf <+43>: add 0x14(%esp,%ebx,4),%eax if(a[i-1]+j!=a[i])
0x08048bb3 <+47>: cmp %eax,0x18(%esp,%ebx,4)
0x08048bb7 <+51>: je 0x8048bbe <phase_2+58>
0x08048bb9 <+53>: call 0x80491b6 <explode_bomb> 爆炸
0x08048bbe <+58>: add $0x1,%ebx
0x08048bc1 <+61>: cmp $0x6,%ebx
<-0x08048bc4 <+64>: jne 0x8048bad <phase_2+41> ---------------------------------
0x08048bc6 <+66>: add $0x38,%esp
0x08048bc9 <+69>: pop %ebx
0x08048bca <+70>: ret
End of assembler dump.

故我们只需要保证输入的第一个数大于0,且后面的数按照 +1 +2 +3 +4 +5的规律递增,就可成功拆除炸弹。

1
2
3
4
5
6
7
wujean@DESKTOP-8K47HOB:~/bomb216$ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Houses will begat jobs, jobs will begat houses.
Phase 1 defused. How about the next one?
2 3 5 8 12 17
That's number 2. Keep going!

Phase 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 1│ Dump of assembler code for function phase_3:
2│ 0x08048bcb <+0>: sub $0x3c,%esp //预留堆栈
3│ 0x08048bce <+3>: lea 0x28(%esp),%eax //args 3
4│ 0x08048bd2 <+7>: mov %eax,0x10(%esp)
5│ 0x08048bd6 <+11>: lea 0x2f(%esp),%eax //args 2
6│ 0x08048bda <+15>: mov %eax,0xc(%esp)
7│ 0x08048bde <+19>: lea 0x24(%esp),%eax //args 1
8│ 0x08048be2 <+23>: mov %eax,0x8(%esp)
9│ 0x08048be6 <+27>: movl $0x804a2da,0x4(%esp)
10│ 0x08048bee <+35>: mov 0x40(%esp),%eax
11│ 0x08048bf2 <+39>: mov %eax,(%esp)
12├──> 0x08048bf5 <+42>: call 0x8048870 <__isoc99_sscanf@plt>
13│ 0x08048bfa <+47>: cmp $0x2,%eax
14│ 0x08048bfd <+50>: jg 0x8048c04 <phase_3+57>

有了上一题的提示我们可以知道这题的参数为3个,再进一步探究,我们对存储在$0x804a2da中的数据很感兴趣,故打开gdb查看。我们可以看到这个地址下的值是作为__isoc99_sscanf的第二个参数的,我们先来了解一下__isoc99_sscanf

__isoc99_sscanf

1
int __isoc99_sscanf(const char *str, const char *format, ...);
  • str:要被解析的字符串。
  • format:格式字符串,指定了要解析的值的类型和顺序。
  • ...:可变参数列表,用于存储解析出来的值。
1
2
(gdb) x/8cb 0x804a2da
0x804a2da: 37 '%' 100 'd' 32 ' ' 37 '%' 99 'c' 32 ' ' 37 '%' 100 'd'

我们可以看到该地址下的格式化字符串为 “%d %c %d”,我们就明确了我们需要输入两个整形和一个字符。其返回值存储在寄存器eax中,通过比较eax的值来判断输入的参数个数是否正确。

1
2
16│    0x08048c04 <+57>:    cmpl   $0x7,0x24(%esp)
17│ 0x08048c09 <+62>: ja 0x8048d08 <phase_3+317>

由这两条指令可以看出参数1的值要小于7;

1
2
18│    0x08048c0f <+68>:    mov    0x24(%esp),%eax
19│ 0x08048c13 <+72>: jmp *0x804a300(,%eax,4)

到这里就能很清楚看出这是一个跳转表了,我们可以可以查看*0x804a300的值来验证我们的猜想:

1
2
(gdb) x 0x804a300
0x804a300: 0x08048c1a
1
2
3
4
5
6
7
8
9
10
20│    0x08048c1a <+79>:    mov    $0x67,%eax
21│ 0x08048c1f <+84>: cmpl $0x25f,0x28(%esp)
22│ 0x08048c27 <+92>: je 0x8048d12 <phase_3+327> //switch的结束
23│ 0x08048c2d <+98>: call 0x80491b6 <explode_bomb>
24│ 0x08048c32 <+103>: mov $0x67,%eax
25│ 0x08048c37 <+108>: jmp 0x8048d12 <phase_3+327>
26│ 0x08048c3c <+113>: mov $0x79,%eax
27│ 0x08048c41 <+118>: cmpl $0x39c,0x28(%esp)
28│ 0x08048c49 <+126>: je 0x8048d12 <phase_3+327>
.......................................

在这一条条switch中我们发现先比较每块中某个特殊数字与第2个参数的值,若相等再给eax赋值,跳转到:

1
2
70│    0x08048d12 <+327>:   cmp    0x2f(%esp),%al
71│ 0x08048d16 <+331>: je 0x8048d1d <phase_3+338>

第二个参数输入的字符与switch块中给eax赋的值对比,若相匹配贼炸弹拆除成功,可以通过对着ASCII码表给出答案。

一组通过测试的示例:

1
2 g 55
1
2
3
4
5
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!

小技巧

我们查看bomb.c的内容发现该代码接受一个文件输入,我们可以将已经得到的答案保存在一个res文件中,只需要运行:

1
./bomb res

就可得到得到结果。

在gdb中同样可以使用这个技巧,在gdb中输入:

1
set args res

即可设置参数进行调试。

Phase 4

根据func4的汇编代码可以写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func4(int a, int b, int c){
int d = c;
d -= b;
int f = d;
f = f >> 31;
d += f;
d = d >> 1;
d += b;
if(d > a){
d -= 1;
return 2*fun4(a, b, d);
}
else{
if(d == a){
return 0;
}
else{
return 2*fun4(a, d+1, c)+1;
}
}
}
1
2
23│    0x08048de3 <+85>:    call   0x8048d21 <func4>
24│ 0x08048de8 <+90>: cmp $0x3,%eax

我们可以看到最终的返回值为3,反推回去可以推出:

我们需要

  • 调用两次 2*fun4(a, d+1, c)+1
  • 调用一次 2*fun4(a, b, d)
  • 最终 return 0结束

故只有12 和 13符合条件,第二个数固定为3

另一个题

1
2
3
4
5
6
7
8
9
10
11
12
13
fun4(int i,int b)

if(i==0) return 0;

if(i==1) return b;

int tmp1 = fun4(i-1,b)

int di = tmp1+b;

int tmp2 = fun4(i-2,b)

return tmp1+tmp2+b;

1 b+0

2 b+b

3 b+2b+b

4 b+2b+4b

5 b+4b+7b

6 b+7b+12b

7 b+12b+20b

8 b+20b+33b

9 b+33b+54b

88

Phase 5

查看输入的类型:

1
2
3
7│    0x08048e10 <+19>:    movl   $0x804a4c3,0x4(%esp)
(gdb) p (char*)0x804a4c3
$1 = 0x804a4c3 "%d %d"

查看数组中的值:

1
2
3
4
5
6
22│    0x08048e4b <+78>:    mov    0x804a320(,%eax,4),%eax
(gdb) x/15d 0x804a320
0x804a320 <array.3000>: 10 2 14 7
0x804a330 <array.3000+16>: 8 12 15 11
0x804a340 <array.3000+32>: 0 4 1 13
0x804a350 <array.3000+48>: 3 9 6

写出代码的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a[15] = {
10,2,14,7,8,12,15,11,0,4,1,13,3,9,6
};
phase5(int x,int y){
int c = 0;
int d = 0;
for(d;d<15&&x!=15;d++){
x = a[x]
c+=x;
}
if(x==15&&d==15) return 0;
else return -1;
}

计算答案的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int a[15] = {
10,2,14,7,8,12,15,11,0,4,1,13,3,9,6
};
int phase5(int x){
int c = 0;
int d = 0;
for(d=0;d<15&&x!=15;d++){
x = a[x];
c+=x;
}
if(x==15&&d==15) return c;
else return -1;
}
int main(){
for(int i=0;i<15;i++){
printf("%d %d \n",i,phase5(i));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 -1
1 -1
2 -1
3 -1
4 -1
5 115
6 -1
7 -1
8 -1
9 -1
10 -1
11 -1
12 -1
13 -1
14 -1

Phase6

输入六个数:

1
9│    0x08048e85 <+20>:    call   0x80492eb <read_six_numbers>

六个数的地址:

1
5│    0x08048e76 <+5>:     lea    0x10(%esp),%eax
1
2
3
(gdb) x/6d $esp+0x10
0xffffcf30: 10 11 12 13
0xffffcf40: 14 15

输入的第数减去1要小于等于5:

1
2
3
20│    0x08048eaa <+57>:    mov    0x10(%esp,%ebx,4),%eax
21│ 0x08048eae <+61>: cmp %eax,0xc(%esp,%esi,4)
22│ 0x08048eb2 <+65>: jne 0x8048eb9 <phase_6+72>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
phase6(int a[6]){
//检查是否有相同元素
for(int i=0;i!=6;i++){
for(int j=i;j<=5;j++){
if(a[i]!=a[j+1]) continue;
}
}

int d = 0x804c13c;
for(int i=0;i<6;i++){
int eax = 1;
if(a[i]>1){
while(eax!=a[i]){
eax++;
d+=8; //计算权值的偏移量
}
if(eax==a[i]) a[6+i] = d; //存放权值的地址
}
else a[6+eax] = *d;

}
1
2
3
4
(gdb) x/12d $esp+0x10
0xffffcf30: 1 2 3 4
0xffffcf40: 5 6 134529340 134529352
0xffffcf50: 134529364 134529376 134529388 134529400
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
45│    0x08048ef7 <+134>:   mov    0x28(%esp),%ebx	
46│ 0x08048efb <+138>: mov 0x2c(%esp),%eax
47│ 0x08048eff <+142>: mov %eax,0x8(%ebx)
48│ 0x08048f02 <+145>: mov 0x30(%esp),%edx
49│ 0x08048f06 <+149>: mov %edx,0x8(%eax)
50│ 0x08048f09 <+152>: mov 0x34(%esp),%eax
51│ 0x08048f0d <+156>: mov %eax,0x8(%edx)
52│ 0x08048f10 <+159>: mov 0x38(%esp),%edx
53│ 0x08048f14 <+163>: mov %edx,0x8(%eax)
54│ 0x08048f17 <+166>: mov 0x3c(%esp),%eax
55│ 0x08048f1b <+170>: mov %eax,0x8(%edx)
56│ 0x08048f1e <+173>: movl $0x0,0x8(%eax)
57│ 0x08048f25 <+180>: mov $0x5,%esi
58│ 0x08048f2a <+185>: mov 0x8(%ebx),%eax
59│ 0x08048f2d <+188>: mov (%eax),%edx
60│ 0x08048f2f <+190>: cmp %edx,(%ebx)
61│ 0x08048f31 <+192>: jge 0x8048f38 <phase_6+199> //降序排列
62│ 0x08048f33 <+194>: call 0x80491b6 <explode_bomb>
63│ 0x08048f38 <+199>: mov 0x8(%ebx),%ebx
64│ 0x08048f3b <+202>: sub $0x1,%esi
65│ 0x08048f3e <+205>: jne 0x8048f2a <phase_6+185>
66│ 0x08048f40 <+207>: add $0x44,%esp
67│ 0x08048f43 <+210>: pop %ebx
68│ 0x08048f44 <+211>: pop %esi
69│ 0x08048f45 <+212>: ret
70│ End of assembler dump.
1
2
0xffffcf48:     134529340       134529352       134529364       134529376
0xffffcf58: 134529388 134529400