🌺1. CSAPP与Bomb简介
🍀1.1 CSAPP
《CSAPP》是指计算机系统基础课程的经典教材《Computer Systems: A Programmer's Perspective》,由Randal E. Bryant和David R. O'Hallaron编写。该书的主要目标是帮助深入理解计算机系统的工作原理,包括硬件和软件的相互关系,其涵盖了计算机体系结构、汇编语言、操作系统、计算机网络等主题,旨在培养学生系统级编程和分析的能力。
🍀1.2 Bomb
"Bomb实验" 是与CSAPP教材相关的一项编程实验。它是一种反汇编和逆向工程任务,旨在教授如何分析和解决复杂的程序问题。Bomb实验的目标是解开一系列的"炸弹",每个炸弹都有不同的解锁方法,需要分析程序的汇编代码,理解其工作原理,并找到正确的输入来解除炸弹。这个实验教授了计算机系统的底层知识,包括汇编语言和程序执行的原理。
资源获取:关注文末公众号回复 CSAPP Bomb实验
🌺2. bomb
🍀2.1 实验环境
- VMware Workstation虚拟机环境下的Ubuntu 64位。
🍀2.2 实验过程
实验准备阶段:首先需要使用ubuntu联网环境跳转到链接下载实验所需的bomblab:Bomblab源文件
下载bomblab压缩包并输入
tar –xvf bomb.tar
进行解压缩,进入该目录所有文件如下所示:
在终端输入
sudo apt-get install gdb
安装调试器。基本用法参考下图:
实验过程阶段:
“Binary bombs”是一个可在Linux系统上运行的C程序,它由6个不同的阶段(phase1~phase6)组成。在每个阶段,程序会要求输入一个特定的字符串。如果输入的字符串符合程序的预期输入,那么这个阶段的炸弹就会被“解除”,否则炸弹就会“爆炸”,并输出“BOOM!!!”的提示信息。实验的目的是尽可能多地解除这些炸弹的阶段。
每个炸弹阶段考察了机器级语言程序的一个不同方面,难度逐级递增:
* 阶段1:字符串比较
* 阶段2:循环
* 阶段3:条件/分支
* 阶段4:递归调用和栈
* 阶段5:指针
* 阶段6:链表/指针/结构
在炸弹拆除任务中,还存在一个隐藏阶段。然而,只有在第四个阶段解决后添加特定的字符串后,该隐藏阶段才会出现。为了完成任务,需要使用gdb调试器和objdump反汇编炸弹的可执行文件,然后单步跟踪每个阶段的机器代码,理解每个汇编语言的行为或作用。这将帮助“推断”出拆除炸弹所需的目标字符串。为了调试,可以在每个阶段的开始代码前和引爆炸弹的函数前设置断点。
在终端输入
objdump -d bomb > bomb.asm
得到bomb的反汇编文件bomb.asm如下所示。
🍀2.3 Secret_phase
在bomb.c中存在这样一段话:“
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */”
说明这个炸弹之中还有一个隐藏关卡,寻找进入secret_phase 的入口
进入bomb.asm中发现了如下汇编代码:
需要找到 secret_phase 函数的入口,也就是调用了 secret_phase 的函数。在 bomb.asm 文件中搜索,发现 phase_defused 函数调用了 secret_phase 函数。而在 bomb.c 文件中,每个 phase 后面都有一个 phase_defused 函数调用。因此可以通过分析 phase_defused 函数来找到调用 secret_phase 函数的位置。
对phase_defused的反汇编内容如图:
解释详细如下:
00000000004015c4 <phase_defused>: 4015c4: 48 83 ec 78 sub $0x78,%rsp 4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4015cf: 00 00 4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp) 4015d6: 31 c0 xor %eax,%eax 4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings> //num_input_strings 表示我们已经输入了多少串字符串了,判断是否等于6, //如果不等于6,直接跳转到最下方,则secret_phase无法进入 //所以进入secret_phase的则先决条件是:完成phase 1 - 6 4015df: 75 5e jne 40163f <phase_defused+0x7b> 4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8 4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 4015f0: be 19 26 40 00 mov $0x402619,%esi 4015f5: bf 70 38 60 00 mov $0x603870,%edi 4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt> //调用sscanf函数 4015ff: 83 f8 03 cmp $0x3,%eax //判断返回值%eax是否等于3 401602: 75 31 jne 401635 <phase_defused+0x71> //如果返回值%eax不等于3的话,则跳转到最下方,跳过了401630 callq 401242 <secret_phase> //即secret_phase无法进入,所以必须要让sscanf函数的返回值为3 401604: be 22 26 40 00 mov $0x402622,%esi //%esi=0x402622 401609: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 40160e: e8 25 fd ff ff callq 401338 <strings_not_equal> //调用字符串比较函数,判断输入的字符串和%esi中存的字符串是否相等 401613: 85 c0 test %eax,%eax 401615: 75 1e jne 401635 <phase_defused+0x71> //若相等,则跳转至401635 //(gdb) print (char*) 0x402622 得到字符串 DrEvil //以下的代码不影响解码,暂且不做分析 401617: bf f8 24 40 00 mov $0x4024f8,%edi 40161c: e8 ef f4 ff ff callq 400b10 <puts@plt> 401621: bf 20 25 40 00 mov $0x402520,%edi 401626: e8 e5 f4 ff ff callq 400b10 <puts@plt> 40162b: b8 00 00 00 00 mov $0x0,%eax 401630: e8 0d fc ff ff callq 401242 <secret_phase> 401635: bf 58 25 40 00 mov $0x402558,%edi 40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt> 40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax 401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 40164b: 00 00 40164d: 74 05 je 401654 <phase_defused+0x90> 40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt> 401654: 48 83 c4 78 add $0x78,%rsp 401658: c3 retq 401659: 90 nop 40165a: 90 nop 40165b: 90 nop 40165c: 90 nop 40165d: 90 nop 40165e: 90 nop 40165f: 90 nop
在4015fa行的代码中,我们可以观察到调用了sscanf函数,它的作用是格式化读取指定的字符串。在调用sscanf函数之前,代码使用了两条mov语句,这两个参数分别是指定的字符串和格式化读取字符串。
根据代码可以猜测,我们需要输入两个数字。为了查看这几个参数对应的字符串,我们可以使用GDB调试器。我们可以先输入之前完成的字符串,并在0x4015fa处设置断点,最后查看断点处的参数。在gdb试探性输入print (char*)0x402619和print (char*)0x603870。
得到格式化字符串 %d %d %s ,而7 0就是phase 4的解码,联系sscanf函数的返回值%eax需要等于3,可以猜想需要在7 0 后面再输入一串字符串,即可进入隐藏关卡。
对phase_defused进行分析发现,在401604行出现了一个奇怪的地址为0x402622,在gdb输入print (char*)0x402622进行解析
为了进入隐藏关卡,我们需要在第四关的解码7 0后面加上字符串"DrEvil"。每次输入密钥可能会很繁琐,因此可以通过创建名为"bomb_idea.txt"的文件来存储所有的拆弹密码如下
并使用命令
./bomb bomb_idea.txt
来运行可执行文件。如果每一关调试结束后,我们可以将新的拆弹密码写入".txt"文件,这样就可以通过验证是否爆炸来避免重复输入之前关卡的拆弹密码。
系统提示成功找到了secret phase!
开始分析secret_phase内容:
Secret_phase汇编代码的解释内容如下:
0000000000401242 <secret_phase>: 401242: 53 push %rbx 401243: e8 56 02 00 00 callq 40149e <read_line> //调用read_line函数,读取字符串 401248: ba 0a 00 00 00 mov $0xa,%edx 40124d: be 00 00 00 00 mov $0x0,%esi 401252: 48 89 c7 mov %rax,%rdi 401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt> //调用strtol函数,将字符串转换为整型数据num,存在%rax中 40125a: 48 89 c3 mov %rax,%rbx //%rbx=%rax=num 40125d: 8d 40 ff lea -0x1(%rax),%eax 401260: 3d e8 03 00 00 cmp $0x3e8,%eax 401265: 76 05 jbe 40126c <secret_phase+0x2a> //num-1>1000(0x3e8),则会爆炸,所以输入的数字必须小于等于1001 401267: e8 ce 01 00 00 callq 40143a <explode_bomb> 40126c: 89 de mov %ebx,%esi //%esi=%ebx=num %esi存放输入的数据num,作为参数代入fun7 40126e: bf f0 30 60 00 mov $0x6030f0,%edi //%edi=6030f0 作为参数代入fun7 401273: e8 8c ff ff ff callq 401204 <fun7> //调用函数fun7 401278: 83 f8 02 cmp $0x2,%eax //将fun7的返回值%eax与2比较 //因为fun7为调用phase_defusd之前最后调用的一个函数,所以如果%eax=2,则跳过炸弹,拆弹成功! //所以需要对fun7进行分析 40127b: 74 05 je 401282 <secret_phase+0x40> 40127d: e8 b8 01 00 00 callq 40143a <explode_bomb> 401282: bf 38 24 40 00 mov $0x402438,%edi 401287: e8 84 f8 ff ff callq 400b10 <puts@plt> 40128c: e8 33 03 00 00 callq 4015c4 <phase_defused> 401291: 5b pop %rbx 401292: c3 retq 401293: 90 nop //nop 方便指令读取,不影响分析 401294: 90 nop 401295: 90 nop 401296: 90 nop 401297: 90 nop 401298: 90 nop 401299: 90 nop 40129a: 90 nop 40129b: 90 nop 40129c: 90 nop 40129d: 90 nop 40129e: 90 nop 40129f: 90 nop
由于 fun7 函数是调用 phase_defused 函数之前的最后一个函数,因此如果 fun7 函数的返回值 %eax = 2,那么炸弹就会被跳过,拆弹成功。因此需要对 fun7 函数进行分析。首先阅读 fun7 函数的源代码。
在gdb输入下列指令进行解析
x/150 0x6030f0
首先,查看0x6030f0中存放的数据,发现它类似于phase 6中的结构体。在6304xxx地址处应该是一个指针。同时,我们意外地发现,phase 6的指针数组就在下方。这里实际上是一个带有两个指针的结构体。前面的7个结构体的两个指针都是有值的,它们指向其他的结构体。而最后8个结构体的指针是没有值的,只有头部数据。这些指针所指的数据结构是一个二叉树。
fun7函数的逻辑较为复杂,为了便于之后的分析,将其转换为C语言的形式展示如下所示。
int func7(Type *p, int input) { if(p == NULL) return -1; if(&p <= input) { if(&p == input) return 0; else { p = p + 0x10; int n = func7(p, input); return 2 * n + 1; } } else { p = p + 0x8; int n = func7(p, input); return 2 * n; } }
需要得到返回值 %eax=2,说明递归顺序为:
1.最底层得到0 return 0
2.向上经过一层 %eax = %eax*2 + 1 得到1 return 1
3.再向上经过一层% eax = %eax*2 得到2 return 2
p所指向的数据结构是二叉搜索树,该树的结构为p = p + 0x10是加载右结点,p = p + 0x8是加载左结点。返回路径如下:
分析可得顺推思路:
1.首先,我们来到二叉树的首地址0x6030f0,对应的数据为36。因为36需要大于x,才能使得%eax = %eax*2成立。因此,指针值应该是%rdi + 8,即加载左结点。指针值为6304016,查看得到值为8。
2.在前文提到的分析过程中,需要注意节点 8 对应的位置。根据题目要求,需要让 %eax 的值乘 2 后再加 1,因此 8 的值需要小于等于 x。根据代码逻辑,我们需要加载右子节点,因此指针值为 0x603110 + 16,即 6304080。通过查看该位置内存的值,我们可以得到节点 8 的值为 22。因此可以推断出,对于输入的 x 值,当 x 大于等于 11 时,答案为 x*2+1;当 x 小于 11 时,答案为 fun7(0x6030f0, x)。
3.最后我们得到了数据22,当我们输入22的时候,因为和指针所处位置对应头部数据的值相等,所以%eax = 0。
因此22为可行解,如下。
在查看22对应的位置时,我们发现该位置还有两个指针,并且不是空指针。我们猜想,如果22大于所需解码,返回值为%eax = %eax*2,同样符合要求。那么指针值应该为0x603150 + 8,即加载左结点。指针值为6304368,查看得到值为20。然而,该位置指针为空,不再继续指向下一结点。因此,20也是一个可行解。
终端验证:
在bomb_idea.txt文件末尾添加20 22如下:
在终端输入
./bomb bomb_idea.txt
系统显示全部关卡通关成功。
🍀2.4 实验结果
以上代码均存储在bomb_idea.txt文件中,每行代表对应的关卡,各阶段密钥如下所示:
在终端输入
./bomb result.txt
显示全部通关。
🍀2.5 实验体会
- 实验环境深刻: BombLab实验在CSAPP课程中为学习者提供了深入理解计算机系统的机会。通过搭建实验环境,深刻感受了底层系统编程的挑战与乐趣。
- 逆向分析揭秘奥秘: 解密Secret_phase成为实验的重点,透过逆向分析的手法,揭示了程序内部的隐藏逻辑。这一过程不仅考验了逻辑思维,也拓展了对程序运行机制的认识。
- 程序攻击实战体验: 通过实际的程序攻击,实战演练了对炸弹的拆解过程。这不仅对计算机系统安全性有了更深层次的认识,同时也提高了解决问题和调试技能。整个实验过程既充实又充满挑战,为深入学习计算机系统打下了坚实基础。
📝 总结
计算机系统的世界,如同一座未被揭示奥秘的古老迷宫,引领你勇敢踏入计算机科学的神秘领域。CSAPP的Bomblab实验便是这场独特的学习冒险,从基本概念到底层实现,逐步揭示更深层次的计算机系统内核、汇编语言和数据结构的奥秘。