The Hardware/Software Interface Lab2 bomb
这个 bomb 是 Couresa 上面的一门课 The Hardware/Software Interface 中第四章的一个实验。同时也是 CSAPP 里面的一个作业。花了1天时间把这个做了。期间主要是用到了 gdb,objdump 的一些知识,当然还有一些汇编的基础知识,比如说在 64 位系统下,参数通过 rdi
, rsi
, rdx
, rcx
, r8
, r9 传递,返回值在 rax中。其他的利用 gdb 差不多就可以完成了[这里只讲前五个关卡,不包括后面的附加关卡和隐藏]。
首先我们不知道任何有关 bomb 的输入,所以直接 gdb 运行即可,随便输入看看程序需要什么[下面所有红色的斜体字表示命令]。
一: 运行 gdb bomb。然后在 gdb 的命令行里面执行 _b phase1。然后运行程序,会发现程序停在那,等你输入,这个时候随便输入一些字符即可。然后发现程序执行到了 phase_1 处,利用 gdb 的命令 disas 反汇编指令查看 phase_1 函数的汇编语句,如下所示
=> 0x0000000000400e70 <+0>: sub $0x8,%rsp
0x0000000000400e74 <+4>: mov $0x401af8,%esi
0x0000000000400e79 <+9>: callq 0x40123d <strings_not_equal>
0x0000000000400e7e <+14>: test %eax,%eax
0x0000000000400e80 <+16>: je 0x400e87 <phase_1+23>
0x0000000000400e82 <+18>: callq 0x40163d <explode_bomb>
0x0000000000400e87 <+23>: add $0x8,%rsp
0x0000000000400e8b <+27>: retq
0x0000000000400e74 <+4>: mov $0x401af8,%esi
0x0000000000400e79 <+9>: callq 0x40123d <strings_not_equal>
0x0000000000400e7e <+14>: test %eax,%eax
0x0000000000400e80 <+16>: je 0x400e87 <phase_1+23>
0x0000000000400e82 <+18>: callq 0x40163d <explode_bomb>
0x0000000000400e87 <+23>: add $0x8,%rsp
0x0000000000400e8b <+27>: retq
发现调用了一个叫做 string_notequal 的函数,用 stepi_ 执行到第三行,然后根据函数返回结果(函数返回结果在 rax 中,eax 是 rax 的低 32 位)。判断是否 explodebomb。那么我们利用 stepi_ 指令运行到 callq 0x40123d
二: 在等待输入的时候,继续随便输入一些字符(我们只是用这些字符来调试的,从而得到正确的答案)。
=> 0x0000000000400e8c <+0>: mov %rbx,-0x20(%rsp)
0x0000000000400e91 <+5>: mov %rbp,-0x18(%rsp)
0x0000000000400e96 <+10>: mov %r12,-0x10(%rsp)
0x0000000000400e9b <+15>: mov %r13,-0x8(%rsp)
0x0000000000400ea0 <+20>: sub $0x48,%rsp
0x0000000000400ea4 <+24>: mov %rsp,%rsi
0x0000000000400ea7 <+27>: callq 0x401743 <read_six_numbers>
0x0000000000400eac <+32>: mov %rsp,%rbp
0x0000000000400eaf <+35>: lea 0xc(%rsp),%r13
0x0000000000400eb4 <+40>: mov $0x0,%r12d
0x0000000000400eba <+46>: mov %rbp,%rbx
0x0000000000400ebd <+49>: mov 0xc(%rbp),%eax
0x0000000000400ec0 <+52>: cmp %eax,0x0(%rbp)
0x0000000000400ec3 <+55>: je 0x400eca <phase_2+62>
0x0000000000400ec5 <+57>: callq 0x40163d <explode_bomb>
0x0000000000400eca <+62>: add (%rbx),%r12d
0x0000000000400ecd <+65>: add $0x4,%rbp
0x0000000000400ed1 <+69>: cmp %r13,%rbp
0x0000000000400ed4 <+72>: jne 0x400eba <phase_2+46>
0x0000000000400ed6 <+74>: test %r12d,%r12d
0x0000000000400ed9 <+77>: jne 0x400ee0 <phase_2+84>
0x0000000000400edb <+79>: callq 0x40163d <explode_bomb>
0x0000000000400ee0 <+84>: mov 0x28(%rsp),%rbx
0x0000000000400ee5 <+89>: mov 0x30(%rsp),%rbp
0x0000000000400eea <+94>: mov 0x38(%rsp),%r12
0x0000000000400eef <+99>: mov 0x40(%rsp),%r13
0x0000000000400ef4 <+104>: add $0x48,%rsp
0x0000000000400ef8 <+108>: retq
在上面的额汇编代码中,我们看到首先,是会调用一个叫做 read_six_numbers 的函数,也就是说需要读入的是6个数字。然后接下来我们发现12行中把 0xc($rbp) 所对应的内存中的数据赋值给 %eax, 然后用 %eax 和 0x0($rbp) 做比较,如果不相等就爆炸,也就是说我们输入的6个数字中第1个数字和第4个数字必须相等. 我用的是数字 4. 从第11行到第19行,是一个循环,
三: 继续输入无关字符,我们停在 phase_3 处,得到如下汇编代码
=gt; 0x0000000000400ef9 <+0>: sub $0x18,%rsp
0x0000000000400efd lt;+4>: lea 0x8(%rsp),%rcx
0x0000000000400f02 lt;+9>: lea 0xc(%rsp),%rdx
0x0000000000400f07 lt;+14>: mov $0x401ebe,%esi
0x0000000000400f0c lt;+19>: mov $0x0,%eax
0x0000000000400f11 lt;+24>: callq 0x400ab0 <__isoc99_sscanf@plt>
0x0000000000400f16 lt;+29>: cmp $0x1,%eax
0x0000000000400f19 lt;+32>: jg 0x400f20 <phase_3+39>
0x0000000000400f1b lt;+34>: callq 0x40163d <explode_bomb>
0x0000000000400f20 lt;+39>: cmpl $0x7,0xc(%rsp)
0x0000000000400f25 lt;+44>: ja 0x400f63 <phase_3+106>
0x0000000000400f27 lt;+46>: mov 0xc(%rsp),%eax
0x0000000000400f2b lt;+50>: jmpq 0x401b60(,%rax,8)
0x0000000000400f32 lt;+57>: mov $0x217,%eax
0x0000000000400f37 lt;+62>: jmp 0x400f74 <phase_3+123>
0x0000000000400f39 lt;+64>: mov $0xd6,%eax
0x0000000000400f3e lt;+69>: jmp 0x400f74 <phase_3+123>
0x0000000000400f40 lt;+71>: mov $0x153,%eax
0x0000000000400f45 lt;+76>: jmp 0x400f74 <phase_3+123>
0x0000000000400f47 lt;+78>: mov $0x77,%eax
0x0000000000400f4c lt;+83>: jmp 0x400f74 <phase_3+123>
0x0000000000400f4e lt;+85>: mov $0x160,%eax
—Type lt;return> to continue, or q <return> to quit—
0x0000000000400f53 lt;+90>: jmp 0x400f74 <phase_3+123>
0x0000000000400f55 lt;+92>: mov $0x397,%eax
0x0000000000400f5a lt;+97>: jmp 0x400f74 <phase_3+123>
0x0000000000400f5c lt;+99>: mov $0x19c,%eax
0x0000000000400f61 lt;+104>: jmp 0x400f74 <phase_3+123>
0x0000000000400f63 lt;+106>: callq 0x40163d <explode_bomb>
0x0000000000400f68 lt;+111>: mov $0x0,%eax
0x0000000000400f6d lt;+116>: jmp 0x400f74 <phase_3+123>
0x0000000000400f6f lt;+118>: mov $0x39e,%eax
0x0000000000400f74 lt;+123>: cmp 0x8(%rsp),%eax
0x0000000000400f78 lt;+127>: je 0x400f7f <phase_3+134>
0x0000000000400f7a lt;+129>: callq 0x40163d <explode_bomb>
0x0000000000400f7f lt;+134>: add $0x18,%rsp
0x0000000000400f83 lt;+138>: retq
我们看到第6行调用 sscanf,然后第7行对 sscanf 的返回结果做判断,也就是说我们必须输入至少两个数字(或字符串),否则就爆炸了。然后跳到第10行,用我们输入的的第一个数字和7比较,不能大于7,否则就爆炸了。接下来需要知道13行中的代码表示是一个 switch 语句。其中 0x401b60 表示 jump table 的地址,后面的 rax 表示第几个,8表示数据类型。由于我一开始输入的数字是 2,然后跳转到相应的位置(我们可以用 print *0x401b60 来查看 jump table 的起始位置,其中 gdb 的 print 命令用来输出值, x 命令用来显示相应位置的的内存内容,通俗的说 print 可以看成一个值,x 看成一个指针。)跳到第16行。然后把 $eax 和 第二个输入的数值做对比($eax 是在前面第 16 行进行的赋值,0xd6),所以我们的第二个参数设置位 0xd6(214) 就行了.然后到了第四关
四:来到第四关,我们得到如下汇编代码
=> 0x0000000000400fc1 <+0>: sub $0x18,%rsp
0x0000000000400fc5 <+4>: lea 0xc(%rsp),%rdx
0x0000000000400fca <+9>: mov $0x401ec1,%esi
0x0000000000400fcf <+14>: mov $0x0,%eax
0x0000000000400fd4 <+19>: callq 0x400ab0 <__isoc99_sscanf@plt>
0x0000000000400fd9 <+24>: cmp $0x1,%eax
0x0000000000400fdc <+27>: jne 0x400fe5 <phase_4+36>
0x0000000000400fde <+29>: cmpl $0x0,0xc(%rsp)
0x0000000000400fe3 <+34>: jg 0x400fea <phase_4+41>
0x0000000000400fe5 <+36>: callq 0x40163d <explode_bomb>
0x0000000000400fea <+41>: mov 0xc(%rsp),%edi
0x0000000000400fee <+45>: callq 0x400f84 <func4>
0x0000000000400ff3 <+50>: cmp $0x37,%eax
0x0000000000400ff6 <+53>: je 0x400ffd <phase_4+60>
0x0000000000400ff8 <+55>: callq 0x40163d <explode_bomb>
0x0000000000400ffd <+60>: add $0x18,%rsp
0x0000000000401001 <+64>: retq
首先看到 sscanf 函数,然后判断 eax 是否等于1,也就说说这里有且只有一个输入,然后在第8行把这个参数和0比较,必须大于0,否则爆炸。然后把这个输入作为参数调用 func4 。下面得到的是 func4 的汇编代码
=> 0x0000000000400f84 <+0>: mov %rbx,-0x10(%rsp)
0x0000000000400f89 <+5>: mov %rbp,-0x8(%rsp)
0x0000000000400f8e <+10>: sub $0x18,%rsp
0x0000000000400f92 <+14>: mov %edi,%ebx
0x0000000000400f94 <+16>: mov $0x1,%eax
0x0000000000400f99 <+21>: cmp $0x1,%edi
0x0000000000400f9c <+24>: jle 0x400fb2 <func4+46>
0x0000000000400f9e <+26>: lea -0x1(%rbx),%edi
0x0000000000400fa1 <+29>: callq 0x400f84 <func4>
0x0000000000400fa6 <+34>: mov %eax,%ebp
0x0000000000400fa8 <+36>: lea -0x2(%rbx),%edi
0x0000000000400fab <+39>: callq 0x400f84 <func4>
0x0000000000400fb0 <+44>: add %ebp,%eax
0x0000000000400fb2 <+46>: mov 0x8(%rsp),%rbx
0x0000000000400fb7 <+51>: mov 0x10(%rsp),%rbp
0x0000000000400fbc <+56>: add $0x18,%rsp
0x0000000000400fc0 <+60>: retq
End of assembler dump.
这份代码一开始的时候还是有点绕的,这个函数是一个递归函数。带回我们就可以看到这个函数的原函数了。
首先我们看到,如果这个函数的参数小于等于1的话,那么直接返回(第7,8行的比较和跳转),设置的返回值是1(第6行,记着我们的返回值存在 $rax 中,$eax 是 $rax 的低位)。如果大于1的话,那么就调用两次改函数(调用自己),第一次的参数是 $rdi-1(这里的 $rdi 是函数传入的参数), 第二次的参数是 $rdi-2,其中第一个在第9行设置成 $rdi-1, 第二个函数在第12行,这里的 $rbx 是保存的 $rdi,然后把两个函数的结果相加得到改函数的返回结果,也就是变成了如下的原函数
int func4(int x)
{
if(x<=1)
return 1;
return func4(x-1)+ func4(x-2);
}
接下来我们用这个原函数来计算相应的值,我们需要得到的结果等于 0x37.这个是在第四关的第13行。得到的是 9.到此我们第四关完成了,接下来是第无关
五:第无关来了,得到如下的汇编代码
=> 0x0000000000401002 <+0>: sub $0x18,%rsp
0x0000000000401006 <+4>: lea 0x8(%rsp),%rcx
0x000000000040100b <+9>: lea 0xc(%rsp),%rdx
0x0000000000401010 <+14>: mov $0x401ebe,%esi
0x0000000000401015 <+19>: mov $0x0,%eax
0x000000000040101a <+24>: callq 0x400ab0 <__isoc99_sscanf@plt>
0x000000000040101f <+29>: cmp $0x1,%eax
0x0000000000401022 <+32>: jg 0x401029 <phase_5+39>
0x0000000000401024 <+34>: callq 0x40163d <explode_bomb>
0x0000000000401029 <+39>: mov 0xc(%rsp),%eax
0x000000000040102d <+43>: and $0xf,%eax
0x0000000000401030 <+46>: mov %eax,0xc(%rsp)
0x0000000000401034 <+50>: cmp $0xf,%eax
0x0000000000401037 <+53>: je 0x401065 <phase_5+99>
0x0000000000401039 <+55>: mov $0x0,%ecx
0x000000000040103e <+60>: mov $0x0,%edx
0x0000000000401043 <+65>: add $0x1,%edx
0x0000000000401046 <+68>: cltq
0x0000000000401048 <+70>: mov 0x401ba0(,%rax,4),%eax
0x000000000040104f <+77>: add %eax,%ecx
0x0000000000401051 <+79>: cmp $0xf,%eax
0x0000000000401054 <+82>: jne 0x401043 <phase_5+65>
0x0000000000401056 <+84>: mov %eax,0xc(%rsp)
0x000000000040105a <+88>: cmp $0xc,%edx
0x000000000040105d <+91>: jne 0x401065 <phase_5+99>
0x000000000040105f <+93>: cmp 0x8(%rsp),%ecx
0x0000000000401063 <+97>: je 0x40106a <phase_5+104>
0x0000000000401065 <+99>: callq 0x40163d <explode_bomb>
0x000000000040106a <+104>: add $0x18,%rsp
0x000000000040106e <+108>: retq
End of assembler dump.
同样我们看到 sscanf,然后判断返回值,必须大于1个参数,然后把输入的第一个参数与上 0xf。也就是把这个参数调整到 [1,15] 这个范围内,接下来17-22行一个循环,我们可以还原成一个函数,如下
int a[] = {a, 2, e, 7, 8, c, f, b, 0, 4, 1, d, 3, 9, 6, 5};//16进制
ecx = 0
edx = 1;
eax = a[eax];
ecx += eax;
while(eax != f)
{
++edx;
eax = a[eax];
ecx += eax;
}
然后把 edx 和7比较,也就是说 我们必须让 edx =7.然后把 ecx 和设置的值做比较(也就是说我们输入的第二个参数),我们可以用反推出来的函数计算结果。最后就行了。最后就完全完成了。至此无关完全完成。 Oh,yeah!