Bomblab是第三章 程序的机器级表示 的实验,主要涉及汇编语法和gdb调试。
这个lab要求反编译一个程序,得到六个密码。
Phase 1
使用objdump
命令来反编译bomb:
objdump -d bomb > bomb.s
在out.s里就可以看到整个程序反编译出的汇编代码了。
找到main函数的部分,然后找到有关调用phase_1部分的代码,其之前的那一部分就是读入。
复制 400e32: e8 67 06 00 00 call 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 call 400ee0 <phase_1>
可以看到这里把%rax
的内容移到%rdi
储存,然后就调用了phase_1 。于是可以推测输入的内容在%rdi
里面。
再看phase_1部分的代码。
复制 0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 call 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 call 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 ret
很明显这个程序在比较输入的字符串是否相等,调用的部分是这个strings_not_equal
。那么在调用之前,这个程序将地址$0x402400
移到了%esi
,很明显就是给比较字符串的函数用的。于是可以推测地址 $0x402400
就存了那个我们需要的固定密码。
现在用gdb调试来获取运行时那个地址储存的密码。
使用命令gdb bomb
开始调试,然后在phase_1处设置断点:break phase_1
接下来使用r
命令来运行它,然后随便输一段密码使程序到达断点位置。
到达我们需要的位置后,输入disas
来查看当前阶段的汇编代码,是phase_1无误。
下面就是查看那个地址了。使用x
命令即可:x $0x402400
获得密码:Border relations with Canada have never been better.
Phase 2
还是一样,在phase_2处设置断点,然后查看phase_2处的汇编代码。
这里调用了一个函数read_six_numbers
,查看它的代码,发现它的主要功能是利用sscanf
函数读入6个整数。为什么知道是6个呢?因为sscanf
的参数除了要读入的参数外还有两个,而read_six_numbers
中将两个参数储存到内存中来传递。我们知道只利用寄存器我们可以给函数传递6个参数,剩下两个通过内存的就是额外的参数,于是我们知道总共传递了8个参数,其中6个是输入的数。
复制 401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
sscanf
读入的整数会存在栈里,所以下面留意有关%rsp
的操作。
再阅读phase_2的代码:
复制 Dump of assembler code for function phase_2:
=> 0x0000000000400efc <+0>: push %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: call 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: call 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax
0x0000000000400f1a <+30>: add %eax,%eax
0x0000000000400f1c <+32>: cmp %eax,(%rbx)
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: call 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx
0x0000000000400f29 <+45>: cmp %rbp,%rbx
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
其实仔细看就会发现这是一个循环,循环部分是+25到+62。
还原一下,首先将栈顶(sscanf返回的值)与1比较,必须是1才能继续,说明第一个密码是1.
接下来eax来保存第一个数,然后*2,与第二个数比较,然后给rbx+0x4,使得下一次比较的是第三个数。以此类推,就是要求是公比为2的等比数列,即1,2,4,8,16,32
密码:1 2 4 8 16 32
Phase 3
一样的,反编译代码观察。
复制 => 0x0000000000400f43 <+0>: sub $0x18,%rsp
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx
0x0000000000400f51 <+14>: mov $0x4025cf,%esi
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmp *0x402470(,%rax,8)
0x0000000000400f7c <+57>: mov $0xcf,%eax
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: call 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: ret
这里还是用到了sscanf
,注意到一个特殊点,第二个参数mov $0x4025cf,%esi
,其实就是scanf那个格式化串,如"%d"这种。所以可以用命令x 0x4025cf
把它打出来看看:
0x4025cf: "%d %d"
所以我们知道密码是两个整数。下面跟着流程继续走。
观察发现,sscanf
的第三、四个参数分别是0x8(%rsp)
(即%rsp+8)和0xc(%rxp)
(即%rxp+c)。重点留意这两个地址。
由+39得,第一个参数得<=7.
注意到代码里有一部分很奇怪: 0x0000000000400f75 <+50>: jmp *0x402470(,%rax,8)
它的意思是,跳转到(0x402470 + 8 * %rax)的位置,而我们的%rax此时就是第一个输入,<=7。所以可以推测这是一个以0x402470为起始,8字节一个元素,有8个元素的数组。
用指令x/8a 0x402470
打印一下这个数组:
复制 0x402470: 0x400f7c <phase_3+57> 0x400fb9 <phase_3+118>
0x402480: 0x400f83 <phase_3+64> 0x400f8a <phase_3+71>
0x402490: 0x400f91 <phase_3+78> 0x400f98 <phase_3+85>
0x4024a0: 0x400f9f <phase_3+92> 0x400fa6 <phase_3+99>
可以发现,这是一个根据第一个输入不同而决定跳转位置不同的跳转表。因此可以猜测原来的程序是一个switch结构。
继续观察结构,发现不同的跳表位置就是给%eax这个参数赋不同值。最后的判定就是%eax最后的值和第二个参数相同。所以我们可以假设第一个参数取0,于是下一步就跳到phase_3+57,赋的值是0xcf(207)。所以我们就可以取密码为0 207
。
Phase 4
复习一下寄存器相关的内容。对于32位的寄存器,%eax:储存函数返回值;%edi:第一个参数;%esi:第二个参数;%edx:第三个参数。
还是和前面一样分析sscanf,发现依然是读入两个整数。但是后面发现他将2和%eax的值进行了比较,%eax就是上面说的返回值的寄存器,也就是说这是scanf的返回值。scanf的返回值就是成功匹配的个数,也就是读入的数的个数。于是看到下面的部分就能明白,这是要求只能读入两个整数,否则爆炸:
复制 0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
接下来程序又进行了一个判断:
复制 0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: call 0x40143a <explode_bomb>
将第一个参数与0xe比较,参数必须<=0xe(14)才行,否则会爆炸。
下面有三组mov操作,可以看出这是在传参,给后面的func4
调用做准备。
复制 0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
传参分别是,第一个参数、0、14. 接下来跟着看下func4
的代码。
出现了一个第一次见的指令,shr $0x1f,%ecx
,意思是逻辑右移。后面还有sar,意思是算数右移。
继续阅读,发现代码后面出现了这句:callq 400fce <func4>
明显是在自己调用自己,也就是递归。
既然这是个递归的函数,最好把他写成C代码,这样方便知道他在干什么。
复制 int func4 ( int nums , int x , int y){
int ret = y - x;
int k = ( unsigned )(ret) >> 31 ;
ret = (k + ret) >> 1 ;
k = ret + x;
if (k > nums) return 2 * func4(nums , x , k - 1 ) ;
ret = 0 ;
if (k < nums) return 2 * func4(nums , k + 1 , y) + 1 ;
return ret;
}
接下来看func4返回后程序干了什么。
复制 0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
这是test指令的一个典型用法,用于判断返回值%eax 是否==0. 也就是说func4的返回值必须为0.
那么回过头去看func4,代入x=0,y=14尝试一下,可以写个暴力程序验证,发现0就可以使得func4不死循环且最终返回0.
后面还发现,
复制 0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
所以密码的第二个数必须为0.
于是得到密码:0 0
Phase 5
一样,看Phase5的代码。注意在main函数这里用了readline放在%rdi里传递给phase_5,所以这次我们应该输入一个字符串。
注意到这里用了一个函数来获得字符串的长度,然后和6比较,因此得知我们必须输入6个字符。
复制 0x000000000040107a <+24>: call 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: call 0x40143a <explode_bomb>
接下来往下读,发现一个比较明显的回溯jump,并且还带计数和比较,所以可以发现这是一个for循环:
复制 0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1)
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
可以看出循环总共会进行6次。
循环里有一个神秘地址,用x/s 0x4024b0
打出来看看:
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
再回看上面的代码,就算不能完全理解意思,也可以大概猜到,我们输入的6个数是一个索引,程序在根据输入的索引在上面这个字符串里找出6个字符。
继续往下读,找到另外一个神秘地址,x/s 0x40245e
打出来看看:
0x40245e: "flyers"
所以尽管我们没有怎么研究其他的代码,我们已经知道这个程序是要我们从上面的长字符串中索引出六个字符“flyers"。那么这个索引可以是9 15 14 5 6 7
事实上,上面那些操作是取我们输入的字符的ascii码的右边四个bit,我们只用找最右边的四个bit对应上面6个数的6个字符就行了。
查表,任意找一组符合上面条件的字符。这里取:)/.%&'
Phase 6
Phase_6的代码反编译出来非常的长,足足被分了四页,说明开始有点复杂了。我们得一点点来看。
在看的时候,首先先观察各种jmp语句,把可能的程序结构给推测出来。
首先发现 0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
回跳很明显是一个循环,看看循环前面干了些什么。
首先是又调用了read_six_numbers
,看来这次输入又是6个数,每次循环处理一个数。
在理解循环的时候,更多要忽略循环本身的那些操作,更加关注那些显得比较突兀的实际操作,比如下面:
复制 0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: call 0x40143a <explode_bomb>
翻译一下就是每个输入必须<=6,否则会爆炸。
在循环中往下读,竟然发现又一个回跳:0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
怎么理解呢?循环嵌套。
那么这个内层循环干了什么?
复制 0x000000000040113b <+71>: cmp %eax,0x0(%rbp)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81>
0x0000000000401140 <+76>: call 0x40143a <explode_bomb>
其中%eax是上一个循环的操作数。嗯,这样理解就是一个遍历,如果我们输入的数有任何一个出现相等就会炸。意思就是输入的六个数要互不相同。
读完这个大循环,我们知道输入必须<=6,且互不相同。
继续阅读,下面又发现一个新循环:
复制 0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi
0x0000000000401158 <+100>: mov %r14,%rax
0x000000000040115b <+103>: mov $0x7,%ecx
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx
0x0000000000401164 <+112>: mov %edx,(%rax)
0x0000000000401166 <+114>: add $0x4,%rax
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
这个循环很好理解,使得每个输入都等于7-它自己。
继续往下看,下面的代码是最难理解的一段。注意到其中有两行可疑的片段:
复制 0x0000000000401183 <+143>: mov $0x6032d0,%edx
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx)
可以看到这里给%edx赋了一个神秘地址,然后后面又有大量对%edx相关的%rdx进行的8字节的偏移。于是合理猜测,那是个以8字节为单位的数组。我们试着8个字节的看一下这个地址的信息:
x/8 0x6032d0
,但是数据没有任何值得总结的,怀疑是因为数据没有输入和处理就被断点卡住了。取消该处断点,在炸弹爆炸处加一个断点,重新调试,这里密码尝试1 2 3 4 5 6。
结果:
复制 0x6032d0 <node1>: 0x0000014c 0x00000001 0x00000000 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032d0 0x00000000
可以看到第二行就是1 2,我输入的数字。再看名字,node1、node2,可以得出这是由结构体产生的数据。
理论上应该有6个数字,于是再扩大范围看看那些结构体:
复制 (gdb) x/24 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x00000000 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032d0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x006032e0 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x006032f0 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603300 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00603310 0x00000000
还是有点不明白这结构体有什么用,换个输入6 5 4 3 2 1
试试看:
复制 0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000
似乎有变化!数据排序依然是从node1到6,但第三个数据变了。根据数据的顺序相反,两个结构体的属性的头和尾也会相反。。可以想到,这完全就是一个链表,第三个数据就是节点的next指针!
好了,现在我们知道我们输入的六个数会被存在类似链表的结构中。下面理解(猜测)代码就会容易一些了。
继续往下阅读代码,找到又一个循环:
复制 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 call 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
前面了解过,0x8(%rbx)的地址偏移刚好可以取到下一个数的对应地址。这时候比的就是两个node的第一个数据,假设为value。如果node1.value<node2.value,就会爆炸。
所以这行代码的作用就是验证下一节点的val值是否严格小于当前节点的val值。
为了使得链表的顺序符合要求,前面的程序肯定是根据某种规则安排节点的顺序(由上面我们的测试,猜测是根据输入的数的顺序)。那么我们就必须想办法安排输入的顺序使得前面的程序完成工作后value值符合递减要求。
首先把上面node1-6的value算出来:332 168 924 691 477 443
,按递减顺序是3 4 5 6 1 2
。由于传递到这里的值前面是用7-输入的值得到的,所以原始的输入应该对7取个差: 4 3 2 1 6 5
复制 $ ./bomb ans
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Congratulations! You've defused the bomb!
顺利解决。
这个lab还是挺有意思,但难度也挺大的。有些地方真的很难完全理解,只能借助猜测和感性理解、调试测试来完成。完成后对汇编表示的理解以及gdb的使用都有很大帮助。