bomblab-0x12攻略
计算机组成原理的一个实验,在做bomblab之前需要一些基础知识,包括x86的一些指令和寄存器(寄存器知识不需要详解,这篇文章的寄存器部分就足够了),以及gdb相关。
用到的一部分gdb调试指令:
终端输入 gdb file_name,进入文件。
输入layout asm, 进入调试窗口。
b func_name, 在func_name设置断点。
r 运行; c 继续运行; q 退出。
x/s $reg_name ,查看某寄存器值。
p/x *(int*)(address)@len ,查看从address开始len长的数组值。
delete breakpoint_name, 删除断点。
phase_1 :
观察代码:
其中text %eax, %eax
意义为判定eax==0
是否成立,eax
是上一个函数的返回值,所以应该是判断字符串是否相等,并且返回相等。
因此,进入strings_not_equal
:
我们可以发现rsi, rdi
是两个参数,查看这两个寄存器的值,结果即为答案:
所以答案为:I can see Russia from my house!
。
phase_2 :
这部分应该是是读取数据,看到了read_six_numbers
可以猜想是读了六个数字,但是具体按照什么格式读取需要进入read_six_numbers
查看:
这些代码中,mov $0x4025a3, %esi
比较有用(来自百度),这个可以查看输入格式:输入x/s 0x4025a3
。
因此是六个按照空格分隔的数字。继续观察代码:
可以看到第一行比较了args[0] == 1
如果相等则跳过炸弹。因此我们确定第一个值为1。并且rbp = args[0x14/4] = args[5]
即输入参数的最后一个。
接下来是核心代码:
由于phase_2+70
跳转到了phase_2+49
,可以看出这是一个循环。循环的跳出条件为rbp == rbx
。也就是说当rbx = args[5]
时结束循环,在这之前每次循环都不能引爆炸弹。将这个伪代码用cpp表示:
分析其作用,应该是判断第一个数的二倍是否为第二个数,第二个数的二倍是否为第三个数······直到第五个和第六个进行比较,rbx
被赋值为args[5]
即第六个数,代码跳出循环。
跳出循环之后再无炸弹,便可以放心。因此我们知道,第一个数必定为1,之后每一个数为第一个数的2倍,总共有六个数,答案即为:1 2 4 8 16 32
。
phase_3 :
观察代码:
查看输入格式,得到:
因此要输入两个数字。
下一部分代码:
这部分除去输入格式判断,第一个有用的是cmpl $0x7, (%rsp)
。这句话决定了第一个数字不能超过7 。假设该数字为3。接着就把3赋给了eax
。最后一行进行了一次跳转,查看跳转地址x/s *(0x402420+3*8)
得到:
可以得到跳转到了phase_3+78
的位置。查看这个位置代码:
此时赋值eax = 0x2b1
并跳转到phase_3+130
。观察phase_3+130
:
直接进行了args[1] == eax
的判断并且相等则跳过炸弹,释放栈空间。因此args[1] = eax = 0x2b1
。答案即为:3 689
。经过尝试,应该是有7种答案,又试了一个arg[0] = 2
的答案为:2 690
。
phase_4:
首先设置断点观察phase_4的代码,前几行预处理代码如下:
查看输入格式。输入x/s 0x4025af
得到:
可以看出要输入的是两个数字。
观察剩余的代码:
可以看出edx = e; esi = 0; edi = args[0](输入的第一个值)
。接着调用了func4
并返回eax(返回值专用寄存器)
。然后比较eax == 0x2d
如果为false
则引爆炸弹,因此func4 return 0x2d
。然后又有args[1] == 0x2d
的比较,若不等则不跳转,引爆炸弹,可以看出第二个参数为0x2d
,第一个参数为经过func4
返回0x2d
的值。
使用cpp写出来func4
的代码:
分析如上, 代码如下:
int func4(int esi, int edx, int edi)
{
int eax = edx;
eax -= esi;
int ebx = eax;
ebx = ebx>0?0:1;
eax += ebx;
eax /= 2;
ebx = esi + eax;
if(ebx > edi)
{
edx = ebx - 1;
int ret1 = func4(esi, edx, edi);
return ret1+ebx;
}
else if(edi <= ebx) return ebx;
else
{
esi = ebx + 1;
int ret2 = func4(esi, edx, edi);
return ret2+ebx;
}
}
接着循环找出哪个数字返回0x2d
,我做了1到100的循环找到了14 。所以可以确定答案是14 0x2d
,即14 45
。
phase_5 :
和4的思路一样,首先查看0x4025af
,确定输入格式:
可以看出这次的输入仍然是两个数字。
这次剩余的代码较为复杂:
从phase_5+48
到phase_5+67
为第一部分,通过这部分可以看出使用eax
作为中间寄存器对args[0]
进行了与0xf
的按位与,并且比较了0xf == args[0]&0xf
,也就是说第一个输入值不能是大于等于16的值。看到后面的映射之后可以发现这是为了避免非法访问。
从phase_5+72
到phase_5+98
为第二部分,由于phase_5+89
跳转到了phase_5+72
,因此这部分应该是一个循环。其中循环结束条件为eax == 0xf
。但是之后在phase_5+98
也比较了edx == 0xf
,如果不相等就爆炸。而edx
在初始值为0
并且只在循环中改变值。所以正确的跳出循环条件为eax == 0xf && edx == 0xf
。循环中除了这两个之外还有一个寄存器叫做ecx
,其初始值为0。观察phase_5+77
行,可以看出该行为一个映射复制,查看以0x402460
为起始地址的数组:
至此就很清楚了:该循环使用一个固定的数组进行多次映射,eax = args[0]
为映射初始值,每次映射之后更新eax
;使edx+=1
;并且使ecx+=eax
。所以ecx
相当于求和,edx==0xf
表明循环进行了15次,但由于是映射之后再加上eax
,因此eax
初始值并没有算进去(第二次才发现这个坑)。15次映射之后的eax
应该是0xf
,因此采用倒推的方法,从0xf
向前映射15次,观察初始值。对应的cpp代码:
int reg[] = {10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6, 5};
int eax = 15, ecx = 15;
for(int edx=0; edx<15; edx++)
for(int index=0; index<15; index++)
if(reg[index] == eax)
{
eax = index;
ecx += eax;
cout<<index<<' ';
break;
}
ecx -= eax;
cout<<endl<<ecx;
输出结果
由此得到ecx = 115; args[0] = 5
。
第三部分为phase_5+103
到phase_5+139
。这部分除去释放栈空间等处理,有用的在第一句上,第一句比较了ecx == args[1]
,相等跳过炸弹,因此第二个参数确定为ecx
即115。答案即为:5 115
。
phase_6 :
查看代码:
由于read_six_numbers
是之前已经能查看过的函数,可以直接判定这次也是读入六个空格分隔数字。
由于在最后一行看到了跳转至phase_6+48
,可以看出这是一个循环,循环的结束很有意思,不在最后而在中间,也就是类似于whlie(1){··· if(xxx) break; ···}
的循环体。可以看出跳出循环的条件在phase_6+76
,为r14d == 0x6
。在这之前又有r14d += 1
,所以猜想r14d
也许是一个计数器,每次加一,六次跳出,即:for(r14d = 0; r14d!=6; r14d++)
。每次循环做的事情是从phase_6+48
到phase_6+109
之间的事,观察到phase_6+103
处还有个跳转,因此是一个小循环。尝试写出循环的cpp伪代码:
分析伪代码的作用,我们发现在第一个跳过炸弹的时候判定的是某一个数是否小于等于6,也就是说,args[i]<=6
。接着在第二个循环里,我们发现本质上是比较args[r14d+1] == args[r14d]
,也就是说相邻两个数不能像等,我们得到了这六个数都小于等于6且各不相同。接着:
发现这也是一个循环,首先得初始化阶段为edx = 7
,跳转条件为r12 != rcx
。而在上一部分,我们知道r12
表示的是rsp
的首地址,又因为phase_6+131
对r12
做了自增操作,因此r12
应该是指示每一个值,也就是说对输入的六位数组进行遍历,对每一项的操作为:rax = edx; eax -= *(r12); *(r12)= eax
这三句的意思是*(r12) = 7-*(r12)
。也就是说每个数对7取补。
这段代码是超多循环的集合,尝试对应的伪代码:
很遗憾,因为下面的循环是大循环,上面的循环是小循环,也就是说,晚开始循环的反而包括了早开始的循环体。在不使用goto
语句似乎无法写出。观察这个伪代码,是一种奇怪的扫描方式,是通过一个地址找到另一个地址的方式,类似于c++里的链表格式。并且出现了一个让人在意的地址:0x6032f0
,猜想这个应该是初始地址。查看初始地址里的值:
果然找到了另外一个地址,并且和这个地址相邻,基本可以确实是链表。通过伪代码可以看出来前三个值比较有用,分别为:data, index, address
。最后一个也就是ecx
。由跳出条件,我们需要一直找到ecx==0
的状态:
正好到第六个地址为0,跳出循环。接下来还是有很多循环,下一个循环:
从phase_6+211
到phase_6+229
为一个循环,循环跳出条件为rax == rsi
循环体中有rax += 8
,初始为rax = *(rsp+0x20); rsi = *(rsp+0x48)
。所以共循环5次,也就是遍历所有值。每次循环操作为*(rcx+8) = rcx = *(rax+8)
。这个循环要和下一个循环结合理解:这是最后一个循环:
这个循环的跳出很奇怪,cmp
在phase_6+250
,跳出在phase_6+252
和phase_6+266
,应该是一个判断决定两个跳转。而第一个判断是躲炸弹的,因此必须要求每次都有*(rbx) >= eax
。观察循环,可以发现eax = *(rax) = *(rbx+8)
,也就是说,*(rbx) > *(rbx+8)
必须成立,即这个数组应该是降序排列。回想之前得到的链表,我们可以发现,data降序排列时的index值为:1 4 3 5 6 2
,由于进行过一次对7取补,真正的序列为 : 6 3 4 2 1 5
。
至此全部解决,给出运行截图:
上一篇: 体系结构与操作系统拾遗
推荐阅读