bomblab
炸弹人,炸弹魂,拆弹人是人下人。CSAPP 终于要我们做Lab02了。首先我们来介绍一下GDB
GDB
GDB指令很多,我这里只列举一下在bomblab调试中常用的指令
gdb 文件名
调试某个文件
break+函数名/地址
在某一个函数上设置断点
run
运行程序
i r 寄存器名
查看寄存器当中的内容
ni
单步执行(不会进入调用函数内部)
si
单步进入(会进入调用函数内部)
display /9i $pc
在每次运行一步之后会显示之后要执行的9行汇编代码,非常好用。
set var *地址 =
修改某个地址上的值
x/<n/f/u> <addr>
查看某个地址中的值
n:是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义。
f:表示addr指向的内存内容的输出格式,s对应输出字符串
此处需特别注意输出整型数据的格式:
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
我们常用
x/s 地址
来看一个地址上存储的字符串
x/wd 地址
来看一个地址上存储的数字
x/x 地址
来看一个地址上存储的十六进制的地址 。同理,x/3x 0x54320
比如说以地址0x54320为起始地址,返回3个单元的值,输出格式为十六进制。
phase_1
1 | Phase_1: |
我们首先用 i r esp
指令来看看当前的栈顶指针为多少:
esp 0xffffd23c 0xffffd23c
运行第一句之后,esp减去了 0x14
esp 0xffffd228 0xffffd228
运行,将地址 0x804a024 上的值压栈,用 x/s 0x804a024
来查看地址上的内容并用字符串的形式输出
0x804a024: "All your base are belong to us."
运行,phase_1函数调用了 strings_not_equal 函数,我们来看看在这个函数的内部逻辑:
strings_not_equal: 第五行、第六行是将 两个栈上的地址分别存入到 %ebx,%esi 中,
我们可以先用 i r esp 看看栈顶指针
esp 0xffffd210 0xffffd210
可以看看栈顶指针分别加上 0x10, 0x14 p/x *0xffffd220
p/x *0xffffd224
发现值:0x804a024 和 0x804c3e0
在存入寄存器后,我们发现寄存器中存入的值就是上面的两个地址,来看看这两个寄存器当中存入了什么值:
esi 0x804a024 134520868
ebx 0x804c3e0 134530016
要看看这个地址上的数据, 可以用 x/s 地址
1 | (gdb) x/s 0x804a024 |
我们看到,ebx 中存放的是我们输入的字符串, esi 中存放我们要对比的。
1 | strings_not_equal: |
如果相等,那么就返回1,否则返回0。 然后函数Phase_1会对返回值进行一个test,也就是按位与操作,如果返回值是1,那么就不会爆炸;如果返回值是0,那么就会爆炸。
于是我们知道第一关的密码是 All your base are belong to us.
Phase_2
1 | Phase_2 |
我们先看看栈顶指针:esp 0xffffd23c 0xffffd23c
然后push了两个寄存器,现在 esp 变成了 0xffffd234
随后栈顶指针减去了 0x2c也就是减了44个字节,这时候esp 0xffffd208 0xffffd208
push %eax和0x3c(%esp) 之后,栈顶指针现在是 0xffffd200
然后phase_2调用了 read_six_numbers,我们进入到函数一窥究竟
栈顶指针又减去了 0xc,栈顶指针现在是0xffffd1f0
随后做的一系列操作就是将我们输入的六个数字的地址(这时候还未读入)压入栈中
在scanf函数之前,esp 已经来到了0xffffd1cc
调用scanf之后,esp现在的值是0xffffd1d0
加上0x20之后,现在 esp是 0xffffd1f0
然后会用 %eax和5作比较,%eax记录了scanf函数的返回值也就是输入数字的个数,这里我已经输入了6、5、4、3、2、1 一共6个数所以现在eax中的值是6.
最后%esp加上了0xc后返回,现在esp的值为 0xffffd1fc
1 | 0x08049135 <+0>: sub $0xc,%esp |
回到phase_2,%esp又加上了0x10,现在 esp中的值为0xffffd210
接着让 %esp+4 地址上存放的东西和0去比,必须和0相等,否则就爆炸。那么我们来看看 %esp+4 地址上存放的是什么值:
p/x *0xffffd214 : $8 = 0x6
发现是6,所以我们推断这个地址上存放的值是我们输入的第一个数
以此类推,我们看一看 0xffffd218 , 0xffffd21c,0xffffd220,0xffffd224,0xffffd228
中得知,发现分别是:5,4,3,2,1 猜想成立。
所以我们直到第一个值必须要等于0,否则就爆炸。同理,第二个值必须等于1,否则就爆炸。
现在来到了 +53 这行指令。我们把第一个参数的地址移到 %ebx中,将第5个数字移动到%esi中
+61
将 0x4(%ebx) 也就是第二个参数的值移动到了%eax当中
+64
并让其和 (%ebx) 也就是第一个参数的值 相加,这就让 %eax = 第二个参数的值+第一个参数的值
+66
让 0x8(%ebx) 也就是第三个参数和 %eax相比,不相等就爆炸。于是我们推测第三个参数应该等于前两个参数之和。
+76
将 %ebx加上4,也就是现在 %ebx指向了第二个参数的地址
+81
跳转回 +61
重复上述操作。
至此我们可以推断出这是一个6位数的斐波那契数列,输入的值应该为: 0 1 1 2 3 5
1 | Phase_2_continue: |
Phase_3
首先我们设个断点 b phase_3
,并随便输入2个数,我输入的是 1 43
直接看看+25
处藏着什么东西。0x804a1ef: "%d %d"
,是两个 整数,我们猜测第三关要输入两个整数。
+42
中的eax存放的是 scanf的返回值,让其与1相比,一定要大于才能继续,所以更应证了我们的猜想,即输入两个整数。
+52
开始,函数对输入的数字进行了一系列操作。因为首先是用 0x4(%esp)
和 7相比,我们就看看这个地址上存入的是什么
esp 0xffffd1c0 0xffffd1c0
0xffffd1c4: 1
0xffffd1c8: 43
我们发现我们输入的两个数存放到了栈上。
+57
说,如果above 0x7,那么就直接爆炸,那么我们知道了我们输入的第一个数是无符号整数,且要小于7
<+171>: cmpl $0x5,0x4(%esp)
说,如果第一个参数大于5,也要爆炸,所以我们直到第一个整数是无符号的,且小于等于五。
因此可以取 0,1,2,3,4,5
<+59>
是将第一个参数移到 eax中去
1 | Phase_3 |
+63
开始,是一个switch 语句,我们可以用 x/x 0x804a080
来查看这个地址上存放的东西。
0x804a080: 0x08048c05
发现正好是 +70
0x804a084: 0x08048c0c
发现是+77
0x804a088: 0x08048c18
发现是+89
0x804a08c: 0x08048c24
发现是+101
0x804a090: 0x08048c30
发现是+113
0x804a094: 0x08048c3c
发现是+125
0x804a098: 0x08048c48
发现是 +137
0x804a09c: 0x08048c54
发现是+154
所以我们可以进行一个计算:
如果第一个参数是 0 $\rightarrow$ 跳转到 +70
如果第一个参数是 1 $\rightarrow$ 跳转到 +77
如果第一个参数是2 $\rightarrow$ 跳转到+89
如果第一个参数是3 $\rightarrow$ 跳转到+101
如果第一个参数是4$\rightarrow$ 跳转到+113
如果第一个参数是5$\rightarrow$ 跳转到+125
然后再线性运算。
因此我们只要看+178
的时候,eax中最后的数字就可以了。因为这句话的意思是让 eax和我们输入的第二个参数相比,如果不相等就爆炸;相等就成功过关
1 |
|
最后的结果是: 0 673 或者 1 270 或者 2 671 或者 3 -297 或者 4 0 或者 5 -297
Phase_4
phase_4 是我认为最简单的题目了,因为好像根本用不到fun4,但是这是解题博客,所以我们还是来认真看一下
我输入的值是 108 ,2
首先看一下+25
, 0x804a1ef: “%d %d” 发现又是输入两个数
然后我们看 +39
,结束以后用 i r esp
查看当前栈指针:0xffffd220
+42
中,scanf的返回值也要大于等于2,这刚好印证了+25
的猜想
我们用查看一下 0x4(%esp) 和 0x8(%esp),发现刚好是 2 和 108。
+47
到 +59
对第我们输入的第二个数字进行一个范围限定
首先将其减去2,然后让其于2相比,一定要比2小且是无符号整数才不会爆炸。因此我们可以得出第二个参数的值必然为2或者3
+73
调用了fun4,然后+81
让fun4的返回值和我们输入的第一个参数相比。不相等就爆炸否则就过关。所以我看都没看就直接ni,然后看 eax中的值,发现2对应的是108而3对应的是162
1 | phase_4 |
但是作为数据学院的学子,我们必须对自己高标准严要求(不是)。现在我们来探索一哈fun4中发生了什么不可告人的秘密
func4是一个递归函数
在<+71>
出我们push了一个0x8,者在后面是要用到的
+3
和 +7
分别把 8 和我们输入的第二个数移动进了 ebx
和 edi
然后我们判断 ebx是不是小于等于0,jle是判断$(SF)$^$(OF)|ZF$的,所以ebx=0,ZF等于1,跳转;如果ebx小于0,SF=1,OF=0,ZF=0 ,$(SF)$^$(OF)|ZF$ =1 ,跳转,如果小于0直接返回0
+15
,+17
是让 ebx中的数字和1比,如果相等就跳出func4,返回值是 eax中的值
+26
的作用是将ebx(初始值为8)减去1放到eax当中,并将edi压栈
+30
开始是一个递归,在这个递归中,ebx更新为减去1后的ebx(第二轮中是7),edi仍然为edi。
这样进行了很多次调用之后,最后当ebx=1的时候,函数调转到63并将ebx之类的pop出来,这时候ebx=2,edi=3
1 | func4 |
Phase_5
第一阶段是读入数字,还是老样子。0x804a1ef是要我们读入 两个整数,+42
也让scanf的返回值大于1,于是我们推断phase_5 也让我们读入两个整数。
1 | Phase_5 |
我在phase_5输入了5 115 两个数,现在我们来找找看它们藏在哪:
i r esp
: 0xffffd220
我们用 x/d 0xffffd224
和 x/d 0xffffd228
分别查看栈上的两个地址,发现:
0xffffd224: 5
0xffffd228: 115
刚好是我们输入的两个值。
+52
到 +66
是对我们输入的第一个参数进行一系列的操作:
首先将其移入 eax,并让eax和 0xf 按位与。再把结果复制到第一个参数当中。最后让这个结果和0xf比较,如果相等就爆炸。我觉得这个设置就是让我们输入的第一个数的二进制表达的低四位一定不能是1111.
1 | Phase_5_Continued |
接下来是正式操作了:首先是让 ecx=0,edx=1
+81
是让我们跳转到某一个地址,并将地址指向的值复制到eax。具体跳转到哪里,取什么值?
我们用 x/20dw 0x804a0a0一系列指令可以将这些数字都展现出来:
1 | (gdb) x/20dw 0x804a0a0 |
我们发现这个数组中一共有16个数字。
+88
跳完以后我们将eax加到ecx中去。并eax其和15比较。如果不相等那么跳转到78再次循环,并让edx++;如果相等那么就继续。
+103
则是让edx和15比较,如果不相等就跳到115,相等就让ecx和我们输入的第2个参数相比。
所以这个函数可以写成这样
1 | int a,b;//输入的第一个和第二个参数 |
我们的目标就是确定a选什么值,在经过15次循环后能让a=15,并让b等于循环过程中a的数值之和。
我们可以逆向推断。既然要输出15,那么在第十四次循环后a就要等于6,这样才能让a[6]=15
同理,要让a=6,那么就让第十三次循环后a=14,这样才能让 a[14]=6
以此类推,我们得到了一个链:
$15\rightarrow6\rightarrow14\rightarrow2\rightarrow1\rightarrow10\rightarrow0\rightarrow8\rightarrow4\rightarrow9\rightarrow13\rightarrow11\rightarrow7\rightarrow3\rightarrow12\rightarrow5$
因为Eax是先变化之后,才被ecx累加,所以eax的初值5并没有被加到ecx中!
因此,ecx =12+3+7+11+13+9+4+8+0+10+1+2+14+6+15=115
1 | Phase_5_Continued |
Phase_5的答案就是5,115.问题解决。
Phase_6
Phase_6是整个炸弹关卡最难的一关,我们将其分为3个阶段
首先来看看第一个阶段: +26
是读入6个数字
+39
是将当前的栈顶指针 esp+4*esi+12 地址上存储的值存放到eax当中去
那么我们具体来看看当前的栈上存放着哪些数字吧~,我填入的数字是:5 4 1 2 3 6
我们看到这6个数字,是从下到上依次存储的
1 | (gdb) i r esp |
+43到+46
是将eax中的内容减去1和5比较,如果大于5就直接爆炸,所以我们推断出所有的六个数都是无符号的且是小于等于6的。
esi是个计数器,初始值为0(+34
) 每次比较成功之后会自增1,如果加到了6就跳出循环。如果没有到6的话就运行 +64
从+66
开始,程序做的就是将我们输入的数和后面的所有数进行比较(第一个和2、3、4、5、6相比,第二个和3、4、5、6相比),如果相等就爆炸,因此我们输入的6个数字必须是两两不等的。
其原函数类似以下逻辑:
1 | for(int i =0;i<6;i++) |
1 | Phase_6: |
我们输入的满足了条件之后,来到了第二个阶段:
+91
我们将第一个参数移动到eax当中去。
+95
我们将 栈顶指针加上0x24的地址中存放的内容移动到eax当中,即a[6](超出数组空间但也可以这么写)
+99和+104
我们将ecx和edx都置为7
+106
让%edx(7)减去第一个数,得到某一个数。然后修改eax地址上存储的数(即变为7-a[0])
然后将eax地址+4,指向数组的下一个数,现在是a[1]。然后再将其值改为(7-a[1]) 以此类推
等到eax的地址指向 a[6] 的时候,跳出循环。并将 ebx置为0
+122
让我们跳转到 +146
并将当前ebx赋值给esi,即现在esi=0. 然后在+148
第一个输入的值(本来是5,现在是7-5=2) 赋给ecx
+152
是将1赋值给eax
+157
是将某一个地址赋给edx
+162
是将ecx也就是我(7减去输入的第一个数) 和1比较如果大于1,就跳转到+124
.否则 (等于1) 跳转到+134
跳转到124,我们发现 +124
是将edx+=8,然后将edx上存放的数据取出放到edx当中去。然后让eax+1,并和我们输入的数作比较,如果相等,就将edx中存放的数存放到栈里面去。否则就edx继续加8,继续让eax+1,一直到eax=ecx。所以最后edx中存储的数取决于我输入的数。
现在我们来看看0x804c13c 到底是什么:
1 | (gdb) x/3x 0x804c13c |
我们发现0x804c13c 开始,是一个链表,比如说处理好的数是2,那么经过上面这个程序,会将0x388取出来放到栈里,同理,如果处理好的数是3,那么会将0x269压入栈中
这个阶段仍然是一个两层循环,目的在于将我们从链表中取到的和输入数字相应的结点地址放入栈中保存,将六个都取完之后,到下一个阶段
1 | 0x08048e2f <+91>: lea 0xc(%esp),%eax |
上面的循环了6次,我们现在来到了第三阶段
1 | 0x08048e7d <+169>: mov 0x24(%esp),%ebx |
可以看出esi是循环计数变量,要对里面的的五个数进行判断,一共5轮,判断结点保存的六个数中相邻的数是不是都比后面一个数大,一言以蔽之,就是判断结点里面的值是不是非递增的!
所以,最后的排列数是递减的,查询链表发现:$0x388\rightarrow 0x269 \rightarrow0x1ea\rightarrow 0x93 \rightarrow 0x74$
对应链表中的位置是 :2 3 6 5 4 1
但是别忘了这是处理好的数据,没有处理过的数据是7-i,所以我们输入的数据应该是 5 3 1 2 3 6
Defused_Phase and Secret_Phase
1 | phase_defused: |
1 | secret_phase |
1 | fun7: |