CSAPP:二进制炸弹实验

二进制炸弹是作为一个目标代码文件提供给学生们的程序,运行时,它提示用户输入6个不同的字符串。如果其中任何一个不正确,炸弹就会“爆炸”:打印出一条错误信息。学生通过反汇编和逆向工程来确定是哪六个字符串,从而解除他们各自炸弹的雷管。该实验教会学生理解汇编语言,并强制他们学习怎样使用调试器。

对这个实验慕名已久,做了一下不禁感叹:果然牛x,不愧是从美国进口的!

因为提供的二进制炸弹是根据特定的平台而生成的,所以必须在”官方“提供的服务器上进行拆弹,我们一般用putty登录。在这里,我没有研究怎么搞putty,而是直接找别人的炸弹研究一下,理解汇编指令即可,后面有空,在慢慢学习gdb调试!

首先来看phase1 的代码:

分析解答:

注意strings_not_equal 这个函数,从字面上可以猜想地理解为:把输入的字符串和内存某处字符串相比较,不相等的时候函数返回值的为1。再看接下来两句:

test %eax,%eax
je 8048bb2 <phase_1+0x22>

test 是把两个操作数进行与运算,而通常这两个操作数是一样的,此操作的意义就在于影响标志位,当%eax 为0 时,零标志位置1,否则零标志为0。而从je指令可以知道当%eax 为0 时程序会跳过接下来一句对爆炸函数的引用,所以我们的目标就是要是%eax 即strings_not_equal 的返回值为0,即要是输入的字符串与内存某处的存放的字符串相等。于是现在的关键就是找出那个字符串是放在内存的哪个地方。其实非常明显,$0x80498c0 这个地址在程序里实在太显眼,用x/s 0x80498c0 命令一查,果然那里存放有一句话:"I am not part of the problem.I am a Republican."于是run ,输入,果然:Phase 1 defused. How about the next one?


然后来看phase2:

分析解答:

注意到read_six_numbers 这个函数,同样故名思义我先猜他为输入6 个数字。暂且不看read_six_numbers 函数的内容,先注意8048bcf 这句,cmpl 显然是要引起我高度注意的,因为比较的结果往往直接关系到爆不爆的问题。这条指令的内容很明确,就是看0xffffffd8(%ebp)处的数字是否为1,不是的话就会调用爆炸函数。于是我非常坚定地认为1 便是我该输入的第一个数字,而输入的数字是存放在0xffffffd8(%ebp)开始的地址处的。

接着看下去,lea 0x1(%ebx),%eax 这句将%eax 置为2。8048be2 这句中的0xffffffd4(%ebp,%ebx,4)这个存储器操作数稍作思考便可以知道其实就是0xffffffd8(%ebp),即是我们要输入的第一个数字,因为此时%ebx 为1。所以这句是将第一个数字乘以2 放入%eax。8048be7 又来cmp 了,很容易看出0xffffffd8(%ebp,%ebx,4)是我们输入第2 个数字的地方,还是因为此时%ebx 为1,所以这里告诉我们,要输入的第2 个数字就是第一个乘以2 的结果。

8048bf2 和8048bf3 两句告诉我们从%ebx 等于1 到5 分别进行以上两段的操作,即后面一个数字由前面一个数字乘以%eax 得到,而每次的乘数%eax 为%ebx 加1,即乘数分别为2,3,4,5,6,所以可以确定这六个数字分别为1,2,3,4,5,6 的阶乘,即1 2 6 24 120 720。于是输入,果然:That's number 2. Keep going!


来看第3 个bomb:

分析解答:

Bomb3 的代码就比较长了,先抓住重点:jmp *0x8049938(,%eax,4)这句话让我想起了switch 语句。再翻翻书,发现这段代码就是一个跳转表结构。从语句上便可以看出备选的跳转地址存放在0x8049938 开始的地址处,通过%eax 的值来选择。通过打印0x8049938 处的16 进制数可以确认:
(gdb) print /x *0x8049938
$3 = 0x8048c49
(gdb) print /x *0x804993c
$4 = 0x8048c54
(gdb) print /x *0x8049940
$5 = 0x8048c5f
(gdb) print /x *0x8049944
$6 = 0x8048c67
(gdb) print /x *0x8049948
$7 = 0x8048c72
(gdb) print /x *0x804994c
$8 = 0x8048c7d
(gdb) print /x *0x8049950
$9 = 0x8048c88
(gdb) print /x *0x8049954
$10 = 0x8048c93

switch转换表是这样的:


可以看到这8 个16 进制数正好是程序中的8 个地址(都用黑体标出),对应于%eax为0 到7 时的跳转地址。由于

cmp $0x2,%eax
jg 8048c39 <phase_3+0x3b>


两句限定了%eax 大于2,所以我取%eax 为3,然后查表到8048c67 处。

mov$0x6e,%bl 和cmp 0xfffffff7(%ebp),%bl 告诉了我们0xfffffff7(%ebp)处应该输入的值,cmpl $0xcb,0xfffffff8(%ebp)与je 8048ca7 <phase_3+0xa9>告诉了我们0xfffffff8(%ebp)处应该输入的值,然后我很自然的把0x6e 和0xcb 换算成了10 进制数110 和203,然后迫不及待地输入3 110 203,结果很失望的到了break point1。
这里我困惑了挺久,我还试了16 进制的输入,甚至怀疑自己整个的理解是否有问题。终于我发现了一个问题:第2 个“数字”的地址为0xfffffff7(%ebp)而第三个为0xfffffff8(%ebp),显然这是不合常理的,不可能只占一个字节。而只占一个字节的东西,我就只能想到字符了,但明明分析程序看到的是一个“数字”,要把数字与字符联系起来,就是ASCII 码了!查表,6e 果然对应着一个字母:n于是迫不及待地输入:3 n 203,果然Halfway there!
当然对应于不同的第一个数字,有不同的答案。

事后我还发现, 语句8048c1c: c7 44 24 04 26 99 04 movl$0x8049926,0x4(%esp)中的$0x8049926 处的字符串:

(gdb) x/s 0x8049926
0x8049926 <_IO_stdin_used+514>: "%d %c %d"


原来对输入的格式早有说明!

 

第4 个bomb:

分析解答:

 

首先要研究下func4 的功能。
容易看出是一个递归函数。lea 0xffffffff(%esi),%eax 是得到%esi-1 的值然后调用func4,同样lea 0xfffffffe(%esi),%eax 是得到%esi-2 的值然后调用func4,lea (%eax,%ebx,1),%eax 即是将f(%esi-1)的返回值(在%ebx 里面)与f(%esi-2)的返回值相加放在%eax 中作为func4 的返回值,很明显这是一个斐波那契数列的函数。

 

细节研究可知:

func4(0) = 1,func4(1) = 1;

func4(2) = func4(1) + func4(0) = 2;

func4(3) = func4(2) + func4(1) = 3;

func4(4) = func4(3) + func4(2) = 5;

...

func4(10) = func4(9) + func4(8) = 89;


明白了func4 的意思,我直插phase4 的心脏:

cmp $0x59,%eax je 8048d3b <phase_4+0x45>
很清楚,当%eax 等于0x59 的时候就可以过关了。%eax 是什么?是call 8048cb8 <func4>后的返回值!那此次调用的函数参数是什么呢?就是我们输入的东西了。8048d11 和8048d16 两句也说明了输入的是一个数字。然后把斐波那契数列一排,0x59 对应的序号为10,输入,果然:So you got that one. Try this one.


看第5 个:

分析解答:

 

首先,call 8048ff9 <string_length>和cmp $0x6,%eax 两句告诉我们要输入的是6 个字符。在指令movsbl (%edx,%ebx,1),%eax 中,%ebx 为6 个字符的起始地址,通过循环增加%edx 的值来依次将这6 个字符的ASCII 码传给%eax进行下一步操作。

 

说明一下movsbl (%edx,%ebx,1),%eax具体含义:我们知道,movsbl S ,D的意思是将S(不论寄存器还是内存地址)里面存放的值的一个最低位字节拷贝出来,设置其高24位为此字节最高有效位,然后传送到D中。简单描述就是传送符号扩展的字节。因此,此指令的含义为将%ebx+%edx里面的内容(其实应该为一个地址),把这个地址中的值得最低位字节符号扩展,然后传送到%eax中。

与movsbl相对的是movzbl,它是0扩展,其余与 movsbl 一样。


研究下这句:movzbl 0x804a5c0(%eax),%eax。

说明一下这个格式0x804a5c0(%eax),它的原型为Imm(Eb),操作为M[Imm+R[Eb]],表示为(基址+偏移量)寻址,它很容易让我们联想到数组的操作。
刚才已经说了%eax 是我们输入字符的ASCII 码,经过and $0xf,%eax 处理后也就是相应ASCII 码的低4 位。查看0x804a5c0 处存放的东西:
(gdb) x/s 0x804a5c0
0x804a5c0 <array.0>: "isrveawhobpnutfgs/001"
0x804a5c0(%eax) 的寻址方式说明了这条指令是将字符串"isrveawhobpnutfgs/001"中的第%eax 个字符传送给%eax,%eax 起到了一个索引的作用。循环6 次以后,以输入的6 个字符的的ASCII 码低4 位为索引得到的字符串"isrveawhobpnutfgs/001"中的6 个字符,被装入0xffffffe8(%ebp)为起始地址的连续存储空间中。
接着可以看到8048d8a 处调用了strings_not_equal,8048d7c 处清楚地说明了比较的对象,查看0x804992f 处的字符串:
(gdb) x/s 0x804992f
0x804992f <_IO_stdin_used+523>: "giants"
说明我们只要使索引得到的字符串为"giants"就可以了!
反推回去,g、i、a、n、t、s 对应的索引值为15、0、5、11、13、1。所以只要使我们输入的6 个字符的ASCII 码低4 位依次为15 0 5 11 13 1 就可以了。
我取为o0ekma,于是:Good work! On to the next...

 

当然,这里的答案不是固定的,只要满足要求即可!

 

第六个:

第六个炸弹代码太长,应该步步调试来获取答案,下面简述一下gdb调试

gdb调试:
作为调试,我觉得最重要的就是要搞清楚如何单步调试,接下来区分step、stepi和next、nexti
1,step和next是对c源程序进行调试,每步一行。而stepi和nexti主要对汇编指令进行调试,每步一个指令语句。
2,step和stepi是遇到调用函数则会进入调用函数里面单步执行,而next和nexti遇到调用函数则不进入,跳过继续执行本函数的下一行或下一指令语句。
3,对汇编进行调试时,若需要到达调试点有三种方式:step、next以及continue,以coninue使用最多。

GBD常用命令

1.启动GDB
你可以输入GDB来启动GDB程序。GDB程序有许多参数,在此没有必要详细介绍,但一个最为常用的还是要介绍的:如果你已经编译好一个程序,我们假设文件名为hello,你想用GDB调试它,可以输入gdb hello来启动GDB并载入你的程序。如果你仅仅启动了GDB,你必须在启动后,在GDB中再载入你的程序。
2.载入程序 === file
在GDB内,载入程序很简单,使用file命令。如file hello。当然,程序的路径名要正确。
退出GDB === quit
在GDB的命令方式下,输入quit,你就可以退出GDB。你也可以输入'C-d'来退出GDB。
3.运行程序 === run
当你在GDB中已将要调试的程序载入后,你可以用run命令来执行。如果你的程序需要参数,你可以在run指令后接着输入参数,就象你在SHELL下执行一个需要参数的命令一样。
4.查看程序信息 === info
info指令用来查看程序的信息,当你用help info查看帮助的话,info指令的参数足足占了两个屏幕,它的参数非常多,但大部分不常用。我用info指令最多的是用它来查看断点信息。
4.1查看断点信息
info br
br是断点break的缩写,记得GDB的补齐功能吧。用这条指令,你可以得到你所设置的所有断点的详细信息。包括断点号,类型,状态,内存地址,断点在源程序中的位置等。
4.2查看当前源程序
info source
4.3查看堆栈信息
info stack
用这条指令你可以看清楚程序的调用层次关系。
4.4查看当前的各寄存器值
info registers
5.列出源一段源程序 === list
5.1列出某个函数
list FUNCTION
5.2以当前源文件的某行为中间显示一段源程序
list LINENUM
5.3接着前一次继续显示
list
5.4显示前一次之前的源程序
list -
5.5显示另一个文件的一段程序
list FILENAME:FUNCTION 或 list FILENAME:LINENUM
6.设置断点 === break
现在我们将要介绍的也许是最常用和最重要的命令:设置断点。无论何时,只要你的程序已被载入,并且当前没有正在运行,你就能设置,修改,删除断点。设置断点的命令是break。有许多种设置断点的方法。如下:
6.1在函数入口设置断点
break FUNCTION
6.2在当前源文件的某一行上设置断点
break LINENUM
6.3在另一个源文件的某一行上设置断点
break FILENAME:LINENUM
6.4在某个地址上设置断点,当你调试的程序没有源程序是,这很有用
break *ADDRESS
除此之外,设置一个断点,让它只有在某些特定的条件成立时程序才会停下,我们可以称其为条件断点。这个功能很有用,尤其是当你要在一个程序会很多次执行到的地方设置断点时。如果没有这个功能,你必须有极大的耐心,加上大量的时间,一次一次让程序断下,检查一些值,接着再让程序继续执行。事实上,大部分的断下并不是我们所希望的,我们只希望在某些条件下让程序断下。这时,条件断点就可以大大提高你的效率,节省你的时间。条件断点的命令如下,在后面的例子中会有示例。
当你设置一个断点后,它的确省状态是有效。你可以用enable和disable指令来设置断点的状态为有效或禁止。例如,如果你想禁止2号断点,可以用下面的指令:
disable 2
相应的,如果想删除2号断点,可以有下面的指令:
delete 2
7.检查数据
最常用的检查数据的方法是使用print命令。
print exp
print指令打印exp表达式的值。却省情况下,表达式的值的打印格式依赖于它的数据类型。但你可以用一个参数/F来选择输出的打印格式。F是一个代表某种格式的字母,详细可参考输出格式一节。表达式可以是常量,变量,函数调用,条件表达式等。但不能打印宏定义的值。表达式exp中的变量必须是全局变量或当前堆栈区可见的变量。否则GDB会显示象下面的一条信息:
No symbol "varible" in current context
8.修改变量值
在调试程序时,你可能想改变一个变量的值,看看在这种情况下会发生什么。用set指令可以修改变量的值:
set varible=value
例如你想将一个变量tmp的值赋为10,
set tmp=10
9.检查内存值
检查内存值的指令是x,x是examine的意思。用法如下:
x /NFU ADDR
其中N代表重复数,F代表输出格式(见2.13),U代表每个数据单位的大小。U可以去如下值:
b :字节(byte)
h :双字节数值
w :四字节数值
g :八字节数值
因此,上面的指令可以这样解释:从ADDR地址开始,以F格式显示N个U数值。例如:
x/4ub 0x4000
会以无符号十进制整数格式(u)显示四个字节(b),0x4000,0x4001,0x4002,0x4003。
10.输出格式
缺省情况下,输出格式依赖于它的数据类型。但你可以改变输出格式。当你使用print命令时,可以用一个参数/F来选择输出的打印格式。F可以是以下的一些值:
'x' 16进制整数格式
'd' 有符号十进制整数格式
'u' 无符号十进制整数格式
'f' 浮点数格式
11.单步执行指令
单步执行指令有两个step和next。Step可以让你跟踪进入一个函数,而next指令则不会进入函数。
12.继续执行指令
当程序被断下后,你查看了所需的信息后,你会希望程序执行下去,输入 continue, 程序会继续执行下去。
13.帮助指令help
在GDB中,如果想知道一条指令的用法,最方便的方法是使用help。使用方法很简单,在help后跟上指令名。例如,想知道list指令用法,输入help list。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值