15-213 bomb lab

Table of Contents

1. phase_1

tips: 首先要学会使用 gdb 才可以开始这个实验

查看关于 phase_1 函数的汇编代码:

image-20220517193015629

可以看到,phase_1 函数只是调用了 strings_not_equal 函数就判断了炸弹是否爆炸,而对于 phase_1 函数,由源码可以知道会将读取的第一行传入作为第一个参数,所以到调用 strings_not_equal 时,函数一共有两个参数,而想要炸弹不爆炸,就必须保证传入的两个参数他们对于 string_not_equal 函数 返回值(保存在 %eax )必须为1,而第二个参数就存储在 %esi 中, 在函数调用时打断点,同时将 %esi 中保存的地址转化为 char * 类型打印出来,答案就出来了

Strings_not_equal 比较两个字符串,内容相同 返回0, 不同 返回 正值

查看汇编发现函数的比较逻辑是这样的:

  1. 先计算两个字符串的长度,相同,则继续到第二步,不同则返回正值
  2. 依次从字符串的第一个 char 开始比较,相同,则将依次比较下一个,直到比较到字符串结尾,或者不相同为止

2. phase_2

这个函数比较有意思 phase_2 函数首先通过 read_six_numbers 将 6 个数字读取到栈顶,而 read_six_numbers 的汇编代码比较有意思:

image-20220517200324847

函数通过两个寄存器 %rdi 存储 phase_2 调用时栈顶指针的地址, %rsp 存储当前函数栈顶指针的地址,然后将 %rdi 的高地址的各个位置要么写进 %rsp 的高地址中存储, 要么写进其他参数寄存器,这样就可以在 0x40148a 这里函数调用时,动态的赋值这些地址的值,从而巧妙的将 6 个数字存储在了 phase_2 的栈帧中,这里读的其实就是我们输入的答案,这时候在查看 phase_2 函数,什么情况炸弹不会爆炸:

image-20220517200859156

可以看到,一开始 %rbx 存储 0x4(%rsp) 的地址, 然后 %rbp 存储 0x18(%rsp) 的地址,中间刚好可以容纳 24 个字节,也就是读取的 6 个数,然后一个循环,一开始 %eax 为 1,从第一个数字开始比较,每次正确以后,%eax 中的数字翻倍, 一直比较到 当 %eax 的值为 %rbp 的地址的值时,此时也就比对 6 个数字全部正确,炸弹不爆炸,答案就是 1 2 4 8 16 32

3. phase_3

通过打断点调试可以知道,经由函数 __isoc99_sscanf@plt 调用以后,栈中 0xc(%rsp)、0x8(%rsp) 分别存储读取的第一个和第二个数字,同时汇编有:

image-20220517202337941

第一个数字应该小于 7, 然后通过间接地址引用,断点调试出只有当数字为0 或者 1 时,计算出的跳转地址才是可行的

通过gdb 的调试命令 x /u *(0x402470) 以及 x /u *(0x402470), 得到当第一个数字为 0 时,下一步在 0x400f7c; 为 1 时,下一步在 0x400fb9 所以本题有两个答案: 0 207 以及 1 311

4. phase_4

有了前面三个题目的基础可以很直接得出以下已知条件

  • 本题答案需要两个数字,第一个数必须小于等于 14, 第二个数字必须为 0,
  • 对于调用的 func4 函数返回值必须为 0,
  • Func4 有三个参数 第一个数字 0 14

查看 func4 的汇编,发现其实是一个递归函数,简单还原了这个函数的 c语言源代码:

int func4 (int a, int b, int c) {
	int tmp = c - b;
	int e = tmp >> 31;
	tmp = e + tmp;
	tmp /= 2;
	int x = tmp + b;
	if(x <= a) {
		if(x == a) return 0;
		else {
			b = x + 1;
			return 2 * func4(a, b, c) + 1;
		}
	}
	else {
		c = x - 1;
		return 2 * func(a, b, c);
	}
}

带入具体数据可以知道,当 b = 0, c = 14 时,a可以为 7, 3, 1

所以本题答案有三个

5. phase_5

这一个炸弹是一个很有意思的含有类似加密/解密的思想的题目,可以看到主要就是对输入的 6 个字符进行一定的操作,然后传入到 strings_not_equal 函数中,使用 gdb 调试出另一个参数时字符串 flyers,接下就是看怎么对输入的字符串进行操作,可以看到其实就是对每个字符一次进行一种算法操作,将输入的字符转换成 flyers,然后传入函数进行比较,答案就出来了。计算按照字符的 ascii 码计算,参照表的位置,可以通过内存打印出来: 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx

由于参照表长度为 16,而一共有 26 个英文字母,所以答案显然是不唯一的

本题答案不唯一

6. phase_6

这应该是整个 lab 最难的题目了,代码巨长,但是,而且地址,值到处乱飞,看的时候真的很难受但好在分阶段,其实可以一点一点做出来的,很长的汇编其实对应起来也就只有几个循环加几个操作,耐心调试还是可以做出答案的,这里分段给出几个代码段的功能:

  • 读取输入的 6 个数字,存放在栈顶的 24 个字节
  • 循环判断每个数字都必须是 <= 6,且 >= 0
  • 将原本位置上的参数 用 7 减去,并保存在栈上
  • 通过数字的大小 存放链表中结点的值在栈的位置
  • 判断栈中节点的值是按照从大到小的顺序排的

最终答案:4 3 2 1 6 5

7. secret_phase

这是一个隐藏关卡,其实就是一个彩蛋,查看汇编可以通过 在第 4 个输入的时候,在输入的答案末尾加上 DrEvil 进入

查看其实也是一个 递归 + 结构体 的过程,结果是要求 fun7 函数 返回 2

我们通过查看汇编中内存的引用可以看到一块 关于树的内存,其最后构成的树结构就是

  								36
					
  					8						50

  			6		  22		  45		 107

  			1	7	20	35	40	47	99	1001

而 fun7 推算出来的情况就是

int fun7(struct node *p, int v) {
	if(p == null) return -1;
	else if(v < p->val) {
		return 2 * fun7(p->left, v);
	}
	else if(v == p->val) {
		return 0;
	}
	else {
		return 2 * fun7(p->right, v) + 1;
	}
}

要求返回 2,输入 20 即可

总结

做到后面,实在是觉得累,就没有贴 汇编截图了,其实做完,感觉对汇编还是没有那么有把握读懂是怎么回事?!不过对整个程序运行真的是有全新的认识,确实是很有帮助的,也加深我继续学下去的意志,虽然真的挺难的,但也是切切实实感觉到在进步。

一直下定决心要写这个 lab,总是各种各样的原因,已经拖了快半个学期了,好在现在终于是写完了,其实真的花的时间做 lab(不包括学 csapp 的时间,加起来我估计有一两个礼拜了),也只是做了两个下午,但是眼睛一闭,一睁,从中午的太阳到晚上的夜色,还是有一些让人觉得恍惚的。lab 个人觉得没有网上说的那么有趣,但是绕到寄存器和栈指针的各种转换的时候,真的有一种被卷入其中的感觉。名校的 lab,也是非常灵活,就是这样的 6 个 phase,就有 3 个的答案是不唯一的,还隐藏了一个彩蛋,真的能感受到课程的用心,反观自己的学校的课程设置和教学质量,还是有点失落的。