二进制炸弹任务描述
"二进制炸弹包含若干个阶段,每个阶段需要输入特定的字符串,所有输入正确则炸弹被排除,否则….."
拆弹的任务也就是找出这些字符串将字符串记录到solution.txt文件中,用换行区别不同阶段的字符串,
Linux环境下可按下列方式验证拆弹结果:
主要方法
objdump反汇编与gdb调试。
分析流程
已知数据有编译好的二进制可执行文件bomb,也就是反汇编目标文件,以及bomb.c文件,用于辅助理解代码。
查看bomb.c可知程序利用phase_*函数(*为1~6) 检查输入字符串是否合法,不合法就引爆炸弹。
bomb.c没有给出phase的源码,我们实际的任务就是逆向出每个phase的检查规则,构造出合法字符串。
首先使用 反汇编得到bomb的汇编文件,
以下将逐个分析phase1-phase6。
phase1【字符串对比】
<phase_1>汇编段前三行申请了8字节栈空间,
movl指令在高4字节存入了0x80499a8处的一份数据,随后的两条mov指令将我们提供的phase1的参数存入了栈的低4字节,
紧跟着一条call指令,跳转到<strings_not_equal>处,显然是在进行字符串对比,
通过分析我们可以得到,phase_1实际上是将输入参数与0x80499a8处存储的数据做对比,
在汇编文件中,数据段的内容并没有包含,所以我们需要通过gdb断点输出该处的存储数据,
断点处输出可以得到0x80499a8处的数据,即phase_1的字符串答案I turned the moon into something I like to call a Death Star.
phase2【循环数字】
从8048bbe行可以看出,phase_2的执行与读取6个数字有关,那么输入也应该是6个数字。
从8048bc3、8048bf0、8048bf3行可以得到phase_2的执行,进行了3次循环,也就是6个输入数字进行了3次对比。
有一个误区是,一开始去分析了read_six_numbers段的汇编码,没有得到什么有用信息,
由此可知后面对phase的分析,主要基于phase段的代码,其中涉及到的一些子操作只要按照字面意思理解即可。
分析循环语句中的8048bcc到8048bdd行,可以看到edx寄存器先从栈中读取了一个数字,
而后eax寄存器在索引+3(8048bd6行)的情况下,也从栈中读取了一个数字,
之后edx寄存器与eax寄存器进行了对比。
从已经分析到的信息可知,phase_2输入6个数字,对比只循环了3次,而对比的两个数字都在栈中(与phase_1不同)且下标相差3,可以猜测phase_2是将用户输入的6个数字中的前3个数字与后3个数字做了对比,下面通过gdb进行调试:
为了避免巧合,测试数据将数字设置成两组五位数,phase_2通过,证明phase_2确实是在对输入的数字进行索引加3的对比检查。
phase3【case分支】
有了phase1和phase2的经验,直接从8048c36行开始分析,根据<sscanf@plt>可以得到,phase_3的参数应该是调用了C语言中sscanf函数来输入,sscanf读取格式化的字符串中的数据。随后的mov、cmpl、jg、call四条指令中出现了对比操作与<explode_bomb>的调用,显然是在对输入数据进行分析判断。cmpl语句将寄存器eax与常数1进行对比,若不大于1则爆炸,而eax寄存器中存放的是上一次操作的返回值,查看sscanf函数定义可以得知, 返回值为int类型,返回的是读取到的有效数据个数。
接下来,测试phase_3需要的参数个数,首先给两个输入参数:
得到sscanf函数返回值为2,接下来我们再给三个输入参数:
给了三个输入参数,但sscanf函数返回值仍然为2,由此可知,sscanf函数只读取两个格式化字串,也就是phase_3需要两个输入参数。
8048c49行将输入的参数存入eax寄存器,用gdb调试可以得到存入的是第一个输入参数,参数1与常数7进行对比跳转,若大于7则跳转到爆炸指令,可知参数1取值范围为0-7。8048c5f行根据eax寄存器的内容进行直接寻址跳转,8048c58行在计算跳转地址时,用到了地址0x80499ec,且以4*%edx作为偏移量,查看反汇编得到的汇编码,并没有0x80499ec行,gdb调试输出可以得到:
显然该段是数据段,存储了case跳转表,而跳转地址正是phase_3代码段的8个地址入口。
不同的入口地址,分别给-0x8(%ebp)赋予了不同的常数值。
再读取参数2,将参数2与case段中的赋值进行比较,相等则通过。
0x3d4=980;0x2a3=675;0x23e=574;0x14a=330;0x1b3=435;0x2e5=741;0x154=340;0x9c=156;
所以有6组可用输入:0 980; 1 675; 2 574; 3 330; 4 435; 5 741; 6 340; 7 156;
phase4【递归求幂】
有了phase3的经验,可以看出phase_4需要一个输入参数。
test指令判断输入参数是否为0,可知输入参数>0,随后将输入参数传递给func4函数并执行。
func4执行的返回值与常数$0x1cb91(117649D)对比,相等则通过,可以得知func4是对输入参数进行了运算,且运算结果应该等于111649。接下来分析func4函数,反推输出111649的输入值应该是多少。
快速分析func4的功能:func4汇编段中出现了调用自身的call指令(0x8048cd9),显然func4函数是在进行递归运算。
再寻找递归结束条件,发现递归出口在0x48cd0行。每层递归参数值-1,最后一层递归返回1后,汇编代码执行到0x8048cde,该处汇编代码对下一层递归的返回值进行7倍乘,然后将值返回给上一层,由此可以推导出func4的C代码——7的n次幂。
log(7,117649)=6,phase_4输入6通过。
phase5【静态链表】
汇编码前一部分已经很熟悉了,可以看出phase_5的输入大于1个参数,下面用3个参数去测试,若sscanf返回值不等于3,则需要2个参数。
phase_5的输入参数确实是两个。
通过gdb调试,可以得到phase_5还是对参数1进行操作。
直接找到汇编码的主体,jne返回调用8048d98明显是循环,循环体之前是在做一些初始化操作。
循环判断条件是用返回值eax(也就是-0x14(%ebp))与常数15对比,而incl指令每次循环自增,在循环体内并没有其他地方使用该计数变量。
所以此处逻辑应该是类似while循环的循环体,当满足eax等于15则跳出循环。
先不分析循环内部,当跳出循环之后,cmpl指令将循环计数变量与10对比,等于10则通过,说明前面的循环体应该在第10次时使eax第一次满足等于15的条件跳出。
继续往下观察汇编码可以发现,当-0xc(%ebp)与-0x18(%ebp)相等时则phase_5完全通过,-0x18(%ebp)是我们输入的第二个参数(通过读之前的汇编码或gdb可以看出来),而-0xc(%ebp)在0x8048dab处出现过,在循环的每次都做了+=操作。
综合以上分析,得以得到,循环主体做的工作应该是,每次循环都对-0xc(%ebp)处参数进行+=操作,当-0x14(%ebp)等于15则跳出循环。而我们需要满足的条件是,循环应该进行10次,且最终累加得到的-0xc(%ebp)应该等于我们输入的第二个参数。那么,我们输入的第一个参数应该是什么范围呢?
往回分析汇编码,可以看到0x8048d7c行有一个and指令,效果是%16,而操作数正是-0x14(%ebp),这个操作数很眼熟,正是循环跳出需要等于15的那个数。说明,我们输入的第一个参数,在循环体中会被赋予新的值。(直接分析最上面几行初始化的汇编码也可以得到第一第二参数,这里没有使用该方法。phase_6也没有分析,事实证明分析粒度太细会让事情变得很复杂,没有直接分析逻辑并代入来的快)
到这里我们已经得到了除循环内部以外所有信息,接下来再分析循环内部会简单很多。
在0x8048d9e行出现了 ,地址结构是线性存储,首地址是0x804a5c0,偏移4*eax。而eax又是等于一直在变化的参数一,结构又与链表有关(以上一结点的值作为当前结点的索引),所以循环体内部应该是在进行静态链表的遍历操作,打印0x804a5c0地址为首的16个数(因为参数一%16有16个值)验证猜测:
输出值的范围正好遍布0-15,且从第17个数开始明显超出了合理数值,证实了静态链表的猜想。
假设参数一%16等于6,那么循环一次参数一就变成了15,若参数一%16等于14,则循环两次变成15。
那么拆除phase_5的任务就变成了,从谁开始索引,需要索引10次才使参数一变成15,且参数二等于循环过程中(不包括初始值)所有参数一的累加和。列表推导一下,可以得到初始参数的低八位应该等于13,循环十次累加和为69。
phase6【寻找静态链表的降序索引】
phase6的分析踩了不少坑,一开始去直接分析汇编码,但是汇编码很长而且到处跳转,实在进行不下去。之后又去gdb调试,过程中出现了不少变量,也越查越乱。最后去看了下之前几次拆除炸弹一直忽略的初始化部分,发现phase_6设置了72字节的栈,说明phase_6中用来操作的变量比较多。最后索性代入参数逐步调试,直接将栈打印出来,这样有了输入和输出分析中间的过程会简单一点。
代码主要分为四段,下面逐步分析:
【第一部分】
根据图示,大体结构是进行了两重循环,循环操作的对象是线性结构存储的一系列参数,通过gdb调试或者代码分析可以得出实际上就是我们输入的六个数值。每层循环下标只索引到5,循环体的内层循环是在将外层循环索引的值与后面索引的值逐个对比。
再看一下第一部分需要我们满足的条件,首先外层循环要求 0<输入参数<=6,内存循环要求从参数1开始,每个参数都要与其后的参数不同。也就是6个输入参数要两两不同,且在1-6之间,那么只能是1,2,3,4,5,6的某一种排列。
【第二部分】
还是两层循环,这里外层遍历每一个输入参数,输入6 5 4 3 2 1参数,通过gdb调试,发现经过第二部分后栈内数据发生了以下变化:
多出了六个地址,每个距离相差0xc,gdb输出一下这六个地址对应的值:
汇编码0x8048e68行与0x8048e74行表示,每次循环都在通过-0xc(%ebp)对那六个地址赋值,最后gdb输出的-0xc(%ebp)也确实与六个地址值的最后一个相等。对汇编码继续分析,或者改动六个数字的排列方式按照上面的方式将栈打印,最终可以得到那六个地址值与1-6是一一对应的:
6—0x0804a600=274,5—0x0804a60c=920,4—0x0804a618=100,
3—0x0804a624=313,2—0x0804a630=586,1—0x0804a63c=125。
所以,第二部分的作用就是根据输入的六个参数,给出一组对应顺序的地址,地址指向6个数字。
【第三部分】
代码运行过第三部分之后,栈内数据没有任何改变,而且第三段也不存在指向爆炸的跳转,直接去分析第四部分。
【第四部分】
循环执行5次,1处汇编码是在对比两个线性存储的数字,也就是之前第二部分添加的那六个中的数字。
2处的操作和phase5的静态链表索引很像,每次加0x8向后索引。
8048ee0行的跳转指令又显示,每次循环需要当前索引值大于等于后一个索引值。
到这一步,我们已经有了足够的信息,可以猜测第三部分应该是在对第二部分获得的六个数初始化索引链接,而第四部分验证的要求是,前一个索引数要大于后一个索引数,也就是125、586、313、100、920、274降序排列。而我们要给出的,则是降序的索引。总结下来,phase_6的功能为,当程序按照我们输入的六个参数去生成六个对应的地址时,这六个地址对应的值要满足降序排列。所以答案为:5 2 3 6 1 4。
至此程序需要的六个输入我们都已经分析出来,炸弹也成功拆除。
----------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------