写在前面
这个实验要能看懂些汇编,然后会用一些gdb的基本操作。
打开bomb.c可以看到我们实际上就是要找到那6个字符串,错误的话就会爆炸。
phase_1
打开bomb.s 文件,可以看一下phase_1 的汇编代码:
发现就是比对了两个字符串,如果不相等,就会调用爆炸函数。
而调用之前,有一句 mov $0x402400, %esi,这个传递的值就是答案字符串。
我们gdb打印一下:
所以phase_1的答案就是:
Border relations with Canada have never been better.
phase_2
可见,phase_2读了六个数字,我们要找的答案就是6个数字。
蓝框代码说明第一个数字是1,然后看后面的逻辑
这个是 读六个数字的逻辑,可以从里面发现六个数字用的寄存器以及在栈上的位置
跳到 400f30 后,把栈上存的一个地址存在了rbx,然后跳到400f17
到 400f17 后,取出刚才存的地址所存的数,这个数就是上一个数字,然后将其 * 2,与下一个数比对
由此可得,剩下的5个数是不断 * 2 得到的。
答案:
1 2 4 8 16 32
phase_3
汇编上看到了scanf,而且前面已经拿到了前两个阶段的答案,所以阶段三可以调试来理解下程序。
在 89行:phase_3(input) 处打断点。
然后发现 scanf 了 两个 int
finish 之后返回值是2,说明读入2个字符。
第一个蓝框,对scanf 的返回值进行测试,因为我们输入两个参数,返回2,所以会跳到第二个蓝框
第二个蓝框的逻辑说明第一个输入值 <= 7,又因为是ja进行无符号数比较,所以符号位得是0,所以第一个数在[0, 7] 内
我们发现下面的逻辑其实就是一个 switch(因为 [0, 7] 这 8个数对应了 8 个jump mov)
我们不妨输入0,往下走,看0 对应的下一个数是多少(通过汇编代码找到jumpq的地址,跳过去)
然后打印发现值为207
phase_4
cmp $0x2, %eax,如果不是2,后面会跳到爆炸逻辑,所以scanf读取了两个数。
事实上,我们打印 0x4025cf,会发现:
接着看。
如果第一个数 > 14,直接爆炸,随意确定第一个数的区间为[0, 14]
然后我们发现可以通过func4 的逻辑来确定第一个数是多少。
看一下func4的逻辑
因为解有多个,我们找到一个特解就好。
我们发现第一个条件判断:如果 ecx <= edi,我们就会返回0,而func4调用后的逻辑正是要确保 返回值是0。
我们不妨假设 edi(我们输入的第一个答案) 取何值时,能在第一层函数调用直接通过第一个条件判断返回0。
我们计算 第一次条件判断 ecx的值为7,所以我们不妨取 edi = 7,即答案的第一个数为7.
然后发现,func4后面这条逻辑直接限制了 第二个数必须是0,所以得到一组答案为:
7 0
phase_5
这个phase巨简单。
蓝框说明输入是一个长度为6的字符串,否则就爆炸了。
然后会进入一个六次循环。
这个循环的作用就是把输入的长度为6的字符串取每个字符的低四位(和 0xf 做与运算)作为索引,从起始地址0x4024b0 处拿字符,压栈
出来之后,会拿着刚才得到的新的长度为6的字符串和 0x40245e 处的字符串 进入一个字符串比较的逻辑,不相等就爆炸。
我们分别打印 0x4024b0 和 0x40245e 处的内容,发现:
(gdb) p (char*)0x4024b0
$4 = 0x4024b0 <array> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
(gdb) p (char*)0x40245e
$5 = 0x40245e "flyers"
目标字符串是 flyers,那么我们输入的六个字符的低四位只要保证 对应0x4024b0 处相应偏移的字符组成 flyers即可。
打印一下"flyers" 每个字符在 上面那个array中的索引
于是得到答案 )/.%&’ (我对每个索引加了 32 得到的对应ASCII值的字符)
phase_6
这个phase 非常的勾石。
phase_6的逻辑:
- 读入六个数
- 判断六个数为 [1, 6] 的一个排列
- 把这个排列映射到 7 - x
- 按照映射后的元素作为下标重排长度为6的链表的元素
- 如果重排后链表降序,那么就过,否则就爆炸
怎么分析出来的呢?
开头read_six_numbers 说明了读入六个数
然后下面有两层循环,是判重 以及数在 [1, 6] 内
这段逻辑,从六个数的起始位置开始,依次变为7 - x
401176开始,就是依次找到第 nums[i] 个节点,放在数组里面
这段就是链接各个节点,即 nodes[i]->next = nodes[i + 1];
把尾节点的next指针置空
最后就是判断降序,然后弹栈恢复现场。
因为知道原链表头指针,我们可以获取原来的值,从而推断出我们应该如何输入来使得重排后降序。
答案:
4 3 2 1 6 5
secret_phase
看汇编会发现一个fun7,它只被 secret_phase 调用。
secret_phase 又只被 phase_defused调用。
查看 phase_defused,我们发现,对于 phase_4的检查会看你的scanf 读入了几个值。
明面上我们phase_4的时候是输入两个数,其实我们如果输入了它隐含的那个字符串的话,我们会进入隐藏phase
打印一下,可以发现第三个字符串是什么。
我们如果把phase_4 的答案加上这个字符串就会进入隐藏phase.
我们先看 secret_phase 要求我们读入什么?
看来我们要输入一个 <= 1000 的无符号整数。
把输入的整数作为参数传入 fun7,如果返回值是2,那么成功。
下面来看fun7 做了什么?
fun7 非常的短,我们来分析猜测一下。
如果 rdi 是0,那么返回-1
其实rdi是一个二叉搜索树的节点指针,即指针为空的时候我们就返回-1.
根据 后面比较的值是从 rdi 所在地址取出,可以得出rdi 就是一个指针。
然后后面根据 取出值和 我们输入的数进行比较,从而进行递归,我们发现递归时候的 比较的数的参数没有变,但是传入的指针分别是rdi 相邻的两个地址。
然后发现比较递归的过程和二叉搜索树上查询指定值的过程极其相似,所以合理推断这就是二叉搜索树。
可以大概翻译成C语言:
struct node {
int val;
struct node* left;
struct node* right;
};
int fun7(struct node* p, int x) {
if (p == NULL) {
return -1;
}
int val = p->val;
if (val > x) {
return 2 * fun7(p->left, x);
} else if (val == x) {
return 0;
} else {
return 2 * fun7(p->right, x) + 1;
}
}
因为最后fun7的结果为2,所以我们可以从 0x6030f0 开始打印来推断 二叉树的结构,从而得出解。
└─ 36
├─ 8
│ ├─ 6
│ │ ├─ left: 1
│ │ └─ right: 7
│ └─ 22
│ ├─ left: 20
│ └─ right: 35
└─ 50
├─ 45
│ ├─ left: 40
│ └─ right: 47
└─ 107
├─ left: 99
└─ right: 1001
我们如果返回2,那么22满足条件
所以答案为
22
最终答案
1 2 4 8 16 32
0 207
7 0 DrEvil
)/.%&'
4 3 2 1 6 5
20