type
date
slug
summary
status
tags
category
password
Last edited time
Apr 25, 2025 10:09 AM
icon
系WHU2021级《系统级程序设计》实验1,节选于CSAPP Lab。
引言
任务介绍
本实验要求你使用课程所学知识拆除“binary bombs”,增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。
一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 "BOOM!!!"。
实验的目标是拆除尽可能多的炸弹层次。 每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:
- 阶段1:字符串比较
- 阶段2:循环
- 阶段3:条件/分支
- 阶段4:递归调用和栈
- 阶段5:指针
- 阶段6:链表/指针/结构
另外还有一个隐藏阶段,只有当你在第4阶段的解后附加一特定字符串后才会出现。
为完成二进制炸弹拆除任务,你需要使用gdb调试器和objdump来反汇编炸弹的可执行文件并跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。(比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点)
实验语言:C;实验环境:Linux
实验步骤
第一步:获取Bomb
在远程桌面的浏览器中打开
http://172.16.2.207:15213
(或打开桌面上的Bomblab Download Page),在二进制炸弹请求表格中输入你的学号和邮箱地址,点击Submit按钮。服务器会构造属于你的炸弹,并以tar文件的形式bombXXX.tar
返回给你,其中XXX是一个你的bomb的唯一标识。解压该tar文件(
tar -xvf bombX.tar
)得到一个目录./bombXXX
,其中包含如下文件:- README:标识该bomb和所有者。
- bomb:bomb的可执行程序。
- bomb.c:bomb程序的main函数。
第二步:拆除Bomb
本实验的任务就是拆除炸弹。一定要在指定的虚拟机上完成作业,在其他的环境上运行有可能导致失败。
运行
./bomb
可执行程序需要0或1个命令行参数(详见bomb.c
源文件中的main()
函数)。如果运行时不指定参数,则该程序打印出欢迎信息后,期望你按行输入每一阶段用来拆除炸弹的字符串,根据你当前输入的字符串决定你是通过相应阶段还是炸弹爆炸导致任务失败。你也可将拆除每一阶段炸弹的字符串按行组织在一个文本文件中,然后作为运行程序时的唯一一个命令行参数传给程序,程序读入文件中的每一行直到遇到EOF,再转到从stdin等待输入。这样对于你已经拆除的炸弹,就不用每次都重新输入,只用放进文件里即可。
前四个阶段每个10分,第五和第六阶段更难一些,每个15分,满分70分。每输入错误一次,炸弹爆炸,会扣除0.5分(最多扣除20分)。所以你必须小心!要学会单步跟踪调试汇编代码以及学会设置断点。你还要学会如何检查寄存器和内存状态。很好的使用调试器是你在未来的职业生涯中赚到更多money的一项重要技能!
第三步:提交结果
这是一项独立实验,每个人单独完成。bomb程序会自动发送结果到服务器,可以在
http://172.16.2.207:15213/scoreboard
(或打开桌面上的Bomblab Scoreboard)查看所有人的成绩结果。由于不同班要求不同,不用点击“提交评测”,以scoreboard分数为准。
提示
下面简要说明完成本实验所需要的一些实验工具:
gdb
为了从二进制可执行程序
./bomb
中找出触发bomb爆炸的条件,可使用gdb
来帮助对程序的分析。GDB是GNU自由软件组织发布的一个强大的交互式程序调试工具。一般来说,GDB主要帮忙你完成下面几方面的功能(更详细描述可参看GDB文档和相关资料):- 装载、启动被调试的程序。
- 让被调试的程序在你指定的调试断点处中断执行,方便查看程序变量、寄存器、栈内容等运行现场数据。
- 动态改变程序的执行环境,如修改变量的值。
gdb相关资料:
objdump
objdump –t
该命令可以打印出bomb的符号表。符号表包含了bomb中所有函数、全局变量的名称和存储地址。你可以通过查看函数名得到一些目标程序的信息。
objdump –d
该命令可用来对bomb中的二进制代码进行反汇编。通过阅读汇编源代码可以发现bomb是如何运行的。但该命令不能告诉你bomb的所有信息,例如一个调用sscanf函数的语句可能显示为:
8048c36: e8 99 fc ff ff call 80488d4 <_init+0x1a0>
,你还需要gdb来帮助你确定这个语句的具体功能。strings
该命令可以显示二进制程序中的所有可打印字符串。
实验步骤提示
下面以第一阶段(第一关)为例介绍实验步骤:首先调用
objdump –d bomb > bomb_disas.txt
对bomb进行反汇编并将汇编源代码输出到boomb_disas.txt
文本文件中。查看该汇编源代码文件,我们可以在main函数中找到如下语句,从而得知第一关的处理程序包含在main()
函数所调用的函数phase_1()
中,判断的过程可以参照bomb.c文件源码。汇编代码中地址0x14e8处调用了phase_1
函数,
我们在反汇编代码中寻找这个子函数
phase_1
:
可以看到这个子函数比较小,只有几行汇编代码,可以进行简单阅读(如果汇编代码较多,不建议逐句阅读,而是借用gdb调试工具进行辅助):我们看到(教科书中已经提到过调用函数的过程),………, 还调用了
string_not_equal
函数,接着测试%eax
是否为零,如果是就跳转到+0x1d
处,否则就调用explode_bomb
,可以判定这是一个判断两个字符串是否相等的过程。 接下来,使用gdb调试bomb二进制文件:gdb bomb
后,运行break phase_1
,也就是在phase_1
函数处设置一个断点,然后运行run
,开始调试。运行disa phase_1
,得到如下信息:
可以看到
rsi
中装入的就是待比较的目标字符串地址,后面给出的0x555555557150就是计算出来的值。可以输入print (char *)0x555555557150
,输出是
也可以
stepi
运行到0x00005555555555f6,然后print /x $rsi
得到,
说明%rsi的值确实是0x555555557150,目标字符串的起始位置。 接下来,去设置断点去检测这个答案是否正确,我们kill当前程序的调试。再在
explode_bomb
处设置断点break explode_bomb
,然后run
开始运行,按照提示输入这个字符串:“When I get angry, Mr. Bigglesworth gets upset.”(不包括引号)
第一关解除。
特别注意
不要试图去修改发送给服务器端的数据,一旦服务器端检测到错误数据,会自动记录invalid成绩,后续提交的结果及时正确也将无法记录。
参考解说视频
这里还有一段对实验操作的解说视频,供大家参考: 链接:
提取码: 9v9s
实验记录
phase | 答案字符串 | 备注 |
1 字符串比较 | The future will be better tomorow. | ㅤ |
2 循环 | 0 1 3 6 10 15 | ㅤ |
3 条件/分支switch | 1 r461 | 每个分支都对应一个答案 |
4 递归 | 5 2 DrEvil | DrEvil用于触发secret phase |
5 指针 | 0 48 | ㅤ |
6 链表/指针/结构 | 4 5 3 1 2 6 | ㅤ |
secret 二叉树 | 1001 | ㅤ |
调试操作:
基本思路:盯着
callq 401689 <explode_bomb>
语句,拐弯不要进去,慢慢凑出答案附:Bomb文件
文件很长,谨慎点开
bomb.c
反汇编文件bomb_disas
phase_1 字符串比较
可知其进行的是字符串比较,待比较的基准字符串地址为
0x4025e0
;于是gdb调试运行:
print (char *)0x4025e0
,结果如下: 
可知phase_1的目标字符串是
The future will be better tomorrow.
phase_1
phase_2 循环
答案是
0 1 3 6 10 15
phase_2
phase_3 条件/分支switch
题目考察switch分支结构,涉及转发表的查看和理解,但题目的难点在于发现输入的读取格式。
此题的关键在于发现函数
<__isoc99_sscanf@plt>
的读取格式:终端输入
x/s 0x40262e
,以字符串形式读取,结果如下:
可知其读取格式为
<int char int>
,其中后两者之间可以不空格之前一直以为是三个整数,然后总是发现读第二个数会覆盖第三个数,其实就是因为程序将第二个数拆开读——第一个数字作为字符,剩下的部分作为整数——之后忽略第三个数
可知跳转表的起始地址为
0x402640
,且应该包含有8个地址,查看如下:
本题的每一个switch都对应一个答案。对于
.L1
,答案是1 r461
。phase_3
phase_4 递归
此题主要要将递归函数
func4()
给捋明白,可写成C语言样式辅助理解。答案是
5 2
phase_4
func4
写成C语言大概是:
phase_5 指针
已知一个数组,根据题目要求来安排遍历的起点,规定了遍历的规则、次数和终点。
根据第
40117f
行中的地址,可以通过gdb查看该数组中的元素:
可知,我们需要选定遍历数组的起点,使得遍历的次数刚好为6,且在元素
0xf
处跳出循环。(以当前元素的值作为下一次遍历的下标)可知遍历路线应当为:
0xa
→0x1
→0x2
→0xe
→0x6
→0xf
输入的参数为两个整数,一个指示遍历的起点下标,一个记录遍历元素的累积和。
因此答案为
0 48
。phase_5
phase_6 链表/指针/结构
已知一个节点数为6的链表,需要将其重新排序,使得其满足“从大到小”的指向顺序。具体而言,代码会将链表中的节点存入栈中,再将低一级的节点指向高一级节点,实现重新排序,而存入栈中的顺序由我们的输入指定。
根据
0x401238
行,可知链表的首节点地址为0x6042f0
,且指针前面的部分占8字节。查看链表内容,结果如下图。

可以发现数据部分由序号和整数组成,而且这个链表刚好连续存储,于是结构体的声明如下:
而节点被存入栈中(准备进行重新排序时),结构大概为:

于是答案为
4 5 3 1 2 6
。phase_6
secret_phase
(1)函数phase_defused
跟踪
secret_phase
的调用者,发现是函数phase_defused
,而这个函数在main
里的每一个阶段后都会调用,用以检查炸弹是否拆除。其代码如下:phase_defused
根据第
0x401835
行可知,函数剩余的部分只有在程序读取完6个字符串之后才能执行,因此secret只有在phase_6
之后才能出现。可以发现,函数中有很多奇怪的常数地址,通过gdb打印结果如下:

结合先前
phase_4
的输入是5 2
,且此处地址提示<inpupt_strings+240>
,猜想是需要在phase_4
的答案后追加字符串”DrEvil”
,尝试发现确实如此——
(2)函数secret_phase
该函数会接收终端的标准输入(如先前图中所示的绿标),之后将其转化为long类型处理。
而程序的规则有二:
- 输入不得超过1001;
- 调用函数
fun7
的返回值须为7。
于是重点就落在了对函数
fun7
的分析。但在此之前,可以看到调用
fun7
时传入的参数除了我们的输入外还有一个地址0x604110
,通过gdb打印查看——可以发现这是一个链表(节点的后半段显然是地址,而且还有两个),而
phase_6
中的链表竟然就在下面,作者巧思啊hhhh
也因此可以猜测这是一个循环双链表or二叉树,又发现后面的节点几乎没有指针值,于是笃定是二叉树。
节点的声明大致为:
根据打印结果可知,结构体的内存分配为data的8字节 + n1的8字节 + n2的8字节。 一般我习惯认为data是4字节的int类型,而这里占8字节为了数据对齐;但由于另一个需要我输入的参数被转化为了long类型,这里大概率要保持一致。
secrets_phase
(3)函数fun7
转化为C语言如下:
<n48>
<n48>
edx>m
我是笨蛋,要求只是不超过、而不是不能取等,我说怎么怎么都不可能🥹
在捋路径的时候图方便把这个节点关系画了出来,发现竟然还是个二叉搜索树!(first左second右)
于是答案为
1001
既然如此,节点结构体声明就应该是——

- 作者:Antony_Zhang
- 链接:https://antonyzhang.cn/article/csapp-bomblab
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。