-
先导知识
- 基本Linux命令行代码
- 一定的汇编语言阅读能力
- 对gdb的简单了解(gdb指令可查阅:GDB Cheat Sheet (whu.edu.cn))
- 如果你好奇炸弹炸了是什么样子,请不要尝试hhh,因为我已经给你们试过了:
准备过程
解压你获得到的tar文件(tar -xvf bombX.tar)得到一个目录./bombXXX,其中包含如下文件:
- README:标识该bomb和所有者。
- bomb:bomb的可执行程序。
- bomb.c:bomb程序的main函数。
然后在该文件目录下打开终端,就可以开始拆炸弹啦!
开始拆炸弹!——phase_1
phase1的考点就是字符串:进行字符串比较,如果输入的字符串与预设的字符串不相等,则”炸弹爆炸“。
具体解法:
gdb bomb
使用 gdb 运行 bombb explode_bomb
设置断点,即使当我们错误输入时也能阻止炸弹爆炸b phase_1
从bomb.c
中可以知道,phase_1(input)
处理输入字符串,故我们在此函数入口处设置断点r
运行程序- 随便输入一些字符
layout asm
显示反汇编窗口
可以看到,调用了 strings_not_equal
函数,b strings_not_equal
在该函数入口处设置断点
c
继续运行
又调用了 string_length
函数,b string_length
设置断点,c
继续运行
通过阅读这段代码,我们可以知道:
string_length
函数实现:将输入字符串的长度存在寄存器 %rax
中返回
回到 strings_not_equal
函数,我们能够知道它首先得到我们输入字符串长度,然后得到正确答案字符串长度,进行比较,二者不相等则在 %eax
中存 1 返回;若二者长度相等,则逐个比较二者字符串内容,若全部相等则在 %rax
中存 0 返回,若有任意一个不相等则在 %eax
中存 1 返回
回到 phase_1
函数,我们现在就能知道 0x402500
处就是答案字符串地址,使用 x/s 0x402500
查看该处字符串
复制下来这句话,使用 q
命令退出 gdb,gdb bomb
重新进入调试模式,b phase_2
在 phase_2 入口处打上断点,r
运行,输入刚才复制的这句话,phase_1 的炸弹成功拆除:
开始拆炸弹!——phase_2
phase_2的考点:
- 数组
- 循环
其功能简单来说就是:
- 将在字符串解析为6个数字的整数数组array
- array第三个个整数为0、1、1
- array中每个元素必须是前两个个元素之和
具体解法:
- 从 phase_1 结尾处继续
b explode_bomb
设置断点,即使当我们错误输入时也能阻止炸弹爆炸- 随便输入一些字符,回车,进入 phase_2 断点,
layout asm
显示反汇编窗口,可以看到首先调用read_six_numbers
函数,这提示我们需要输入六个数
b read_six_numbers
在 read_six_numbers
函数入口处打上断点,c
继续执行
首先栈顶指针寄存器 %rsp
减去 0x18
,在内存中开辟了一块 24 字节的栈空间
在第 2 步中的图中我们能看到,调用 read_six_numbers
前先将栈顶指针减去了 0x28
,然后把栈顶指针值赋给 %rsi
大致的栈变化和寄存器变化如下图所示(括号代表栈变化)
我们注意到在0x4015f3位置出现了一个地址:0x4027f5,使用 x/s
尝试打印一下内容
再结合前面的函数名为 read_six_numbers
,可以知道我们需要输入六个整型数,且用空格分开,故我们先使用 q
退出当前调试,重新输入六个整数
Tips:
当我们想重新开始调试拆解 phase_2 时,就不得不把 phase_1 的拆弹密码再输入一遍,可以想象,将来我们调试拆解 phase_6 时,前面 5 个密码每次都要输一遍,这实在是太浪费时间了。幸运的是,作者考虑到了这种情况,我们可以在当前目录下新建一个文本文件,其中存有我们的密码,在运行或调试时带上参数即可
Vim 编辑器的具体使用不在此介绍,我们在此只需要知道:
Vim 首先进入的是命令模式,按 i 进入编辑模式,输入完成后按两次 esc 键退出编辑模式。之后按冒号:,输入 wq,按 enter 键,表示保存并退出。
接着gdb bomb
进入调试,b phase_2
打断点,r key.txt
带参数运行
帮我们省去了重复输入字符串的功夫
上图若干条指令对照内存走一遍大致明白就是栈顶处为整型数 0
栈顶指针 +4 (整型数占用 4 个字节)指向的下一个整数应该1
第三个整数(栈顶指针 +8)为前两个数之和(1+0=1)即为1
可以看出cmp rbp rbx这附近的一段是一个循环
所以第四个及以后的整数也为前两个数之和,分别是2,3,5
回头看我们知道了第 3 步中那些操作是通过栈传递参数,我们输入的六个数是逆序入栈,第一个数最后入栈,为栈顶。所以我们输入的字符串应该是:0 1 1 2 3 5
将其存入key文件然后 q
退出当前调试,gdb bomb
重新进入,b phase_3
在 phase_3 入口处打上断点,r key
运行
开始拆炸弹!——phase_3
phase_3的考点:
- switch-case
phase_3的功能总的来说:
- 将字符串解析为两个数字x, y
- x不能大于7
- 通过switch-case可以将x的值映射为一个num值
- y必须等于num
具体解法:
随便输入一些字符,回车,进入 phase_3 断点,layout regs
显示反汇编和寄存器窗口
可以看到在 <phase_3+14> 处出现一个内存地址 0x402801,我们来用x/s 查看一下
结合在 phase_2 中的经验,我们可以猜测phase_3 的输入是两个数字,以空格隔开
然后程序调用一个什么sscanf程序,这次我们不直接打断点去看它的汇编
新方法!
我们根据之前的假设输入任意两个数字进key
接着stop再run key:
接着我们stepi一步一步来看,一直step到sscanf这个函数
这里我们也可以看出它需要的参数是两个整数。接下来我们finish:
看到这个return value,联系我们的已有知识,我们不难猜这里就是在判断我们输入了几个数字。
之后代码判断返回值是否大于1,如果大于,跳转至phase_3+39,很好,我们又跳过了一个炸弹!
接下来一连串比较&跳转,我们慢慢来看:
首先是比较0xc(%rsp)和7的大小,大于则跳转phase_3+106
我们一看,好的,又是炸弹。说明,0xc(%rsp)不能大于7
那么这个0xc(%rsp)是什么呢,我们打出来看看
不要忘记我们之前在key中输入的值,这个2就是我输入的第一个数。这说明我们输入的第一个数不能大于7。接着我们继续来看,代码将这个数传给了eax,再根据eax的值跳转
那我们stepi一下,看看根据我们现在输入的第一个数跳转到哪里去了
这里是把0x117赋给了eax,然后又跳转:
这里很明显我们可以看出,就是比较0x8(%rsp)和eax,相等就跳转,不相等就爆炸。
那我们赶紧把key中的第二个数字设为279(0x117的十进制数),再重新运行看看(别忘了改一下断点位置)
很好,我们已经解决了一半的炸弹!
开始拆炸弹!——phase_4
phase_4考点:
- 递归
从 phase_3 结尾处继续,随便输入一个字符,回车,进入 phase_4 断点。一样的,我们layout asm
开头与之前类似,我们同样关心这个0x402801是什么,打印出来看看
这个参数形式,我们同样猜测这次的输入也是两个整数,结合从开头一直到<phase_4+32>的代码,我们基本上可以肯定这个猜测。
然后继续看,程序将0xe(十进制14)与0xc(%rsp)比较,如果小于等于,就跳过下一个炸弹。
同样的,我们输入两个数到key里面,然后更改断点位置,再stepi测试一下
注意这里要加个*号
然后我们开始stepi一步一步来看,首先我们打印一下0xc(%rsp)的值
说明是我们输入的第一个值
然后继续,我们进入了func4,和之前一样,我们直接查看返回值
还不是很理解这里的返回值15是怎么得来的。先看看后面的代码,我们知道,第二个数要是0x2d(十进制的45),于是我们换个输入值再试试。
同样的,我们再次进入func4
这里我不详细说明func4的运行流程,不过值得注意的是sar只有一个参数的时候是什么意思:
func4大致是根据输入参数计算结果,两者成正相关,最后根据自己推导出的原函数计算,得出要使eax结果为45,输入参数应为14
那我们更改key里的参数,再次运行
很好,我们又解决了一个炸弹!
开始拆炸弹!——phase_5
phase_5考点:
- 数组
- for循环
其功能可总结为:
- 在数组里跳转
- 根据输入的第一个值x确定跳转次数
- 将跳转到的所有数字求和,即输入的第二个数字y
又是熟悉的开头
我们同样猜测这里是要输入两个整数,测试一下,果然没问题
接下来,我们看以上这段代码(假设输入的两个数字分别为x y):
eax=x
eax与1111(二进制)相与,即取eax低四位
将修改后的eax传回给栈
将eax与1111相比较,若相等,则爆炸
若不等,ecx=0;edx=0;
edx=edx+1;
之后一个比较少见的汇编代码cltq:把eax符号扩展到rax(猜测:为了下文取值,因为基址寄存器必须是64位)
根据rax值进行赋值,eax=0x4025a0+4*rax
ecx=ecx+eax
比较1111和eax,若不等,跳转至phase_5+65
若相等,x=1111
比较edx(1)和1111,若不等,爆炸
比较y和ecx,若相等则成功结束,不等则爆炸。
因此,我们发现关键要让edx=1111(15),在这种循环次数下,得到ecx,就是我们第二个输入的数字。
于是我们打印出整个数组的内容,推导x应等于几。我们发现rax=6,时,eax=1111,反推15次循环,6、14、2、1、10、0、8、4、9、13、11、7、3、12、5。因此,我们知道了x=5,从而ecx=115。
由于edx=1111是一个比较大的数,其实,我们还可以采用穷举法():
当x=0时,edx=6;x=1时,edx=4;x=2时,edx=3;x=3时,edx=13;x=4时,edx=8;
当x=5时,edx=15,这时我们再查看ecx的值
因此我们更改我们的key文件,再次运行
令人兴奋的消息~
开始拆炸弹!——phase_6
总的来说就是将输入的数据n对应到node_n,然后根据输入数据顺序将node重排,重排要求每个node递减。
从 phase_5 结尾处继续,随便输入一些字符,回车,进入 phase_6 断点,layout asm
显示反汇编窗口
虽然这个比前面 5 个都要长上不少,但不要畏惧,这并不复杂
首先同样是从 read_six_numbers
可知,我们需要输入的依旧是六个以空格隔开的数字,因此我们随意输入六个数来测试一下
确实是六个数字
接下来我们逐步看这个汇编,之后的代码行数n我都是以汇编中的+n来称呼。
0x00000000004010dd <+0>: push %r13
0x00000000004010df <+2>: push %r12
0x00000000004010e1 <+4>: push %rbp
0x00000000004010e2 <+5>: push %rbx
#以上均为对调用者寄存器保存的过程
0x00000000004010e3 <+6>: sub $0x58,%rsp
0x00000000004010e7 <+10>: lea 0x30(%rsp),%rsi
0x00000000004010ec <+15>: callq 0x4015cf <read_six_numbers>
#读取输入并检查输入数字个数
0x00000000004010f1 <+20>: lea 0x30(%rsp),%r13 #r13=rsp+30
0x00000000004010f6 <+25>: mov $0x0,%r12d #r12d=0
0x00000000004010fc <+31>: mov %r13,%rbp #rbp=r13=rsp+30
#:rbp:第二个数
0x00000000004010ff <+34>: mov 0x0(%r13),%eax #eax=(r13)输入的第一个数
#第二个数
0x0000000000401103 <+38>: sub $0x1,%eax #eax=eax-1
0x0000000000401106 <+41>: cmp $0x5,%eax #eax和5比较
0x0000000000401109 <+44>: jbe 0x401110 <phase_6+51> #eax须<=5
#第一个数小于等于6
#第二个数小于等于6
##其实就是每个数小于等于6
0x000000000040110b <+46>: callq 0x401599 <explode_bomb>
0x0000000000401110 <+51>: add $0x1,%r12d #r12d=r12d+1 #r12d=1
#r12d=2
0x0000000000401114 <+55>: cmp $0x6,%r12d #r12d和6比较
0x0000000000401118 <+59>: jne 0x401121 <phase_6+68> #r12d不等于6,跳转
0x000000000040111a <+61>: mov $0x0,%esi #if r12d=6, then esi=0
0x000000000040111f <+66>: jmp 0x401163 <phase_6+134>
##整个循环一直到100行,如果判断完了所有输入的数,则从这跳转出去到134行
0x0000000000401121 <+68>: mov %r12d,%ebx #ebx=r12d(初值为1)
#ebx=2
0x0000000000401124 <+71>: movslq %ebx,%rax #rax=ebx (初值为1)2 3 4 5
#rax=2
0x0000000000401127 <+74>: mov 0x30(%rsp,%rax,4),%eax #eax=(rsp+4rax+30)#即输入数据,按rax的值为几就是第n+1个
0x000000000040112b <+78>: cmp %eax,0x0(%rbp) #比较eax和rbp,rbp初值为输入数据第一个值的地址)
0x000000000040112e <+81>: jne 0x401135 <phase_6+88>
0x0000000000401130 <+83>: callq 0x401599 <explode_bomb>
0x0000000000401135 <+88>: add $0x1,%ebx #ebx=ebx+1(初值为1)(加完为2)2 3 4 5 6
0x0000000000401138 <+91>: cmp $0x5,%ebx #比较5和ebx (2 3 4 5 6
0x000000000040113b <+94>: jle 0x401124 <phase_6+71>
0x000000000040113d <+96>: add $0x4,%r13 #r13原为输入的第一个数,加完变成第二个数
0x0000000000401141 <+100>: jmp 0x4010fc <phase_6+31>
##实际上就是每个数的值不能相等
##第一个输入判断的大逻辑结束
以下代码中出现了一个地址,我们读其中的内容:
x/128x 0x6042f0
它的名字叫node,从这里我们可以进行一些推测这里与节点和树(图)相关。 并且我们惊奇地发现这也是六个节点。接下来我们来看看汇编代码。
0x0000000000401143 <+102>: mov 0x8(%rdx),%rdx #node的下一个值赋给rdx
0x0000000000401147 <+106>: add $0x1,%eax
0x000000000040114a <+109>: cmp %ecx,%eax
0x000000000040114c <+111>: jne 0x401143 <phase_6+102>
0x000000000040114e <+113>: jmp 0x401155 <phase_6+120>
#根据输入数据,若等于n,跳转,此时为node_n
0x0000000000401150 <+115>: mov $0x6042f0(node1),%edx #edx=node1
0x0000000000401155 <+120>: mov %rdx,(%rsp,%rsi,2) #(rsp+2rsi)=rdx=x(rsi初值0)
##输入数据为n,则赋值node_n
0x0000000000401159 <+124>: add $0x4,%rsi #rsi+=4
0x000000000040115d <+128>: cmp $0x18,%rsi #rsi与24比较,即判断1是输入的第几个数
0x0000000000401161 <+132>: je 0x401178 <phase_6+155> #若是最后一个数,则去155
##进入新的阶段,这里是入口
0x0000000000401163 <+134>: mov 0x30(%rsp,%rsi,1),%ecx#rsi初值0,ecx:输入的第一个值
#ecx为输入的第二个值
0x0000000000401167 <+138>: cmp $0x1,%ecx #ecx与1比较
0x000000000040116a <+141>: jle 0x401150 <phase_6+115> #ecx小于等于1跳转至115行
#根据前面的判断条件,我们可以知道这里只有一个输入可以小于等于1
0x000000000040116c <+143>: mov $0x1,%eax #eax=1
0x0000000000401171 <+148>: mov $0x6042f0,%edx #这个地址我们发现是node1
0x0000000000401176 <+153>: jmp 0x401143 <phase_6+102>
##这一部分结束,总的来说,逻辑就是遍历每一个输入,若输入为n,则找到node_n赋值,并赋值到从rsp开始的栈内,每个占八字节
因此我们需要来看看,node的每个值是多少
然后,聪明的我们发现,每个node可以分成两部分,后面八个字节是下一个node的地址!因此我们推翻之前的猜测,这里的node更应该是链表。接下来,我们继续来看汇编代码
0x0000000000401178 <+155>: mov (%rsp),%rbx #rbx=(rsp),第一个node的地址
0x000000000040117c <+159>: lea 0x8(%rsp),%rax #rax=rsp+8,下一个node在栈内的地址
0x0000000000401181 <+164>: lea 0x30(%rsp),%rsi #rsi=rsp+30,我们输入的数的开始地址
0x0000000000401186 <+169>: mov %rbx,%rcx #rcx=rbx
0x0000000000401189 <+172>: mov (%rax),%rdx #rdx=(rax)
0x000000000040118c <+175>: mov %rdx,0x8(%rcx) #
0x0000000000401190 <+179>: add $0x8,%rax
0x0000000000401194 <+183>: cmp %rsi,%rax
0x0000000000401197 <+186>: je 0x40119e <phase_6+193>
0x0000000000401199 <+188>: mov %rdx,%rcx
0x000000000040119c <+191>: jmp 0x401189 <phase_6+172>
0x000000000040119e <+193>: movq $0x0,0x8(%rdx)
##总的来说,这是把节点链接起来,即第一个数对应的结点的指针域(后8字节)存储第二个数对应节点的地址,依此类推,最后让数6对应节点的指针域指向 Null
0x00000000004011a6 <+201>: mov $0x5,%ebp
0x00000000004011ab <+206>: mov 0x8(%rbx),%rax
0x00000000004011af <+210>: mov (%rax),%eax
0x00000000004011b1 <+212>: cmp %eax,(%rbx) #rbx里的值要小于等于eax
0x00000000004011b3 <+214>: jle 0x4011ba <phase_6+221>
0x00000000004011b5 <+216>: callq 0x401599 <explode_bomb>
0x00000000004011ba <+221>: mov 0x8(%rbx),%rbx
0x00000000004011be <+225>: sub $0x1,%ebp
0x00000000004011c1 <+228>: jne 0x4011ab <phase_6+206>
##最后这里就是让数1对应节点的内容(取4个字节)小于数 2 对应节点的内容,数2对应节点的内容小于数3对应节点的内容,以此类推。
#之后就是很平常的结束phase_6调用的代码啦,就不放在这里占地方了。
因此我们需要将每个node的值都给出来:
node_1 | 0x20a |
node_2 | 0x7a |
node_3 | 0x358 |
node_4 | 0x1b8 |
node_5 | 0x1a4 |
node_6 | 0x35b |
因此,node:6>3>1>4>5>2;所以我们的key应该是2 5 4 1 3 6!
让我们来试试!
好耶~= ̄ω ̄=
当然...事情没有那么简单——secret_phase
隐藏关卡的解法就不在这里详细写了hhhh毕竟是隐藏关卡
tips:想找到它,可以去看看phase_defused函数!
最终的key文件:
然后!解决!