最近计算机系统基础课程要求完成这项作业,故记录一下完成过程。
bomb.c
首先,出题者给我们提供了bomb的原型,通过
bomb.c,大致可以了解这个bomb的执行流程:从键盘读入一行,然后执行phase_(x)然后解除炸弹。总共有6?个phase等待我们取解开。
环境准备
这里,我是用了radare2 作为分析工具。(然而第6题太给力,用了
IDA)首先,执行
r2 -A bomb加载二进制文件并执行分析。
然后,执行
afl看一下函数列表,我们发现了几个包含phase的函数,也就是我们要重点关注的东西了。同时也注意到列表里包含sym.secret_phase,估计这是个bonus。
Phase_1
先执行
s sym.phase_1跳转到phase_1,然后执行pdf查看反汇编代码。
phase_1的汇编代码比较简洁。我们的输入存放在
rdi中,然后phase_1中将Border relations with Canada have never been better.这句话放入esi中作为参数调用strings_not_equal。strings_not_equal函数负责判断两个字符串是否相等。接下来是test指令判断eax的值是否为0,不为0则引爆炸弹。所以
phase_1的答案就是Border relations with Canada have never been better.Phase_2
一样的跳转到
phase_2然后查看反汇编代码。
可以看到,在
phase_2中程序读入了6个数字,然后判断第一个数字是否为1,不为1则爆炸。若为1则跳转到
0x00400f30处,这里radare2用qword [local_4h]表示rsp+4,也就说local_4h到local_18h刚好是第二个数到第六个数和“第七个数”(用于判断循环结束)(也刚好应证了开头sub rsp, 0x28开辟的7个空间)。将一头一尾分别保存到rbx,rbp后,一个JMP直接跳到了0x00400f17开始执行循环。在循环体中,程序先取出前一个值(
rbx-4),然后将这个值乘2,在与当前值比较(rbx)。如果相等那么将rbx+4也就是循环迭代下一个值,如果刚好到最后一个值了那么就跳出循环。所以这个循环就是判断这六个数是否是以1为首项,2为公比的等比数列。
故
phase_2的答案是1 2 4 8 16 32。Phase_3
同样的,跳转到
phase_3然后查看反汇编代码。
这里先调用
sscanf读取读取两个数("%d %d"),然后判断返回值是否大于1。查阅资料后得知sscanf的返回值的含义应该是:如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。
然后我们根据调用的约定可以得出
local_ch存放第二个数,local_8h存放第一个数。然后有个
CMP比较(0x00400f6a),可以得出第一个数不能大于7,同时又由接下来的ja得出第一个数应该是无符号的,也就第一个数还需大于0。然后进入了一个
switch-case语句。首先是jmp qword [rax*8 + 0x402470],这里可以想到应该是通过switch-case的跳转表确定结果。由于前面已经推出第一个数只可能是0到7共8个值,故执行px/8xg 0x402470查看那8个case的跳转表。
所以设第一个数为
x,那么x的跳转表如下:地址 x 0x00400f7c 0 0x00400fb9 1 0x00400f83 2 0x00400f8a 3 0x00400f91 4 0x00400f98 5 0x00400f9f 6 0x00400fa6 7 对于每一个
case都修改了eax的值,然后跳转到0x00400fbe与第二个数的值进行比较,如果相等就成功拆弹,否则就失败。故本题答案有多组,设第二个数位y,本题答案如下:x y 0 207(0xcf) 1 311(0x137) 2 707(0x2c3) 3 256(0x100) 4 389(0x185) 5 206(0xce) 6 682(0x2aa) 7 327(0x147) Phase_4
同样的,跳转到
phase_4然后查看反汇编代码。phase_4和phase_3开头很像,同样是读入了两个数(设为x和y)。
完成输入后,在
0x0040102e对x进行比较,其中x<=14。接着将
edx赋值为14,将esi赋值为0,edi赋值为x,再进入fun4。
这里是一个递归调用,我们尝试逆向出C语言代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int func4(int edx,int esi,int edi) {
int eax = edx - esi;
int ecx = eax >> 31; // 符号位
eax += ecx;
eax /= 2;
ecx = eax + esi;
if(ecx<=edi) {
if(ecx>=edi) {
return 0;
} else {
esi = eax+1;
eax = func4(edx,esi,edi);
return 2*eax+1;
}
} else {
edx = ecx-1;
eax = func4(edx,esi,edi);
return 2*eax;
}
}这里可以大致看出是一个二分算法,稍微修改一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int func4(int r, int l, int t)
{
int mid = (r + l) >> 1; // ecx,同时假定r>l
if (mid == t)
{ // 找到目标
return 0;
}
else if (mid < t)
{ // mid < t 中值小于目标x,扩大左边界
l = ((r - l) >> 1) + 1;
return func4(r, l, t) * 2 + 1;
}
else
{ // mid > t 中值大于目标x,缩小右边界至mid-1
r = mid - 1;
return func4(r, l, t) * 2;
}
}在
phase_4中执行了func4(14,0,x)其返回值要求为0,同时第二个输入也必须为0(??)才能过关。关于
func4不妨写一个程序遍历一遍(x取值在0到14之间),得到的结果如下:x func4 0 0 1 0 2 4 3 0 4 2 5 2 6 segmentation fault 7 0 8 1 9 1 10 segmentation fault 11 1 12 segmentation fault 13 segmentation fault 14 segmentation fault 这里比较有意思的是
segmentation fault,估计是模拟给出的C语言代码有些问题吧!同时实际测试发现bomb没有给出segmentation fault而是直接引爆了炸弹。所以综上,本题答案为
0 0或1 0或3 0或7 0。Phase_5
同样的,跳转到
phase_5然后查看反汇编代码。
其实这段汇编有些内容我还不清楚,但是不影响解题。2020-4-27:
关于
fs:[0x28]:这里实际上是通过段寻址方式获取了一个“金丝雀”(canary)值并把这个值送入栈中,这个动作是一种栈溢出保护手段。
先将这个特殊的值压栈,当函数结束后再判断栈中的这个值是否被修改过,以此来验证是否出现栈溢出。
关于
0x28这个偏移量:这个
bomb是64位,并且与glibc链接。fs寄存器被glibc定义为存放TLS(Thread Local Storage)信息。在
Github上的一份glibc的mirror中我们可以找到tls的定义:Github其中
stack_guard存放的就是这里偏移为0x28的金丝雀值。这段代码的大意是:
- 输入一个字符串,长度为6
- 遍历输入的每个字符
c - 获取
c的ASCII二进制码的后四位,将这个数作为索引从maduiersnfotvbylSo_you_think_you_can_stop_the_bomb_with_ctrl_c__do_you中取出字母拼接一个新的字符串。 - 比较拼接出的字符串是否是
flyers
这里由于是4位二进制串故我们只用考虑在前16位(
maduiersnfotvbyl)中找字母就可以了。其中
f-9,l-15,y-14,e-5,r-6,s-7。这里由于只考虑后4位二进制,所以可以直接printf出几个不可见字符或者找可见字符后四位满足要求的。比如:1
2
3printf("%c%c%c%c%c%c",9,15,14,5,6,7);
printf("IONEFG");
...Phase_6
同样的,跳转到
phase_6然后查看反汇编代码。
这一题汇编代码比较长。一开始就分配了20个4字的栈空间。
这道题静态分析不好整(
我太菜了),所以使用IDA进行动态调试。
首先定位到
phase_6其中左半边的比较好研究,先看左半边。
左半边有两重循环,其中左半边的逻辑大至如下:
- 首先读入六个数字
- 遍历这六个数字,记当前数字为
a - 如果
a比6大则爆炸 - 从
a开始遍历以后的数字,如果存在与a相等的则爆炸 - 完成第3,4步后迭代
a
也就是说这1-5步就是确定这六个数字互不相等且都小于等于6(大于等于1)
执行完以上步骤之后又是一个循环:
在这个循环内,用7减去输入的每个数字。
接着,到了最为复杂的部分。
首先是一个神秘常数
node1(应该是IDA自动命名的)。因为一些神神秘秘的原因,我无法直接获取它的值,所以我在动态调试时使用python脚本来获取。
最后我们拿到了六个数所对应的神秘数值:
node 7-x x 332 1 6 168 2 5 924 3 4 691 4 3 477 5 2 443 6 1 接着程序按照我们输入的值将
node的值对应按顺序存入RBX中,最后检查RBX中的值是否是递减的。
所以我们手动给这几个数排序,也就是
924>691>477>443>332>168对应成我们要输入的数就是4 3 2 1 6 5最后
其实还有一个
secret_phase没分析,就等下一次填坑吧!总结一下,这6个
phase从简单到难,的确是很考验基础知识的。UPDATE:来填坑了
secret_phase
关于这个
secret_phase如不是题目明确指明,我或许都不会去想有这一关。开启
secret_phase的机关在phase_defused中:
在这个函数里,先判断是否完成了六次输入,如果完成了就会判断是否触发隐藏关。
在这里调用了
sscanf来进行输入,而需要转换的源字符串是什么,这里通过动态调试,得知在执行到这儿后,第四次输入的内容将被传送到这。所以触发隐藏关就是在第四次输入后多加一个
DrEvil。下面就来看
secret_phase:
在
secret_phase中首先读入了一个十进制的数字字符串,然后通过strtol函数把它转化为整数。如果这个数小于等于1001则可以继续,反之引爆炸弹。
接着,调用函数
fun7,传入两个参数,一个是”$”(?)一个是转换后的那个数,如果返回值为2则顺利通关。接着分析
fun7:
这里尝试还原成C语言代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int fun7(long long *edi, long long esi)
{
if (!edi)
{
return 0xffffffff;
}
if (*edi <= esi)
{
if (*edi == esi)
{
return 0;
}
else
{
edi += 16;
return 2 * fun7(edi, edi) + 1;
}
}
else
{
edi += 8;
return 2 * fun7(edi, esi);
}
}稍微整理一下代码:
1
2
3
4
5
6
7
8
9
10
11int fun7(long long *x, long long y)
{
if (!x)
return 0xffffffff; // -1
if (*x > y)
return 2 * fun7(x + 1, y);
if (*x == y)
return 0;
if (*x < y)
return 2 * fun7(x + 2, y) + 1;
}然后回到最初传入的那个地址,执行
px/8xg 0x6030f0查看一下存放的值:
然后再反向推这个
fun7,得到结果2的步骤应该是0->2*0+1->2*(2*0+1)。- 那么第一次输入要先满足
*x>y,而第一次输入时*x=36故y<36。 - 然后取
x的下一个数,也就是0x603110指向的内容,即8。此时要满足*x<y,即y>8。 - 接着取
x的下两个数,也就是0x603150指向的内容,即22。此时要满足*x==y,即y=22。
所以
secret_phase的答案是22。UPDATE-2020-4-29:
其实这个隐藏关还有另一个答案
20我们不妨认为
x+1就是左子树,x+2就是右子树,x对应的就是当前节点的值,可以画出以可二叉树:
关于另一个答案
20,可以这样推出:我们知道,最后找到相等时返回的是0,所以在找到0的上一层可以嵌套多个*x>y(0*2还是0)。那么按照上面画的二叉树来判断就是在22的左子树上也就是20(比22的多遍历一次)- 那么第一次输入要先满足