(原创)攻击方式学习之(3) - 缓冲区溢出(Buffer Overflow)
堆栈溢出
堆栈溢出通常是所有的缓冲区溢出中最容易进行利用的。了解堆栈溢出之前,先了解以下几个概念:
- 缓冲区
- 简单说来是一块连续的计算机内存区域,可以保存相同数据类型的多个实例。
- 堆栈
- 堆 栈是一个在计算机科学中经常使用的抽象数据类型。堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来,这个特性通常称为后进先出 (LIFO)队列。堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个 元素,并将堆栈的大小减一。
- 寄存器ESP、EBP、EIP
- CPU的ESP寄存器存放当前线程的栈顶指针,
- EBP寄存器中保存当前线程的栈底指针。
- CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
现 代计算机被设计成能够理解人们头脑中的高级语言。在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function)。从这一 点来看,一个过程调用可以象跳转(jump)命令那样改变程序的控制流程,但是与跳转不同的是,当工作完成时,函数把控制权返回给调用之后的语句或指令。 这种高级抽象实现起来要靠堆栈的帮助。堆栈也用于给函数中使用的局部变量动态分配空间,同样给函数传递参数和函数返回值也要用到堆栈。
堆栈由逻辑堆栈帧组成。当调用函数时逻辑堆栈帧被压入栈中,当函数返回时逻辑堆栈帧被从栈中弹出。堆栈帧包括函数的参数,函数地局部变量,以及恢复前一个堆栈帧所需要的数据,其中包括在函数调用时指令指针(IP)的值。
当一个例程被调用时所必须做的第一件事是保存前一个 FP(这样当例程退出时就可以恢复)。然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间。这称为例程的序幕(prolog)工 作。当例程退出时,堆栈必须被清除干净,这称为例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作。
下面我们用一个简单的例子来展示堆栈的模样: example1.c:
为了理解程序在调用function()时都做了哪些事情, 我们使用gcc的-S选项编译, 以产生汇编代码输出:
通过查看汇编语言输出, 我们看到对function()的调用被翻译成:
以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 指令call会把指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做的第一件事情是例程的序幕工作:
将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间. 我 们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间. 这就是为什么SP要减掉20的原因. 这样我们就可以想象function()被调用时堆栈的模样:
所以,从上图来看,假如我们输入的buffer1超长了,直接覆盖掉后面的sfp和ret,就可以修改该函数的返回地址了。下面来看一个示例吧。
示例
关于如何编写Shell Code,如何在内存中预先准备好一段危险的执行代码以及如何精确计算通过缓冲区溢出执行那段危险代码同时又让返回地址调回原来返回地址……这中间涉及太 多的底层汇编知识,小弟不才也只是走马观花,成不了真正的黑客高手。但从黑客朋友的水平之高看来,提高我们的代码安全性是非常必要的!
因此,在这个例子中,我们假设所谓的危险代码已经在 源代码中,即函数bar。函数foo是正常的函数,在main函数中被调用,执行了一段非常不安全的strcpy工作。利用不安全的strcpy,我们可 以传入一个超过缓冲区buf长度的字符串,执行拷贝后,缓冲区溢出,把ret返回地址修改成函数bar的地址,达到调用函数bar的目的。
用GCC编译上面的程序,同时注意关闭Buffer Overflow Protect开关:
为了找出返回地址,我用gdb调试上面编译出来的程序。
Breakpoint 1, main (argc=2, argv=0xbfe5ab24) at test.c:24 24 foo(argv[1]); //在调用foo函数前,我们查看ebp值 (gdb) info registers ebp ebp 0xbfe5aa88 0xbfe5aa88 //ebp值为0xbfe5aa88 (gdb) n
Breakpoint 2, foo (input=0xbfe5c652 "abc") at test.c:4 4 { (gdb) n 6 printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n"); //执行到foo后,我们再查看ebp值 (gdb) info registers ebp ebp 0xbfe5aa68 0xbfe5aa68 //ebp值变成了0xbfe5aa68 //我们来查看一下地址0xbfe5aa68究竟是啥东东: (gdb) x/ 0xbfe5aa68 0xbfe5aa68: 0xbfe5aa88 //原来地址0xbfe5aa68存放的居然是我们之前的ebp值,其实豁然开朗了,因为这是执行了push %ebp后将之前的ebp保存起来了,和前面说的居然是一样的! (gdb) n My stack looks like: 0xb7ee04e0 0x8048616 0xbfe5aa74 0xbfe5aa74 0xb7edfff4 0xbfe5aa88 //看,在代码中输入堆栈信息中也出现了熟悉的0xbfe5aa88,因此可以断定该处为保存的上一级的ebp值。对应上上面那个图中的sfp。 0x8048499 //假如0xbfe5aa88就是sfp的话,那0x8048499应该就是ret(返回地址)了,下面来验证一下
7 strcpy(buf, input); //查看0x8048499里面是什么东东 (gdb) x/i 0x8048499 0x8048499 <main+108>: movl $0x8048653,(%esp) //这句代码是main函数中的代码,正是我们执行完foo函数后的下一个地址。不信,看看main的assemble: (gdb) disassemble main Dump of assembler code for function main: 0x0804842d <main+0>: lea 0x4(%esp),%ecx 0x08048431 <main+4>: and $0xfffffff0,%esp 0x08048434 <main+7>: pushl -0x4(%ecx) 0x08048437 <main+10>: push %ebp //(中间省略……) 0x08048494 <main+103>: call 0x80483d4 <foo> 0x08048499 <main+108>: movl $0x8048653,(%esp) //就是这里了!哈 0x080484a0 <main+115>: call 0x8048340 <puts@plt>
因此,我们只要输入一个超长的字符串,覆盖掉0x08048499,变成bar的函数地址0x8048419,就达到了调用bar函数的目的。为了将0x8048419这样的东西输入到应用程序,我们需要借助于Perl或Python脚本,如下面的Python脚本:
注意上面的08 04 84 19要两个两个反着写。执行一下:
buf = ABCDEFGHIJKLMN� Now the stack looks like: 0xbf83246e 0x8048616 0x42412484 0x46454443 0x4a494847 0x4e4d4c4b 0x8048419 //瞧,返回地址被修改为了我们想要的bar的函数地址0x8048419
Augh! I've been hacked! //哈哈!bar函数果然被执行了!
堆溢出及其他溢出
堆溢出
堆是内存的一个区域,它 被应用程序利用并在运行时被动态分配。堆内存与堆栈内存的不同在于它在函数之间更持久稳固。这意味着分配给一个函数的内存会持续保持分配直到完全被释放为 止。这说明一个堆溢出可能发生了但却没被注意到,直到该内存段在后面被使用。这里只是简单了解一下,下面看一个最简单的堆溢出例子:
我们来看执行结果:
normal output [root@localhost]# ./heap1 hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks input at 0x8049728: hacks1hacks2hacks3hacks4hacks5hacks6hacks7hackshackshackshackshackshackshacks output at 0x8049740: hackshackshackshacks5hacks6hacks7
hackshacks5hackshacks6hackshacks7 [root@localhost]# ./heap1 "hackshacks1hackshacks2hackshacks3hackshacks4what have I done?" input at 0x8049728: hackshacks1hackshacks2hackshacks3hackshacks4what have I done? output at 0x8049740: what have I done? //我们看到,output变成了what have I done?
what have I done? [root@localhost]#
我们来看看是如何溢出的:
格式化字符串错误
这类错误是指使用printf,sprintf,fprint等函数时,没有使用格式化字符串,比如:正确用法是:
如果直接写成:
将会出现漏洞,当input输入一些非法制造的字符时,内存将有可能被改写,执行一些非法指令。
Unicode和ANSI缓冲区大小不匹配
我们经常碰到需要在Unicode和ANSI之间互相转换,绝大多数Unicode函数按照宽字符格式(双字节)大小,而不是按照字节大小来计算缓冲区大小,因此,转换的时候不注意的话就可能会造成溢出。比如最常受到攻击的函数是MultiByteToWideChar,看下面的代码:
wszUserName是宽字符的,因此,sizeof(wszUserName)将会是256*2个字节,因此存在潜在的缓冲区溢出问题。正确的写法应该是这样的:
曾真实出现的Internet打印协议缓冲区溢出就是由于此类问题导致的。
预防和发现问题
不安全的函数
避免使用不安全的字符串处理函数,比如使用安全的函数代替:
不安全的函数 |
安全函数 |
strcpy |
strncpy |
strcat |
strncat |
sprintf |
_snprintf |
gets |
fgets |
Visual C++ NET的/GS选项
/GS选项能够阻止堆栈的破坏,保证堆栈的完整性,但是不能完全防止缓冲区溢出问题,比如,对于堆溢出,/GS是无能为力的。
源代码扫描
最简单的源代码扫描:
然后就是一些开源的或是商业的源代码扫描工具了。
工具
源代码工具包含ApplicationDefense、SPLINT、ITS4和Flawfinder。
- 二进制工具包含各种fuzzing工具包和静态分析程序,例如Bugscan。
参考资料
Michael Howard, David LeBlanc. "Writing Secure Code"
- Mike Andrews, James A. Whittaker "How to Break Web Software"
- 缓冲区溢出的原理和实践(Phrack)
(原创)攻击方式学习之(3) - 缓冲区溢出(Buffer Overflow)的更多相关文章
- 缓存溢出Buffer Overflow
缓存溢出(Buffer overflow),是指在存在缓存溢出安全漏洞的计算机中,攻击者可以用超出常规长度的字符数来填满一个域,通常是内存区地址.在某些情况下,这些过量的字符能够作为“可执行”代码来运 ...
- Linux下缓冲区溢出攻击的原理及对策(转载)
前言 从逻辑上讲进程的堆栈是由多个堆栈帧构成的,其中每个堆栈帧都对应一个函数调用.当函数调用发生时,新的堆栈帧被压入堆栈:当函数返回时,相应的堆栈帧从堆栈中弹出.尽管堆栈帧结构的引入为在高级语言中实现 ...
- Linux下缓冲区溢出攻击的原理及对策
前言 从逻辑上讲进程的堆栈是由多个堆栈帧构成的,其中每个堆栈帧都对应一个函数调用.当函数调用发生时,新的堆栈 帧被压入堆栈:当函数返回时,相应的堆栈帧从堆栈中弹出.尽管堆栈帧结构的引入为在高级语言中实 ...
- 全方位解读及介绍windows网络安全及常见攻击方式
本来我就是来逛逛论坛的,可是看到前面有位一样是干网络安全的同行,留下来过的痕迹,发了一篇相对不错的文章,我寻思咱既然来这一趟,也不能显得就比别人差啊.所以我也就写了这一片不算成熟小文章,望各位共勉之哈 ...
- 使用Linux进行缓冲区溢出实验的配置记录
在基础的软件安全实验中,缓冲区溢出是一个基础而又经典的问题.最基本的缓冲区溢出即通过合理的构造输入数据,使得输入数据量超过原始缓冲区的大小,从而覆盖数据输入缓冲区之外的数据,达到诸如修改函数返回地址等 ...
- CSAPP缓冲区溢出攻击实验(上)
CSAPP缓冲区溢出攻击实验(上) 下载实验工具.最新的讲义在这. 网上能找到的实验材料有些旧了,有的地方跟最新的handout对不上.只是没有关系,大体上仅仅是程序名(sendstring)或者參数 ...
- CMD.EXE中dir超长字符串缓冲区溢出原理学习
最近看逍遥的<网络渗透攻击与安防修炼>讲到CMD命令窗口的dir传超长字符串溢出的例子.自己实验了一下,的确会产生程序崩溃,但是具体什么原理没太详细说,这里做一下原理探究,权当学习笔记了. ...
- 用于阻止缓冲区溢出攻击的 Linux 内核参数与 gcc 编译选项
先来看看基于 Red Hat 与 Fedora 衍生版(例如 CentOS)系统用于阻止栈溢出攻击的内核参数,主要包含两项: kernel.exec-shield 可执行栈保护,字面含义比较“绕”, ...
- CSAPP缓冲区溢出攻击实验(下)
CSAPP缓冲区溢出攻击实验(下) 3.3 Level 2: 爆竹 实验要求 这一个Level的难度陡然提升,我们要让getbuf()返回到bang()而非test(),并且在执行bang()之前将g ...
随机推荐
- 多气体组分DEM流动的DMP并行内存错误
今天踩到一个坑.调DEM反应的时候,气体需要设置为多组分,这时就不能用 DES_INTERP_ON = .T. DES_INTERP_SCHEME = 'GARG_2012' 这个差值格式了,否则DM ...
- AtCoder Beginner Contest 113 A
A - Discount Fare Time limit : 2sec / Memory limit : 1024MB Score: 100 points Problem Statement Ther ...
- POJ - 2891 中国剩余定理
\(mod\)存在不互素情况下的CRT #include<iostream> #include<algorithm> #include<cstdio> #inclu ...
- C# DictionaryHelper
1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System ...
- [转] Scala 2.10.0 新特性之字符串插值
[From] https://unmi.cc/scala-2-10-0-feature-string-interpolation/ Scala 2.10.0 新特性之字符串插值 2013-01-20 ...
- ndoejs解析req,伪造http请求
require("./m3m4") var http = require('http'); var server = http.createServer(); server.lis ...
- C. Nice Garland-------字符串
C. Nice Garland time limit per test 1 second memory limit per test 256 megabytes input standard inpu ...
- vue父子组件生命周期函数执行顺序
vue父组件加载和销毁执行最后一个钩子函数之前先执行一遍子组件的钩子: 1.加载 父:beforecreate-created-beforeMount-(子:beforecreate-created- ...
- scrapy安装和框架内容
在cdm中:直接,pip install scrapy 有可能让你升级一下pip先,就输入这个:python -m pip install --upgrade pip 当它报错的话,看看它是缺了什么, ...
- 从指定Dictionary中移除指定值项
void Removeltems(Dictionary<int, ltem> _dicltemMap, ltem _item) { List<ltem> keys=new Li ...