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 section
和rodata 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 $0 x8,%rsp 400ee4: be 00 24 40 00 mov $0 x402400,%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 $0 x8,%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,%rsp
和retq
指令作用是栈帧的空间清理并将控制权交还给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 $0 x1,%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 $0 x1,%rbx 401376 : 48 83 c5 01 add $0 x1,%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 $0 x0,%edx 401386 : eb 13 jmp 40139b <strings_not_equal+0x63 > 401388 : ba 00 00 00 00 mov $0 x0,%edx 40138d : eb 0c jmp 40139b <strings_not_equal+0x63 > 40138f: ba 01 00 00 00 mov $0 x1,%edx 401394 : eb 05 jmp 40139b <strings_not_equal+0x63 > 401396 : ba 01 00 00 00 mov $0 x1,%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' ){ 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 $0 x0,(%rdi) 40131e: 74 12 je 401332 <string_length+0x17 > 401320 : 48 89 fa mov %rdi,%rdx 401323 : 48 83 c2 01 add $0 x1,%rdx 401327 : 89 d0 mov %edx,%eax 401329 : 29 f8 sub %edi,%eax 40132b: 80 3a 00 cmpb $0 x0,(%rdx) 40132e: 75 f3 jne 401323 <string_length+0x8 > 401330 : f3 c3 repz retq 401332 : b8 00 00 00 00 mov $0 x0,%eax 401337 : c3 retq
下面为函数string_length的等价C代码:
int string_length (char *input) { if (*input == '\0' ){ return 0 ; } int count = 0 ; do { count++; } while (input[count]!='\0' ); return count; }
至于引爆炸弹的函数 explode_bomb 函数汇编指令内容如下:
000000000040143a <explode_bomb>: 40143a: 48 83 ec 08 sub $0 x8,%rsp 40143e: bf a3 25 40 00 mov $0 x4025a3,%edi 401443 : e8 c8 f6 ff ff callq 400b10 <puts@plt> 401448 : bf ac 25 40 00 mov $0 x4025ac,%edi 40144d : e8 be f6 ff ff callq 400b10 <puts@plt> 401452 : bf 08 00 00 00 mov $0 x8,%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 $0 x28,%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 $0 x1,(%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 $0 x4,%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 $0 x28,%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 $0 x18,%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 $0 x4025c3,%esi 401485 : b8 00 00 00 00 mov $0 x0,%eax 40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> 40148f: 83 f8 05 cmp $0 x5,%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 $0 x18,%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 $0 x18,%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 $0 x4025cf,%esi 400f56: b8 00 00 00 00 mov $0 x0,%eax 400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> 400f60: 83 f8 01 cmp $0 x1,%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 $0 x7,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 $0 xcf,%eax 400f81: eb 3b jmp 400fbe <phase_3+0x7b > 400f83: b8 c3 02 00 00 mov $0 x2c3,%eax 400f88: eb 34 jmp 400fbe <phase_3+0x7b > 400f8a: b8 00 01 00 00 mov $0 x100,%eax 400f8f: eb 2d jmp 400fbe <phase_3+0x7b > 400f91: b8 85 01 00 00 mov $0 x185,%eax 400f96: eb 26 jmp 400fbe <phase_3+0x7b > 400f98: b8 ce 00 00 00 mov $0 xce,%eax 400f9d: eb 1f jmp 400fbe <phase_3+0x7b > 400f9f: b8 aa 02 00 00 mov $0 x2aa,%eax 400fa4: eb 18 jmp 400fbe <phase_3+0x7b > 400fa6: b8 47 01 00 00 mov $0 x147,%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 $0 x0,%eax 400fb7: eb 05 jmp 400fbe <phase_3+0x7b > 400fb9: b8 37 01 00 00 mov $0 x137,%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 $0 x18,%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 ; } if (b!=key){ explode_bomb(); } }
所以phase_3有八组答案分别对应每个case:0 207
、1 311
、2 707
、3 256
、4 389
、5 206
、6 682
、7 327
phase_4 在反汇编代码段文件bomb_att_s.s中找到 phase_4 函数的汇编指令内容:
000000000040100c <phase_4>: 40100c: 48 83 ec 18 sub $0 x18,%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 $0 x4025cf,%esi 40101f: b8 00 00 00 00 mov $0 x0,%eax 401024 : e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> 401029 : 83 f8 02 cmp $0 x2,%eax 40102c: 75 07 jne 401035 <phase_4+0x29 > 40102e: 83 7c 24 08 0e cmpl $0 xe,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 $0 xe,%edx 40103f: be 00 00 00 00 mov $0 x0,%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 $0 x0,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 $0 x18,%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; 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 0
、1 0
、0 0
。 所以phase_4有四组答案分别为:7 0
、3 0
、1 0
、0 0
phase_5 在反汇编代码段文件bomb_att_s.s中找到 phase_5 函数的汇编指令内容:
0000000000401062 <phase_5>: 401062 : 53 push %rbx 401063 : 48 83 ec 20 sub $0 x20,%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 $0 x6,%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 $0 xf,%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 $0 x1,%rax 4010a8: 48 83 f8 06 cmp $0 x6,%rax 4010ac: 75 dd jne 40108b <phase_5+0x29 > 4010ae: c6 44 24 16 00 movb $0 x0,0x16 (%rsp) 4010b3: be 5e 24 40 00 mov $0 x40245e,%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 $0 x0,%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 $0 x20,%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 $0 x50,%rsp 401100 : 49 89 e5 mov %rsp,%r13401103 : 48 89 e6 mov %rsp,%rsi401106 : 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 $0 x0,%r12d
首先指令将栈空间开辟80个字节大小,然后和phase_2一样调用read_six_numbers函数从标准输入流读取6个数字。将%rsp拷贝至r13和r14变量作为读取数组的地址,并将r12寄存器赋值为0
401114 : 4c 89 ed mov %r13,%rbp401117 : 41 8b 45 00 mov 0x0 (%r13),%eax40111b: 83 e8 01 sub $0 x1,%eax 40111e: 83 f8 05 cmp $0 x5,%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 $0 x1,%r12d40112c: 41 83 fc 06 cmp $0 x6,%r12d 401130 : 74 21 je 401153 <phase_6+0x5f >401132 : 44 89 e3 mov %r12d,%ebx401135 : 48 63 c3 movslq %ebx,%rax401138 : 8b 04 84 mov (%rsp,%rax,4 ),%eax40113b: 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 $0 x1,%ebx401148 : 83 fb 05 cmp $0 x5,%ebx40114b: 7e e8 jle 401135 <phase_6+0x41 > 40114d : 49 83 c5 04 add $0 x4,%r13401151 : 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),%rsi401158 : 4c 89 f0 mov %r14,%rax40115b: b9 07 00 00 00 mov $0 x7,%ecx 401160 : 89 ca mov %ecx,%edx401162 : 2b 10 sub (%rax),%edx401164 : 89 10 mov %edx,(%rax)401166 : 48 83 c0 04 add $0 x4,%rax40116a: 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 $0 x0,%esi 401174 : eb 21 jmp 401197 <phase_6+0xa3 >401176 : 48 8b 52 08 mov 0x8 (%rdx),%rdx40117a: 83 c0 01 add $0 x1,%eax 40117d : 39 c8 cmp %ecx,%eax40117f: 75 f5 jne 401176 <phase_6+0x82 > 401181 : eb 05 jmp 401188 <phase_6+0x94 >401183 : ba d0 32 60 00 mov $0 x6032d0,%edx401188 : 48 89 54 74 20 mov %rdx,0x20 (%rsp,%rsi,2 )40118d : 48 83 c6 04 add $0 x4,%rsi401191 : 48 83 fe 18 cmp $0 x18,%rsi401195 : 74 14 je 4011ab <phase_6+0xb7 >401197 : 8b 0c 34 mov (%rsp,%rsi,1 ),%ecx40119a: 83 f9 01 cmp $0 x1,%ecx 40119d : 7e e4 jle 401183 <phase_6+0x8f >40119f: b8 01 00 00 00 mov $0 x1,%eax 4011a4: ba d0 32 60 00 mov $0 x6032d0,%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 $0 x8,%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 $0 x0,0x8 (%rdx)
这段指令的作用是遍历排好序的指针数组,令数组中第n个元素指向节点的next值为第n+1个元素指向的节点的地址(*arr[n]->next=arr[n+1]),当遍历到最后时,最后一个元素指向节点的next值赋值为NULL,使其成为一个完整的链表。
4011da: bd 05 00 00 00 mov $0 x5,%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 $0 x1,%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:
而且我们在反汇编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 $0 xa,%edx 40124d : be 00 00 00 00 mov $0 x0,%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 $0 x3e8,%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 $0 x6030f0,%edi 401273 : e8 8c ff ff ff callq 401204 <fun7> 401278 : 83 f8 02 cmp $0 x2,%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 $0 x402438,%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 $0 x8,%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 $0 x0,%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 $0 xffffffff,%eax 40123d : 48 83 c4 08 add $0 x8,%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 $0 x78,%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 $0 x6,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 $0 x402619,%esi 4015f5: bf 70 38 60 00 mov $0 x603870,%edi 4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt> 4015ff: 83 f8 03 cmp $0 x3,%eax 401602 : 75 31 jne 401635 <phase_defused+0x71 > 401604 : be 22 26 40 00 mov $0 x402622,%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 $0 x4024f8,%edi 40161c: e8 ef f4 ff ff callq 400b10 <puts@plt> 401621 : bf 20 25 40 00 mov $0 x402520,%edi 401626 : e8 e5 f4 ff ff callq 400b10 <puts@plt> 40162b: b8 00 00 00 00 mov $0 x0,%eax 401630 : e8 0d fc ff ff callq 401242 <secret_phase> 401635 : bf 58 25 40 00 mov $0 x402558,%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 $0 x78,%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 $0 x8,%rsp 4013a6: be a0 12 40 00 mov $0 x4012a0,%esi 4013ab: bf 02 00 00 00 mov $0 x2,%edi 4013b0: e8 db f7 ff ff callq 400b90 <signal@plt> 4013b5: 48 83 c4 08 add $0 x8,%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 $0 x8,%rsp 4012a4: bf c0 24 40 00 mov $0 x4024c0,%edi 4012a9: e8 62 f8 ff ff callq 400b10 <puts@plt> 4012ae: bf 03 00 00 00 mov $0 x3,%edi 4012b3: e8 98 f9 ff ff callq 400c50 <sleep@plt> 4012b8: be 82 25 40 00 mov $0 x402582,%esi 4012bd: bf 01 00 00 00 mov $0 x1,%edi 4012c2: b8 00 00 00 00 mov $0 x0,%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 $0 x1,%edi 4012dd : e8 6e f9 ff ff callq 400c50 <sleep@plt> 4012e2: bf 8a 25 40 00 mov $0 x40258a,%edi 4012e7: e8 24 f8 ff ff callq 400b10 <puts@plt> 4012ec: bf 10 00 00 00 mov $0 x10,%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试图拆解炸弹时,会弹出相应的提示,还蛮有意思的