深入理解计算机系统CMU-15213 CSAPP(三):BombLab暨GDB调试
拆弹实验是csapp中比较巧妙的一个实验设计之一,在这个实验中我们会阅读一系列Linux64代码和掌握软件调试的基本工具gdb以及方法,以达成实验目的,总而言之在这个实验中,你的两方面能力会得到基本的培养:
阅读x64_86汇编代码的能力,了解代码意图是解题的前提;
使用gdb的能力,帮助验证你的猜想和获取信息;
在进入BombLab前我们会记录一些必须的x86_64汇编基础和gdb指令基础。
x86_64 Linux寄存器
x86_64架构是比较常用的64位Linux系统指令集,大部分Linux系统的x86_64架构遵循System V ABI规范,其提供了16个64位的通用寄存器,其用处如下:
rax:函数返回值;
rdi、rsi、rdx、rcx、
r8、r9:一般用作六个函数参数寄存器,对应1-6个参数,若有更多的参数会保存于函数栈中;rsp、rbp:栈指针(指向栈顶)和栈帧基址指针,栈是从高地址向低地址延申;
其余通用寄存器:
r10-r15、rbx(常作基址),用作临时变量存储等;
其中rbx、rbp及r12-r15共六个寄存器是Callee寄存器,由被调用者(Callee)负责保存和恢复,其余寄存器都是Caller寄存器。
在汇编中,还会看到edi、esi等寄存器,并不是新的通用寄存器,而是指rdi、rsi寄存器的低32位,di、si等则指向其低16位,dih、dil则指向其低16位的高八位和低八位,具体如下:

gdb与x86_64汇编基础
一些常用汇编:
| 指令 | 含义 | 指令 | 含义 |
|---|---|---|---|
| add rdi, rsi | rdi = rdi + 1 | inc rdi | rdi = rdi + 1 |
| sub rdi,rsi | rdi = rdi - rsi | dec rdi | rdi = rdi - 1 |
| imul rdi,rsi | rdi = rdi * rsi (有符号乘法) | idiv rsi | RAX = RDX:RAX / rsi; RDX = RDX:RAX % rsi |
| mul rsi | RDX:RAX = RAX * rsi(无符号乘法) | div rsi | RAX = RDX:RAX / rsi; RDX = RDX:RAX % rsi |
| neg rdi | rdi = -rdi | test rdi, rdi | 若rdi=0,ZF = 1 |
| cmp rdi, rsi | rsi-rdi,算术比较大小 | test rdi, rsi | rdi&rsi,根据结果设flag |
跳转逻辑
| 指令 | 含义 | 指令 | 含义 |
|---|---|---|---|
| jl | 小于,有符号 | jg | 大于,有符号 |
| jle | 小于等于,有符号 | jge | 大于等于,有符号 |
| jb | 小于,无符号 | ja | 大于,无符号 |
| jbe | 小于等于,无符号 | jae | 大于等于,无符号 |
| jz/je | 等于(0) | jnz/jne | 不等于 |
| js | 符号位=1跳转,负数 | jns | 符号位非1跳转,非负数 |
逻辑/算术左移右移
| 指令 | 含义 | 指令 | 含义 |
|---|---|---|---|
| shl eax, 1 | 逻辑左移,高位丢弃,低位补零 | shr eax, 1 | 逻辑右移,高位补零 |
| sal eax, 1 | 算术左移,高位丢弃,低位补零 | sar eax, 1 | 算术右移,高位补符号位 |
位宽
在gdb和汇编中,常常使用不同的位宽划分,相似又不完全相同:
| 汇编指令 | 含义 | gdb指令 | 含义 |
|---|---|---|---|
| movsb | 拷贝一个字节 | x/b | 查看一个字节 |
| movsw | 拷贝两个字节 | x/h | 查看两个字节 |
| movsd | 拷贝四个字节 | x/w | 查看四个字节 |
| movsq | 拷贝八个字节 | x/g | 查看八个字节 |
符号扩展
在执行转型时,在汇编层实际上是一种符号扩展,如: 1
2int a = 10;
uint64_t b = (uint64_t)a;
对带符号数扩展称符号(signed)扩展,对无符号数扩展称零(zero)扩展,例如一部分例子:
| 指令 | 含义 | 指令 | 含义 |
|---|---|---|---|
| movzbl %al, %eax | uint8_t to uint32_t | movswq %ax, %rax | int16_t to int64_t |
| movzwq %ax, %rax | uint16_t to uint32_t | movslq %eax, %rax | int32_t to int64_t |
| movsbl %al, %eax | int8_t to int32_t | movzq %eax, %rax | uint32_t to uint64_t |
区分x(examine)命令和p(print)命令
查看内存x命令和内容打印p命令:x命令和p命令都能够打印变量值、寄存器值等,但是其并不是等效的:
x命令的对象是指针(地址),能够直接查看该地址的值。
p命令的对象是值,直接输出该值。
因为寄存器既可能存储值,也可能存储地址,因此效果不同,例如当eax寄存器存储一个整数值时,使用x就不能正常输出了,因为该整数值往往不能被当成地址值访问:
1
2
3
4
5(gdb) x/d $eax #异常
0x7: Cannot access memory at address 0x7
(gdb) p /d $eax #ok
$2 = 7
而当寄存器存储的是地址时,使用p打印的是存储的地址值,x能直接解析出该存储的地址指向位置存储的值是什么:
1
2
3
4
5(gdb) p /d $rax
$5 = 140737488346656
(gdb) x /d $rax
0x7fffffffde20: 1
打印地址
1 | (gdb) p *(int*)0x6032d0 |
lea指令
1 | lea 0x4(%rsi),%rcx |
lea指令是一条地址计算指令,不会触发内存访问,故其意思是将rsi+0x4记录的地址值赋予rcx寄存器。
括号对寄存器解析影响
在做phase_6时,一个括号基本让整个汇编解释垮掉。在高级程序语言中,括号往往只有计算顺序影响,但对寄存器是会影响语义的,这三条语句效果完全不同:
1
2
3mov %edx, %rax #将edx存储的值拷贝到rax
mov %edx, (%rax) #将edx存储的值拷贝到rax存储地址指向的值,rax本身存储的地址不会变化
mov (%rax), %edx #读取rax寄存器存储的值拷贝到edx存储
无符号和带符号比较
一个负数被解析成无符号数往往是很大的,此时要注意大小比较关系:
1
2
3
4
5mov $-1, %eax #eax = -1
cmp $0x5, %eax
jl label #-1小于5,会跳转
jb label #-1不小于5,不会跳转(-1对应最大无符号)1
2
3
4
5
6
7int a = -1;
if(a < 5){
...
}
if((unsigned int)(a) < 5){
...
}
rip寄存器
rip是x64_86架构下的指令寄存器,存储着下一条指令地址,在比较运算符下容易出错:问题是num_input_strings存储地址是什么?
1
24015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
4015df: 75 5e jne 40163f <phase_defused+0x7b>
在0x4015d8断点,用这样的命令查看是一个误区:
1
2(gdb) x/d $rip+0x202181 #这是错误指令
0x603759: 04015d8指令真正执行前,rip会更新为0X4015df,所以正确的num_input_strings地址应该是:
1
2
3
4
5(gdb) ni #应该先走到rip的下一条指令
0x00000000004015df in phase_defused ()
(gdb) x/d $rip+0x202181
0x603760 <num_input_strings>: 1num_input_strings = 1。
gdb调试初试
CSAPP中给学生的参考资料提及了来自诺曼·马特洛夫(Norman Matloff)的更快、更轻松的调试手册,这里通过简单的素数程序排查让学生快速上手gdb,事实上真正常用的gdb命令是非常有限的,因此在实验前我们重新回顾一下这段来自2002年的程序,无兴趣或具备相关gdb基础的完全可以跳过此段叙述,因为确实价值一般。
一个有问题的素数查找程序
在使用gdb以前你必须读清代码意图:该示例是一种素数查找程序,用于查找[2-UpperBound]范围中所有的质数,其思想是一种迭代方法,例如判断19是否一个质数,要先检查19能否整除19以前的质数,而且根据定理可知你不需要遍历所有19以前的质数,只需要保证不能整除那些小于根号19的那些质数即可,即2和3,但是此前,你必须算出19前的所有质数,因为只有知道所有质数,你才能确定小于根号19的那些质数(例如你怎么知道4是合数还是质数),这被称为埃拉托色尼筛选(Sieve
of Eratosthenes)方法,程序如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//main.c
int primeArray[MaxPrimes],UpperBound;
int main(){
printf("enter UpperBound: \n");
scanf("%d",UpperBound);
primeArray[2] = 1;
for(int n=3; n<=UpperBound; n+=2){ //大于3不存在相邻的质数,步长为2
checkPrime();
if(primeArray[n]) //是质数
printf("%d is a prime\n", n);
}
return 0;
}
1 | //checkPrime.c |
1 | //defs.h |
1 | //externs.h |
将文件编译和链接:-c代表仅编译不链接,-g表示保留调试信息,对gdb调试-g是必要选项:
1
2
3gcc -c -g main.c #得到main.o
gcc -c -g checkPrime.c #得到checkPrime.o
gcc -g -o findPrimeTest main.o checkPrime.o #链接成可执行程序findPrimeTest
尝试运行: 1
2
3
4
5
6./findPrimeTest
#效果:
# enter UpperBound:
# 20
# zsh: segmentation fault (core dumped) ./findPrimeTest
上述示例存在3处错误,如果你对素数相关原理清晰,可能一下子就发现它们,但我们要使用gdb将问题准确定位。
gdb Test
每条命令前都标识了(gdb): 1
2
3
4
5(gdb) gdb findPrimeTest
(gdb) r
#input:
20 1
2
3Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e0b335 in __vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7fffffffde30, mode_flags=mode_flags@entry=2) at vfscanf-internal.c:1895
1895 vfscanf-internal.c: No such file or directory.
查看调用栈: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(gdb) bt
#0 0x00007ffff7e0b335 in __vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7fffffffde30, mode_flags=mode_flags@entry=2) at vfscanf-internal.c:1895
#1 0x00007ffff7e06162 in __isoc99_scanf (format=<optimized out>) at isoc99_scanf.c:30
#2 0x00005555555551ba in main () at main.c:9
(gdb) list main.c:9
# 5 int primeArray[MaxPrimes],UpperBound;
# 6
# 7 int main(){
# 8 printf("enter UpperBound: \n");
# 9 scanf("%d",UpperBound);
# 10
# 11 primeArray[2] = 1;
# 12 for(int n=3; n<=UpperBound; n+=2){
# 13 checkPrime();
可见第一个问题出在第9行,打印变量值: 1
2(gdb) p UpperBound
$1 = 01
2//main.c
scanf("%d", &UpperBound); //加入引号
重新编译和运行: 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(gdb) shell gcc -g -o findPrimeTest main.c checkPrime.c
(gdb) r
enter UpperBound:
20
Program received signal SIGSEGV, Segmentation fault.
0x000055555555526c in checkPrime (k=-9776) at checkPrime.c:9
9 primeArray[k] = 0;
(gdb) bt
#0 0x000055555555526c in checkPrime (k=-9776) at checkPrime.c:9
#1 0x00005555555551d6 in main () at main.c:13
(gdb) list checkPrime.c:9
4 void checkPrime(int k){
5 int j = 2;
6 while(1){
7 if(primeArray[j] == 1){
8 if(k%j==0){
9 primeArray[k] = 0;
10 return;
11 }
12 }
13 j++;
(gdb) p k
$3 = -9776k=-9776,可见这是一个未定义参数导致的UB,即checkPrime()根本没有传参,修复一下:
1
2//main.c
checkPrime(n);
再来一次: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25enter UpperBound:
20
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555248 in checkPrime (k=3) at checkPrime.c:7
7 if(primeArray[j] == 1){
(gdb) bt
#0 0x0000555555555248 in checkPrime (k=3) at checkPrime.c:7
#1 0x00005555555551db in main () at main.c:13
(gdb) list checkPrime.c:7
# 2 #include "externs.h"
# 3
# 4 void checkPrime(int k){
# 5 int j = 2;
# 6 while(1){
# 7 if(primeArray[j] == 1){
# 8 if(k%j==0){
# 9 primeArray[k] = 0;
# 10 return;
# 11 }
(gdb) p j
# $4 = 347921
2
3
4//checkPrime.c
while(j*j <= k){
...
}
于是完成了: 1
2
3
4
5
6
7
8
9
10
11
12
13
14(gdb) shell gcc -g -o findPrimeTest main.c checkPrime.c
(gdb) r
enter UpperBound:
20
3 is a prime
5 is a prime
7 is a prime
11 is a prime
13 is a prime
17 is a prime
19 is a prime
[Inferior 1 (process 394872) exited normall]
BombLab
进入bomb文件夹查看bomb.c代码: 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
88
89
90
91
92
93
/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/
FILE *infile;
int main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}
/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
return 0;
}argc=1时,读取stdin作为输入流,当argc=2时,输入流为第二个参数的文件,所以答案输入可以从bash直接输入,也可以任意创建一个文件,每行为1个答案,这个实验需要的6个read_line对应6个答案,不触发phase_1
to phase_6中的炸弹即算挑战成功。
设计者没有提供这六个函数源码,包括屏蔽了gdb查看源码的功能,因此这里只能反汇编,使用objdump工具:
1 | objdump -d bomb >bomb.S |
初始化代码印证了C代码的入口,即定向输入流、读取输入、初始化炸弹:
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
31400da0: 53 push %rbx
400da1: 83 ff 01 cmp $0x1,%edi #argc == 1 ?
400da4: 75 10 jne 400db6 <main+0x16> #不等时跳转至#1
400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5>
400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile>
400db4: eb 63 jmp 400e19 <main+0x79>
400db6: 48 89 f3 mov %rsi,%rbx #1:rbx = rsi = argv[0]
400db9: 83 ff 02 cmp $0x2,%edi #argc == 2 ?
400dbc: 75 3a jne 400df8 <main+0x58> # 不等时跳转至#2
400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi #rdi = rsi+8 = argv[1]
400dc2: be b4 22 40 00 mov $0x4022b4,%esi #字符串"r"
400dc7: e8 44 fe ff ff callq 400c10 <fopen@plt> #fopen
400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile>,infile = fopen(...)
400dd3: 48 85 c0 test %rax,%rax #若infile等于0,ZF = 1
400dd6: 75 41 jne 400e19 <main+0x79> #infile != 0, 跳入正常入口函数(initialize_bomb)
400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx # rcx = argv[1]
400ddc: 48 8b 13 mov (%rbx),%rdx # rdx = argv[0]
400ddf: be b6 22 40 00 mov $0x4022b6,%esi #printf字符串类型
400de4: bf 01 00 00 00 mov $0x1,%edi #printf合法性检查启用
400de9: e8 12 fe ff ff callq 400c00 <__printf_chk@plt> #printf
400dee: bf 08 00 00 00 mov $0x8,%edi
400df3: e8 28 fe ff ff callq 400c20 <exit@plt> #exit(8)
400df8: 48 8b 16 mov (%rsi),%rdx #2 :rsi(argv[0])-> rdx
400dfb: be d3 22 40 00 mov $0x4022d3,%esi #printf字符串类型
400e00: bf 01 00 00 00 mov $0x1,%edi #printf合法性检查启用
400e05: b8 00 00 00 00 mov $0x0,%eax #打印0个浮点数
400e0a: e8 f1 fd ff ff callq 400c00 <__printf_chk@plt> #调用printf
400e0f: bf 08 00 00 00 mov $0x8,%edi #edi = 8
400e14: e8 07 fe ff ff callq 400c20 <exit@plt> #调用exit(8)
First:phase_1
从C源码和汇编都可以看到,将read_line的返回值rax放到了phase_1第一个参数地址(rdi):
1
2
3400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
phase_1将0x402400位置的字符串放在第二个参数esi位置(rsi的低32位),然后test返回值是否为0,若为0(je)则字符串相等,跳过炸弹函数explode_bomb,故第一个字符串答案来自0x402400。
1
2
3
4
5
6
7
8
90000000000400ee0 <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 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22gdb bomb #开始调试
(gdb) b explode_bomb #防止意外爆炸
Breakpoint 1 at 0x40143a
(gdb) b phase_1
Breakpoint 2 at 0x400ee0
(gdb) b *0x400ee4
Breakpoint 3 at 0x400ee4
(gdb) r
Starting program: /home/linux/Desktop/CMU_csapp/csapplab/bomblab/bomb/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
iamironman #先随便一个输入
Breakpoint 2, 0x0000000000400ee0 in phase_1 ()
(gdb) c
Continuing.
Breakpoint 3, 0x0000000000400ee4 in phase_1 ()
(gdb) p (char*)0x402400
$1 = 0x402400 "Border relations with Canada have never been better."Border relations with Canada have never been better.。
Second:phase_2
第二个phase,此处可不止一个炸弹,入口程序将读取到的参数传入phase_2:
1
2
3
4
5400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
400e5b: e8 64 07 00 00 callq 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
查看phase_2,将rsp放到第二个参数就调用了read_six_numbers函数,将栈指针放到第二个参数似乎有点奇怪,这是传入数组指针典型的代码,而第一个参数是readline返回的字符流,所以对应的C语言大概是类似void read_six_numbers(str, int num*):
1
2
3
4
5
6
70000000000400efc <phase_2>:
400efc: 55 push %rbp #保存Callee寄存器
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp #开辟40字节函数栈,如定义了int num[10];
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
......
从read_six_numbers汇编可以看到,read_six_numbers仅使用了其中6个参数,即将num[0] to num[5]设置为scanf函数的参数,scanf会将str中包含的数字分别填充到六个参数中,其中num[0] to num[3]四个是寄存器存储,而num[4] to num[5]存储在函数栈上,如果提供scanf函数的字符串解析出的int小于6个,炸弹爆炸,所以此处的答案之一是必须传入6个及以上数量的int类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx # rdx = num[0]
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx # rcx = num[1]
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) # rsp+0x8 = num[5]
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp) # rsp = num[4]
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 #r9 = num[3]
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 #r8 = num[2]
401480: be c3 25 40 00 mov $0x4025c3,%esi #scanf函数设置
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax #scanf结果 >5 个?
401492: 7f 05 jg 401499 <read_six_numbers+0x3d> #大于则跳过爆炸函数
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp #返回函数帧
40149d: c3 retq
查看完整的phase_2,可见这六个值是有大小关系的:其一是num[0]必须为1,而且num[1] to num[5],后面元素必须是前一个元素的两倍。当循环来到6,跳出解析,所以尽管传入大于6数量的int,也不会导致炸弹爆炸,因此答案是至少输入1 2 4 8 16 32,
后面任何输入都不会导致爆炸。 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
260000000000400efc <phase_2>:
400efc: 55 push %rbp #保存Callee寄存器
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp #开辟40字节函数栈
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) #long值(低32位)比较,rsp = num[0] = 1 ?
400f0e: 74 20 je 400f30 <phase_2+0x34> #等于则跳过爆炸函数
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax #eax = num[0]
400f1a: 01 c0 add %eax,%eax #eax = 2*num[0]
400f1c: 39 03 cmp %eax,(%rbx) #num[1] == 2*num[0] ?
400f1e: 74 05 je 400f25 <phase_2+0x29> #相等则跳过炸弹函数
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx # rbx = num[2]
400f29: 48 39 eb cmp %rbp,%rbx # num[6] != num[2]
400f2c: 75 e9 jne 400f17 <phase_2+0x1b> # 不等则继续,要求num[2] == 2*num[1]......直到num[5]==2*num[4]
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx #rbx = num[1]
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp #rbq = num[5] + 0x04,预先开辟了40字节,不会越界
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
Third:phase_3
为了方便后面phase的gdb调试,可以先把前面的答案写在myans文件中:
来到phase_3,仅读代码可知需要输入2个以上的数字参数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24linux@ubuntu:~/Desktop/CMU_csapp/csapplab/bomblab/bomb$ gdb --args ./bomb myans
......
Reading symbols from ./bomb...done.
(gdb) b phase_3
Breakpoint 1 at 0x400f43
(gdb) r
Starting program: /home/linux/Desktop/CMU_csapp/csapplab/bomblab/bomb/bomb myans
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!
1 2 3 4 5 6 #随便一个输入
Breakpoint 1, 0x0000000000400f43 in phase_3 ()
(gdb) b *0x400f51
Breakpoint 2 at 0x400f51
(gdb) c
Continuing.
Breakpoint 2, 0x0000000000400f51 in phase_3 ()
(gdb) p (char*)0x4025cf
$1 = 0x4025cf "%d %d" #可见需要输入两个数字rax = 0 to 7,随即会跳转到特定的指令位置0x402470+(rax*8),第二个参数必须等于这个指令位置赋的值,使用gdb全部打出来即可:
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(gdb) p /x 0x402470
$3 = 0x402470
(gdb) x /x 0x402470
0x402470: 0x00400f7c
(gdb) p/d 0xcf
$4 = 207 #对应第一个参数 = 0
(gdb) x /x 0x402478
0x402478: 0x00400fb9
(gdb) p/d 0x137
$5 = 311 #对应第一个参数 = 1
(gdb) x /x 0x402480
0x402480: 0x00400f83
(gdb) p/d 0x2c3
$6 = 707 #对应第一个参数 = 2
(gdb) x /x 0x402488
0x402488: 0x00400f8a
(gdb) p/d 0x100
$7 = 256 #对应第一个参数 = 3
(gdb) x /x 0x402490
0x402490: 0x00400f91
(gdb) p/d 0x185
$8 = 389 #对应第一个参数 = 4
(gdb) x /x 0x402498
0x402498: 0x00400f98
(gdb) p/d 0xce
$9 = 206 #对应第一个参数 = 5
(gdb) x /x 0x4024a0
0x4024a0: 0x00400f9f
(gdb) p/d 0x2aa
$10 = 682 #对应第一个参数 = 6
(gdb) x /x 0x4024a8
0x4024a8: 0x00400fa6 #对应第一个参数 = 7
(gdb) p/d 0x147
$11 = 3277 327即可通过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
370000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx #rcx = rsp + 0xc
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx #rdx = rsp + 0x8
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax #scanf参数个数大于1跳过explode_bomb
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) #scanf第一个参数小于等于7跳过explode_bomb
400f6f: 77 3c ja 400fad <phase_3+0x6a>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax #eax = num[0]
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) #跳转到 0x402470+(rax*8)储存的地址值
400f7c: b8 cf 00 00 00 mov $0xcf,%eax # 207
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax # 707
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax # 256
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax # 389
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax # 206
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax # 682
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax # 327
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax # 311
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax #eax == num[1] ?
400fc2: 74 05 je 400fc9 <phase_3+0x86> #等于则正常返回
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq
Fourth:phase_4
在func4前从phase_4首先可以获取到两条信息:
phase_4接收两个参数的输入,其中第一个参数num[0]应小于等于0xe,第二个参数num[1]应该为0;func4只使用了num[0],num[0]需满足让func_4返回0;
满足此二条件,得以跳过所有的explode_bomb,phase_4注释如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29> #非两个参数即爆炸
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) #num[0]需 <= 0xe
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx # edx = 0xe,func4第三个参数
40103f: be 00 00 00 00 mov $0x0,%esi # esi = 0,func4第二个参数
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi # edi = num[0],func4第一个参数
401048: e8 81 ff ff ff callq 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c> #func4返回不是0,跳转爆炸
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51> #num[1]需为0,否则爆炸
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq
现在只需要看一下func4,可见只需要通过gdb算出ecx = rax+rsi*1存储的地址值指向的值是什么即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19(gdb) b *0x400fe2
Breakpoint 2 at 0x400fe2
(gdb) c
Continuing.
Breakpoint 2, 0x0000000000400fe2 in func4 ()
(gdb) p/d $ecx
$1 = 7
(gdb) c
Continuing.
Breakpoint 2, 0x0000000000400fe2 in func4 ()
(gdb) p/d $ecx
$2 = 3
Breakpoint 2, 0x0000000000400fe2 in func4 ()
(gdb) p/d $ecx
$3 = 17 0或3 0或1 0。
func4注释: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
230000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax # eax = 参数3 - 参数2 = 14
400fd6: 89 c1 mov %eax,%ecx # ecx = eax = 14
400fd8: c1 e9 1f shr $0x1f,%ecx # 第一次逻辑右移,变为0(而不是7)
400fdb: 01 c8 add %ecx,%eax # eax = eax + ecx = 14
400fdd: d1 f8 sar %eax # eax = eax/2 = 7
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx # ecx = rax+rsi*1处存储的地址值存储的值
400fe2: 39 f9 cmp %edi,%ecx # 第一次ecx = 7,第二次ecx = 3, 第三次ecx = 1
400fe4: 7e 0c jle 400ff2 <func4+0x24> # ecx <= edi时跳转,edi为第一个输入参数
400fe6: 8d 51 ff lea -0x1(%rcx),%edx # 否则edx = rcx-0x1 处存储的值
400fe9: e8 e0 ff ff ff callq 400fce <func4> # 调用func4
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx # ecx >= edi,返回0退出
400ff9: 7d 0c jge 401007 <func4+0x39>
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff callq 400fce <func4> # 否则继续递归
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq
Fifth:phase_5
第五段代码其实不难看懂,但是一个地方误导性很大,具体来看看爆炸条件:
1
2
3
4
5
64010b3: be 5e 24 40 00 mov $0x40245e,%esi #第二个参数字符串来自0x40245e
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi #第一个参数字符串来自0x10(%rsp)
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax #字符串不等,爆炸
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>rdi)等于0x40245e即可,于是直接查看0x40245e字符串:
1
2
3
4
5
6
7
8(gdb) b *0x4010b3
Breakpoint 3 at 0x4010b3
(gdb) c
Continuing.
Breakpoint 3, 0x00000000004010b3 in phase_5 ()
(gdb) p (char*)0x40245e
$1 = 0x40245e "flyers"flyers时,哦呵,炸弹爆炸,这个坑挖给任何没给explode_bomb打断点、功利地猜想答案的人(我自己)。
还是得脚踏实地分析一下: 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
410000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx # rbx = rdi = readline输入字符流
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax # rax = fs段寄存器偏移0x28存储的值,仅内存备份,与题目无关
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax #自身异或,eax = 0
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax #字符串长度需等于6,否则爆炸
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx # ecx = (rbx + rax*1)地址的储值,输入的6个字符
40108f: 88 0c 24 mov %cl,(%rsp) # rsp = cl(rcx的低八位) ,取6个输入的低八位
401092: 48 8b 14 24 mov (%rsp),%rdx # rdx = rsp
401096: 83 e2 0f and $0xf,%edx # edx = edx低四位,6个输入的低四位
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx # edx = (0x4024b0+rdx)的储值
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) # (rsp + rax*1)+0x10 = dl,可见这是逐一赋值到0x10(%rsp)
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
4010ac: 75 dd jne 40108b <phase_5+0x29>
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) #设置“\0”,刚好6个字节
4010b3: be 5e 24 40 00 mov $0x40245e,%esi #第二个参数字符串来自0x40245e
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi #第一个参数字符串来自0x10(%rsp)
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax #字符串不等,爆炸
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) # 字节对齐
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq0x4024b0+rdx),rdx是来自输入字符的ASCII码值的低四位(构成0-15),先看看0x4024b0里装了什么东西,虽然没有scanf告诉我们参数类型,但是因为对比对象是字符,所以也能想到用字符打印吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15gdb --args ./bomb myans
(gdb) r
Starting program: /home/linux/Desktop/CMU_csapp/csapplab/bomblab/bomb/bomb myans
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.
iamman #随便输入一个6字符
Breakpoint 1, 0x0000000000401099 in phase_5 ()
(gdb) p (char*)0x4024b0
$1 = 0x4024b0 <array> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"0x4024b0+rdx)是索引了前16个字符,因此只要输入的字符的ASCII码值的低四位作为索引数字,恰好能索引出“flyers”即可,即索引9 f e 5 6 7,在大佬提醒下才发现man ascii命令恰好提供了ASCII码的低四位值:
在这六行中,按索引顺序每行任选一个组成字符串即可,为了方便此处均选取小写字母作为答案:ionuvw。
Sixth:phase_6
循环体1信息:是一个双层循环,跳出外循环只能让
r12d == 6,跳出内循环只能让ebx大于5,而且ebx依赖于r12d,故这结构类似1
2
3
4
5for(int i=0; i<6; i++){
for(int j=i+1; j<=5; j++){
...
}
}触发爆炸的两个条件是
eax == rbp, 以及eax-1大于5,揣摩一下即可知前者是让6个数字必须互不相等,而且六个输入数字都不能大于6,这个双层循环完全可以推断出来;另一个细节是判断eax-1大于5时使用的是无符号数:1
240111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34> #eax大于5则爆炸,注意此处是无符号比较;如果
eax-1是负数,jbe的小于等于条件是不成立的,因为-1对应的无符号数正好是最大的无符号数,所以第三个信息是此处输入的六个数不能含0。循环体2信息:循环体短短几行代码简直是非汇编圣体的滑铁卢,其中会诞生著名的为什么3+4 = 5、1+4 = 0等违背宇宙常识的结果,问题出在:
1
2mov %edx,(%rax) # rax存储地址指向的值 = edx(rax本身存储的地址不变)
add $0x4,%rax # rax地址 += 4,是存储的地址+=4,而不是指向的值+=4 !!edx是一个值,因为(%rax)是带括号的,代表mov效果是将其拷贝到rax存储的地址指向的值,而rax本身存储的地址是不变的,因此add的效果不是在edx值上加4,而是在rax存储的地址上加偏移4,因此问题应该变成找到哪个地址存储的内容是0。根据gdb,rsp的起始指针
0x7fffffffde20,24个字节(0x7fffffffde20-0x7fffffffde34)对应输入6个数字:因为从循环1条件中可知输入6个数不能为0,因此循环只会在1
2x/w $rsp
0x7fffffffde20: 10x7fffffffde38处(即空栈,num[6]==0)出退出,所以循环体2的作用只是将6个数变成7的补数,例如1,2,3,4,5,6会依次称为6,5,4,3,2,1。循环体3和4信息:这两个循环要在一起看,因为存在可能循环4的循环先运行,再到循环3。循环体3同样是一个工具人,旨在找到第一个大于1的数,如原始输入
1,2,3,4,5,6,经循环2输出是6,5,4,3,2,1,那么6就是大于1的,令ecx = 6,这种假设情况不会进入#4所在的循环体3中的循环,而是跳转到循环4,循环4是一个链表结构,具体而言是这样的:为了方便,0x6032d0简记为0x2d0,不带0x的都是十进制整数value,有:可见这是一段连续内存,0结尾的地址存储了数值数据value,8结尾的地址存储了下一个地址的指针。1
2
3
4
5
6
7
8
9
10
110x2d0 - 332 --value1
0x2d8 - 0x2e0
0x2e0 - 168 --value2
0x2e8 - 0x2f0
0x2f0 - 924 --value3
0x2f8 - 0x300
0x300 - 691 --value4
0x308 - 0x310
0x310 - 477 --value5
0x318 - 0x320
0x320 - 443 --value6若
ecx = 6,那么循环4出口的rdx返回的就是链表对应的value6,此时回到#4所在的循环3,这个value6将被赋值到rsp后面的一段内存中,地址计算公式是(rsp+2*rsi) + 0x20 = rdx,即rsp + 0x20 = value6;总而言之,循环3和4完成的结果是,当输入
1,2,3,4,5,6,循环2后变体为6,5,4,3,2,1,(rsp+0x20 - rsp+0x48)依次存放了链表结点的value6 - value1;循环体5信息:一个循环,
rcx + 0x8固定存入rsp+0x48的值;此处我们用num[0] ~ num[5]描述rsp ~ rsp+0x18的六个数字(每个数字占4字节),在循环2前依次存储6个输入数字,在循环2后依次变成7的补数;用array[0] ~ array[5]形容rsp + 0x20 ~ rsp + 0x48的六个数字(此处每个数字占8字节);循环体6信息:可见这是一个关于array数组的大小比较,要求array前面的数字必须大于后面的数字,否则爆炸,即array[0] > array[1] > ... > array[6];
综上所述,输入的6个数字是结点的索引,每个结点存储一个整数,输入的数字索引必须满足使得结点存储整数从大到小排列,根据循环体3列出的6个数,可见使得结点存储整数从大到小的排序索引应该是3 4 5 6 1 2,
该结果是循环2求7的补数之后的,所以原始答案输入应该为4 3 2 1 6 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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
10200000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
4010fc: 48 83 ec 50 sub $0x50,%rsp #begin......
401100: 49 89 e5 mov %rsp,%r13 # r13 = rsp
401103: 48 89 e6 mov %rsp,%rsi # rsi = rsp
401106: e8 51 03 00 00 callq 40145c <read_six_numbers>
40110b: 49 89 e6 mov %rsp,%r14 # r14 = rsp
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d # r12d(r12寄存器的低32位)清零
#循环体1
401114: 4c 89 ed mov %r13,%rbp # rbp = r13 = num[0]
401117: 41 8b 45 00 mov 0x0(%r13),%eax # eax = r13 = num[0]
40111b: 83 e8 01 sub $0x1,%eax # eax -= 1
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34> #eax大于5则爆炸,注意此处是无符号比较;
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d # r12d += 1
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f> # r12d == 6则跳转#1
401132: 44 89 e3 mov %r12d,%ebx # 不等时,ebx = r12d
#循环体1之内循环
401135: 48 63 c3 movslq %ebx,%rax # 符号扩展,32 to 64bits,rax = ebx
401138: 8b 04 84 mov (%rsp,%rax,4),%eax # eax = (rsp + rax*4)的储值,可见这是遍历5个数字
40113b: 39 45 00 cmp %eax,0x0(%rbp) # eax == rbp则爆炸
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx # ebx += 1
401148: 83 fb 05 cmp $0x5,%ebx # ebx <= 5则小循环
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13 # 否则r13 += 4,大循环
401151: eb c1 jmp 401114 <phase_6+0x20>
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi #1:rsi = rsp + 0x18 = num[6] = 0
401158: 4c 89 f0 mov %r14,%rax # rax = r14 = num[0]
40115b: b9 07 00 00 00 mov $0x7,%ecx # ecx = 7
#循环体2
401160: 89 ca mov %ecx,%edx # edx = ecx = 7
401162: 2b 10 sub (%rax),%edx # edx -= rax, 7-num[0]
401164: 89 10 mov %edx,(%rax) # rax存储地址指向的值 = edx(rax本身存储的地址不变)
401166: 48 83 c0 04 add $0x4,%rax # rax地址 += 4,是存储的地址+=4,而不是指向的值+=4 !!
40116a: 48 39 f0 cmp %rsi,%rax # rsi == rax?
40116d: 75 f1 jne 401160 <phase_6+0x6c> # 不等则循环
40116f: be 00 00 00 00 mov $0x0,%esi # esi = 0
401174: eb 21 jmp 401197 <phase_6+0xa3> # 跳转至#2
#循环体4
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx #3:rdx = rdx+0x8
40117a: 83 c0 01 add $0x1,%eax # eax += 1
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82> # eax != ecx == 1 循环
401181: eb 05 jmp 401188 <phase_6+0x94> # 相等则跳转#4
#循环体3
401183: ba d0 32 60 00 mov $0x6032d0,%edx # edx = *0x6032d0 = 332
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) #4:(rsp+2*rsi) + 0x20 = rdx
40118d: 48 83 c6 04 add $0x4,%rsi # rsi += 4
401191: 48 83 fe 18 cmp $0x18,%rsi # rsi == 18?
401195: 74 14 je 4011ab <phase_6+0xb7> # 相等则跳转#5
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx #2: ecx = rsp+rsi储值 = num[0]
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f> # ecx小于等于1,循环
40119f: b8 01 00 00 00 mov $0x1,%eax # eax = 1
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx # edx = 0x6032d0,存储是地址值
4011a9: eb cb jmp 401176 <phase_6+0x82> # 跳转至#3
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx #5:rbx = rsp + 0x20 = array[0]地址值
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax # rax = rsp + 28 = array[1]地址值
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi # rsi = rsp + 50 = 0地址值
4011ba: 48 89 d9 mov %rbx,%rcx # rcx = rbx = array[0]地址值
#循环体5
4011bd: 48 8b 10 mov (%rax),%rdx # rdx = rax, 初值rax = array[1]地址值 = 0x28
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) # rcx+0x8 = rdx, rcx初值array[0]
4011c4: 48 83 c0 08 add $0x8,%rax # rax += 8
4011c8: 48 39 f0 cmp %rsi,%rax # rax == rsi ?0x30 to 0x50, 固定需循环4次
4011cb: 74 05 je 4011d2 <phase_6+0xde> # 相等跳转#6
4011cd: 48 89 d1 mov %rdx,%rcx # rcx = rdx
4011d0: eb eb jmp 4011bd <phase_6+0xc9> # 循环
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) #6: rdx + 0x8 = 0
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp #ebq = 5
#循环体6
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax # rax = rbx+0x8 = 初值array[1]
4011e3: 8b 00 mov (%rax),%eax # eax = rax,rax存储的是地址,现在直接转为存储值
4011e5: 39 03 cmp %eax,(%rbx) # eax == rbx? rbx初值为array[0]
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb> # rbx小于eax,爆炸
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx # rbx = rbx + 8
4011f2: 83 ed 01 sub $0x1,%ebp # ebp -= 1
4011f5: 75 e8 jne 4011df <phase_6+0xeb> # rbx != eax 循环
4011f7: 48 83 c4 50 add $0x50,%rsp # end.......
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq
Final: secret_phase
How to Enter
最后的脚本提示: 1
2/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */phase_defused还藏着一个secret_phase的调用,说明还有隐藏关卡,分析一下phase_defused代码是如何进入该关卡的:
1
2
34015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>,判断是否到第6关
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8phase_defused,但只有第6关能运行到lea,查看0x202181(%rip)位置可知,此处存储了每个关卡数字:
1
2(gdb) x/d 0x4015df+0x202181 #注意rip是下一条指令指针
0x603760 <num_input_strings>: 6scanf输入:可见要求输入是两个整数和一个字符串类型:而两个数字恰好是第四关输入的答案:
1
2
3
4(gdb) p (char*)0x603870
$13 = 0x603870 <input_strings+240> "3 0"
(gdb) p (char*)0x402619
$14 = 0x402619 "%d %d %s"1
2(gdb) p (char*)0x402622
$9 = 0x402622 "DrEvil"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
3600000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>,判断是否到第6关
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax # scanf是否输入3个字符
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi #<input_strings+240> "3 0",为第四关存储的答案
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi # 0x402619 "%d %d %s"
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal>
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff callq 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq
How to Deal
对比phase_6,secret_phase的长度友好很多,开始代码做了简单的事情,首先读取输入字符,通过strtol将字符转换到整数,如果输入整数大于999,爆炸,否则继续调用fun7,只有fun7返回2才能让炸弹不爆炸:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
240000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx # edx = 0xa
40124d: be 00 00 00 00 mov $0x0,%esi # esi = 0
401252: 48 89 c7 mov %rax,%rdi # rdi = rax = 输入的字符
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx # rbx = rax = 输入字符转的整数
40125d: 8d 40 ff lea -0x1(%rax),%eax # eax -= 1
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a> # eax > 0x3e8则爆炸,即输入字符串不能大于1000-1
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi # esi = ebx = 输入的原始整数
40126e: bf f0 30 60 00 mov $0x6030f0,%edi # edi = 0x6030f0储值
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax # fun7只有返回2才安全退出,否则爆炸
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
于是需要看看fun7内容: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
210000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi # rdi不能为0,否则炸弹爆炸
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx # edx = rdi储值地址指向的值,初值是0x6030f0储值
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c> # edx <= esi,跳转至#1,esi初值是输入的原始整数
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi # rdi = rdi+0x8
401217: e8 e8 ff ff ff callq 401204 <fun7> # 递归fun7
40121c: 01 c0 add %eax,%eax # eax += eax
40121e: eb 1d jmp 40123d <fun7+0x39> # 函数返回
401220: b8 00 00 00 00 mov $0x0,%eax #1: eax = 0
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39> # edx==esi返回
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi # rdi = rdi+0x10
40122d: e8 d2 ff ff ff callq 401204 <fun7> # 递归fun7
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax # eax = rax + rax*1储值 + 0x1
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 retq 0x6030f0储值是什么,它会和输入的整数做大小比较:
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(gdb) r
Starting program: /home/Desktop/CSAPP_Lab/bomblab/bomb/bomb myans
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...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
^Z
Program received signal SIGTSTP, Stopped (user).
0x00007ffff7eb11f2 in __GI___libc_read (fd=0, buf=0x6058a0, nbytes=1024) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
(gdb) b fun7
Note: breakpoint 3 also set at pc 0x401204.
Breakpoint 4 at 0x401204
(gdb) c
Continuing.
Program received signal SIGTSTP, Stopped (user).
0x00007ffff7eb11f2 in __GI___libc_read (fd=0, buf=0x6058a0, nbytes=1024) at ../sysdeps/unix/sysv/linux/read.c:26
26 in ../sysdeps/unix/sysv/linux/read.c
(gdb) n
120 #初始时随便一个小于等于999的数
_IO_new_file_underflow (fp=0x7ffff7f8f980 <_IO_2_1_stdin_>) at fileops.c:519
519 fileops.c: No such file or directory.
(gdb) c
Continuing.
Breakpoint 3, 0x0000000000401204 in fun7 ()
(gdb) p *0x6030f0
$17 = 36rdi初值是36。
虽然涉及了递归,但接下来的逻辑其实也很简单,首先令edx = rdi,反复比较edx的值和输入的整数值,如果edx较小,应该更新rdi为rdi存储的地址值+0x10(此处地址值存储一个整数),然后edx
=
rdi再递归比较,否则edx更大的话,更新rdi为rdi+0x08地址储的值,再递归比较。
本题唯一的难点是找到返回2的可能性,如果怎么看都觉得直接爆炸,那就是递归和回溯理解不到位,得益于设计者心善,本题可能返回2的路径是:
- 递归树最深时满足edx==esi,且让eax返回0,回溯到上一层正好落在
eax = rax + rax*1储值 + 0x1,此层返回了2,或者本层eax=1,继续回溯到上一层eax += eax返回,或者是本层的eax让上层的eax = rax + rax*1储值 + 0x1刚好返回2,以此类推。
1 | 0000000000401204 <fun7>: |
此处最容易误解是对rdi的更新,很容易陷入rdi + 0x10 = rdi+两次的0x8的错觉,所以弄清楚寄存器存的是值还是指针对整个实验都很重要,此处让他跳到第一个赋值断点:
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
31gdb --args ./bomb myans
(gdb) b *0x401213
Breakpoint 1 at 0x401213
(gdb) r
Starting program: /home/Desktop/CSAPP_Lab/bomblab/bomb/bomb myans
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...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
24 #输入一个小于等于36的数
Breakpoint 1, 0x0000000000401213 in fun7 ()
Breakpoint 1, 0x0000000000401213 in fun7 ()
(gdb) p/x $rdi
$1 = 0x6030f0
(gdb) p/x $rdi+0x8
$2 = 0x6030f8
(gdb) x/x $rdi+0x8
0x6030f8 <n1+8>: 0x00603110
(gdb) x/d 0x603110
0x603110 <n21>:mov 0x8(%rdi),%rdi会计算0x8(%rdi)的地址,即0x6030f8,取出该地址储的值,即另一个地址值0x00603110存储到rdi,执行结果就是rdi存储了0x603110,而查看该地址可知其存储了8,所以rdi储值地址指向的是8;
换言之,当偏移量为0x10时,rdi的地址取决于0x10偏移量的那个地方存储地址是什么,而并没有线性关系。
而且能够发现,存储值和地址的区域都在同一片内存,例如上面0x6030f8存储了地址0x603110,而地址0x603110存储了8,这是一种类似二叉树指针的效果,使用x命令能够很方便得到整个二叉树结构:
x86_64是64位架构,虽然此处存储的地址都是低四字节(一般64位系统虚拟地址都在6字节内),但用64位宽查看比较直观,
8字节地址和8字节数据间隔存储:
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(gdb) x/128gx 0x6030f0
0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110
0x603100 <n1+16>: 0x0000000000603130 0x0000000000000000
0x603110 <n21>: 0x0000000000000008 0x0000000000603190
0x603120 <n21+16>: 0x0000000000603150 0x0000000000000000
0x603130 <n22>: 0x0000000000000032 0x0000000000603170
0x603140 <n22+16>: 0x00000000006031b0 0x0000000000000000
0x603150 <n32>: 0x0000000000000016 0x0000000000603270
0x603160 <n32+16>: 0x0000000000603230 0x0000000000000000
0x603170 <n33>: 0x000000000000002d 0x00000000006031d0
0x603180 <n33+16>: 0x0000000000603290 0x0000000000000000
0x603190 <n31>: 0x0000000000000006 0x00000000006031f0
0x6031a0 <n31+16>: 0x0000000000603250 0x0000000000000000
0x6031b0 <n34>: 0x000000000000006b 0x0000000000603210
0x6031c0 <n34+16>: 0x00000000006032b0 0x0000000000000000
0x6031d0 <n45>: 0x0000000000000028 0x0000000000000000
0x6031e0 <n45+16>: 0x0000000000000000 0x0000000000000000
0x6031f0 <n41>: 0x0000000000000001 0x0000000000000000
0x603200 <n41+16>: 0x0000000000000000 0x0000000000000000
0x603210 <n47>: 0x0000000000000063 0x0000000000000000
0x603220 <n47+16>: 0x0000000000000000 0x0000000000000000
0x603230 <n44>: 0x0000000000000023 0x0000000000000000
0x603240 <n44+16>: 0x0000000000000000 0x0000000000000000
0x603250 <n42>: 0x0000000000000007 0x0000000000000000
0x603260 <n42+16>: 0x0000000000000000 0x0000000000000000
0x603270 <n43>: 0x0000000000000014 0x0000000000000000
0x603280 <n43+16>: 0x0000000000000000 0x0000000000000000
0x603290 <n46>: 0x000000000000002f 0x0000000000000000
0x6032a0 <n46+16>: 0x0000000000000000 0x0000000000000000
0x6032b0 <n48>: 0x00000000000003e9 0x0000000000000000
0x6032c0 <n48+16>: 0x0000000000000000 0x0000000000000000
0x6032d0 <node1>: 0x000000010000014c 0x00000000006032e0
0x6032e0 <node2>: 0x00000002000000a8 0x0000000000000000
0x6032f0 <node3>: 0x000000030000039c 0x0000000000603300
0x603300 <node4>: 0x00000004000002b3 0x0000000000603310
0x603310 <node5>: 0x00000005000001dd 0x0000000000603320
0x603320 <node6>: 0x00000006000001bb 0x00000000006032d0
0x603330: 0x0000000000000000 0x0000000000000000
0x603340 <host_table>: 0x0000000000402629 0x0000000000402643
于是可以整理二叉树,例如开始时: 1
2
3
4
50x6030f0 <n1>: 0x0000000000000024 0x0000000000603110
0x603100 <n1+16>: 0x0000000000603130 0x0000000000000000
0x603110 <n21>: 0x0000000000000008 0x0000000000603190
0x603120 <n21+16>: 0x0000000000603150 0x0000000000000000
0x603130 <n22>: 0x0000000000000032 0x0000000000603170
起始值0x24(36),若输入数字小于36,取八字节偏移地址0x603110,否则取十六偏移地址地址0x603130,分别对应0x8和0x32,同理整理成整个二叉树如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15└─ 0x24
├─ 0x8
│ ├─ 0x6
│ │ ├─ 0x1 - null
│ │ └─ 0x7 - null
│ └─ 0x16
│ ├─ 0x14 - null
│ └─ 0x23 - null
└─ 0x32
├─ 0x2d
│ ├─ 0x28 - null
│ └─ 0x2f - null
└─ 0x6b
├─ 0x63 - null
└─ 0x3e9 -null1
2
3
4
5
6
7
8
9
10
11
12
13
14
15└─ 36
├─ 8
│ ├─ 6
│ │ ├─ 1 - null
│ │ └─ 7 - null
│ └─ 22
│ ├─ 20 - null
│ └─ 35 - null
└─ 50
├─ 45
│ ├─ 40 - null
│ └─ 47 - null
└─ 107
├─ 99 - null
└─ 1001 -null
通过回溯可以计算每个节点返回eax的数值,其中返回时eax = 0,左子树的回溯是乘2,右子树的回溯是乘2加1,所有计算结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
141: 0
7: 4
20: 2
35: 6
40: 1
47: 5
99: 3
1001: 7
6: 0
22: 2
45: 1
107: 3
8: 0
50: 120和22,这就是最终答案。
答案
综上,BombLab全部答案依次为(注意除了第1、6个phase外,其余phase答案都不唯一):
1
2
3
4
5
6
7Border relations with Canada have never been better.
1 2 4 8 16 32
4 389
3 0 DrEvil
ionefg
4 3 2 1 6 5
20
参考链接:
