第一个炸弹
gdb基本用法
进入bomb
和bomb.c
所在的目录之后,运行gdb -tui bomb
进入GDB Text User Interface界面。敲一下回车然后依次输入layout split
和layout regs
,此时你的终端应该长这样:
最上面的一栏显示寄存器情况(现在程序还没运行所以没有内容),中间一栏显示汇编代码,下面一栏用于输入命令。
接下来查看bomb.c
,发现第74行是phase_1
的入口,因此我们用break 74
或者b 74
命令在这一行打上断点,这样在执行run
命令时,程序就会在这一行执行之前停下。
打完断点之后,执行run answer.txt
命令让程序运行。这样在后续执行过程中,程序就会从answer.txt
读取内容。
解密
执行run answer.txt
之后如下图:
在call phase_1
之前,还执行了mov %rax,%rdi
,这条指令的意思是把%rax
的值放到%rdi
中,同时%rax
的值保持不变。对照通用寄存器的用途约定,这个操作的意思是将read_line
函数的返回值作为phase_1
的第1个参数。和input = read_line(); phase_1(input);
这两行源码是对应的。也就是说,%rdi
此时是一个char *
,指向的字符串就是answer.txt
的第一行(实际上是这个字符串的值是第一行的内容,能明白这个意思就行)。
接下来我们输入stepi 2
,意思是再执行两条指令,然后停下,这时候我们应该进入函数phase_1
内部了:
函数逻辑很简单,总共也就8条指令,我们一条条分析:
sub $0x8,%rsp
:栈顶指针往下移8个字节,也就是开辟8个字节的临时空间给当前函数用。mov $0x402400,%esi
:将0x402400
这个数字放到%esi
中,显然这应该是一个地址,地址只需要32bit(因为是%esi
而不是%rsi
)说明编译代码时的环境是32的系统,但是没关系,在64位系统上也是兼容的。%esi
的约定功能是函数的第2个参数,注意此时%rdi
的值还没有改变,也就是函数string_not_equal
的入参是两个地址,从函数名字来看,这两个地址都是字符串的首地址,函数内部判断这两个字符串是否不相等。call 0x401338
:调用位于这个地址的函数。<string_not_equal>
是注释内容,不包括在指令内。不过从这个注释也能看出,在编译器眼中,函数名其实就是函数的入口地址,这和数组名就是数组首地址是一样的逻辑。test %eax,%eax
:%eax
的值和自己做与运算,但是不保存计算结果,只是用这个计算结果更新标志寄存器。je 0x400ef7
:Jump if Equal to zero。检查ZF标志位,如果ZF是1(意味着上一条指令的计算结果是0),就跳转到0x400ef7
这条指令。call 0x40143a
:从注释来看,这个函数用于引爆炸弹。add $0x8,%rsp
:归还栈帧空间。ret
:phase_1
函数指定完毕,返回。参考Introduction中的介绍。
总结起来就是,比较answer.txt
的第一行是否和0x402400
这个地址的字符串相等,相等返回0否则返回1(这也和函数名含义一致),然后判断返回值是否是0,如果是0,跳过引爆函数。
所以第一个炸弹的密码就是0x402400
这里的字符串,直接用x /s 0x402400
命令读取这个字符串即可。
结果是Border relations with Canada have never been better.
。
然后输入q
会quit
退出程序,将answer.txt
的第一行修改为正确密码。
最后执行./bomb answer.txt
查看第一个炸弹是否被拆除:
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
BOOM!!!
The bomb has blown up.
总结
第一个炸弹比较简单,只需要了解基本的x86指令和gdb用法即可拆除。
在实际操作中,可以多使用stepi
(没有参数默认执行1条指令)一步步执行指令,观察寄存器值的变化,辅助理解或者验证指令的执行。另外对于非核心函数,也可以用nexti
(也可以指定参数)跳过函数的执行,或者在函数内部时用finish
命令执行完当前函数。
退出gdb可以输入q
、quit
或exit
。