实验简介
本实验需要拆除一个“二进制炸弹”,“二进制炸弹”是一个可执行目标程序。运行时,它会提示用户键入6个不同的字符串。如果其中任何一个错误,炸弹就会“爆炸”。必须通过逆向工程和汇编语言知识,推导出六个字符串分别是什么。从而拆除炸弹。
开始实验
下载实验包后,会解压得到一个bomb.c源码和bomb.o可执行目标文件。bomb.c源码如下:
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;
}
可以看到代码中有许多没有给出定义(其实是定义在spport.h头文件中,这个头文件没有给出)的函数,也就是说我们仅仅通过bomb.c代码是不能得知我们要输入的六个字符串的,不过好在题目给出了代码编译出的可执行目标文件bomb.o,这样我们就有机会通过逆向工程确定bomb.c的源码了。
接下来运行可执行目标文件bomb.o,先随便输入个abc试一试效果:
果然会爆炸。。
看来现在可以对bomb.o的汇编代码进行分析了,使用objdump工具,将bomb.o的反汇编代码导入到txt文件中,这里我用
下面是主函数的反汇编代码:
0000000000400da0 <main>:
400da0: 53 push %rbx
400da1: 83 ff 01 cmp $0x1,%edi
400da4: 75 10 jne 400db6 <main+0x16>
400da6: 48 8b 05 9b 29 20 00 mov 0x20299b(%rip),%rax # 603748 <stdin@@GLIBC_2.2.5>
400dad: 48 89 05 b4 29 20 00 mov %rax,0x2029b4(%rip) # 603768 <infile>
400db4: eb 63 jmp 400e19 <main+0x79>
400db6: 48 89 f3 mov %rsi,%rbx
400db9: 83 ff 02 cmp $0x2,%edi
400dbc: 75 3a jne 400df8 <main+0x58>
400dbe: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400dc2: be b4 22 40 00 mov $0x4022b4,%esi
400dc7: e8 44 fe ff ff callq 400c10 <fopen@plt>
400dcc: 48 89 05 95 29 20 00 mov %rax,0x202995(%rip) # 603768 <infile>
400dd3: 48 85 c0 test %rax,%rax
400dd6: 75 41 jne 400e19 <main+0x79>
400dd8: 48 8b 4b 08 mov 0x8(%rbx),%rcx
400ddc: 48 8b 13 mov (%rbx),%rdx
400ddf: be b6 22 40 00 mov $0x4022b6,%esi
400de4: bf 01 00 00 00 mov $0x1,%edi
400de9: e8 12 fe ff ff callq 400c00 <__printf_chk@plt>
400dee: bf 08 00 00 00 mov $0x8,%edi
400df3: e8 28 fe ff ff callq 400c20 <exit@plt>
400df8: 48 8b 16 mov (%rsi),%rdx
400dfb: be d3 22 40 00 mov $0x4022d3,%esi
400e00: bf 01 00 00 00 mov $0x1,%edi
400e05: b8 00 00 00 00 mov $0x0,%eax
400e0a: e8 f1 fd ff ff callq 400c00 <__printf_chk@plt>
400e0f: bf 08 00 00 00 mov $0x8,%edi
400e14: e8 07 fe ff ff callq 400c20 <exit@plt>
400e19: e8 84 05 00 00 callq 4013a2 <initialize_bomb>
400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff callq 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
400e44: bf a8 23 40 00 mov $0x4023a8,%edi
400e49: e8 c2 fc ff ff callq 400b10 <puts@plt>
400e4e: e8 4b 06 00 00 callq 40149e <read_line>
400e53: 48 89 c7 mov %rax,%rdi
400e56: e8 a1 00 00 00 callq 400efc <phase_2>
400e5b: e8 64 07 00 00 callq 4015c4 <phase_defused>
400e60: bf ed 22 40 00 mov $0x4022ed,%edi
400e65: e8 a6 fc ff ff callq 400b10 <puts@plt>
400e6a: e8 2f 06 00 00 callq 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 callq 400f43 <phase_3>
400e77: e8 48 07 00 00 callq 4015c4 <phase_defused>
400e7c: bf 0b 23 40 00 mov $0x40230b,%edi
400e81: e8 8a fc ff ff callq 400b10 <puts@plt>
400e86: e8 13 06 00 00 callq 40149e <read_line>
400e8b: 48 89 c7 mov %rax,%rdi
400e8e: e8 79 01 00 00 callq 40100c <phase_4>
400e93: e8 2c 07 00 00 callq 4015c4 <phase_defused>
400e98: bf d8 23 40 00 mov $0x4023d8,%edi
400e9d: e8 6e fc ff ff callq 400b10 <puts@plt>
400ea2: e8 f7 05 00 00 callq 40149e <read_line>
400ea7: 48 89 c7 mov %rax,%rdi
400eaa: e8 b3 01 00 00 callq 401062 <phase_5>
400eaf: e8 10 07 00 00 callq 4015c4 <phase_defused>
400eb4: bf 1a 23 40 00 mov $0x40231a,%edi
400eb9: e8 52 fc ff ff callq 400b10 <puts@plt>
400ebe: e8 db 05 00 00 callq 40149e <read_line>
400ec3: 48 89 c7 mov %rax,%rdi
400ec6: e8 29 02 00 00 callq 4010f4 <phase_6>
400ecb: e8 f4 06 00 00 callq 4015c4 <phase_defused>
400ed0: b8 00 00 00 00 mov $0x0,%eax
400ed5: 5b pop %rbx
400ed6: c3 retq
400ed7: 90 nop
400ed8: 90 nop
400ed9: 90 nop
400eda: 90 nop
400edb: 90 nop
400edc: 90 nop
400edd: 90 nop
400ede: 90 nop
400edf: 90 nop
第一个炸弹
从bomb.c的代码片段可以看出,每道题目都是由一个函数开始的,我们在反汇编代码中找到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
可以看到在第二行,程序将一个好像是地址的变量赋给了%esi,然后调用了<strings_not_equal>函数,从字面上可以看出这个函数好像是判断字符串是否相等。
容易想到,mov $0x402400,%esi
这句指令是在为调用<strings_not_equal>构造参数,而此时寄存器%rdi的值并没有变,所以可以想到<strings_not_equal>函数有两个参数,一个是位于 0x402400 位置的数据,另一个就是 <phase_1> 本身的参数,也就是我们输入的字符串。
从<strings_not_equal>返回后,执行test指令,如果返回值不为0,那么就会执行<explode_bomb>,这个显然是让炸弹爆炸的函数,我们不能让程序执行到这里。
然后找到 0x402400:发现其位于可执行目标文件的.rodata节:
也就是说,第一个炸弹的逻辑是,输入的字符串与程序中内置的格式串比对,相同就通过测试。观察0x402400处的字节序列,可以发现答案就Border relations with Canada have never been better.
,这句话最后的句号坑了我很长时间,因为.rodata节里的不可见字符全都会显示成"."
。总的来说第一个炸弹十分简单。
第二个炸弹
找到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
可以看到有一个叫<read_six_numbers>的函数,字面意思是要读取6个数字的输入。而且注意到400f02: 48 89 e6 mov %rsp,%rsi
,它似乎需要栈指针作为第二个参数,和第一个炸弹一样,此时%rdi同样没有改变,说明<read_six_numbers>函数的第一个参数也是我们输入的字符串。
通过观察400f0e一行,可以得知调用<read_six_numbers>后,如果栈指针处的值为1,那么将跳转到400f30(核心代码所在处),否则直接执行炸弹。那么我们可以猜想如果我们以正确的格式输入六个数字的话,栈指针此时一定是等于0x1的。我们可以找到<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
可以看到整个函数的前半部分都在进行参数的构造,只是为了能够调用__isoc99_sscanf@plt,而之前我们推导过,这个函数的第二个参数%rsi其实是栈指针,也就是说这些操作%rsi的指令其实都是对栈指针的操作。这里的<__isoc99_sscanf>函数是动态链接进来的C标准库函数,在可执行目标文件里是看不到它的信息的,所以我们只能上网查这个函数的功能了。经过查资料,我大概可以知道这个函数在<read_six_numbers>里的作用是,将输入的六个整数保存在 (%rsp),(%rsp+0x4),(%rsp+0x8),(%rsp+0xc),(%rsp+0x10),(%rsp+0x14),这六个位置。也就是栈上的一片地址连续的空间中。
然后就是从位置400f30
处开始的核心代码了,可以看到指令定义了两个指针,%rbx和%rbp,一个指向 0x4(%rsp),一个指向 0x18(%rsp),这中间就是刚才我们输入的六个整数保存的位置。从:
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>
这几句可以看出,程序不断将 %rbx 与 -0x4(%rbx) 对比,400f1a 一句表明当且仅当 (%rbx) 是 -0x4(%rbx)的二倍时,炸弹才不会爆炸。
400f25: 48 83 c3 04 add $0x4,%rbx
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
这三句又说明,%rbx只有和 %rbp 相等时,才能结束程序。否则将%rbx的值加4(一个int的大小),之后跳回400f17。这明显是一个循环语句。刚才我们了解到 %rbp = 0x18(rbx),也就是说当%rbx = %rbp时,程序刚好扫描到我们输入的六个数字,而且每个数字都应该是前一个的二倍,第一个数字等于 (%rsp) = 0x1,那么这个炸弹就被成功解开了。答案是:
1 2 4 8 16 32
第三个炸弹
截至最终更新时,作者已经解开了四个炸弹,但这个实验总归还是比较繁琐,还剩下两个没有解开,而且像这样详细地复盘也比较费时间,剩下的之后再补 😃 。