CSAPP之BombLab详解

博客 动态
0 185
羽尘
羽尘 2022-05-14 14:59:12
悬赏:0 积分 收藏

CSAPP 之 BombLab 详解

前言

本篇博客将会展示 CSAPP 之 BombLab 的拆弹过程,粉碎 Dr.Evil 的邪恶阴谋。Dr.Evil 的替身,杀手皇后,总共设置了 6 个炸弹,每个炸弹对应一串字符串,如果字符串错误,炸弹就会被引爆??,如下图所示:

炸弹引爆

字符串的长度未知,所以暴力破解是不可取的,也就是说这个实验就是要逼着拆弹小分队将 bomb 可执行文件反汇编,根据汇编代码推测出每个炸弹对应的字符串。在终端输入 objdump -d bomb > bomb.asm ,就可以将汇编代码写入 bomb.asm 文件中,方便后续分析。同时,Dr.Evil 还提供了 bomb.c 源文件,通过它可以看到炸弹程序的主要结构,代码如下:

/*************************************************************************** * Dr. Evil's Insidious Bomb, Version 1.1 * Copyright 2011, Dr. Evil Incorporated. All rights reserved. * * LICENSE: * * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the * VICTIM) explicit permission to use this bomb (the BOMB).  This is a * time limited license, which expires on the death of the VICTIM. * The PERPETRATOR takes no responsibility for damage, frustration, * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other * harm to the VICTIM.  Unless the PERPETRATOR wants to take credit, * that is.  The VICTIM may not distribute this bomb source code to * any enemies of the PERPETRATOR.  No VICTIM may debug, * reverse-engineer, run "strings" on, decompile, decrypt, or use any * other technique to gain knowledge of and defuse the BOMB.  BOMB * proof clothing may not be worn when handling this program.  The * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of * humor.  This license is null and void where the BOMB is prohibited * by law. ***************************************************************************/#include <stdio.h>#include <stdlib.h>#include "support.h"#include "phases.h"/* * Note to self: Remember to erase this file so my victims will have no * idea what is going on, and so they will all blow up in a * spectaculary fiendish explosion. -- Dr. Evil */FILE *infile;int main(int argc, char *argv[]){    char *input;    /* Note to self: remember to port this bomb to Windows and put a     * fantastic GUI on it. */    /* When run with no arguments, the bomb reads its input lines     * from standard input. */    if (argc == 1) {	infile = stdin;    }    /* When run with one argument <file>, the bomb reads from <file>     * until EOF, and then switches to standard input. Thus, as you     * defuse each phase, you can add its defusing string to <file> and     * avoid having to retype it. */    else if (argc == 2) {	if (!(infile = fopen(argv[1], "r"))) {	    printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);	    exit(8);	}    }    /* You can't call the bomb with more than 1 command line argument. */    else {	printf("Usage: %s [<input_file>]\n", argv[0]);	exit(8);    }    /* Do all sorts of secret stuff that makes the bomb harder to defuse. */    initialize_bomb();    printf("Welcome to my fiendish little bomb. You have 6 phases with\n");    printf("which to blow yourself up. Have a nice day!\n");    /* Hmm...  Six phases must be more secure than one phase! */    input = read_line();             /* Get input                   */    phase_1(input);                  /* Run the phase               */    phase_defused();                 /* Drat!  They figured it out!				      * Let me know how they did it. */    printf("Phase 1 defused. How about the next one?\n");    /* The second phase is harder.  No one will ever figure out     * how to defuse this... */    input = read_line();    phase_2(input);    phase_defused();    printf("That's number 2.  Keep going!\n");    /* I guess this is too easy so far.  Some more complex code will     * confuse people. */    input = read_line();    phase_3(input);    phase_defused();    printf("Halfway there!\n");    /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */    input = read_line();    phase_4(input);    phase_defused();    printf("So you got that one.  Try this one.\n");    /* Round and 'round in memory we go, where we stop, the bomb blows! */    input = read_line();    phase_5(input);    phase_defused();    printf("Good work!  On to the next...\n");    /* This phase will never be used, since no one will get past the     * earlier ones.  But just in case, make this one extra hard. */    input = read_line();    phase_6(input);    phase_defused();    /* Wow, they got it!  But isn't something... missing?  Perhaps     * something they overlooked?  Mua ha ha ha ha! */    return 0;}

不过我们也可以使用下述指令在终端查看 bomb.c

$ gdb bomb(gdb) l

可以看到六个炸弹分别对应着 phase_1()phase_6() 函数,有了这些信息就可以着手分析汇编代码了。

拆弹过程

第一炸弹

在汇编代码中搜索 phase_1,可以看到 phase_1 共出现三次,左侧是 phase_1() 被调用,右侧是 phase_1() 的代码:

phase_1 出现的地方

可以看到 phase_1 中把 $0x402400 写到了寄存器 %rsi 中,接着调用了 strings_not_equal 函数,相当于 strings_not_equal(%rdi, 0x402400),如果字符串不相等即 %rax 为 1 时,第一炸弹就会被引爆。容易猜到 0x402400 应该是正确字符串的起始地址,而 %rdi 中存的应该是拆弹小分队输入的字符串的起始地址。可以使用 GDB 来验证一下这个猜想:

输入字符串的起始地址

先在 phase_1 的第三行 callq 401338 <strings_not_equal> 处打上断点,然后运行程序并输入 zhiyiYo,使用 display /x $rdi 查看 %rdi 的值 0x603780,最后使用 x/8c 0x603780 查看从该地址开始的8个字符,发现和我们输入的一样(多了结束符),验证了上述猜想。

由于我们还不知道真实字符串的长度,只知道它的起始地址是 0x402400,所以可以试下长一点的,只要看到 \000 就能说明字符串结束了。

(gdb) x/64c 0x4024000x402400:       66 'B'  111 'o' 114 'r' 100 'd' 101 'e' 114 'r' 32 ' '  114 'r'0x402408:       101 'e' 108 'l' 97 'a'  116 't' 105 'i' 111 'o' 110 'n' 115 's'0x402410:       32 ' '  119 'w' 105 'i' 116 't' 104 'h' 32 ' '  67 'C'  97 'a'0x402418:       110 'n' 97 'a'  100 'd' 97 'a'  32 ' '  104 'h' 97 'a'  118 'v'0x402420:       101 'e' 32 ' '  110 'n' 101 'e' 118 'v' 101 'e' 114 'r' 32 ' '0x402428:       98 'b'  101 'e' 101 'e' 110 'n' 32 ' '  98 'b'  101 'e' 116 't'0x402430:       116 't' 101 'e' 114 'r' 46 '.'  0 '\000'        0 '\000'        0 '\000'        0 '\000'0x402438:       87 'W'  111 'o' 119 'w' 33 '!'  32 ' '  89 'Y'  111 'o' 117 'u'

将上述字符连接得到 Border relations with Canada have never been better.,这个就是第一炸弹的正确答案。

第一炸弹被拆除

第二炸弹 —— 枯萎穿心攻击

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 显示进行了被调用者保护,然后将 %rsp 栈指针减小 28 以腾出空间并将 %rsp 的值赋给 %rsi。接着调用了 read_six_numbers 函数,从名字可以看出这个函数应该用来读入 6 个数字,具体代码为:

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 可以看出,6个数字的地址分别为 %rsp%rsp+0x4%rsp+0x8%rsp+0xc%rsp+0x10%rsp+0x14。调用完 read_six_numbers 之后,phase_2 又将 (%rsp)0x1 进行比较,如果不相等就引爆第二炸弹。接下来的代码可以翻译为:

rbx = rsp + 0x4;rbp = rsp + 0x18;while (rbx != rbp) {    rax = 2 * M[rbx - 0x4];    if (rax != M[rbx]) {        explode_bomb();    }    rbx += 0x4;}

说明读入的 6 个数字应该是一个等比数列,且 a[n] = 2*a[n-1],由于 (%rsp) 为 1,后续的几个数字就是 2、4、8、16 和 32。测试一下发现第二炸弹确实被成功拆除了:

第二炸弹被解除

第三炸弹 —— 败者食尘

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 先读入了一个数字,接着跳转到 0x400f6a, 将 (%rsp + 0x8)0x7 进行比较,如果比它大就引爆炸弹,其中 (%rsp + 0x8) 的值就是我们输入的第一个数字。可以运行 GDB 来验证一下这个猜想,将断点打在 0x400f6a 处,对第三炸弹输入 8 9,结果如下图所示:

第三炸弹引爆

如果输入的第一个数字小于等于 7,就暂时不会引爆炸弹。接着运行了 jmpq *0x402470(,%rax,8) 指令,这个指令的作用就是让程序跳转到 M[0x402470 + %rax * 8] 处,此处 %rax 就是输入的第一个数字,由于每个地址 64 位为 8 个字节,所以将 %rax 乘上了 8。下图显示了跳转之前 %rax 的值和跳转表的 8 个地址:

第三炸弹跳转表

可以看到输入的第一个数字 0~7 分别对应着:0x400f7c0x400fb90x400f830x400f8a0x400f910x400f980x400f9f0x400fa6。在 phase_3 中,上述地址都是一条对 %rax 进行赋值的指令,赋的值的十进制为 207、311、707、256、389、206、682 和 327。接着将 (%rsp + 0xc)%rax 进行比较,如果不相等就引爆炸弹,否则退出 phase_3,解除炸弹。也就是说第三炸弹的前 2 个数字对应着 8 种正确答案(后面的字符串就无所谓了),当前两个数字为 1 311 时运行结果如下图所示:

第三炸弹被解除

第四炸弹

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 读入了两个数字(并且只允许输入两个),然后判断输入的第一个数字是否小于等于 14,大于 14 就会引爆炸弹。接着调用了 func4(输入的第一个数字, 0, 14) 。从 func4() 返回后判断 %eax 是否为 0,不为 0 就引爆炸弹,如果 (%rsp + 0xc) 也就是输入的第二个数字不为 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

func4 的操作可以使用如下的代码来描述:

do {    rsp -= 0x8;    rax = rdx;    rax -= rsi;        // 接下来的三行代码感觉没啥大用,因为正数逻辑右移 31 位之后一定为 0    rcx = rax;    rcx >>= 31 // 此处为逻辑右移    rax += rcx;        rax >>= 1; // rax = rax / 2    rcx = rax + rsi;        if (rcx <= rdi) {        rax = 0;                // 其实就是两个相等        if (rcx >= rdi) {            rsp += 0x8;            continue;        }                rsi = rcx + 1;        // 下面这条语句应该在 rsp -= 0x8 后执行        // M[rsp] = 0x401003;    } else {        rdx = rcx - 1;        // 下面这条语句应该在 rsp -= 0x8 后执行        // M[rsp] = 0x400fee;    }    } while (rsp != 0x40104d)

有上述伪代码可以看到,要想返回的时候 %rax 为 0,就一定不能走到第 23 行,不然执行完 retq 语句后会将 M[%rsp] 的值也就是 0x401003 送到 %rip,程序跳到 lea 0x1(%rcx),%esi 继续执行,之后 %rax 不可能变成 0。在第 23 行没被执行的情况下,%rsi 的值一直都是 0,%rcx 的值可以为下面几种:

  • 14/2 = 7,当 %rdi = 7 时可以直接返回 phase_4 并解除炸弹
  • (7 - 1) / 2 = 3,当 %rdi = 3 时会跳到 add %eax,%eax 1 次才返回 phase_4 并解除炸弹
  • (3 - 1) / 2 = 1,当 %rdi = 1 时会跳到 add %eax,%eax 2 次才返回 phase_4 并解除炸弹
  • (1 - 1) / 2 = 0,当 %rdi = 0 时会跳到 add %eax,%eax 3 次才返回 phase_4 并解除炸弹

来测试一下最后一种情况,因为它最复杂:

(gdb) b *0x40100bBreakpoint 1 at 0x40100b(gdb) rStarting program: /home/zhiyiyo/Documents/CSAPP/bomblab/bomb Welcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Border relations with Canada have never been better.Phase 1 defused. How about the next one?1 2 4 8 16 32That's number 2.  Keep going!0 207Halfway there!0 0Breakpoint 1, 0x000000000040100b in func4 ()(gdb) disassembleDump of assembler code for function func4:   0x0000000000400fce <+0>:     sub    $0x8,%rsp   0x0000000000400fd2 <+4>:     mov    %edx,%eax   0x0000000000400fd4 <+6>:     sub    %esi,%eax   0x0000000000400fd6 <+8>:     mov    %eax,%ecx   0x0000000000400fd8 <+10>:    shr    $0x1f,%ecx   0x0000000000400fdb <+13>:    add    %ecx,%eax   0x0000000000400fdd <+15>:    sar    %eax   0x0000000000400fdf <+17>:    lea    (%rax,%rsi,1),%ecx   0x0000000000400fe2 <+20>:    cmp    %edi,%ecx   0x0000000000400fe4 <+22>:    jle    0x400ff2 <func4+36>   0x0000000000400fe6 <+24>:    lea    -0x1(%rcx),%edx   0x0000000000400fe9 <+27>:    callq  0x400fce <func4>   0x0000000000400fee <+32>:    add    %eax,%eax   0x0000000000400ff0 <+34>:    jmp    0x401007 <func4+57>   0x0000000000400ff2 <+36>:    mov    $0x0,%eax   0x0000000000400ff7 <+41>:    cmp    %edi,%ecx   0x0000000000400ff9 <+43>:    jge    0x401007 <func4+57>   0x0000000000400ffb <+45>:    lea    0x1(%rcx),%esi   0x0000000000400ffe <+48>:    callq  0x400fce <func4>   0x0000000000401003 <+53>:    lea    0x1(%rax,%rax,1),%eax   0x0000000000401007 <+57>:    add    $0x8,%rsp=> 0x000000000040100b <+61>:    retq   End of assembler dump.(gdb) si0x0000000000400fee in func4 ()(gdb) disassembleDump of assembler code for function func4:   0x0000000000400fce <+0>:     sub    $0x8,%rsp   0x0000000000400fd2 <+4>:     mov    %edx,%eax   0x0000000000400fd4 <+6>:     sub    %esi,%eax   0x0000000000400fd6 <+8>:     mov    %eax,%ecx   0x0000000000400fd8 <+10>:    shr    $0x1f,%ecx   0x0000000000400fdb <+13>:    add    %ecx,%eax   0x0000000000400fdd <+15>:    sar    %eax   0x0000000000400fdf <+17>:    lea    (%rax,%rsi,1),%ecx   0x0000000000400fe2 <+20>:    cmp    %edi,%ecx   0x0000000000400fe4 <+22>:    jle    0x400ff2 <func4+36>   0x0000000000400fe6 <+24>:    lea    -0x1(%rcx),%edx   0x0000000000400fe9 <+27>:    callq  0x400fce <func4>=> 0x0000000000400fee <+32>:    add    %eax,%eax   0x0000000000400ff0 <+34>:    jmp    0x401007 <func4+57>   0x0000000000400ff2 <+36>:    mov    $0x0,%eax   0x0000000000400ff7 <+41>:    cmp    %edi,%ecx   0x0000000000400ff9 <+43>:    jge    0x401007 <func4+57>   0x0000000000400ffb <+45>:    lea    0x1(%rcx),%esi   0x0000000000400ffe <+48>:    callq  0x400fce <func4>   0x0000000000401003 <+53>:    lea    0x1(%rax,%rax,1),%eax   0x0000000000401007 <+57>:    add    $0x8,%rsp   0x000000000040100b <+61>:    retq   End of assembler dump.(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.Breakpoint 1, 0x000000000040100b in func4 ()(gdb) cContinuing.So you got that one.  Try this one.

可以看到当输入的第一数字为 0 时,确实跳转到 add %eax,%eax 才能返回,最终第四炸弹被成功拆除。

第五炸弹

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

可以看到 phase_5 开启了金丝雀保护机制,用于防范缓冲区溢出可能造成的安全问题。接着判断输入的字符串的长度,如果不为 6 就引爆炸弹。接下来的代码可以翻译为:

rbx = rdi; // %rdi 保存了输入的字符串的起始地址for (rax = 0; rax < 6; rax++) {    rcx = M[rbx + rax];    M[rsp] = cl;	// %rcx 低八位,将值限制在 0~255 之间    rdx = M[rsp];    rdx &= 0xF;		// 将 %rdx 的值限制在 0~15 之间    rdx = M[rdx + 0x4024b0];    M[rsp + rax + 0x10] = dl; // %rdx 的低八位}M[rsp + 0x16] = 0;rsi = 0x40245e;rdi = rsp + 0x10;if (strings_not_equal(rdi, rsi)) {    explode_bomb()}

根据第一炸弹的套路,%rsi 中存的应该是正确字符串的起始地址,由于字符串的长度为 6,所以结果如下图所示:

第四炸弹的

可以看到字符串是 flyers,当然第五炸弹可能没有第一炸弹那么简单,直接将 flyers 输到终端就企图解除炸弹只会无功而返,毕竟在 strings_not_equal 之前还有一段干了脏活的代码。再来认真分析一下 for 循环里面都发生了什么。

由于 %rdx 保存了输入的字符串的起始地址,所以 M[rbx + rax] 取出了字符串的一个字符,只留下字符的低 8 位并赋值给 %rdx%rdx 更进一步,只留下了低四位(取值范围 0~15)。之后 rdx = M[rdx + 0x4024b0]0x4024b0 为基地址,加上 0~15 的偏移量,从内存中取出了一个字符并赋给 %rdx。最后把 %rdx 的低 8 位赋值给 M[rsp + rax + 0x10],由于循环进行了 6 次,所以 strings_not_equal 是将 %rsp + 0x10%rsp + 0x15 组成的字符串和 flyers 进行比较。

下图显示了 0x4024b0 开始的 16 个( rdx &= 0xF )字符,里面出现了 flyers 所需的 6 个字母:

0x4024b0 开始的 16 个字符

只要让给 0x4024b0 加上 9 就能取到 f,所以字符串的第一个字符应该是 9。而为了取到 l,我们应该给 0x4024b0 加上 15,但是这里对应的字符就不该是 15 的十六进制 F,而应该是低四位全为 1 的某个字符,字符 ? 的的二进制值为 0b11111,满足低四位全 1 的条件,所以字符串的第二位取 ?。根据这个原理可以分别确定出后面几个字符为 >567,最终结果是 9?>567

第五炸弹被拆除

第六炸弹

phase_6 的汇编代码如下所示:

00000000004010f4 <phase_6>:  4010f4:	41 56                	push   %r14  4010f6:	41 55                	push   %r13  4010f8:	41 54                	push   %r12  4010fa:	55                   	push   %rbp  4010fb:	53                   	push   %rbx  4010fc:	48 83 ec 50          	sub    $0x50,%rsp  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  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>  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>  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>  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)  4011d9:	00  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>  4011f7:	48 83 c4 50          	add    $0x50,%rsp  4011fb:	5b                   	pop    %rbx  4011fc:	5d                   	pop    %rbp  4011fd:	41 5c                	pop    %r12  4011ff:	41 5d                	pop    %r13  401201:	41 5e                	pop    %r14  401203:	c3                   	retq

这段代码体积有点大,不过拆弹小分队还是得忍一下。phase_6 首先读入了 6 个数字,接着从 0x40110e0x401151 的代码可以翻译为:

r12d = 0;while (True) {    rbp = r13;		// 输入的数字的地址    rax = M[r13];    rax -= 1;        // M[r13] 的取值范围在 1~6 之间    if (rax > 5 ) {        explode_bomb();    }        r12d += 1;    if (r12d == 6) break;        for (rbx = r12d; rbx <= 5; rbx++) {        rax = rbx;        if (rax == M[rsp]) {            explode_bomb();        }    }        r13 += 4;}

上述代码中 %r13 存储的是输入的数字的地址,如下图所示:

输入数字的地址

根据上述代码可以确定输入的数字应该在 1~6 之间,并且不能重复,不然就会引爆第六炸弹。从 0x4011530x40116d 的代码可以翻译为:

rsi = rsp + 18;rax = r14;  // 输入数字的开始地址do {    rdx = 7 - M[rax];    M[rax] = rdx;    rax += 4;} while (rsi != rax)

此处 %r14%r13 一样存的是输入数字的开始地址(不贴图了),而上述代码的作用就是用 7 减去每个输入的数字并作为新值。接下来的代码就有点混乱了,反复横跳,所以这里先看下最后一段代码 0x4011da0x4011f5 的翻译:

for (rbp = 5; rbp > 1; rbp--) {    rax = M[rbx + 8];    eax = M[rax];		// 只保留四个字节,高位填充 0        if (M[rbx] < eax) {        explode_bomb();    }        rbx = M[rbx + 8];	// rbx 滞后 rax}

由此可见 %rbx 是一个二重指针,需要解引用两次才能拿到正确的值,这段代码用来确定一段内存是否已降序排列,如果不满足降序条件就会引爆第六炸弹。如果输入的 6 个数字为 6~1,在开始检查是否满足倒序条件之前暂停程序,可以看到内存中以 %rbx 为起始地址的 12 个 8 字节数,其中左侧一列的低 32 位用来比较大小,右侧一列用来作为 %rax 和下次 %rbx 存放的地址:

%rbx 对应的内存内容

根据上述信息,再来审视没提及的那一部分汇编代码,其实就是根据用户输入的数字,改变右侧一栏的地址和 %rbx 的初始值。如果将 %rbx 的初始值设置为 0x6032f0,沿着左侧一栏向下,到底之后回到 0x6032d0,一路得到的序列就是递减的。由此可以得到输入的数字为 4 3 2 1 6 5(被 7 减过得到的)。结果如下:

第六炸弹被解除

总结

通过这次实验,可以熟悉 GDB 调试工具的使用方法,同时也能看懂一些汇编代码了(不过有一说一,第六炸弹的代码真的就是又臭又长)。拆了这么久的炸弹,中间还失败过好多次,希望人质没事,以上~~

posted @ 2022-05-14 13:49 之一Yo 阅读(1) 评论(0) 编辑 收藏 举报
回帖
    羽尘

    羽尘 (王者 段位)

    2335 积分 (2)粉丝 (11)源码

     

    温馨提示

    亦奇源码

    最新会员