苦逼的暑假开始了。
在 lcb 大神 的推荐下,毫不犹豫地在六月份就选了 Coursera 上的 CSAPP(Computer Systems: A Programmer’s Perspective)。一方面正好自己操作系统也学得很烂,所以趁这个暑假充实一下自己的暑假生活;另一方面,这 CSAPP 的课机会难得,不容错过啊。
这篇文章的话,主要还是讲一下 Lab 2 的拆炸弹作业。俗话说:
DDL 是第一生产力!
赶在 DDL 前两天,晚上花了 5 个小时终于做完了!
本文分上下两篇,主要介绍一下 Assembly、gdb 的使用,还有拆炸弹的解题过程/w\
Phase_1
反编译 phase_1 的代码 disassemble 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 |
除去 1,7,8 行,关注剩下的 2 到 6 行。
程序调用了 strings_not_equal()
函数,比较输入字符串与 0x401af8
指向的字符串是否相等。使用 x /sb 0x401af8
查看 0x401af8
指向的字符串,就得到了第一个答案:
Science isn’t about why, it’s about why not?
phase_1 结束!
Phase_2
进入 phase_2,disassemble phase_2
得到:
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
函数,disassemble read_six_numbers
:
0x0000000000401743 <+0>: sub $0x18,%rsp | |
0x0000000000401747 <+4>: mov %rsi,%rdx | |
0x000000000040174a <+7>: lea 0x4(%rsi),%rcx | |
0x000000000040174e <+11>: lea 0x14(%rsi),%rax | |
0x0000000000401752 <+15>: mov %rax,0x8(%rsp) | |
0x0000000000401757 <+20>: lea 0x10(%rsi),%rax | |
0x000000000040175b <+24>: mov %rax,(%rsp) | |
0x000000000040175f <+28>: lea 0xc(%rsi),%r9 | |
0x0000000000401763 <+32>: lea 0x8(%rsi),%r8 | |
0x0000000000401767 <+36>: mov $0x401eb2,%esi | |
0x000000000040176c <+41>: mov $0x0,%eax | |
0x0000000000401771 <+46>: callq 0x400ab0 <__isoc99_sscanf@plt> | |
0x0000000000401776 <+51>: cmp $0x5,%eax | |
0x0000000000401779 <+54>: jg 0x401780 <read_six_numbers+61> | |
0x000000000040177b <+56>: callq 0x40163d <explode_bomb> | |
0x0000000000401780 <+61>: add $0x18,%rsp | |
0x0000000000401784 <+65>: retq |
read_six_numbers
调用了 sscanf
,格式字符串由地址为 0x401eb2
中的格式解析。查看 0x401eb2
地址中的格式字符串,使用 x /sb 0x401eb2
:
0x401eb2: “%d %d %d %d %d %d”
回到 phase_2 中,我们要知道读完六个数之后做了什么,继续看第 8 行开始的代码。第 8 行让 rbp = rsp
并且注意到第⑨行中 r13
寄存器保存了 rsp + 12
(即 rbp + 12
)的地址,以及第 12、13 行的 eax
取出了 rbp + 12
的数并且用 eax
和 rbp
两个寄存器之间的书比较是否相等。之后的第 17 行,rbp = rbp + 4
,让指针往后走一个 int
的大小。看到这里也就知道了 phase_2 的含义:
输入数组
a[6]
后,比较是否是一个长度为 3 的循环数组。即是否满足a[0] = a[3]
,a[1] = a[4]
和a[2] = a[5]
。
输入符合条件的六个数即可~难度也不是很大(「・ω・)「
Phase_3
开始进入比较有挑战性的 phase_3,同样使用 disassenmble phase_3
:
0x0000000000400ef9 <+0>: sub $0x18,%rsp | |
0x0000000000400efd <+4>: lea 0x8(%rsp),%rcx | |
0x0000000000400f02 <+9>: lea 0xc(%rsp),%rdx | |
0x0000000000400f07 <+14>: mov $0x401ebe,%esi | |
0x0000000000400f0c <+19>: mov $0x0,%eax | |
0x0000000000400f11 <+24>: callq 0x400ab0 <__isoc99_sscanf@plt> | |
0x0000000000400f16 <+29>: cmp $0x1,%eax | |
0x0000000000400f19 <+32>: jg 0x400f20 <phase_3+39> | |
0x0000000000400f1b <+34>: callq 0x40163d <explode_bomb> | |
0x0000000000400f20 <+39>: cmpl $0x7,0xc(%rsp) | |
0x0000000000400f25 <+44>: ja 0x400f63 <phase_3+106> | |
0x0000000000400f27 <+46>: mov 0xc(%rsp),%eax | |
0x0000000000400f2b <+50>: jmpq *0x401b60(,%rax,8) | |
0x0000000000400f32 <+57>: mov $0x217,%eax | |
0x0000000000400f37 <+62>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f39 <+64>: mov $0xd6,%eax | |
0x0000000000400f3e <+69>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f40 <+71>: mov $0x153,%eax | |
0x0000000000400f45 <+76>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f47 <+78>: mov $0x77,%eax | |
0x0000000000400f4c <+83>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f4e <+85>: mov $0x160,%eax | |
0x0000000000400f53 <+90>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f55 <+92>: mov $0x397,%eax | |
0x0000000000400f5a <+97>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f5c <+99>: mov $0x19c,%eax | |
0x0000000000400f61 <+104>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f63 <+106>: callq 0x40163d <explode_bomb> | |
0x0000000000400f68 <+111>: mov $0x0,%eax | |
0x0000000000400f6d <+116>: jmp 0x400f74 <phase_3+123> | |
0x0000000000400f6f <+118>: mov $0x39e,%eax | |
0x0000000000400f74 <+123>: cmp 0x8(%rsp),%eax | |
0x0000000000400f78 <+127>: je 0x400f7f <phase_3+134> | |
0x0000000000400f7a <+129>: callq 0x40163d <explode_bomb> | |
0x0000000000400f7f <+134>: add $0x18,%rsp | |
0x0000000000400f83 <+138>: retq |
比之前更长了,不是么?
观察一下函数的特征,尤其是 14 - 27行,Switch/Case
的跳转表,非常的明显!
查看 sscanf
的格式字符串,x /sb 0x401ebe
:
0x401ebe: “%d %d”
输入两个数,第一个数用于 Switch/Case
分支判断,第二个数字则用于和 eax
的比较。注意到 11 行的 cmpl
(每次都是你!)判断了 rsp + 12
中的数是否大于 7,也就是输入的第一个数是否大于 7(default
分支):如果大于就引爆了炸弹,否则就进入不同的 case
。通过计算不同的组合我们可以很轻松的得到这道题的不同的 7 个解(一行一个解):
0 535
1 926
2 214
3 339
4 119
5 352
6 919
7 412
当屏幕显示 Phase 3 cleared!
的时候,我们已经解决了一半的问题了!
Phase_4
Move on, 进入到 phase_4,disassemble phase_4
:
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 |
第 3 行查看 sscanf
的格式字符串,x /sb 0x401ec1
:
0x401ec1: “%d”
即输入一个数。将这个输入的数放入 edi
中调用了函数 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 |
7 - 13 行的主要说明了递归函数目的 f(x) = f(x-1) + f(x-2)
,边际条件在第 5 和 6 行 f(1) = 1
(Fibbonacci 数列)。
回到原函数,14 行的 cmp
使用了返回值 eax
和 0x37 = 55
比较,题目意图也很明显了:n
等于几时,有 f(n) = 55
。答案就是:
⑨(这么写当然是错的)
9
Phase_5
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 |
同样地先观察格式字符串,x /sb 0x401ebe
:
0x401ebe: “%d %d”
格式输入正确后跳转到第 10 行执行函数,这里一行一行解释。
第 10 行,eax
存入地址为 rsp + 12
中的数,也就是第二个参数。11 - 12 行用这个数和 0xf
做了与操作,取出了最后两位并重新保存到 rsp + 12
中。13 行判断了这个数是不是 0xf
,若是就引爆了炸弹,否则接下来进入循环。15 - 16 行的两个计数器 ecx
和 edx
清零。
17 到 22 行由 jne
判断出这是一个循环。17 行的作用让 edx = edx + 1
,马上 18 行 cltq
对 eax
进行符号扩展,在 19 行加载 rax * 4 + 0x401ba0
这个地址中的数到 eax
中。20 行 ecx
作为累加器加上 eax
中的数。21 行依旧判断 eax
这个数是不是 0xf
,不是则进行循环。
比较难理解的是 19 行 eax = *(rax * 4 + 0x401ba0)
即取出了起始地址为 0x401ba0
的数组中序号为 eax
的数放入 eax
中。根据 21 行判断数组大小,用 x /16wd 0x401ba0
查看一下 0x401ba0
开始的数组:
0x401ba0 array.3014: 10 2 14 7
0x401bb0 array.3014+16: 8 12 15 11
0x401bc0 array.3014+32: 0 4 1 13
0x401bd0 array.3014+48: 3 9 6 5
整理一下:
10 2 14 7 8 12 15 11 0 4 1 13 3 9 6 5
跳出循环,第 24 行,判断 edx
即函数的循环次数是不是 0xc = 12
;第 26 行判断了第二个参数是否等于 ecx
中的数。phase_5 也就被我们转化成了一道数组倒推问题。计算后得到答案:
7 93
至此,作业要求的 5 个函数已经完成!(撒花)
What’s Next
- Phase 6
- Secret Phase
- Gdb Guide