反汇编调用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),%rax,mov %rax,(%rsp)和lea 0x14(%rsi),%rax,mov %rax,0x8(%rsp)分别将&a[4]和&a[5]压栈,这就完成了sscanf所有参数的传递任务。
接下来判断sscanf函数的返回值,如果返回值大于5则OK,如果小于等于5则触发炸弹。也就是说我们需要输入大于等6个的数字,只要前6个数字正确即可。
OK,read_six_numbers函数分析完毕,返回函数phase_2继续分析。
现在我们可以确定数组a在栈空间中的布局了,如下:
- 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();
- }
- }
- }