CSAPP-Lab-2 实验报告: bomblab

摘要

本文介绍了笔者在做 bomblab 一节的实验报告。该 lab 要求通过 gdb 等工具,调试二进制执行文件,阅读理解汇编指令,并找到避免炸弹的正确口令,阻止炸弹爆炸。

phase 1

首先,通过阅读代码,可以发现 phase_1 这个函数内对读取的口令进行检查,如果函数正确退出,则拆弹完成。使用 gdb 对该函数反汇编可以得到如下

1
2
3
4
5
6
7
8
9
10
Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
End of assembler dump.

可以发现,核心是通过 strings_not_equal 比较口令与 0x402400 地址的字符串是否相同,若不同则引发爆炸。虽然直觉上可以这么理解,但验证正确需要更谨慎一点,需要确认:

  • strings_not_equal 是不是如其名,判断字符串相同
  • strings_not_equal 的返回值,是否 0 代表相同

详细的确认需要逐句读汇编,也可以简单确认一下,直接在 0x0000000000400eee 处打断点。输入时输入 0x402400 地址对应的字符串,观察 \(eax\) 的值即可。测试发现即可直接通过,可以节省阅读汇编的时间。

phase 2

难度略微加大。同样地,首先反汇编 phase_2,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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>: callq 0x40145c <read_six_numbers>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52>
0x0000000000400f10 <+20>: callq 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>: callq 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
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq
End of assembler dump.

可以看到,核心是调用了 read_six_numbers 函数,这个函数需要两个参数,第一个是 read_line 得到的字符串,存储在 rdi 中没有被修改,第二个是 rsi,在栈 rsp 上开辟了 0x28 的空间,猜测应该是一个大小为 10 的 int 数组。同时观察到后续没有使用 eax 的逻辑,这个函数应该是无返回值的。接着反汇编 read_six_numbers,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function read_six_numbers:
0x000000000040145c <+0>: sub $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9
0x000000000040147c <+32>: lea 0x8(%rsi),%r8
0x0000000000401480 <+36>: mov $0x4025c3,%esi
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq
End of assembler dump.

可以看到核心逻辑在 + 54 附近,对__isoc99_sscanf@plt 的返回值进行校验,如果不满足 >5,则引爆炸弹。sscanf 的函数签名如下:

1
int sscanf(const char *str, const char *format, ...)

即从 str 读取格式化输入,返回成功匹配和赋值的个数。打印 0x4025c3 处的模板字符串可知,格式为 "% d % d % d % d % d % d",符合这个函数的命名。因此第一步,口令需要按这个格式以六个整数开头。

回到 phase_2,发现还有两个导致炸弹引爆的因素:

  • +18:第一个元素值不为 1
  • +34:两个值不相等

阅读 + 52 到 + 62,再到 + 34 可知,第二个约束时要求数组的下一个元素需要是这一个的两倍大,进而可以得到正确的口令。

phase 3

与 Phase 2 类似,也是考察 sscanf。反汇编 phase_3 得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Dump of assembler code for function 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>: callq 0x400bf0 <__isoc99_sscanf@plt> // 读入两个整数,存储于rdx(rsp+8), rcx(rsp+12)
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> // 第一个数如果>7, 则爆炸
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) // 第一个数用于计算跳转偏移,基础位置是 0x400f7c
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>: callq 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> // 第二个数与eax不同,则爆炸
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq
End of assembler dump.

可以发现,只需要根据第一个数确定 eax 取值,也就是第二个数,就可以通过这个 phase。根据 jmpq 猜测源码中有 switchjmpq 下面的部分就是跳转表。

phase 4

首先反汇编 phase4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Dump of assembler code for function phase_4:
=> 0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx // num2
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx // num1
0x000000000040101a <+14>: mov $0x4025cf,%esi // "%d %d"
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
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> // num1<=0xe, 避免爆炸
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4>
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76> // 若func4结果不为0, 爆炸
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81> // num2==0, 避免爆炸
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
End of assembler dump.

调用 func4 之前的代码很好理解,依然是读数 + 校验。func4 接收三个参数,分别是 num1、0、0xe,返回值若不为 0 则会爆炸。func4 反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Dump of assembler code for function func4:
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax //
0x0000000000400fd4 <+6>: sub %esi,%eax // eax=para3-para2
0x0000000000400fd6 <+8>: mov %eax,%ecx // ecx=para3-para2
0x0000000000400fd8 <+10>: shr $0x1f,%ecx // ecx>>=31 <=> ecx=sign(ecx) = 0 or 1
0x0000000000400fdb <+13>: add %ecx,%eax // eax+=ecx
0x0000000000400fdd <+15>: sar %eax // eax 算数右移 1 位
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx // ecx=rax+rsi
0x0000000000400fe2 <+20>: cmp %edi,%ecx
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> // ecx<=edi=num1
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx // edx=rcx-1
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> // ecx>=edi
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
End of assembler dump.

可以看出,func4 会递归调用本身。首先要思考,func4 在什么情况下不会调用自己,可以发现:

  • +22->+36:ecx<=edi=num1
  • +43->+57:ecx>=edi

两个均成立时,ecx=edi,返回值 eax=0,那么只需要根据 ecx 确定 num1 即可。

phase 5

首先反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Dump of assembler code for function phase_5:
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> // 字符串长度不为0,则爆炸
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx // ecx=input[i]
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx // rdx=input[i]
0x0000000000401096 <+52>: and $0xf,%edx // edx %=16
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx // maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) // input[i]=s[edx]
0x00000000004010a4 <+66>: add $0x1,%rax // rax+=1
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> // !=6循环
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi // 秘钥 flyers
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi // 修改后的输入
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> // eax!=0,爆炸
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax // 循环变量 eax=0
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq
End of assembler dump.

可以看出,输入的口令需要是一个长度为 6 的字符串,在循环内部对字符串每一位进行了修改,具体是取 ascii%16 的值,作为另一个字符串的下标索引,映射得到新字符串。反过来,就可以得到原始口令。%16 的结果应该为 9,15,14,5,6,7,对应到 ASCII 即可。

phase 6

老规矩,先反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Dump of assembler code for function phase_6:
0x00000000004010f4 <+0>: push %r14
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp
0x0000000000401100 <+12>: mov %rsp,%r13
0x0000000000401103 <+15>: mov %rsp,%rsi // int[6]
0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov %rsp,%r14
0x000000000040110e <+26>: mov $0x0,%r12d // 循环变量i
0x0000000000401114 <+32>: mov %r13,%rbp //
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax // eax=a[i]-1
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> // 1<=a[i]<=6, 避免爆炸, jbe是无符号比较
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add $0x1,%r12d // i+=1
0x000000000040112c <+56>: cmp $0x6,%r12d
0x0000000000401130 <+60>: je 0x401153 <phase_6+95> // 循环结束
0x0000000000401132 <+62>: mov %r12d,%ebx // 内层循环,j=i
0x0000000000401135 <+65>: movslq %ebx,%rax
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax
0x000000000040113b <+71>: cmp %eax,0x0(%rbp)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81> // a[j]!=a[i], j>i, 否则爆炸
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb> //
0x0000000000401145 <+81>: add $0x1,%ebx // j=j+1
0x0000000000401148 <+84>: cmp $0x5,%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65> // j<=5, 跳转至65
0x000000000040114d <+89>: add $0x4,%r13 // a[i] 地址自增4
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
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) // a[i]=7-a[i]
0x0000000000401166 <+114>: add $0x4,%rax // i+=1
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108> // i<6
0x000000000040116f <+123>: mov $0x0,%esi // 循环变量
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx
0x000000000040117a <+134>: add $0x1,%eax
0x000000000040117d <+137>: cmp %ecx,%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130> // 重复a[i]次
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>
0x0000000000401183 <+143>: mov $0x6032d0,%edx
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) // (void*)[6] B B[i]=0x6032d0
0x000000000040118d <+153>: add $0x4,%rsi // i+=1
0x0000000000401191 <+157>: cmp $0x18,%rsi
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183> // 循环结束
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx // 循环开始, ecx=a[i]
0x000000000040119a <+166>: cmp $0x1,%ecx
0x000000000040119d <+169>: jle 0x401183 <phase_6+143> // a[i]<=1
0x000000000040119f <+171>: mov $0x1,%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx // 循环结束, B[0]
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax // &B[1]
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi // 循环边界
0x00000000004011ba <+198>: mov %rbx,%rcx // B[i]
0x00000000004011bd <+201>: mov (%rax),%rdx // B[i+1]
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) // *(B[i]+8)=B[i+1] <=> B[i].next=B[i+1]
0x00000000004011c4 <+208>: add $0x8,%rax // i+=1
0x00000000004011c8 <+212>: cmp %rsi,%rax
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222> // 循环结束
0x00000000004011cd <+217>: mov %rdx,%rcx //
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) // 循环结束
0x00000000004011da <+230>: mov $0x5,%ebp // 循环变量 i=5
0x00000000004011df <+235>: mov 0x8(%rbx),%rax //
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> *(B[0])>=*(*B[0]+8) <=> B[i].val>=B[i].next.val
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx // rbx=*(*rbx+8)
0x00000000004011f2 <+254>: sub $0x1,%ebp
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> // i!=1
0x00000000004011f7 <+259>: add $0x50,%rsp
0x00000000004011fb <+263>: pop %rbx
0x00000000004011fc <+264>: pop %rbp
0x00000000004011fd <+265>: pop %r12
0x00000000004011ff <+267>: pop %r13
0x0000000000401201 <+269>: pop %r14
0x0000000000401203 <+271>: retq

前面的部分很好理解,要求输入数组 int a[6] 内部不能有重复元素,且 a[i] <= 6,然后再做变换 a[i]=7-a[i]

从 + 123 开始的循环,可以看出是要根据 a[i] 初始化一个位于 0x20 的 void * 数组 B,此时还不知道 B 的每个元素指向的是什么。当 a[i]>1 时,+130 处的循环涉及多次寻址,比较复杂,而且难以理解, 可以通过观察打表看 B 的取值。

首先输入 "6 5 4 3 2 1",转换为 "1 2 3 4 5 6",然后打印 B 的值依次为:

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) print *(int **) ($sp+0x20)
$16 = (int *) 0x6032d0 <node1>
(gdb) print *(int **) ($sp+0x28)
$17 = (int *) 0x6032e0 <node2>
(gdb) print *(int **) ($sp+0x30)
$18 = (int *) 0x6032f0 <node3>
(gdb) print *(int **) ($sp+0x38)
$19 = (int *) 0x603300 <node4>
(gdb) print *(int **) ($sp+0x40)
$20 = (int *) 0x603310 <node5>
(gdb) print *(int **) ($sp+0x48)
$21 = (int *) 0x603320 <node6>

可以看出,随着 a[i]+=1b[i]+=0x10。而且联想到 + 130 处的循环,0x8(%rdx),%rdx,可以猜测到 *B[i] 应该是一个结构体(大小超过了 8 Bytes),且内部含有指针(可以自更新),进而可以猜测 0x6032d0 这个地址就是链表头部。猜到这一点,后面理解起来就很容易了。

链表节点指向的值依次为

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) print **(int **) ($sp+0x20)
$23 = 332
(gdb) print **(int **) ($sp+0x28)
$24 = 168
(gdb) print **(int **) ($sp+0x30)
$25 = 924
(gdb) print **(int **) ($sp+0x38)
$26 = 691
(gdb) print **(int **) ($sp+0x40)
$27 = 477
(gdb) print **(int **) ($sp+0x48)
$28 = 443

注意,上面的 int 替换为 long,结果相同,说明有可能结构体里存储的是 int 并发生了对齐填充。

在 + 204 处的循环里,对链表进行了重排,使得 B[i].next=B[i+1]。+243 处判断重排后的链表需要满足 B[i].val>=B[i].next.val,否则会爆炸。只需根据上面的节点值,调整输入的值顺序,使得重排后的链表降序排列即可。

答案

分别是每个 phase 的答案,供参考,部分答案不唯一。

1
2
3
4
5
6
Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
7 0
)/.%&'
4 3 2 1 6 5