CS:APP二进制炸弹phase2

来源:互联网 发布:linux 定时重启tomcat 编辑:程序博客网 时间:2024/05/17 07:07

写在前面

在前文《CS:APP二进制炸弹phase1》中成功“破解”了phase_1,毕竟是第一个阶段,非常简单。本篇来破解第二阶段。let's go!!!


分析

反汇编调用phase_2处的代码如下:


同样的,跟phase_1一样,我们输入的字符串首地址存储在寄存器%rdi中。

反汇编phase_2:


一眼望去,phase_2明显要比phase_1要复杂一些。不过没关系,应该只是复杂一点。

先看前五行代码。先分别将寄存去%rbp%rbx压栈,之所以将这两个寄存器压栈,可以通过后面的汇编语句得知在函数phase_2中用到了它们。

sub    $0x28,%rsp 这条汇编语句是在开辟栈空间。

mov    %rsp,%rsi 将栈首地址传送到寄存器%rsi中。根据x64 ABI文档,%rsi通常用来传递函数的第二个参数。

接下来通过callq调用函数read_six_numbers,从函数名就可以知道是从我们的输入中获取6个数字。那么就可以大胆的猜测,刚才开辟栈空间的操作是在栈上分配了一个具有6个元素的数组,至于数组元素是什么类型,目前不得而知。并且可以判断函数read_six_numbers具有两个参数,第一个参数使我们的输入input,第二个参数就是数组第一个元素的首地址,即函数read_six_numbers的原型为void read_six_numbers(const char *input, TYPE *p)。我们不妨将这个数组取名为a,TYPE a[6],即在函数phase_2有个局部数组a,数组元素总共6个,类型待定。

进入函数read_six_numbers,该函数反汇编如下:


原来函数read_six_numbers是通过函数sscanf从我们的输入中获得6个数字的。这就好办了,根据函数sscanf的原型,我们来仔细看看调用函数sscanf之前,它需要的参数是如何传递进去的。

为了方便,还是从X64 ABI文档中截个图吧,描述函数参数传递时的约定,即calling convention!


第一个参数肯定是我们的输入input的首地址,它目前储存在寄存器%rdi中,那么第二个参数应该是sscanf需要的格式化字符串,从mov    $0x4025c3,%esi中知道这个格式化字符串存储在地址0x4025c3处,用x/s命令查看,原来是"%d %d %d %d %d %d",这下我们可以断定,在函数phase_2中的数组元素类型是int型,即int a[6]。

既然格式化字符串中有个6个%d,自然的sscanf函数还需要6个参数,应该分别是数组a每个元素的地址,即分别是&a[0],&a[1],&a[2],&a[3],&a[4],&a[5]。根据调用约定,前4个元素的地址应该用寄存器传递,分别是%rdx、%rcx、%r8、%r9,最后两个通过栈来传递,通过lea    0x10(%rsi),%raxmov    %rax,(%rsp)lea    0x14(%rsi),%raxmov    %rax,0x8(%rsp)分别将&a[4]和&a[5]压栈,这就完成了sscanf所有参数的传递任务。

接下来判断sscanf函数的返回值,如果返回值大于5则OK,如果小于等于5则触发炸弹。也就是说我们需要输入大于等6个的数字,只要前6个数字正确即可。

OK,read_six_numbers函数分析完毕,返回函数phase_2继续分析。

现在我们可以确定数组a在栈空间中的布局了,如下:

cmpl   $0x1,(%rsp),用a[0]和数字1比较
je     0x400f30 <phase_2+52> 相等,跳转到0x400f30处继续执行,否则调用explode_bomb触发炸弹。这里很显然得出a[0]的值必须为1。
从地址0x0000000000400f30处开始的两条汇编指令lea    0x4(%rsp),%rbxlea    0x18(%rsp),%rbp用到了函数一开始就压入栈的两个寄存器,使之分别指向&a[1]和&a[6]。读者可能会问,数组a只有6个元素啊,那么最后一个元素不是应该是a[5]吗? 注意,这里取的是a[6]的地址,这在标准C中是允许的,只要不取这个地址处的值都是OK的,而标准允许这样做是有原因的,这样就可以很方便在循环中来遍历数组了。
jmp    0x400f17 <phase_2+27>,jmp到地址0x0000000000400f17处继续执行。
如果仔细观察地址0x0000000000400f2c处也有个jmp到地址0x0000000000400f17处的指令,我们可以判定这里应该是个循环语句,那么循环体的内容可以根据地址0x0000000000400f17 ~ 地址0x0000000000400f29处的指令来获知。
假设第一次进入循环体,现在寄存器%rbx指向&a[1],那么mov    -0x4(%rbx),%eax就是讲a[0]的值存储寄存器%eax,add    %eax,%eax将寄存器%eax的值乘以2,cmp    %eax,(%rbx)就是将现在%eax的值与a[1]比较,也就是a[0]乘以2后的值与a[1]比较。如果不相等, 则触发炸弹,相等则%rbx指向下一个元素的地址,在这里就是&a[3]。好,原来数组a的第一个元素的值a[0]为1,而a[1]的值是根据a[0]的值算来的,公式为a[1] = a[0] x 2。更普通的为a[n] = a[n -1] x 2(n >= 1)。
根据以上推演出的公式可以很容易的得出数组a后续元素的值分别为2、4、8、16、32。
验证一下,果然正确。
C源码如下:
static void read_six_numbers(const char *input, int *a){    //                  %rdi         %rsi           %rdx    %rcx   %r8   %r9    (%rsp)  *(%rsp)    int result = sscanf(input, "%d %d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]);    if (result <= 5) {        explode_bomb();    }}void phase_2(const char *input){    int a[6];    read_six_numbers(input, a);    int *begin = &a[1];    int *end = &a[6];    for ( ; begin < end; ++begin) {        int prev_value = begin[-1] * 2;        if (prev_value != begin[0]) {            explode_bomb();        }    }}


总结:
phase_2比phase_1来说要复杂一些,不过仔细分析一下,也不难,不是吗?继续phase_3吧!
原创粉丝点击