bomblab是csapp的第二个配套实验,该实验提供了一个bomb二进制文件和一个bomb.c源文件,我们的目标是运行bomb并按照提示一步步输入字符串,直到完成整个拆炸弹的流程。但是源文件中只提供了部分代码,所以我们需要通过反汇编工具 objDump 来分析bomb的汇编代码,推导出所有能够拆解炸弹的字符串。

准备工作

  • 概览
    首先分析实验提供的不完整的源文件bomb.c,在其main函数中依次调用phase_1到phase_6六个函数,每个函数都接受read_line()的返回作为phase的input参数,依次通过每个phase函数后main函数返回则炸弹拆解成功。可以推测read_line()的作用是从标准输入流读取一行字符串并返回,所以本实验的主要工作就是逆向分析6个phase函数的具体内容并找到对应的字符串。
  • 反汇编代码段
    进入目标文件bomb所在目录下,执行如下指令可以反汇编bomb的text section,并保存到 bomb_att_d.s 文件下,我们后面的主要工作都是分析其汇编指令。注意该指令默认生成AT&T风格的汇编代码,和Intel汇编的语法有所不同,后面都是以 AT&T 作为默认汇编语法。
    objdump -d bomb > bomb_att_d.s
  • 查看数据段
    仅有汇编指令是不足以完成整个逆向工程,我们还需要执行下面的命令,可以显示所有section的十六进制码和对应的ASCII字符,并保存到 bomb_att_s.s 文件下。其中最需要关注的部分就是数据段(包括data sectionrodata section)。data section保存了初始化的全局变量和静态局部变量,rodata保存了常数和字符串常量。
    objdump -s bomb > bomb_att_s.s

phase_1

在反汇编代码段文件bomb_att_s.s中找到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 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

由于该函数比较简单,可以作为切入点熟悉整个逆向流程(需要一定的x86汇编基础),下面进行逐句分析。
sub $0x8, rsp指令将栈指针寄存器减8,作用是给栈空间分配8字节。但是其实该函数并没有使用这段地址空间,这是开启了编译器优化后,为了满足x64 栈字节对齐(Align Stack Frame)的要求,或者说规范:函数调用时%rsp要能整除16字节,编译器不惜牺牲了部分内存,这使得程序提高了兼容性,也提高了程序的性能,所以常能见到函数调用前编译器先让%rsp自减8(尽管该函数本身不需要栈空间)。另外注意开启编译器优化选项后,编译器会通过静态分析在函数的起始将所有需要用到的栈空间分配完毕,而不是等待声明临时变量后再临时分配,这样做会节省部分指令。

在继续分析前需要熟悉下gcc在unix平台下编译x64指令的 调用约定fastcall:优先使用寄存器传递函数参数,传参顺序为rdi、rsi、rdx、rcx、r8、r9,多余的参数从右至左依次入栈;函数返回值保存在rax;栈帧由被调用者清理;调用者保存寄存器(易失)包括rax、rdi、rsi、rcx、rdx、r8、r9、r10、r11;被调用者保存寄存器包括rbx、rsp、rbp、r12、r13、r14、r15。以上约定均遵守 System V ABI 规范。

mov $0x402400, %esi指令将0x402400传送给esi寄存器,callq 401338 <strings_not_equal>指令首先将将返回指令地址%eip压栈(push %eip),然后将eip设为为目标指令地址处(jmp 401338)继续执行,相当于控制权转移。由于该目标文件编译时附带了符号表,所以objDump工具直接将函数的地址0x401338符号化为”string_not_equal”,顾名思义该函数返回两个字符串是否不相等。函数调用时传递了两个参数分别为rdi中phase_1接受的第一个参数和指定的参数0x402400,表示两个字符串的地址。我们在数据段文件bomb_att_s.s中定位地址0x402400如下:

402400 426f7264 65722072 656c6174 696f6e73  Border relations
402410 20776974 68204361 6e616461 20686176 with Canada hav
402420 65206e65 76657220 6265656e 20626574 e never been bet
402430 7465722e 00000000 576f7721 20596f75 ter.....Wow! You

可以发现这个地址在rodata数据段中,即表示一个字符串常量,从地址402400开始直到’\0’则是该字符串的内容:”Border relations with Canada have never been better.“,后面字符串常量也用相同的方法解析。
test %eax %eax指令的作用是检查eax是否为零, 并置上标志寄存器FR的ZF(零标志位),eax即为函数strings_not_equal的返回值。je 400ef7 <phase_1+0x17>指令当ZF被置位时jump 400ef7,从指令地址400ef7开始,后面的add $0x8,%rspretq指令作用是栈帧的空间清理并将控制权交还给phase_1函数的调用方;若ZF没有被置位则继续执行后面的callq 40143a <explode_bomb>,即调用explode_bomb函数引爆了炸弹。下面为函数phase_1的等价C代码:

void phase_1(char *input){
if(strings_not_equal(input, "Border relations with Canada have never been better.")){
explode_bomb();
}
}

phase_1所调用的关键函数:strings_not_equal 函数汇编指令内容如下:

0000000000401338 <strings_not_equal>:
401338: 41 54 push %r12
40133a: 55 push %rbp
40133b: 53 push %rbx
40133c: 48 89 fb mov %rdi,%rbx
40133f: 48 89 f5 mov %rsi,%rbp
401342: e8 d4 ff ff ff callq 40131b <string_length>
401347: 41 89 c4 mov %eax,%r12d
40134a: 48 89 ef mov %rbp,%rdi
40134d: e8 c9 ff ff ff callq 40131b <string_length>
401352: ba 01 00 00 00 mov $0x1,%edx
401357: 41 39 c4 cmp %eax,%r12d
40135a: 75 3f jne 40139b <strings_not_equal+0x63>
40135c: 0f b6 03 movzbl (%rbx),%eax
40135f: 84 c0 test %al,%al
401361: 74 25 je 401388 <strings_not_equal+0x50>
401363: 3a 45 00 cmp 0x0(%rbp),%al
401366: 74 0a je 401372 <strings_not_equal+0x3a>
401368: eb 25 jmp 40138f <strings_not_equal+0x57>
40136a: 3a 45 00 cmp 0x0(%rbp),%al
40136d: 0f 1f 00 nopl (%rax)
401370: 75 24 jne 401396 <strings_not_equal+0x5e>
401372: 48 83 c3 01 add $0x1,%rbx
401376: 48 83 c5 01 add $0x1,%rbp
40137a: 0f b6 03 movzbl (%rbx),%eax
40137d: 84 c0 test %al,%al
40137f: 75 e9 jne 40136a <strings_not_equal+0x32>
401381: ba 00 00 00 00 mov $0x0,%edx
401386: eb 13 jmp 40139b <strings_not_equal+0x63>
401388: ba 00 00 00 00 mov $0x0,%edx
40138d: eb 0c jmp 40139b <strings_not_equal+0x63>
40138f: ba 01 00 00 00 mov $0x1,%edx
401394: eb 05 jmp 40139b <strings_not_equal+0x63>
401396: ba 01 00 00 00 mov $0x1,%edx
40139b: 89 d0 mov %edx,%eax
40139d: 5b pop %rbx
40139e: 5d pop %rbp
40139f: 41 5c pop %r12
4013a1: c3 retq

简要分析下该函数,函数开始阶段先将r12、rbp、rbx三个通用寄存器入栈,是因为根据调用约定,这些寄存器为被调用者保存寄存器,该函数内部使用他们之前要将其入栈保存,待退出函数时再出栈还原这些寄存器的内容。40135c地址处的movzbl (%rbx) %eax指令作用是 零扩展字节拷贝,即将rbx指向地址处的一个字节(刚好存一个字符)传送至eax寄存器,高位补0。40136d地址处的nopl (%rax)作用仅是编译器开启优化后使指令按字对齐,减少取指令的时钟周期。后面的指令都是简单的跳转指令,可以看出当两个地址指向的字符串内容一致时返回0,否则返回1,下面为函数strings_not_equal的等价C代码:

int strings_not_equal(char *a, char *b){
if(string_length(a)!=string_length(b)){
return 1;
}
if(*a == '\0'){ //字符终止0x00
return 0;
}
while(1){
if(*a != *b){
return 1;
}
a++;
b++;
if(*a == '\0'){
return 0;
}
}
}

我们发现在string_not_equal函数中也调用了string_length函数,作用是返回字符串的长度,string_length 函数汇编指令内容如下:

000000000040131b <string_length>:
40131b: 80 3f 00 cmpb $0x0,(%rdi)
40131e: 74 12 je 401332 <string_length+0x17>
401320: 48 89 fa mov %rdi,%rdx
401323: 48 83 c2 01 add $0x1,%rdx
401327: 89 d0 mov %edx,%eax
401329: 29 f8 sub %edi,%eax
40132b: 80 3a 00 cmpb $0x0,(%rdx)
40132e: 75 f3 jne 401323 <string_length+0x8>
401330: f3 c3 repz retq
401332: b8 00 00 00 00 mov $0x0,%eax
401337: c3 retq

下面为函数string_length的等价C代码:

int string_length(char *input){
if(*input == '\0'){ //字符终止0x00
return 0;
}
int count = 0;
do{
count++;
}
while(input[count]!='\0');
return count;
}

至于引爆炸弹的函数 explode_bomb 函数汇编指令内容如下:

000000000040143a <explode_bomb>:
40143a: 48 83 ec 08 sub $0x8,%rsp
40143e: bf a3 25 40 00 mov $0x4025a3,%edi
401443: e8 c8 f6 ff ff callq 400b10 <puts@plt>
401448: bf ac 25 40 00 mov $0x4025ac,%edi
40144d: e8 be f6 ff ff callq 400b10 <puts@plt>
401452: bf 08 00 00 00 mov $0x8,%edi
401457: e8 c4 f7 ff ff callq 400c20 <exit@plt>

该函数很短小,首先连续puts两个字符串,然后调用exit函数退出,返回码为8,我们可以用和phase_1中相同的方法获取对应地址的字符串常量,我们整个拆除炸弹的流程都是在避免执行explode_bomb函数(或所在的条件分支)。下面为函数explode_bomb的等价C代码:

void explode_bomb(){
puts("BOOM!!!");
puts("The bomb has blown up.");
exit(8);
}

phase_1的答案为:Border relations with Canada have never been better.

phase_2

在反汇编代码段文件bomb_att_s.s中找到 phase_2 函数的汇编指令内容:

0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
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)
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
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
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
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
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

phase_2函数本身没有难点,主要是让大脑强制以机器的方式思考,分析各寄存器和栈帧空间在不同阶段所代表的变量和作用。但是phase_2调用了一个新的函数 read_six_numbers,顾名思义是读取六个整数,所以首先看下read_six_numbers的汇编指令内容:

000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
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
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

read_six_numbers函数的前面都是给参数寄存器赋值,分别为传入的参数input, 0x4025c3处的format字符串,传入数组的0-5元素的地址(lea指令用于取址),然后调用sscanf函数,当该函数的返回值大于5时返回(即将数组的所有元素均赋值),否则引爆炸弹。下面为函数read_six_numbers的等价C代码:

void read_six_numbers(char *input, int* arr){
int count = sscanf(input, "%d %d %d %d %d %d", &arr[0], &arr[1], &arr[2], &arr[3], &arr[4], &arr[5]);
if(count>5){
return;
}
explode_bomb();
}

phase_2函数首先开辟了40字节的栈空间,然后将传入的input指针和rsp栈顶指针作为参数传入read_six_numbers,返回时rsp指向赋值后的数组,其在栈上的地址空间为%rsp~18(%rsp),若此时的栈顶元素(即arr[0])不为1,则引爆炸弹。后面遍历整个数组,若后面的元素不为前面元素的2倍,则同样引爆炸弹。下面为函数phase_2的等价C代码:

void phase_2(char *input){
int arr[6];
read_six_numbers(input, arr);
if(arr[0]!=1){
explode_bomb();
}
for(int i=1; i<6; i++){
if(arr[i]!=2*arr[i-1]){
explode_bomb();
}
}
}

所以可推出phase_2的答案为:1 2 4 8 16 32

phase_3

在反汇编代码段文件bomb_att_s.s中找到 phase_3 函数的汇编指令内容:

0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
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
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)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
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
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
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

phase_3函数的前面部分和read_six_numbers类似,只不过该函数的format字符串为”%d %d“,即只读取两个整数,分别存储于0x8(%rsp)和0xc(%rsp)地址处,当读取整数小于等于1时或读取的第一个数大于7时都会引爆炸弹。最难理解的是位于400f75地址处的jmpq *0x402470(,%rax,8)指令,目的操作数使用了比较复杂的间接寻址,跳转的地址为0x402470+8*%rax,编译器通过这种方式实现地址索引表(也叫跳转表),用于 switch关键字 的编译实现,即将switch所有分支的指令跳转地址在编译期存放于数据段,然后执行时根据case值动态读取地址索引表并执行对应分支的跳转。指令中的0x402470即对应跳转表的起始地址,%rax存放读取的第一个整数并作为case的值,8对应地址变量的大小8字节(64位), 如下是数据段文件bomb_att_s.s中0x402470地址处的跳转表:

402470 7c0f4000 00000000 b90f4000 00000000  |.@.......@.....
402480 830f4000 00000000 8a0f4000 00000000 ..@.......@.....
402490 910f4000 00000000 980f4000 00000000 ..@.......@.....
4024a0 9f0f4000 00000000 a60f4000 00000000 ..@.......@.....

注意在unix中对应的地址数据在内存中以 小端字节序 存储,如0x402470处的双字数据为7c0f400000000000,按小端字节序起始其存储的地址为0x400f7c,也就是当%rax的值为0时,跳转至0x400f7c处的指令,跳转表中共8个地址分别对应case 0~7,每个分支都做了相同的事情:给%rax赋不同的值并跳转至400fbe处的指令,跳转指令对应的 break关键字 。最后判断第二个输入值是否和给switch分支中给%rax赋的值一致,如果不相等则引爆炸弹,否则正常返回。下面为函数phase_3的等价C代码:

void phase_3(char *input){
int a, b;
int count = sscanf(input, "%d %d", &a, &b);
if(count<=1){
explode_bomb();
}
if(a>7){
explode_bomb();
}
int key = a;
switch (a){
case 0: key = 0xcf; break;
case 1: key = 0x137; break;
case 2: key = 0x2c3; break;
case 3: key = 0x100; break;
case 4: key = 0x185; break;
case 5: key = 0xce; break;
case 6: key = 0x2aa; break;
case 7: key = 0x147; break;
default: explode_bomb(); a=0;//使用JA指令进行无符号数比较,大于7的都跳转至default分支(将负数视为无符号数,则<0和>7处于无符号数的同一个区间)
}
if(b!=key){
explode_bomb();
}
}

所以phase_3有八组答案分别对应每个case:0 2071 3112 7073 2564 3895 2066 6827 327

phase_4

在反汇编代码段文件bomb_att_s.s中找到 phase_4 函数的汇编指令内容:

000000000040100c <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)
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
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff callq 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq

phase_4函数的起始部分和phase_3几乎完全一样,声明两个临时变量并从标准输入读取两个整数。然后判断当读取数量不为2时或读取的第一个整数大于14时引爆炸弹,401048地址处是关键指令,调用了func4函数,传入了三个参数分别为从输入读取的第一个整数、0、14,且返回值不为0时引爆炸弹。最后又判断从输入读取的第二个整数不为0时引爆炸弹,下面为函数phase_4的等价C代码:

void phase_4(char *input){
int a, b;
int count = sscanf(input, "%d %d", &a, &b);
if(count!=2){
explode_bomb();
}
if(a>14){
explode_bomb();
}
int result = func4(a, 0, 14);
if(result!=0){
explode_bomb();
}
if(b!=0){
explode_bomb();
}
}

仅从phase_4的代码中已经可以确定第二个输入的整数值一定为0,关键在于func4的实现,确保传入的a能够使func4返回0即可。func4 函数汇编指令内容如下:

0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax
400fd4: 29 f0 sub %esi,%eax
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx
400fdb: 01 c8 add %ecx,%eax
400fdd: d1 f8 sar %eax
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24>
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff callq 400fce <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
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

说实话确实被这段又臭又长的代码恶心到了,分析时有几个关键点:要注意区分逻辑右移(shr)和算数右移(sar)的异同;要关注func4函数内部有对自身的递归调用。下面为函数func4的等价C代码:

int func4(int a, int b, int c){
int temp = c-b;
int value = (temp+(unsigned int)temp>>31)>>1+b;
//退化为 int value = c>>1
if(value <= a){
if(value >= a){
return 0;
}
b = value+1;
return 2*func4(a,b,c)+1;
}
c = value-1;
return 2*func4(a,b,c);
}

首先我们可以发现当b=0且c>0时,value的计算会 退化 为c>>1,而恰好对该函数的初次调用满足此条件,这样可以大大简化了我们后面的计算。所以当value的初次计算值(14>>1=7)与传入的第一个参数a相等时,func4返回0恰好满足条件,所以我们得到了一组解:7 0。但是不要忘了后面的分支也有可能返回0,显而易见2*func4(a,b,c)+1的返回值一定大于等于1,则该分支被排除;当value>a时,将value-1=6赋值于c并返回2*func4(a,b,c),返回0的条件依然是func4(a,b,c)为0,但此时的c=7-1=6,所以a的值为6>>1=3时该函数返回0;以此类推后面的递归调用,我们可以得到另外三组解:3 01 00 0
所以phase_4有四组答案分别为:7 03 01 00 0

phase_5

在反汇编代码段文件bomb_att_s.s中找到 phase_5 函数的汇编指令内容:

0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax
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
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
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)
4010b3: be 5e 24 40 00 mov $0x40245e,%esi
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
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 retq

该函数开始和结束的部分指令(地址空间401062-401078、4010d9-4010e9)比较难理解,其实都是编译器为了防止栈溢出攻击而插入的代码。忽略即可。

canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致, 一般32位编译器是在gs:14h,64位是在fs:28h,若一致则正常退出,如果是栈溢出或者其他原因导致canary的值发生变化,那么程序将执行___stack_chk_fail函数,继而终止程序。一般情况只为局部变量中含有数组的函数插入保护。

phase_5函数首先检查输入字符串的长度是否为6,不为6的话直接引爆炸弹,所以本题的答案是一个长度为6的字符串。后面的逻辑比较绕,40108b地址处的指令movzbl (%rbx,%rax,1),%ecx作用是将输入字符串通过rax累加器索引的 字符 保存到ecx寄存器中,指令and $0xf,%edx 0x4024b0(%rdx),%edx再将这个值和0xf进行与运算,作为0x4024b0地址处字符串的索引。我们可以通过检索数据段文件bomb_att_s.s找到该字符串:”maduiersnfotvbyl

4024b0 6d616475 69657273 6e666f74 7662796c  maduiersnfotvbyl

mov %dl,0x10(%rsp,%rax,1)指令的作用是将eax的低位字节(即存在edx中的字符,是前文中索引到的字符值)保存到0x10(%rsp)处的数组,同样通过rax累加器索引。当rax从0累加到6时停止循环,此时0x10(%rsp)数组被填充了6位后用movb $0x0,0x16(%rsp)在数组末尾添加\0字符使其称为一个完整的字符串,随后与0x40245e处的字符串常量(“flyers“)比较,如果不一致则引爆炸弹。

void phase_5(char *input){
if(string_length(input)!=6){
explode_bomb();
}
char str[6];
char phase_5_key[16] = {"maduiersnfotvbyl"};
int i = 0;
do{
str[i] = phase_5_key[input[i] & 0xf];
i++;
}
while(i!=6);
str[6]='\0';
if(strings_not_equal(str, "flyers") != 0){
explode_bomb();
}
}

在字符串中maduiersnfotvbyl分别索引‘f’、‘l’、‘y’、‘e’、‘r’、‘s’,对应的索引分别为9、15、14、5、6、7,这些值对应input[i]&0xf,由于和0xf进行与运算会将字节的高4位清零,上述索引值仅为字节的低4位的值,高4位理论上可以为任意值。所以我们可以根据ascii表中可输入字符的区间去尝试字节高4位。由于答案是所有符合条件字符的排列组合,所以不陈列所有答案了,当前四位为0100时,对应索引值和ascii符号分别为73(I)、79(O)、78(N)、69(E)、70(F)、71(G)。
所以phase_5答案之一为:IONEFG

phase_6

phase_6是最复杂的,嵌套了很多循环所以理解起来比较困难,下面进行分段解析:

4010fc:	48 83 ec 50          	sub    $0x50,%rsp
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 callq 40145c <read_six_numbers>
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d

首先指令将栈空间开辟80个字节大小,然后和phase_2一样调用read_six_numbers函数从标准输入流读取6个数字。将%rsp拷贝至r13和r14变量作为读取数组的地址,并将r12寄存器赋值为0

401114:	4c 89 ed             	mov    %r13,%rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%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
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>

这段指令是一个嵌套循环,r13用作遍历输入数组的指针,每次循环自增4。在每个循环中都会判断当前数组元素减1是否大于5,如果是则引爆炸弹;同时ebx作为该循环内嵌套循环的累加器,判断当前数组元素是否与其后面的任一数组元素相等,如果有相等则引爆炸弹。所以这段指令告诉我们输入的六个整数均小于等于6且互不相等,即分别为1、2、3、4、5、6,但顺序未知。

401153:	48 8d 74 24 18       	lea    0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>

这段指令的作用是遍历整个输入数组,用7减去数组元素的结果值作为每个元素的新值,即arr[i] = 7-arr[i];

40116f:	be 00 00 00 00       	mov    $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>

从这段代码开始是才是phase_6的核心,也是解题的关键,这段指令也有一个嵌套的循环,不得不说开启编译器优化的汇编代码可读性确实很差。首先我们看下这段指令中出现的一个地址:0x6032d0,在数据段文件bomb_att_s.s中定位地址0x6032d0如下:

6032d0 4c010000 01000000 e0326000 00000000  L........2`.....
6032e0 a8000000 02000000 f0326000 00000000 .........2`.....
6032f0 9c030000 03000000 00336000 00000000 .........3`.....
603300 b3020000 04000000 10336000 00000000 .........3`.....
603310 dd010000 05000000 20336000 00000000 ........ 3`.....
603320 bb010000 06000000 00000000 00000000 ................

从0x6032d0地址处开始的连续96个字节(6组数据)都是按照这种规律排列的:四字节数据、四字节index值(1-6)、下一组数据的地址(注意字节序)。所以我们能看出来实际这段地址存储的是一个链表,每个节点占用16个字节,节点Node定义如下:

struct Node {
int value;
int index;
struct Node * next;
};

此时我们在看上面的汇编代码,就容易理解得多了。首先将esi寄存器归零作为累加器(用来遍历输入数组),然后跳转至401197地址处,将输入数组的当前被esi索引的值赋给ecx,假定该值为n,则将数据段中Node数组的第n个元素的地址依次赋给以地址为20(%rsp)的指针数组,相当于用输入数组中的数字对给定Node数组进行排序。

4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)

这段指令的作用是遍历排好序的指针数组,令数组中第n个元素指向节点的next值为第n+1个元素指向的节点的地址(*arr[n]->next=arr[n+1]),当遍历到最后时,最后一个元素指向节点的next值赋值为NULL,使其成为一个完整的链表。

4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
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 callq 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>

到了phase_6最后的校验阶段,上面这段指令也很简单,遍历位于20(%rsp)的指针数组,当任一链表中前面节点的value值小于后面节点的value值时引爆炸弹,所以我们的输入值要保证创建一个value值逐渐递减的链表才能够拆除炸弹。根据前面数据段解析结果,最初的六个节点的value值分别为0x14c、0xa8、0x39c、0x2b3、0x1dd、0x1bb,所以根据他们的index排序结果为3、4、5、6、1、2,由于我们的初始输入数组做了arr[i] = 7-arr[i];运算,所以phase_6的最终答案为4 3 2 1 6 5

secret_phase

在bomb.c源码中的main函数返回前,这段很有意思的注释暗示了存在secret_phase:

/* Wow, they got it!  But isn't something... missing?  Perhaps
something they overlooked? Mua ha ha ha ha!*/

而且我们在反汇编bomb的时候也能够看到secret_phase紧随在phase_6后面,所以查看secret_phase仅在phase_defused函数中被调用,而该函数在每次炸弹拆解后调用一次,下面是 secret_phase 函数的汇编指令内容:

0000000000401242 <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
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
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

该函数首先像其它phase一样调用read_line作为输入字符串,然后使用strtol函数将字符串转为长整型数字,当该数字减1大于0x3e8时引爆炸弹,当传入地址0x6030f0和数字调用函数fun7的返回值不为2时引爆炸弹。fun7后面再解析,我们先在数据段文件中查看地址0x6030f0:

6030f0 24000000 00000000 10316000 00000000  $........1`.....
603100 30316000 00000000 00000000 00000000 01`.............
603110 08000000 00000000 90316000 00000000 .........1`.....
603120 50316000 00000000 00000000 00000000 P1`.............
603130 32000000 00000000 70316000 00000000 2.......p1`.....
603140 b0316000 00000000 00000000 00000000 .1`.............
...

可以发现该地址放的是一个数组,每个元素占32字节,其中第8-15和16-23两处分别为两个地址,很容易联想到这个数组是一个二叉树结构(其实是BST二叉搜索树),每个节点分别存储了左子节点和右子节点,每个节点所携带的数据存放于结构体的前8字节,至于最后八字节的内容则是编译器进行了字节对齐优化的结果,节点的定义如下:

struct Node {
long value;
struct Node * left;
struct Node * right;
};

下面的是secret_phase函数的等价C代码:

void secret_phase(){
char * input = read_line();
long number = strtol(input, NULL, 10);
if(number-1 > 0x3e8){
explode_bomb();
}
if(fun7(nodeArr, number)!=2){
explode_bomb();
}
puts("Wow! You've defused the secret stage!");
phase_defused();
}

接下来看下关键函数 fun7 的汇编指令内容:

0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff callq 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff callq 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
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

该函数内部有对自身的递归调用,但是整体逻辑比较清晰,当传入的节点指针的值为空时返回-1,当与传入的number相等时返回0,大于或小于时修改节点指针为某个子节点并递归调用fun7。下面为fun7的等价C代码:

int fun7(struct Node *nodePointer, long number){
if(nodeArr == NULL){
return -1;
}
if(nodePointer->value <= number){
if(nodePointer->value == number){
return 0;
}
else{
nodePointer = nodePointer->right;
return 2*fun7(nodePointer, number)+1;
}
}
else{
nodePointer = nodePointer->left;
return 2*fun7(nodePointer, number);
}
}

我们的需求时寻找给定的number值使fun7返回2,我们可以构造2*(2*(2*2*2…*0)+1)=2这条路径,也就是从该二叉树的树根开始,节点数据依次小于、大于、小于、小于…、等于number的值(节点路径从树根开始依次为左、右、左、左、左…)时func7返回2,最后满足条件的number值只有0x16,即secret_phase的最终答案为22
但是目前未知我们只是得到了secret_phase的答案,却仍然不知道怎样进入到secret_phase。我们搜索反汇编的代码段寻找对secret_phase函数的调用,发现是在phase_defused函数中调用的,每个phase通过后都会调用一次该函数,下面是 phase_defused 的汇编指令内容:

00000000004015c4 <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>
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
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi
401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
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

这段指令并没有什么难点,只有一处关键地址0x603870很难理解,全局搜索并没有找到其它地方引用该地址,所以可以推测0x603870是由某个数组变量的偏移所得到,其实在read_line函数中每次只从标准输入或文件中读取的80字节大小的字符串并从地址0x603780开始保存,而0x603870刚好是该地址偏移240字节,即0x603870其实是phase_4中输入的字符串,下面是phase_defused函数的等价C代码:

void phase_defused(){
int a,b;
char * str;
if(num_input_strings == 6){
int count = sscanf(input_strings,"%d %d %s",a,b,str);
if(count == 3){
if(!strings_not_equal(str, "DrEvil")){
puts("Curses, you've found the secret phase!");
puts("But finding it and solving it are quite different...");
secret_phase();
}
}
puts("Congratulations! You've defused the bomb!");
}
}

所以只有当解完6个phase并且phase_4中的第三个输入为DrEvil才会进入secret_phase。

彩蛋

在bomb.c的开始读取字符串输入前,调用过一个初始化函数initialize_bomb,我们看一下该函数的汇编指令:

00000000004013a2 <initialize_bomb>:
4013a2: 48 83 ec 08 sub $0x8,%rsp
4013a6: be a0 12 40 00 mov $0x4012a0,%esi
4013ab: bf 02 00 00 00 mov $0x2,%edi
4013b0: e8 db f7 ff ff callq 400b90 <signal@plt>
4013b5: 48 83 c4 08 add $0x8,%rsp
4013b9: c3 retq

这段指令将2和地址0x4012a0作为参数调用signal函数,而0x4012a0地址指向了函数sig_handler,可以合理推测这段代码用来处理信号。initialize_bomb等价C代码如下:

void initialize_bomb(){
signal(2, sig_handler);
}

接下来查看 sig_handler 的汇编指令:

00000000004012a0 <sig_handler>:
4012a0: 48 83 ec 08 sub $0x8,%rsp
4012a4: bf c0 24 40 00 mov $0x4024c0,%edi
4012a9: e8 62 f8 ff ff callq 400b10 <puts@plt>
4012ae: bf 03 00 00 00 mov $0x3,%edi
4012b3: e8 98 f9 ff ff callq 400c50 <sleep@plt>
4012b8: be 82 25 40 00 mov $0x402582,%esi
4012bd: bf 01 00 00 00 mov $0x1,%edi
4012c2: b8 00 00 00 00 mov $0x0,%eax
4012c7: e8 34 f9 ff ff callq 400c00 <__printf_chk@plt>
4012cc: 48 8b 3d 6d 24 20 00 mov 0x20246d(%rip),%rdi # 603740 <__bss_start>
4012d3: e8 08 f9 ff ff callq 400be0 <fflush@plt>
4012d8: bf 01 00 00 00 mov $0x1,%edi
4012dd: e8 6e f9 ff ff callq 400c50 <sleep@plt>
4012e2: bf 8a 25 40 00 mov $0x40258a,%edi
4012e7: e8 24 f8 ff ff callq 400b10 <puts@plt>
4012ec: bf 10 00 00 00 mov $0x10,%edi
4012f1: e8 2a f9 ff ff callq 400c20 <exit@plt>

sig_handler的等价C代码如下:

void sig_handler(int signal){
puts("So you think you can stop the bomb with ctrl-c, do you?");
sleep(3);
printf("Well...");
fflush(stdin);
sleep(1);
puts("OK. :-)");
exit(10);
}

可以看出实验的设计者皮了一下,当我们使用ctrl+c试图拆解炸弹时,会弹出相应的提示,还蛮有意思的