Dear Assembly(1)

By on

苦逼的暑假开始了。

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
view raw phase_1.asm hosted with ❤ by GitHub

除去 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
view raw phase_2.asm hosted with ❤ by GitHub

第七行调用 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 的数并且用 eaxrbp 两个寄存器之间的书比较是否相等。之后的第 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
view raw phase_3.asm hosted with ❤ by GitHub

比之前更长了,不是么?

观察一下函数的特征,尤其是 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
view raw phase_4.asm hosted with ❤ by GitHub

第 3 行查看 sscanf 的格式字符串,x /sb 0x401ec1

0x401ec1: “%d”

即输入一个数。将这个输入的数放入 edi 中调用了函数 func4func4 的代码如下:

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
view raw func4.asm hosted with ❤ by GitHub

7 - 13 行的主要说明了递归函数目的 f(x) = f(x-1) + f(x-2),边际条件在第 5 和 6 行 f(1) = 1(Fibbonacci 数列)。

回到原函数,14 行的 cmp 使用了返回值 eax0x37 = 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
view raw phase_5.asm hosted with ❤ by GitHub

同样地先观察格式字符串,x /sb 0x401ebe

0x401ebe: “%d %d”

格式输入正确后跳转到第 10 行执行函数,这里一行一行解释。

第 10 行,eax 存入地址为 rsp + 12 中的数,也就是第二个参数。11 - 12 行用这个数和 0xf 做了与操作,取出了最后两位并重新保存到 rsp + 12 中。13 行判断了这个数是不是 0xf,若是就引爆了炸弹,否则接下来进入循环。15 - 16 行的两个计数器 ecxedx 清零。

17 到 22 行由 jne 判断出这是一个循环。17 行的作用让 edx = edx + 1,马上 18 行 cltqeax 进行符号扩展,在 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