缓冲区 -- 一块连续的内存区域, 存放程序运行时 加载到内存的 代码和数据。
缓冲区溢出 指程序运行时,向固定大小的缓冲区写入超过其容量的数据,多余的数据会越过缓冲区的边界覆盖相邻内存空间,从而造成溢出。
缓冲区的大小是由用户输入的数据决定的,如果程序不对用户输入的超长数据作长度检查,同时用户又对程序进行了非法操作或者错误输入,就会造成缓冲区溢出。
缓冲区溢出攻击 是指发生缓冲区溢出时,溢出的数据会覆盖相邻内存空间的返回地址、函数指针、堆管理结构等合法数据,从而使程序运行失败、或者发生转向去执行其它程序代码、或者执行预先注入到内存缓冲区中的代码。缓冲区溢出后执行的代码,会以原有程序的身份权限运行。如果原有程序是以系统管理员身份运行,那么攻击者利用缓冲区溢出攻击后所执行的恶意程序,就能够获得系统控制权,进而执行其它非法操作。
造成缓冲区溢出的根本原因 是缺乏类型安全功能的程序设计语言(C、C++等)出于效率的考虑,部分函数不对数组边界条件和函数指针引用进行边界检查。例如,C 标准库中和字符串操作有关的函数,像 strcpy
, strcat
, sprintf
, gets
等函数中,数组和指针都没有自动边界检查。程序员开发时必须自己进行边界检查,防范数据溢出,否则所开发的程序就存在缓冲区溢出的安全隐患,而实际上这一行为往往被程序员忽略或者检查不充分。
被调用的子函数中写入数据的长度,大于栈帧的基址到 esp
之间预留的保存局部变量的空间时,就会发生栈溢出。要写入数据的填充方向是从低地址向高地址增长,多余的数据就会越过栈帧的基址,覆盖基址以上的地址空间。
如果返回地址被覆盖,当覆盖后的地址是一个无效地址,则程序运行失败。如果覆盖返回地址的是恶意程序的入口地址,则源程序将转向去执行恶意程序。
栈的存取采用先进后出,程序用它来保存函数调用时的有关信息,如函数参数、返回地址,函数中的非静态局部变量存放在栈中。举例:
void stack_overflow(char* argument)
{
char local[4];
for (int i = 0; argument[i]; i++)
local[i] = argument[i];
}
样例程序中,函数 stack_overflow
被调用时堆栈布局
local
是栈中保存局部变量的缓冲区,根据 char local[4]
预先分配的大小为4个字节,当向local中写入超过4个字节的字符时,就会发生溢出。如用 AAAABBBBCCCCDDDD
作为参数调用,当函数中的循环执行后,栈顶布局如图右侧。可以看出输入参数中 CCCC
覆盖了返回地址,当 stack_overflow
执行结束,根据栈中返回地址返回时,程序将转到地址 CCCC
并执行此地址指向的程序,如果 CCCC
地址为攻击代码的入口地址,就会调用攻击代码。如果返回临近变量的值,可能会更改程序执行流程。
函数的局部变量在栈中一个挨着一个排列。如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的 EBP
值、返回地址等重要数据。
举例:
#include <stdio.h>
#include <iostream>
#define PASSWORD "1234567"
int verify_password(char * password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password);
return authenticated;
}
void main()
{
int valid_flag = 0;
char password[1024];
while(1)
{
printf("please input password: ");
scanf("%s", password);
valid_flag = verify_password(password);
if(valid_flag)
printf ("incorrect password!\n\n");
else{
printf("Congratulation! You have passed the verification!\n");
break;
}
}
}
在 verify_password
函数的栈帧中,局部变量 int authenticated
恰好位于缓冲区 char buffer[8]
的"下方"。
authenticated
为int类型,在内存中是一个 DWORD
,占4个字节。所以,如果能够让 buffer
数组越界,buffer[8]
、 buffer[9]
、 buffer[10]
、 buffer[11]
将写入相邻的变量 authenticated
中。
authenticated
变量的值来源于 strcmp
函数的返回值,之后会返回给 main
函数作为密码验证成功与否的标志变量:当 authenticated
为 0 时,表示验证成功;反之,验证不成功。
如果我们输入的密码超过了7个字符(注意:字符串截断符NULL将占用一个字节),则越界字符的 ASCII
码会修改掉 authenticated
的值。如果这段溢出数据恰好把 authenticated
改为0,则程序流程将被改变。
什么是格式化字符串
print()
、 fprint()
等 *print()
系列的函数可以按照一定的格式将数据进行输出,举例:
printf("My Name is: %s" , "xxxx")
特性三:自定义打印字符串宽度
在格式符中间加上一个十进制整数来表示输出的最少位数,若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度则补以空格或0。
#include <stdio.h>
main()
{
int num=66666666;
printf("Before: num = %d\n", num);
printf("%.100d%n\n", num, &num);
printf("After: num = %d\n", num);
}
num
值被改为了 100
。
char* integer_overflow(int* data,unsigned int len)
{
unsigned int size = len + 1;
char *buffer = (char*)malloc(size);
if(!buffer)
return NULL;
memcpy(buffer,data,len);
buffer[len]='\0';
return buffer;
}
该函数将用户输入的数据拷贝到新的缓冲区,并在最后写入结尾符0。如果攻击者将 0xFFFFFFFF
作为参数传入 len
,当计算 size
时会发生整数溢出,malloc
会分配大小为 0 的内存块,后面执行 memcpy
时会发生堆溢出。
strKeyword = Request["keyword"];
sqlQuery = "SELECT * FROM Aritcles WHERE Keywords LIKE '%' + strKeyword + '%' ";
按照用户提交的关键字 keyword
,对软件连接数据库中的文件进行搜索,找出所有包含用户关键字的文章。
假设此时,我们提交 hack
,这时,hack
会传递给 keyword
关键变量。keyword
获得数据 hack
后被赋值给 strKeyword
变量,然后 strKeyword
变量被放入查询语句。此时的查询语句表现为:
SELECT * FROM Aritcles WHERE Keywords LIKE '% hack %'
查询 -- 删除Aritcles表
常常出现在论坛程序中进行用户认证的程序:
admin1 = trim(request("name"))
password1 = trim(request("password"))
Set rs = Server.CreatObject("ADODB.Recordset")
sql = "select * from userlogin where name = '"&admin1&"' and password = '"&password1&"'"
rs.Open sql, conn, 1, 1
if rs.eof and rs.bof then
response.write"<SCRIPT language = JAVAScript>alert('用户名或密码不正确!')"
response.write"javascript:history.go(-1)</SCRIPT>"
response.end
else # 设置Session对象,重定向 default.asp
session("name") = rs("name")
session("password") = rs("password")
response.Redirect("default.asp")
end if
假设 guest
-- 密码123456
sql = "select * from userlogin where name = 'guest' and password = '123456'"
用户名 ' or 1 = ' 1 密码' or 1 = ' 1 and then you are ok! why?
sql = "select * from userlogin where name = " or 1 = '1' and password = " or 1 = '1'"
查询语句永远为真---绕过用户认证