二进制炸弹总结
最近在学习gdb的使用和部分汇编指令的意义,在查找资料的过程中发现南京大学的mooc质量很高,于是跟着老师们学完了整个mooc,对gdb和汇编指令有了一个大概的了解。最后磕磕绊绊地完成了很有意思的课程大作业:二进制炸弹,下面对整个大作业进行一个总结。
本次作业的环境为:WSL2 20.04.5 LTS,gcc 9.4.0, gdb 9.2。作业的下载地址为https://cs.nju.edu.cn/sufeng/course/mooc/0809NJU064_bomblab.tar。二进制炸弹一共有7个普通阶段和1个隐藏阶段。
phase_0
phase_0 是所有阶段里面最简单的,从反汇编代码中也能看出来,主要是判断我们的输入和地址0x804a198
中保存的字符串是否一样,直接使用x/s 0x804a198
即可查看应该输入的字符串。然后运行我们的程序,即可看到成功通过phase_0 的提示
phase_1
phase_1 的主要考察内容是浮点数的表示,使用b phase_1
将断点打在phase_1处,然后使用si
在phase_1 内单步执行指令然后查看结果。
这里通过分析汇编指令可以知道,这段代码的意思是先将0x1a4eccf6
整个16进制数送到$ebp-0xc
的位置,然后将其中的值送到浮点寄存器栈顶,再从浮点寄存器栈顶把值存到$epb-0x18
的位置。接下来使用sscanf
从命令行读入,这里有一个明码地址,由于对sscanf的调用比较了解,因此这里猜测,这个明码地址应该是输入格式,使用x/s 0x804a1bd
进行查看,果不其然,这里让我们从命令行输入两个整数,接下来程序判断输入的是不是两个数,如果不是则炸弹爆炸。
了解了上面汇编的意思,下面的代码就很简单了。先将这个浮点数的低32位与第一个输入判断,如果不相等炸弹直接爆炸,否则继续将高32位与第二个输入对比,若都成功则通过phase_1。这里如果不想手动计算,可以有取巧的方法,直接将断点打在0x80494e3
,然后查看寄存器edx
的值,然后第一个输入即为这个值,接下来如法炮制,将断点打在0x80494f2
即可获得第二个输入值,过程如下所示:
当然如果手算也可以,只是会比较麻烦(这里的28是转换成1.x的形式需要移位28位)。
接下来将我们得到的值输入查看结果,成功通过phase_1。
phase_2
phase_2主要是让输入一串数字,数字之间满足一定的规律。老规矩还是分析汇编代码,然后使用gdb调试。可以看到在phase_2中是需要调用read_n_numbers
这个函数的,这样的话就先进入read_n_numbers
,看看它是如何实现的。ps:这里直接说吧,在我的程序中是需要输入8个数,具体可以看汇编代码,这里就不再分析。这里直接进入read_n_numbers
函数查看输入,可以看到内部调用strtok
将从标准输入读取数据进行分割,然后使用sscanf
读取数据。
此外这里还有两个明码地址,我们打印一下,发现读入的应该是8个整数,且整数之间需要用,
隔开。
继续查看汇编代码,使用gdb将断点打在0x8049532
处,因为这里有一个比较$0x8a,%eax
。重新运行程序,设置输入为8, 7, 6, 5, 4, 3, 2, 1
,这样有顺序方便后续检查,可以看到当前eax
的值刚好为我们输入的第一个值,因此可以得到结论,第一个输入的值应该是0x81
即138。
后续可以按照这个方法直接一个一个往下试,这样就可以得到所有的数据,最终为138, 137, 134, 129, 122, 113, 102, 89
。不过这样做太低效了,我们可以继续分析汇编代码,后面可以发现其实这些输入的数据之间满足一定的规律,这里对主要的代码使用红框和绿框进行了标注。假如我们第一个数据输入正确的话,那么会在$ebp-0xc
的地方存入1,即当前读入数据的个数,然后跳转至0x8049584
,比较这个$ebp-0xc
与7的大小,如果小于等于7则会进行一个循环,循环的主体即为红框和绿框的内容。这个循环到底是什么意思呢,其实写成C代码会很容易看出来
for (int i = 1; i <= 7; i++) {
if (array[i] != array[i - 1] - 2 * i + 1) {
explode_bomb();
}
}
i就是eax寄存器中的内容,在红框内将数据备份在ebx,然后在绿框中计算-2*i
的值,再和ecx寄存器中的内容相加,最后再加1。ecx寄存器的值在红框中给出了计算过程,即为$ebp+4*$eax-0x2c
地址中保存的值,经检查当eax寄存器的值为0时,则edx的值为138,
由此可以推断出,从$ebp-0x2c
起始存了8个数,然后继续分析下面的逻辑就很简单了,eax-1是为了方便下面获得首地址,然后将上一个值保存在ecx寄存器中。
接下来将我们获取得到的数据输入到程序中,很轻松就过了phase_2。
phase_3
来到phase_3 我们首先又遇到了熟悉的sscanf输入和明码地址,先查看一下保存的内容,发现又是让输入两个数。
继续查看汇编代码,碰到第一个需要注意的地方,这里要求我们的第一个输入减7之后不能比9小,结合下面代码不难看出这是一个switch-case
结构,数一下跳转数量,发现应该有9个case分支,因此这里可以确定下来第一个输入应该为16。在正确输入第一个数之后,会跳转至0x8049652
, 然后比较输入的第二个数和$ebp-0xc
位置的数 。
这里我们输入16, 666
,设置666方便后面排查错误。将断点打到0x8049652
,然后单步执行再查看这个地方的数据,发现输入应该为973,重新运行程序并输入16 973
可以看到,phase_3 已成功通过。
phase_4
phase_4 应该是所有关卡里面最难的一个了,因为设计递归,所以调试起来会非常痛苦。先来看汇编代码
熟悉的sscanf,熟悉的输入两个数。我一开始看错了,直接输入了4, 30
,结果还真给它通过了。。。然后就没管,继续做后面的,但是等做到secret_phase的时候回头一看心态当场爆炸。这里提前爆料一下,secret_phase是跟phase_4相关的,这里只要再多输入一个密码,就能进入隐藏关卡。误打误撞答案正确了,但是过程是什么样的还是不清楚,这里还是要老老实实地进行调试。
将断点打在0x8049734
,这里push了三个值,说明func4函数有三个形参。第一个是我们输入的第一个数,第二个是4,第三个是48。然后进入func4继续查看该函数的功能,这一段的主要内容就是判断我们的第一个输入和(48-4)/2+4哪个大,主要是用移位操作来完成。
接下来我实在分析不动了,直接上ida,通过反汇编可以得到下面的代码:
int __cdecl func4(int a1, int a2, int a3)
{
int v4; // [esp+Ch] [ebp-Ch]
v4 = (a3 - a2) / 2 + a2;
if ( v4 > a1 )
return func4(a1, a2, v4 - 1) + (v4 >> 1);
if ( v4 >= a1 )
return (a3 - a2) / 2 + a2;
return func4(a1, v4 + 1, a3) + 2 * v4;
}
signed int __cdecl phase_4(int a1)
{
signed int result; // eax
int v2; // [esp+0h] [ebp-18h]
int v3; // [esp+4h] [ebp-14h]
int v4; // [esp+8h] [ebp-10h]
int v5; // [esp+Ch] [ebp-Ch]
v5 = __isoc99_sscanf(a1, "%d %d", &v3, &v2);
if ( v5 == 2 && v3 > 3 && v3 <= 48 )
{
v4 = func4(v3, 4, 48);
if ( v4 == v2 )
{
result = 1;
}
else
{
explode_bomb();
result = 0;
}
}
else
{
explode_bomb();
result = 0;
}
return result;
}
这下差不多明白了,输入了两个数v3和v2,v3要满足大于3小于等于48的条件,然后调用func4(v3, 4, 48)
,要它的结果等于v2。直接看phase_4的结尾,这里将eax的值和$ebp-0x10
进行对比(func4的返回值),如果相等就成功退出pahse_4。
为了验证我们的结论,先输入一个错误的输入4 32
,直接将断点打在0x0804973d
,然后查看这个比较的值,发现它等于30,说明我们输入的第二个值确实应该为30。
然后重新运行程序,输入4 30
,可以看到已成功解决phase_4。
phase_5
phase_5应该是所有的phase里面最有意思的一个了。phase_5要处理的是一个数组,需要先找到一个入口,然后按照数组的顺序走到f的位置,输入起始位置和路径上数字的总和。老样子依旧是看汇编代码,检查明码地址,发现让输入两个整数。
继续往下读,发现了我们的输入每次都要和0xf
进行与位操作,然后一共要进行7次,每次都将当前的结果加到$ebp-0x10
,并且会跳转到下一个节点,最后比较我们的第二个输入和这个值是否相等。
这里我们直接看从这个地址开始存了哪些东西
排列一下
10 2 14 7 8 12 15 11 0 4 1 13 3 9 6 5
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
上面已经分析了要7次跳转到15,可以倒着找15 -> 6 -> 14 -> 2 -> 1 -> 10 -> 0 -> 8
,所以输入应该是8 48
。重新运行程序,输入正确的数对,可以看到结果正确,成功通过phase_5
phase_6
phase_6主要考察链表结构,查看汇编代码,发现一开始要读入n个数,和phase_2一样,因此不做过多分析,继续阅读代码发现输入的应该是7个数。然后从$ebp-0x34
开始依次存入。
继续阅读源码,我们不难发现链表的起始位置应该在$ebp-0x50
处,直接将断点打在0x80498e2
,然后开始逐步打印节点的信息。
可以看到节点的顺序为6 -> 0 -> 8 -> 5 -> 7 -> 4 -> 2
,最后需要按照从大到小的顺序输出,因此最终的输入为2, 7, 6, 4, 1, 5, 3
6 -> 0 -> 8 -> 5 -> 7 -> 4 -> 2
2 7 6 3 1 4 5
phase_secret
最后这一块是综合考察,比较考验耐心。这里由于老师提前说过,需要在phase_4多输入一个“密码”,不然炸弹会自动defuse
,所以我这里直接看phase_defused
这里发生了什么。
发现了一共有三个明码地址,都打印一下,正好是phase_4的输入。
因此我们得修改一下phase_4阶段的输入,能看到这里已经进入了secret_phase
。
继续阅读secret_phase
的源码,发现我们输入需要在一定的范围内
(
0
,
1001
]
(0, 1001]
(0,1001]
然后将输入带入到fun7,然后将结果与1进行比较,之后这里又有一个明码地址,打印一下,发现是结束提示语,说明我们要做的就是让fun7的返回值等于1。
进入fun7,发现代码非常长,于是我直接使用ida来看伪代码,要让返回值等于1,那就只能是最后这个条件。最后这个条件又递归调用了fun7一次,现在需要知道的就是a1到底是多少,继续阅读secret_phase的伪代码,发现这里输入了一个n1,直接查看:
找到了n1的地址是0x0804c1d0
使用gdb打印该节点,因为只需要递归一次,所以只用看下一个节点的值即可,可以看到结果为50
输入50,可以看到隐藏炸弹也被拆除了。
以上就是整个作业的流程了,断断续续做了一天半才勉强做完,有一说一汇编真的很难,不过如果有像ida这种专业的工具的话,其实做起来就很轻松啦,这里分享一个我下载的绿色版ida的网址:https://www.32r.com/soft/42075.html,一个汇编指令具体意思的网址:https://shell-storm.org/x86doc/,以及常用的gdb调试指令的网址:https://jyywiki.cn/pages/OS/manuals/gdb-cheat-sheet.pdf