CSAPP实验-BombLab

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 :
  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 
  400eee:	85 c0                	test   %eax,%eax
  400ef0:	74 05                	je     400ef7 
  400ef2:	e8 43 05 00 00       	callq  40143a 
  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 指令首先将将返回指令地址%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 指令当ZF被置位时jump 400ef7,从指令地址400ef7开始,后面的add $0x8,%rspretq指令作用是栈帧的空间清理并将控制权交还给phase_1函数的调用方;若ZF没有被置位则继续执行后面的callq 40143a ,即调用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 :
  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 
  401347:	41 89 c4             	mov    %eax,%r12d
  40134a:	48 89 ef             	mov    %rbp,%rdi
  40134d:	e8 c9 ff ff ff       	callq  40131b 
  401352:	ba 01 00 00 00       	mov    $0x1,%edx
  401357:	41 39 c4             	cmp    %eax,%r12d
  40135a:	75 3f                	jne    40139b 
  40135c:	0f b6 03             	movzbl (%rbx),%eax
  40135f:	84 c0                	test   %al,%al
  401361:	74 25                	je     401388 
  401363:	3a 45 00             	cmp    0x0(%rbp),%al
  401366:	74 0a                	je     401372 
  401368:	eb 25                	jmp    40138f 
  40136a:	3a 45 00             	cmp    0x0(%rbp),%al
  40136d:	0f 1f 00             	nopl   (%rax)
  401370:	75 24                	jne    401396 
  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 
  401381:	ba 00 00 00 00       	mov    $0x0,%edx
  401386:	eb 13                	jmp    40139b 
  401388:	ba 00 00 00 00       	mov    $0x0,%edx
  40138d:	eb 0c                	jmp    40139b 
  40138f:	ba 01 00 00 00       	mov    $0x1,%edx
  401394:	eb 05                	jmp    40139b 
  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 :
  40131b:	80 3f 00             	cmpb   $0x0,(%rdi)
  40131e:	74 12                	je     401332 
  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 
  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 :
  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 
  401448:	bf ac 25 40 00       	mov    $0x4025ac,%edi
  40144d:	e8 be f6 ff ff       	callq  400b10 
  401452:	bf 08 00 00 00       	mov    $0x8,%edi
  401457:	e8 c4 f7 ff ff       	callq  400c20 

该函数很短小,首先连续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 :
  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 
  400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)
  400f0e:	74 20                	je     400f30 
  400f10:	e8 25 05 00 00       	callq  40143a 
  400f15:	eb 19                	jmp    400f30 
  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 
  400f20:	e8 15 05 00 00       	callq  40143a 
  400f25:	48 83 c3 04          	add    $0x4,%rbx
  400f29:	48 39 eb             	cmp    %rbp,%rbx
  400f2c:	75 e9                	jne    400f17 
  400f2e:	eb 0c                	jmp    400f3c 
  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 
  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 :
  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 
  401494:	e8 a1 ff ff ff       	callq  40143a 
  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 :
  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 
  400f65:	e8 d0 04 00 00       	callq  40143a 
  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)
  400f6f:	77 3c                	ja     400fad 
  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 
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax
  400f88:	eb 34                	jmp    400fbe 
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax
  400f8f:	eb 2d                	jmp    400fbe 
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax
  400f96:	eb 26                	jmp    400fbe 
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax
  400f9d:	eb 1f                	jmp    400fbe 
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax
  400fa4:	eb 18                	jmp    400fbe 
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax
  400fab:	eb 11                	jmp    400fbe 
  400fad:	e8 88 04 00 00       	callq  40143a 
  400fb2:	b8 00 00 00 00       	mov    $0x0,%eax
  400fb7:	eb 05                	jmp    400fbe 
  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax
  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax
  400fc2:	74 05                	je     400fc9 
  400fc4:	e8 71 04 00 00       	callq  40143a 
  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 :
  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 
  40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)
  401033:	76 05                	jbe    40103a 
  401035:	e8 00 04 00 00       	callq  40143a 
  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 
  40104d:	85 c0                	test   %eax,%eax
  40104f:	75 07                	jne    401058 
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)
  401056:	74 05                	je     40105d 
  401058:	e8 dd 03 00 00       	callq  40143a 
  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 :
  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 
  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx
  400fe9:	e8 e0 ff ff ff       	callq  400fce 
  400fee:	01 c0                	add    %eax,%eax
  400ff0:	eb 15                	jmp    401007 
  400ff2:	b8 00 00 00 00       	mov    $0x0,%eax
  400ff7:	39 f9                	cmp    %edi,%ecx
  400ff9:	7d 0c                	jge    401007 
  400ffb:	8d 71 01             	lea    0x1(%rcx),%esi
  400ffe:	e8 cb ff ff ff       	callq  400fce 
  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 :
  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 
  40107f:	83 f8 06             	cmp    $0x6,%eax
  401082:	74 4e                	je     4010d2 
  401084:	e8 b1 03 00 00       	callq  40143a 
  401089:	eb 47                	jmp    4010d2 
  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 
  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 
  4010c2:	85 c0                	test   %eax,%eax
  4010c4:	74 13                	je     4010d9 
  4010c6:	e8 6f 03 00 00       	callq  40143a 
  4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
  4010d0:	eb 07                	jmp    4010d9 
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax
  4010d7:	eb b2                	jmp    40108b 
  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 
  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 
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 
401123:	e8 12 03 00 00       	callq  40143a 
401128:	41 83 c4 01          	add    $0x1,%r12d
40112c:	41 83 fc 06          	cmp    $0x6,%r12d
401130:	74 21                	je     401153 
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 
401140:	e8 f5 02 00 00       	callq  40143a 
401145:	83 c3 01             	add    $0x1,%ebx
401148:	83 fb 05             	cmp    $0x5,%ebx
40114b:	7e e8                	jle    401135 
40114d:	49 83 c5 04          	add    $0x4,%r13
401151:	eb c1                	jmp    401114 

这段指令是一个嵌套循环,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 

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

40116f:	be 00 00 00 00       	mov    $0x0,%esi
401174:	eb 21                	jmp    401197 
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 
401181:	eb 05                	jmp    401188 
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 
401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx
40119a:	83 f9 01             	cmp    $0x1,%ecx
40119d:	7e e4                	jle    401183 
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的核心,也是解题的关键,这段指令也有一个嵌套的循环,不得不说开启编译器优化的汇编代码可读性确实很差。首先我们看下这段指令中出现的一个地址: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 
4011cd:	48 89 d1             	mov    %rdx,%rcx
4011d0:	eb eb                	jmp    4011bd 
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 
4011e9:	e8 4c 02 00 00       	callq  40143a 
4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx
4011f2:	83 ed 01             	sub    $0x1,%ebp
4011f5:	75 e8                	jne    4011df 

到了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 :
  401242:	53                   	push   %rbx
  401243:	e8 56 02 00 00       	callq  40149e 
  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 
  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 
  401267:	e8 ce 01 00 00       	callq  40143a 
  40126c:	89 de                	mov    %ebx,%esi
  40126e:	bf f0 30 60 00       	mov    $0x6030f0,%edi
  401273:	e8 8c ff ff ff       	callq  401204 
  401278:	83 f8 02             	cmp    $0x2,%eax
  40127b:	74 05                	je     401282 
  40127d:	e8 b8 01 00 00       	callq  40143a 
  401282:	bf 38 24 40 00       	mov    $0x402438,%edi
  401287:	e8 84 f8 ff ff       	callq  400b10 
  40128c:	e8 33 03 00 00       	callq  4015c4 
  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 :
  401204:	48 83 ec 08          	sub    $0x8,%rsp
  401208:	48 85 ff             	test   %rdi,%rdi
  40120b:	74 2b                	je     401238 
  40120d:	8b 17                	mov    (%rdi),%edx
  40120f:	39 f2                	cmp    %esi,%edx
  401211:	7e 0d                	jle    401220 
  401213:	48 8b 7f 08          	mov    0x8(%rdi),%rdi
  401217:	e8 e8 ff ff ff       	callq  401204 
  40121c:	01 c0                	add    %eax,%eax
  40121e:	eb 1d                	jmp    40123d 
  401220:	b8 00 00 00 00       	mov    $0x0,%eax
  401225:	39 f2                	cmp    %esi,%edx
  401227:	74 14                	je     40123d 
  401229:	48 8b 7f 10          	mov    0x10(%rdi),%rdi
  40122d:	e8 d2 ff ff ff       	callq  401204 
  401232:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  401236:	eb 05                	jmp    40123d 
  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 :
  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 
  4015df:	75 5e                	jne    40163f 
  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 
  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 
  401613:	85 c0                	test   %eax,%eax
  401615:	75 1e                	jne    401635 
  401617:	bf f8 24 40 00       	mov    $0x4024f8,%edi
  40161c:	e8 ef f4 ff ff       	callq  400b10 
  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	callq  400b10 
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	callq  401242 
  401635:	bf 58 25 40 00       	mov    $0x402558,%edi
  40163a:	e8 d1 f4 ff ff       	callq  400b10 
  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 
  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 :
  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 
  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 :
  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 
  4012ae:	bf 03 00 00 00       	mov    $0x3,%edi
  4012b3:	e8 98 f9 ff ff       	callq  400c50 
  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 
  4012d8:	bf 01 00 00 00       	mov    $0x1,%edi
  4012dd:	e8 6e f9 ff ff       	callq  400c50 
  4012e2:	bf 8a 25 40 00       	mov    $0x40258a,%edi
  4012e7:	e8 24 f8 ff ff       	callq  400b10 
  4012ec:	bf 10 00 00 00       	mov    $0x10,%edi
  4012f1:	e8 2a f9 ff ff       	callq  400c20 

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试图拆解炸弹时,会弹出相应的提示,还蛮有意思的

你可能感兴趣的:(csapp,csapp)