目录
前导说明
本次实验的所有调试过程我基本都是在一个我自己开挂的版本中调试的,这个版本的炸弹是永远不会爆炸的,所以我可以流畅的看完每个phase的过程,开挂版我也写在了此实验报告中,在Bomblab(开挂版)标题内呈现。
phase_1
答案:I am for medical liability at the federal level.
首先将可执行文件bomb反汇编之后,输出到一个.txt文本文档中;
使用文本编辑gedit打开这个.txt文本文档:
找到phase_1的代码段;
可以看到427行处调用了一个string_not_equal函数,通过这个可以猜测可能第一个炸弹是要输入一个字符串与程序自身的答案所比较;
使用gdb打开可执行文件bomb开始调试;
gdb bomb
在phase_1处设置断点;
b phase_1
使用如下命令打开对应代码,便于单步调试;
layout asm
运行到phase_1函数之后开始单步调试;
发现在string_not_equal函数上面有一个立即数,同时对应的行是将某个地址送入%rsi之中,紧接着就进入了string_not_equal函数,所以我们猜测这个地址里存放的就是要比较的对应的答案;
用如下命令查看该立即数对应地址中的值;
x/s 0x555555557150
可以看到我们就拿到了第一个炸弹的答案:
I am for medical liability at the federal level.
phase_2
答案:(答案不唯一) 样例答案 1 2 4 7 11 16
我们先看一下.txt文件中phase_2的代码;
可以看到在445行处有一个read_six_number的函数,从这里大概可以推断出第二个炸弹可能是要输入六个数字;
下面我们在phase_2处设置断点;
b phase_2
运行至断点处;
可以看到我们会进入这个read_six_number函数,这就验证了我们的猜想;
对于phase_2对应的汇编代码,做如下跳转分析:
这其中%ebx中是循环计数器,初始值为1,只进行五次循环,当n等于6时直接退出;
从%rbp寄存器往后显示24个16进制的值,可以看到就是我们输入的6个数;
第n次循环时,%ebx为n,用%ebx值加上a[n-1]与a[n]比较,相等则继续循环,不相等就直接爆炸。
所以这个phase是没有标准答案,只要满足上述加粗字体所表述的规律即可;例如输入1 2 4 7 11 16也可以通过样例;我图中的2 3 5 8 12 17也是可以通过这个phase的;
phase_3
答案:(答案不唯一)1 b 926
我们先看一下.txt中phase_3的代码:
可以看到汇编代码是很长的但其实真正做完会发现其实并不难,因为它包含了很多种情况下的校验;不要怕,拿出我们万能的gdb;
条件1
这里我们看到一个新的函数,就是这个sscanf函数;
有关函数的详细功能可以参考:C 库函数 – sscanf() | 菜鸟教程 (runoob.com)
它其实就是一个格式化输入字符串的函数,类似于stringstream串流类;
在这个函数上面有一个立即数,我们利用如下命令查看一下这个立即数对应的内存中存放了什么;
x/s 0x5555555571ae
可以看到有三个参数,输入的格式必须为“整数 字符 整数”;
条件2
而且我们看到这里还有一个cmp函数,代表输入的参数如果小于2个,则炸弹会直接爆炸,这里我随便输入一个组合:1 b 926,这是我后续验证成功的一个序列;
可以看到rax中存储的是我们输入序列对应的参数个数,是3,与我们的输入相对应;
条件3
这里看到程序将我们输入的第一个整数(存放在0x10(%rsp)中)与7做比较,如果大于则炸弹直接爆炸;我们的测试序列第一个整数是5,所以第一个可以通过的;
491行中,汇编指令movslq实现的是对于一个双字整数 符号扩展 为四字整数;
492行操作后, %rax=0x5555555556fe;
493行中实现了跳转到%rax中存放的地址位置;
可以看到扩展后的数字为-6724,即上述汇编代码491行运行后的结果;
跳转到%rax中存放的地址位置:
条件4
可以看到代码实现的是将一个十六进制62(ascii码对应于b)放入%eax;
将十六进制数39e(十进制数为926)与我们输入的第三个参数(整数)(存放在0x14(%rsp)中)作比较;
可以看到0x14(%rsp)中的确存储的是926;
条件5
将我们输入的第二个参数(字符)(存放在0xf(%rsp)中)作比较;
可以看到0xf(%rsp)中的确存储的是十六进制62,即ascii码对应的b;
以上三个条件均满足则炸弹不会爆炸,通过该phase;
phase_4
答案:12 3
还是先看一下.txt中phase_4的汇编代码;
这汇编代码看着不是很长,但是仔细观察发现它自身还调用了一个名为func4的函数,而这个phase恐怖之处就在于它所调用的这个func4函数——因为它是一个递归函数!
另外,写在考前一点,这个phase在我的解题中涉及到两个参数,其中第二个参数可以在汇编代码中看到是绝对固定的,但第一个完全是我根据范围一个个试出来的;到目前为止我也没找到可以正向解决他的方法;
可以看到,这个phase中依旧用到了格式化输入函数sscanf;
可以看到函数之前还是有一个立即数,我们用如下命令查看这个立即数对应内存地址中的内容;
x/s 0x5555555572ff
可以看到输出了一个如上图红框里的字样,这代表我们这一次要输入的形式应该是“整数 整数”;
条件1
同时我们还是看到,在这个函数下面有一个比较cmp,依旧是对于我们输入的参数个数进行判断;如果我们的输入包含的参数不等于2就会直接爆炸;
条件2
在确认完我们输入的参数个数为2个后,第一步会对我们输入的第一个参数进行大小校验,可以看到会将第一个参数(保存在(%rsp)中)与十六进制数e(也就是十进制下的14)作比较,如果大于或等于,则直接爆炸,小于则跳转执行后面的代码;
(这个截图里是我随便输入的序列,第一个参数为1,这里它满足小于14的条件,所以第一个判断我们是可以正常通过的,炸弹不会爆炸)
下图中用红框框起来的三行设置了个寄存器的值,分别是edx,esi和edi,后面可以看到我们设置好好就进入了func4这个函数中,同时在这个函数中我们也需要用到这个这些寄存器,所以可以推测出,这三个寄存器就是func4函数的三个参数;
接下来我们看一下func4函数对应的汇编代码;
在这个汇编代码读起来还是有一定烦人的,我把他转化为了C++代码;
int func4(int edx, int esi, int edi)
{
int eax = edx;
eax -= esi;
int ecx = eax;
ecx >>= 31;
ecx += eax;
ecx >>= 1;
ecx += esi;
if(edi < ecx)
{
edx = rcx - 1;
func4(edx,esi,edi);
eax += eax;
return eax;
}
else
{
eax = 0;
if(edi > ecx)
{
esi=rcx+1;
func4(edx,esi,edi);
eax=rax+rax+1;
return eax;
}
else
{
return eax;
}
}
}
上面这个代码可能看着还是有点费劲,那么再修改一下;
int func4(int edx, int esi, int edi)
{
int eax = edx - esi;
if(eax < 0) eax-=1;
eax /= 2;
int ecx = eax + esi;
if(edi < ecx)
{
return func4(rcx - 1,esi,edi)*2;
}
else
{
if(edi > ecx)
{
return func4(edx,rcx+1,edi)*2+1;
}
else
{
return 0;
}
}
}
换成上面这个代码,是不是感觉思路清晰了很多;
条件3
同时让我们先看看调用完func4函数后做了什么;
首先第一个红框中,是对于func4函数返回值与3作比较,就说明我们需要让func4返回3;
但他是一个递归函数,我不太明白如何才能直接正着算得知我一开始输入什么可以使得最终返回3。不过好在由于一开始我们比较了第一个数,它是不能大于等于14的,所以我们就一个个试好了,我是从13往小试的,还是比较巧的, 答案就是12,要是我从小往大试那要试好多次。
前面也讲到了,我调试时用的是开挂版boomlab,所以我不担心他会爆炸,我只需要看到func4返回后eax的值即可;
下面我来复刻一下当第一个参数为12时,递归函数的执行过程:
func4(int edx, int esi, int edi)
可以看到的确最终返回值是3;
条件4
同时让我们再来开一下func4函数调用返回后的这段代码,可以看到第二个红框里,将我们输入的第二个参数(保存在0x4(%rsp)中)的值与3比较,因为第二个参数在之前并没有进行任何操作,所以这就表明我们的第二个参数必须为整数3;
phase_5
答案:(答案不唯一)7!-&%7
下面我们来看一下.txt中phase_5的汇编代码;
这个phase的汇编代码看起来还算是比较友善的,本以为到了这么靠后的phase应该会很复杂才对;
条件1
首先我们看到一个比较,是将eax中的值与6作比较,而eax中是什么呢,是一个较string_length的函数的返回值。所以由此我们可以得知我们输入的字符串应该是6位;
条件2
继续向下运行,我们发现又有一个立即数,根据之前几个phase的经验,我们用如下命令查看这个立即数对应内存中存放的值;
x/s 0x5555555571e0
接下来的操作你可能会觉得有些疑惑;
首先他从一个地方取出一个值做0扩展后赋值给寄存器edx;
那么我们看看这个地址中存放的是什么,先看看rax和rbx中存放的东西;
查看内存中地址为 0x555555559840 的值
可以看到rbx中存放的应该是我们输入字符串的第一个字符对应的地址;
所以phase_5+50和phase_5+54就是取出首个字符并将它0扩展后取末4位存放到edx中;
phase_5+57行所做的就是通过之前edx保存的数做索引,去条件2一开始所看到的那个很长的字符串中找到对应位置的字符存储到0x1(%rsp,%rax,1)位置;
让我们查看一下0x1(%rsp,%rax,1)地址中的值;
可以看到第一次循环就存进来了一个s;
如果我们循环结束,可以看到这里存储的字符串如下;
同时作为计数器的rax寄存器+1;rax最初为0,如果等于6则跳出循环;
条件3
在下面还有一个很明显的立即数,我们用如下命令查看,发现是一个6位字符串;
x/s 0x5555555571b7
这个字符串就是我们在条件2中所保存下来的字符串,不过这个是作者给的答案,条件2中保存的是根据我们输入的字符串所得出来的字符串;
因此我们得知,我们是要通过输入一个字符串,根据这个字符串作为索引去作者给定的一个很长的字符串中取出对应的字符拼成一个6位字符串,与作者事先给定的6位字符串答案比较,如果相等就通过,反之则直接爆炸;
所以这个题也是没有标准答案的,对于目标字符串:sabres:
它对应的索引位为:(7,1,13,6,5,7),注意字符串索引位是从0开始基数的,这也是这一题的一个小坑;
第二个坑在于,要从可打印字符(ascii码范围为32-126)选出ascii码对应的16进制数末四位为(7,1,13,6,5,7)的字符即可,不可以从不可打印字符中选取;所以本题另外的样例答案可以为:71m657
phase_6
首先我们还是看一下.txt文件中phase_6的汇编代码;
可以看到这个phase很长很长并且有许许多多的循环;
我们还是利用开挂版本的bomblab,看一遍函数的运行过程;
我们采用 6 1 4 2 5 3 (这就是最终答案)这个序列进行测试;
首先还是我们的老朋友read_six_numbers函数,表明这一题还是输入6个数字;
同时我们看到rsp(里面存放的是我们输入六个数的地址)的值被赋值给寄存器r13;这个不知道后面会不会有用(后面发现的确用到了hhh),先记下来;
条件1
程序运行一开始就是两个循环,不得不说这个phase确实有点难;
这看着有点太费劲了,转成c++代码看一下:
int six_numbers[6] = {/*输入的六个数序列*/};
for (int i=0;i<=5;i++)
{
if(six_numbers[i] -1 > 5)
explode_bomb();
for(int j = i+1 ; j < 5; j++)
{
if(six_numbers[i]==six_number[j])
explode_bomb();
}
}
可以看到这两个循环实现的是判断输入的序列中的每个数是否大于6,并且判断输入序列是否存在重复的数;
条件2
继续向下运行,我们看到又有一个这样的立即数;
我们先用如下命令看一下一个字节中存放的数据;
x/x 0x555555559210
可以看到gdb提示我们这个里面的值属于一个名为node1的变量;
那一个字节显然是不够看的,继续cancanneed,我们直接查看24个字节;
x/24wx 0x555555559210
可以看到这显然是一个链表,可以用如下的结构体来描述他们;
typedef struct node
{
int key;
int index;
struct node* next;
} node;
同时我们想,我们输入的是6个数字,这里只有node1-5,怎么回事?仔细观察node5的next结点地址,你会发现这个结点地址是没有在图中体现的,所以我们可以用如下命令查看;
x/4wx 0x555555559110
我下面的这个图片是在后续循环函数中调试出来的,循环函数下面会讲,你要是想在这里就查看node6的值就用上面的指令,所得出的内容应该与下图中node6相同;
所以我们现在就得到了一个如下链表;
node node1 = { 0x16d, 1, &node2 };
node node2 = { 0x220, 2, &node3 };
node node3 = { 0x3aa, 3, &node4 };
node node4 = { 0x1b3, 4, &node5 };
node node5 = { 0x341, 5, &node6 };
node node6 = { 0x079, 6, NULL };
条件3
接下来又是一个循环;
ecx中的值经调试可以得知,在第n次循环中存储的是我们输入序列中第n个值
转换成C++代码看看;
node* addresses[6] = { 0 };
for (int i = 0; i < 6; i++)
{
int index = six_numbers[i];
addresses[i] = &node1;
while (addresses[i]->index != index)
addresses[i] = addresses[i]->next;
}
这句话就是对应我们的 "addresses[i] = addresses[i]->next;";这也就再次验证了我们上面所给出的结构体的正确性,因为两个整数各占4个字节,所以每次要+8;
函数运行结束后,让我们看看存放的结果;
可以看到,各个结点的地址由我们给出的索引存放了进来;
条件4
下面就是倒腾寄存器,其实就是将最开始的链表按照我们输入的索引重新排序;
条件5
接下来就是我们最后一个函数了;
转换成C++代码看一下;
for (int i = 5; i > 0; i--)
{
if(addresses[i] >= addresses[i-1])
explode_bomb();
}
这就是说,根据我们刚刚重新排序后的链表,比较前一个结点和后一个结点的key值,如果前一个结点的key值比后一个结点的key值大,则直接爆炸;
到此我们终于明白了这个phase所实现的功能,即根据我们输入的数字序列,作为一个已知链表的索引index,将链表中每个结点的index和我们给出的序列对应,使得最终重新排序的链表根据结点的key值升序排列。
综上我们就解决了phase_1-6,至于还有一个secret_phase......呵呵,再说吧,还有操作系统 rCore-lab2 下周验收,听天由命吧;
**************************************************************************************************************
周五晚上的互联网+通识课属实有点无聊,搞搞secret_phase;
Secret_phase
答案:1
首先还是让我们看一下汇编代码;
由于是一个隐藏的关卡,所以首先我们要找到如何进入这个phase;
在bomb汇编代码文档中查找secret_phase,会发现他在一个名为phase_defused函数中调用了;
同时你还能看到,其实在main函数中,每个phase结束后都会进入到这个函数;
这就意味着,是不是说在某个phase的输入中,隐藏着进入最终隐藏关卡的匹配位置;
进入Secret_phase
我们再仔细阅读一下phase_defused函数,发现他会在前六个phase都完成的情况下才能进行隐藏关卡匹配(当然这也符合常识认知hhh);
这又有一个立即数,现在看到立即数就想看看里边是不是什么字符串或者数字,一查看果然,这就是我们phase_4输入的位置,由于之前我已经测出来了所以我故意在原本答案12 3的后面输入了dddddd,盲猜这个dddddd的位置就是进入secret_phase所要匹配的位置;
紧接着就验证了我们的猜想,他进行了一个字符串比较,比较的是一个立即数对应地址位置的字符串,我们用如下命令查看一下;
果然,里面是要匹配的字符串,这个好像就是作者的名字吧;
到这里我们就拿到了进入secret_phase的“钥匙”;
匹配完还有两个立即数,可以看到应该是作者祝贺我们找到了隐藏关卡同时又提醒我们这个phase解决与前面的phase不太一样;
递归与二叉树
上面匹配好字符串后要我们输入一个数字,我们随便输入一个1,函数将它保存在了寄存器rax中;
接下来我们开始调试secret_phase;
可以看到他先对于我们刚刚的输入-1,并且与1000比较,如果大于直接爆炸,这就说明我们输入的答案不能大于1000;
可以看到在secret_phase+42这一行出现了一个注释,是不是和phase_6很像,所以我们逐步查看这个立即数对应地址周围的东西;
这样看有点累,整理一下:
0x555555559130 <n1>: 0x00000024 0x00000000 0x55559150 0x00005555
0x555555559140 <n1+16>: 0x55559170 0x00005555 0x00000000 0x00000000
0x555555559150 <n21>: 0x00000008 0x00000000 0x555591d0 0x00005555
0x555555559160 <n21+16>: 0x55559190 0x00005555 0x00000000 0x00000000
0x555555559170 <n22>: 0x00000032 0x00000000 0x555591b0 0x00005555
0x555555559180 <n22+16>: 0x555591f0 0x00005555 0x00000000 0x00000000
0x555555559190 <n32>: 0x00000016 0x00000000 0x555590b0 0x00005555
0x5555555591a0 <n32+16>: 0x55559070 0x00005555 0x00000000 0x00000000
0x5555555591b0 <n33>: 0x0000002d 0x00000000 0x55559010 0x00005555
0x5555555591c0 <n33+16>: 0x555590d0 0x00005555 0x00000000 0x00000000
0x5555555591d0 <n31>: 0x00000006 0x00000000 0x55559030 0x00005555
0x5555555591e0 <n31+16>: 0x55559090 0x00005555 0x00000000 0x00000000
0x5555555591f0 <n34>: 0x0000006b 0x00000000 0x55559050 0x00005555
0x555555559200 <n34+16>: 0x555590f0 0x00005555 0x00000000 0x00000000
0x555555559010 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x555555559020 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x555555559020 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x555555559020 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x555555559020 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x555555559020 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
0x555555559020 <n43+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n46>: 0x0000002f 0x00000000 0x00000000 0x00000000
0x555555559020 <n46+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x555555559010 <n48>: 0x00000028 0x00000000 0x00000000 0x00000000
0x555555559020 <n48+16>: 0x00000000 0x00000000 0x00000000 0x00000000
很明显这是一棵二叉,我们将他还原成他本来的样子;
接着就是进入fun7这个函数了,这也是一个递归函数,有了phase_4的铺垫,这一题还是比较好还原的;
int fun7(node* current, int input)
{
if (current == NULL)
return -1;
int key = current->key;
if (input > key)
return 2 * fun7(current->right, input) + 1;
else if (input == key)
return 0;
else
return 2 * fun7(current->left, input);
}
那么下面我们再看看fun7最终返回之后又干了什么;
可以看到首先将我们的返回值自身与自身相与,紧接着就是一个jne。
jne会根据ZF零标志位进行跳转,如果ZF为0,则跳转到要后面的地址,否则就继续向下执行。我们看到他后面跳转的地址是引爆炸弹的函数,所以我们不能跳转,要让ZF为1。
而要让ZF为1,就要看看test的作用;test将两个操作数的值进行按位逻辑 "与" 操作,并将结果设置到标志寄存器中。如果两个操作数的 "与" 结果为 0,则设置零标志位(ZF),即ZF=1;否则,清除零标志位。
所以到这里你应该明白了,我们需要fun7最终的返回值为0,你说巧不巧,我试验的1,恰好返回值就是0;
详细的递归过程如下:
还有一个立即数,查看一下,发现是作者祝福我们解决掉秘密关卡的一段字符串;
最终,加上secret_phase,最终解决所有炸弹后的输出如下,整体也就是多了两个红框中的内容;
Bomblab(开挂版)
之前所说,本次实验我都是在我自己开挂版本的bomblab上进行的调试,至于开挂的方法我之前已经上传到优快云网站: