作为一个著名的反汇编语言实验,他的享(作)受(死)程度令我心向往很久。终于,我也悄悄的从CMU官网上下载了这个实验,here we go!
本文将首先讨论一些gdb的基础操作,然后就来逐个拆炸弹。
GDB 基本知识
首先,我们需要将目录切换到准备调试的文件所在目录下,然后打开gdb调试工具。我们这里假设文件名称叫做bomb,那么我们应该在gdb中输入
|
|
下面是几个最常用的命令:
运行至断点(如果没有设置断点就运行到程序结束):run (r)
继续至下一个断点(如果没有则运行到程序结束):continue (c)
设置断点:break (b)
我们有如下几种方式设置断点:
b + line number : 在该行设置断点
b + function name : 在该函数处设置断点
b + *code address : 在该地址处设置断点
删除断点 : delete (d) + number
注意如果没有写number,则删除所有断点
下一步 : s/n/si/ni
他们之间的区别在于是step into (s) 还是 step over (n) 还是针对对应的汇编语言 (si/ni)
这里有一个小窍门:如果我们在单步调试的过程中一直输入si之后输入回车是很麻烦的,因而聪明的gdb给了我们一个简单的方法,直接输入回车就可以默认执行上一个命令。
打印对应变量 : print (p) + variable
这里可以有多种print的格式,他们可以用作p/?,其中?可以分别是:
x: print as hex format
d: print as decimal format
t: print as binary format
s: print as string format
p $eax 可以直接查看eax register中的内容
查看寄存器 : i r
检查连续变量 : x/nfu
当我们需要连续检查一个内存区域中的变量的时候,我们可能需要用到examine命令,他的nfu分别表示:
n代表从起始地址开始显示几个地址
f代表格式,同上文的print
u代表显示几个字节,一般默认代表4字节
举个栗子:x/3xw从起始地址处每次显示3个地址,每个以16进制显示,每个4字节
结束当前调试过程 : kill (k)
退出gdb : quit (q)
基本知识大概就是这么多,现在,炸弹我来也~
拆弹过程
首先我们把bomb文件通过objdump反汇编成汇编语言,之后在每个炸弹的讨论过程中我会分别把相应的汇编代码贴出来。
Phase_1
首先是第一关的汇编代码:
|
|
这应该是一个热身关卡,蛮简单的,主要是让我们学会gdb的基本调试命令,以及熟悉出题人的一些思路。根据0x8048b32那里的string_not_equal函数,我们可以猜测到,应该是让输入的值(%eax)和指定的值(0x80497c0)进行比较,如果相等就进入下一关,如果不相等就调用explode_bomb函数,也就是炸弹爆炸。为此,我们只需要知道0x80497c0中的string到底是什么就可以了:
|
|
很明显,这里的字符串是”Public speaking is very easy”(真的吗,出题人你出来我保证不打死你)。为此,我们进行测试,发现进入了第二关~
Phase_2
首先还是汇编代码:
|
|
这个比刚才的长了好多(然而我想多了,后面的一个比一个长)。下面我会分条写出我的思考过程:
注意到0x8048b5b那里调用了read_six_numbers这个函数,所以我们的Input一定要是六个数。
通过0x8048b56中leal加载的有效地址,可以知道,register %eax points to the first number, which is at address -0x18(%ebp)。
之后的任务就是分析对应的汇编代码 (0x8048b60-0x8048b8c),可以发现这是一个循环,依次来检测下面的数字是否符合条件,如果不符合就引爆炸弹。
首先可以看出,0x8048b63的cmpl指令告诉我们第一个数必须是1。
下面的过程我用java代码写了一下:
1234567public void testNumber (int[] nums) {if (nums[0] != 1) explode();for (int i = 1; i <= 5; i++) {temp = (i + 1) * nums[i - 1];if (nums[i] != temp) explode();}}因此我们发现,最终要求输入的数字是1 2 6 24 120 720.
Phase_3
老规矩,先上代码
|
|
我把代码在一些位置断了行,方便阅读。
我们首先看part1, 从三个leal发现,加载了三个有效地址,因而我们猜测是三个输入输入。通过0x8048bb7我们发现三个input的格式是要通过检测的,因而我们打印0x80497de来看看究竟让我们输入什么样的格式:
123456789101112131415(gdb) b phase_3Breakpoint 3 at 0x8048b9f(gdb) rStarting program: /home/elliot/Desktop/bombWelcome to my fiendish little bomb. You have 6 phases withwhich to blow yourself up. Have a nice day!Public speaking is very easy.Phase 1 defused. How about the next one?1 2 6 24 120 720That\'s number 2. Keep going!1 w 3Breakpoint 3, 0x08048b9f in phase_3 ()(gdb) p/s (char*) 0x80497de$2 = 0x80497de "%d %c %d"清楚的我们发现,需要输入数字,字符,数字一组。这里需要注意的是,我们在调试的时候不能随意输入,因为如果随意输入就不能进入之后的调试环节,因为不能通过sscanf检测。
part2告诉我们,需要让我们的第一个输入大于2并且小于等于7,也就是在3–7之间。
part3中0x8048bd6的 jmp *0x80497e8(,%eax,4) 是这个题目中最关键的一条指令,它告诉了我们这是一个switch函数,并且这个就是跳转表的地址。因此,如果我们第一个输入的是3的话,我们的跳转地址应该是(0x80497e8+0xc)
123456(gdb) x/4 0x80497e80x80497e8: 0x08048be0 0x08048c00 0x08048c16 0x08048c28(gdb) p/s (char) 0x6b$8 = 107 'k'(gdb) p/d 0xfb$10 = 251通过gdb的输出,我们可以发现需要跳转到地址0x08048c28,进而我们发现了0x6b和0xfb,分别用他们适当的格式打印他们,就能得到他们对应的输出。因为这是一个switch语句,所以答案并不唯一,这里我们找到的答案是3 k 251
Phase_4
已经经过了三关了,相信我们已经对汇编代码有了很深(晕)的理(大)解(脑),所以我们当然不会休息,继续!
首先还是汇编代码:
|
|
这个很明显的是 phase_4 调用了 func4 ,因而我们先研究phase_4:
phase_4的0x8048cf6同样调用了sscanf函数,因而通过对前面0x8049808的分析我们发现需要输入的是一个数字,它经过func4的输出结果必须为0x37(55).
func4是一个递归函数,当初始值为1的时候直接输出结果为1,当不为1的时候他会进行递归:
1234public void func4 (int x) {if (x == 1) return 1;return func4(x-1) + func4(x-2);}通过带入计算,发现 func4(9) = 55。所以此处的答案应该为9。
Phase_5
乍一看第五关的代码好少,但是其实这是一个迷惑,这一关比前面任何一关都要有难度的。准备好了吗,我们开始吧。
|
|
part1有一个function是string_length,后面有一个比较,如果不为6就会引爆炸弹。这意味着,我们的input需要是一个包含6个字节的string。
part2中有一个0x804b220被移入了esi register,因而我们打印这个地址:
12(gdb) p/s (char*) 0x804b220$12 = 0x804b220 "isrveawhobpnutfg\260\001"发现这是一串莫名其妙的数字,后面还有\260\001也不知道是什么鬼。前面的“isrveawhobpnutfg”一共是16个字符,我们姑且叫他string1吧。
part3是这个题目的核心部分,它表示,我们的输入字符作为偏移量,对刚才的string1进行一个变换。具体的变换方法是,通过把我们的字符转化为ASCII码,取出其中的最后一位(&0xF),假设为5,那么我们就像对应的取出string1[5]。具体的java代码是这样的:
1234567public void phase5 (string inputString) {String[] strings = new String[6];for (int i = 0; i < 6; i++) {strings[i] = string1[(inputString[i] & 0xF)];return strings;}}我们在part3中得到的字符串需要与part4中题目要求的字符串相同。题目要求的字符串在0x0x804980b中,通过print我们发现他是”giants”,因而通过查询ASCII table我们可以得到一组字符串作为偏移量:opekmq。注意,这里的答案并不唯一,甚至于他们不是字母而是其他的之类的符号都是可以的,但是他们的ASCII码的最后一位必须符合题意。
Phase_6
这是CMU老师声称 “The last phase will even challenge the best students”。 我觉得像我这种学霸(渣)那是一定会被虐的体无完肤的,不过没有困难也要创造困难,何况现在就有困难呢。
|
|
这个题目的代码应该也是这个lab中最长的了,不过不要着急,我们一步步来看。
这里的part1就很复杂,首先,0x8048da4中将一个0x804b26c移动到-0x34(%ebp)中,不过这个家伙和我们现在还没什么关系,等会儿part2和part3的时候才会用到它。
继续往下看part1,可以发现它需要我们输入六个数字,这六个数字都必须位于<6的范围内,否则就会爆炸。
同时,这六个数字每一个都不能相等。
12345678public void phase6_1 (int[] nums) {for (int i = 0; i < 6; i++) {if (nums[i] > 6) explode();for (int j = i + 1; j < 6; j++) {if (nums[i] == nums[j]) explode();}}}至此,part1分析完毕,我们进入part2。
part2揭露了这个问题的本质:刚才我们放过的0x804b26c我们重新回头来看他,发现这个货很奇怪:
12(gdb) x/d 0x804b26c0x804b26c <node1>: 253奇怪就在,它的前面有一个tag:node1,这是什么,这不是传说中的链表吗!因而我们使用x/3d打印他的连续三个相关字节的内容:
12(gdb) x/3d 0x804b26c0x804b26c <node1>: 253 1 134525536结果表明,这确实是一个链表。那么part2的代码就好理解了:他的主要目的是根据我们所提供的六个数字,并以我们的数字为索引进行链表的重新排序,组成一个全新的链表:
123456789101112public void phase6_sort (int[] nums) {\\ "nodes" is the nodes that we already have in our memory\\ We assume we already have the node data structureNode[] nodesSorted = new Node[6];for (int i = 0; i < 6; i++) {Node node = nodes[0];for (int j = 1; j < nums[i]; j++) {node = node.next;}nodesSorted[i] = node;}}在part3中,我们则对我们刚才已经重排序的链表进行重新组织和测试,他们必须满足前一个node的值都比后面的要大:
12345public void phase6_test (node[] nodes) {for (int i = 0; i < 5; i++) {if (nodes[i] < nodes[i+1]) explode();}}现在我们需要做什么?我们需要做的就是把这6个node的值全部找出来(通过p node1/node2/…),然后把他们按照从大到小排序,提取他们的索引,就是我们最后需要的答案。这六个数字分别是:253,725,301,997,212,432, 所以我们最终的次序应该是4 2 6 3 1 5。
至此,6个炸弹全部排解完毕。
关于隐藏关的炸弹,我会在后续的博文中讨论。当然,也有可能直接更新在这里。附上一个通关截图~