深入理解计算机系统bomblab:二进制炸弹

Phase_1:

phase_1函数在分配栈空间后首先向%rsi中加载了一个地址,即(%rip)+0x17f1=0x125f+0x17f1=0x2a50,然后调用了<strings_not_equal>函数。如果返回值%rax=0就释放掉分配的栈空间然后结束,否则跳转到<explode_bomb>函数即引爆炸弹。

<strings_not_equal>函数的作用是判断两个字符串是否相同,如果相同则返回0,否则返回1。它需要两个字符串作为参数,而%rsi是存放第二个参数的寄存器,因此猜测地址0x2a50中存放着一个字符串,使用gdb指令查看地址内容如下:

由此推断存放第一个参数的寄存器%rdi中存放着输入字符串的地址,通过%rdi和%rsi传递地址参数给<strings_not_equal>函数。在phase_1设置断点后运行程序,输入“hello”后单步运行至调用<strings_not_equal>函数前,查看寄存器内容以及(%rdi)内容如下:

猜想得到验证。注意此时%rip的值为0x56287554a25f而不是汇编代码中的0x125f,但是后三位保持一致。同样,%rsi的值也不是0x2a50而是0x56287554ba50。此外,最后一行输出表明输入字符串是通过<input_strings>函数完成的,调用结束后输入字符串的地址存在%rdi中。

%rax是存放返回值的寄存器,只有%rax=0即<strings_not_equal>函数返回0才不会引爆炸弹,这就要求(%rdi)和(%rsi)两个字符串相同,也就是要输入前面0x2a50地址存放的字符串。运行程序验证答案,结果如下:

第一个炸弹解除,答案正确。

 

Phase_2:

phase_2函数先调用<read_six_numbers>函数读取六个数字,然后判断栈顶元素的值(%rsp),如果(%rsp)<0则跳转引爆炸弹。然后利用%rbx计数循环五次,每次循环从栈中取数,判断当前数字与%rbx的和是否等于下一个数字,如果不相等就引爆炸弹,五次循环完成后结束。

通过分析汇编指令大致猜测要输入六个数字,且它们之间需要满足一定关系。与phase_1思路相同,在phase_2设置断点后运行输入“1 2 3 4 5 6”,然后单步运行至调用<read_six_numbers>函数前,查看寄存器内容及(%rdi)内容如下:

与phase_1一样,输入是通过<input_strings>函数完成的,调用结束后输入字符串的地址存在%rdi中,以便之后调用函数传递参数。由于要判断(%rsp)的值,继续单步运行调用<read_six_numbers>函数,然后查看寄存器%rsp所存内容如下:

发现(%rsp)恰好就是输入的第一个数字,由此猜测输入的六个数字地址依次存在栈中,以四个字节为一个单元,查看栈顶内容开始六个单元的内容如下:

猜想得到验证,由此可以判断之后循环从栈中取的数就是输入的六个数字,其中第一个数必须大于等于0,之后的数字与前一个数满足前述条件即x(i+1)=x(i)+i。设输入的六个数字分别为x1、x2、……、x6,则它们之间应该满足:

x1≥0

x2=x1+1

x3=x2+2

x4=x3+3

x5=x4+4

x6=x5+5

因此本题应该存在多解,运行程序验证答案,结果如下:

第二个炸弹解除,两个答案都正确,其他合理答案都可。

但在此处存在一个疑问,输入是通过<input_strings>函数完成的,因此可以输入任意字符串而不一定是数字格式。读取数字的工作是<read_six_numbers>函数完成的,查看该函数代码发现了注释中的地址,查看地址内容如下:

通过<read_six_numbers>函数代码可知,上述内容限制了输入字符串的格式,如果输入不满足相应格式,在<read_six_numbers>函数内部就会跳转引爆炸弹。

 

Phase_3:

phase_3函数向%rsi中加载了一个地址后调用了<__isoc99_sscanf@plt>函数,返回值必须大于1,否则跳转引爆炸弹。然后判断栈顶元素的值(%rsp),如果(%rsp)>7也跳转引爆炸弹。之后将(%rsp)赋给%rax,根据%rax的值来索引内存地址进行不同的跳转,再判断是否(%rsp)小于等于5且计算结果与(%rsp+4)相等,两者同时满足则结束,否则跳转引爆炸弹。

有了上述经验,先查看注释中地址内容如下:

猜测要输入两个数字,中间以一个空格隔开。由此推测%rsi中的地址和%rdi中输入字符串的地址作为参数传递给<__isoc99_sscanf@plt>函数,该函数与phase_2中的<read_six_numbers>函数一样判断输入格式是否正确,并且将输入的数字地址放入栈顶。在phase_3设置断点后运行输入“3 4”,然后单步运行至调用<__isoc99_sscanf@plt>函数后,查看寄存器内容及(%rsp)内容如下:

猜想得到验证,那么接下来的限制条件就变成了输入的第一个数要小于等于7即x1<=7,之后要根据x1的值来进行跳转(实际上后面又限制了x1<=5)。测试发现当x1取负数时也会认为不满足x1<=7,因此推测在前面调用函数时是将输入数字以无符号数的形式存在栈顶地址中。取四字节进行有符号扩展的地址范围是0x2ac0到0x2ac0+4*7=0x2adc之间,查看这八个四字节单元的内容如下:

例如x1=3时,对0xffffe8cf进行有符号扩展得到0xffffffffffffe8cf,再加上%rdx之后%rax=0xffffffffffffe8cf+0x559fba0b1ac0=0x559fba0b038f。注意此时的%rdx一定要用运行之后的值来计算,而不是用运行前的0x2ac0,如下图所示:

跳转的地址由寄存器%rax给出,在实例中为0x559fba0b038f即汇编代码中的0x138f,将%rax清零后继续跳转至0x1349进行计算得到-0x15c,这个结果就是应该输入的第二个数-348。第一个数字不同,跳转的内容不同,第二个数也会不同,因此本题也存在多解,运行程序验证答案,结果如下:

第三个炸弹解除,两个答案都正确,其他合理答案都可。

 

Phase_4:

phase_4的开头与phase_3完全相同,输入两个数字然后调用<__isoc99_sscanf@plt>函数将其放入栈顶。其中第一个数必须小于等于14即x1<=14,然后用寄存器将x1、0、14三个参数传入<func4>函数,要求返回值和x2必须都为7才能结束,否则引爆炸弹。因此重点是分析<func4>函数,其汇编代码如下:

需要注意的是两次移位,%ebx只有四字节即32位,因此逻辑右移31位的结果就是将符号位扩展,只有两种结果:负数得-1,非负数得0。而第二次移位是算术右移,它会将负数变成一个很大的正数。

后面根据最终计算出的%rbx与x1的关系进行比较,当二者相等则返回x1,否则分别跳转进行递归运算。由于第一个参数%rdi始终未改变,所以每次递归计算出的%rbx都是与x1作比较。有趣的是第一次调用<func4>函数中的%rbx值就是需要返回的7,也就是说如果x1=7则不需要进行递归就可以直接返回7,到此时答案已经揭晓,即两个数都为7。

尝试发现本题是否还有其他解,也就是要进行递归运算。注意递归情况下的返回值要加上每次的%rbx值,而第一个%rbx已经为7,也就是要求后面递归的返回值为0。因为每次的%rbx可正可负,所以不排除递归后正负抵消恰好返回0的可能性,但这种可能性下的x1极大概率不属于要求的[0,14]区间内。将0到14逐个代入x1进行递归运算(或者直接运行程序验证),发现的确没有其他值能返回7,因此本题只有唯一解。运行程序验证答案,结果如下:

第四个炸弹解除,答案正确。

 

Phase_5:

phase_5函数在输入之后调用<string_length>函数判断输入字符串长度是否为6,如果不是则跳转引爆炸弹。然后将%rax和%rdi分别初始化为字符串的首地址和结束地址,控制进行六次循环。每次循环取一个字节,然后根据该字节低四位的值进行索引,将六次索引到的值求和。如果最后的和等于48则结束,否则引爆炸弹。

由于一个四位数的值是0到15,因此索引地址在0x2ae0到0x2ae0+4*15=0x2b1c之间,查看这些地址的值如下:

索引值在1到16之间,可以对它们进行任意组合,只要保证六次索引值之和为48即可。例如最简单的组合是每次索引都是8,也就是低四位为13即1101,查ASCII码表可知低四位为1101的字符有‘M’、‘m’、‘=’等等,因此这些字符任意排列组合都是答案。数字ASCII码的低四位就是数字本身,因此不存在低四位为1101的数字,但是可以寻找其他和等于48的组合,例如(12+4)×3,相应答案就是三个4和三个8的任意排列组合。所以本题也存在多解,运行程序验证答案,结果如下:

第五个炸弹解除,三个答案都正确,其他合理答案都可。

 

Phase_6:

phase_6函数调用<read_six_numbers>函数,因此输入格式与phase_2相同,即输入六个数字,之后分成两个主要部分。

第一部分是双重嵌套循环,外循环利用%r13计数循环六次,每次外循环%rbp依次存一个数的地址,该数必须小于等于6且不能为0(因为判据是减一后小于等于5,这里输入同样以无符号数的形式保存),否则跳转引爆炸弹。在内循环中%rax依次存后面每个数,这些数都不能等于(%rbp)即本轮外循环所取的数,因此每轮外循环的内循环次数递减。用C语言简单描述如下:

因此该部分主要表明这六个数在[1,6]之间取值且两两互不相等。

第二部分是链表的排序,依旧是通过双重循环控制。利用%rsi计数外循环六次,每次取一个输入数值,并以该数值控制内循环次数更新%rdx的内容,然后依次放入%rsp+0x20、%rsp+0x28、……、%rsp+0x48中。因为输入的六个数字互不相等,所以每次放入栈的%rdx内容也不同。查看%rdx每次更新内容如下:

当输入数字为1时,%rdx内容保持初始值0x204230;当输入数字为2时,%rdx内容更新一次,即0x204240;……%rdx最多更新五次,即输入数字为6时%rdx=0x204110。这样栈中六个位置与上图中六个值一一对应,实际上是根据输入数字来改变链表顺序。最后从%rsp+0x20开始依次取内容,然后索引它们的值,必须保证前一个值小于等于后一个值,否则就会引爆炸弹。所以实际上是要改变顺序后的链表按照升序排序,查看链表中存储的各值如下:

要按照升序排序,从%rsp+0x20开始各单元存放的应该分别是0x204260、0x204240、0x204110、0x204270、0x204230、0x204250,它们相对于%rdx的初始值0x204230分别更新了3、1、5、4、0、2次,所以输入的数字应该分别为4、2、6、5、1、3。运行程序验证答案,结果如下:

第六个炸弹解除,答案正确。

 

Secret_phase:

secret_phase函数的结构比较清晰,要输入一个在[1,1017]之间的数字,然后将输入和地址0x204150作为参数调用<fun7>函数,返回值必须为5否则引爆炸弹。重点分析<fun7>函数,其汇编代码如下:

<fun7>函数比较输入的数字x和传入地址所存的数字,如果二者相等则返回0,否则跳转进行不同的递归运算,其结构是一个二叉树。用C语言简单描述如下:

最深层递归的返回值一定为0,所以可以逆向推出返回5的过程:0—>1—>2—>5,根据这个过程跟踪%rdi的变化如下:

第一次调用:x>(0x204150)=0x24,%rdi=(0x204150+16)=0x204190

第二次调用:x<(0x204190)=0x32,%rdi=(0x204190+8)=0x2041d0

第三次调用:x>(0x2041d0)=0x2d,%rdi=(0x2041d0+16)=0x2040d0

第四次调用:x=(0x2040d0)=0x2f,返回0

所以要输入的数字为47(即0x2f)。查看<phase_defused>函数,拆除前六个炸弹后在phase_4多输入一个字符串会触发隐藏关卡,查看注释地址寻找密钥如下:

输入前六关答案和密钥字符串进入隐藏关卡,输入答案验证结果如下:

隐藏炸弹拆除,答案正确。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值