缓冲区溢出漏洞学习总结(漏洞原理及其利用方法)


前言

  本文将会介绍关于缓冲区溢出类型漏洞的原理及其利用方法。因为缓冲区溢出漏洞是一个大类,这其中包含了很多种小类漏洞,故笔者专写下此篇博客,以记录自己在学习和内存相关的内容时的知识点记录以及心得体会。以下就是本篇博文的全部内容。


1、缓冲区溢出漏洞概述

1.1、漏洞概述

  在学习缓冲区溢出漏洞之前,我们要先明白关于漏洞的概念。漏洞也称为脆弱性(Vulnerability),是计算机系统的硬件、软件、协议在系统设计、具体实现、系统配置或安全策略上存在的缺陷。这些缺陷一旦被发现并被恶意利用,就会使攻击者在未授权的情况下访问或破坏系统,从而影响计算机系统的正常运行甚至造成安全损害。

  • 对于漏洞有多种称呼,包括Hole、Error、Fault、Weakness、Failure等,这些称呼都不能涵盖漏洞的含义(脆弱性)
  • 软件漏洞专指计算机系统中的软件系统漏洞

1.2、缓冲区溢出漏洞概述

  我们现在已经清楚了漏洞的概念,那什么又是缓冲区溢出漏洞呢?很明显这是漏洞的一种,不过在学习缓冲区溢出漏洞之前,我们还要搞清楚缓冲区这一概念。缓冲区是一块连续的内存区域,用于存放程序运行时加载到内存的运行代码和数据。

  那什么又是缓冲区溢出呢?缓冲区溢出是指程序运行时,向固定大小的缓冲区写入超过其容量的数据,多余的数据会越过缓冲区的边界覆盖相邻内存空间,从而造成溢出。

  一般情况下,缓冲区的大小是由用户输入的数据决定的,如果程序不对用户输入的超长数据进行长度检查,同时用户又对程序进行了非法操作或者错误输入,就会造成缓冲区溢出。

  综合以上,缓冲区溢出漏洞就是由缓冲区溢出引发的程序的脆弱性。

1.3、缓冲区溢出攻击概述

  缓冲区溢出攻击是指发生缓冲区溢出时,溢出的数据会覆盖相邻内存空间的返回地址、函数指针、堆管理结构等合法数据,从而使程序运行失败、或者发生转向去执行其它程序代码、或者执行预先注入到内存缓冲区中的代码。

  值得注意的一点是,缓冲区溢出后执行的代码,会以原有程序的身份权限运行。

1.4、引发缓冲区溢出的原因

  引发缓冲区溢出的原因是缺乏类型安全功能的程序设计语言(C、C++等)。出于效率的考虑,部分函数不对数组边界条件和函数指针引用等进行边界检查。例如,C标准库中和字符串操作有关的函数,像strcpystrcatsprintfgets等函数中,数组和指针都没有自动进行边界检查。

  所以说,程序员开发时必须自己进行边界检查,以防范数据溢出,否则所开发的程序就会存在缓冲区溢出的安全隐患,而实际上这一行为往往被程序员忽略或检查不充分。

  值得注意的是,缓冲区溢出漏洞并不是一种漏洞,而是一类漏洞。常见的缓冲区溢出漏洞包括:

  • 栈溢出漏洞
  • 堆溢出漏洞
  • 整数溢出漏洞
  • SHE结构基础漏洞
  • 单字节溢出漏洞
  • 格式化字符串漏洞
  • C++虚函数漏洞

2、栈溢出漏洞

2.1、栈溢出漏洞概述

  栈溢出漏洞,即发生在栈区的溢出漏洞。当被调用的子函数中写入数据的长度,大于栈帧的基址到ESP之间预留的保存局部变量的空间时,就会发生栈的溢出。要写入的数据的填充方向是从低地址向高地址增长,多余的数据就会越过栈帧的基址,覆盖基址以上的地址空间。

  下面我们通过一个代码示例,来更深刻的了解什么是栈溢出漏洞。

#include <stdio.h>  
  
void why_here(void)  
{
   
     
    printf("why u r here?!\n");  
    exit(0);  
}  
  
void f()  
{
   
     
    int buff[1];  
    buff[2] = (int)why_here;  
}  
  
int main(int argc, char *argv[])  
{
   
     
    f();  
    return 0;  
}  

  这段代码看起来很简单,在main函数中调用了f函数,在f函数中,声明了长度为1的buff数组,并对此数组的第3个位置进行了赋值为why_here函数的地址,并将其类型转换为int类型,看起来这段代码与why_here函数并没有什么直接关系,只是将why_here函数的地址进行了赋值操作,并没有直接调用why_here函数,故更不会打印why_here函数中的why u r here?!。我们可以将其编译并运行。

在这里插入图片描述

  我们惊讶的发现,main函数调用了函数f,并没有调用why_here函数,但是上面的运行结果却显示调用了why_here函数。有的读者可能就有疑问了,为什么会这样呢?其实,这就是一个栈溢出漏洞的经典例子。

  我们清楚,在函数f中,所声明的数组buff长度为1,但是由于没有对访问下标的值进行校验,程序中对数组外的内存进行了读写,这就是一个典型的栈溢出漏洞。

  为了分析这个栈溢出漏洞示例,我们要分析其栈的结构。在调用函数f之前,要将其参数入栈(在这里例子中我们并不关心其参数),然后将调用函数f的返回地址入栈,然后将以上一个函数的EBP入栈,然后给整个函数f分配其栈帧(在这个例子中,栈帧中只有局部变量buff,且其长度为1)。当然,还有其它内容入栈,不过在这里我们并不关心,所以就不讨论了。以上我们讨论的内容如下图所示。
在这里插入图片描述

  基本情况我们了解之后,来主要关注局部变量buff的指针,其指向了buff这个局部变量数组的起始地址,那么我们就可以通过buff[0]来访问这个数组的第1个元素,通过buff[1]来访问这个数组的第2个元素,通过buff[2]来访问这个数组的第3个元素,……。

  重点是,我们在代码中,通过buff[2] = (int)why_here;语句,将函数why_here的入口地址赋值给buff[2],故将栈中的返回地址覆写为函数why_here的入口地址。可以看到,虽然我们并没有直接调用函数why_here,但是我们将栈中的返回地址改写为了函数why_here的入口地址,那么当CPU来到栈中的返回地址中取值的时候,取到的是函数why_here的入口地址,所以说程序发生了跳转,去执行why_here函数了,故最终将why_here函数中的why u r here?!打印了出来。

  故此例就是一个通过栈溢出漏洞改写栈中的返回地址来执行本不应该执行的函数的例子。那么通过这个例子,我们已经清楚了什么是栈溢出漏洞。对于如何利用栈溢出漏洞,请参阅下一章节。

2.2、栈溢出漏洞利用

2.2.1、利用方法一:修改返回地址

  栈的存取采用先进后出的策略,程序用它来保存函数调用时的有关信息,如函数参数,返回地址,函数中的非静态局部变量存放在栈中。如果返回地址被覆盖,当覆盖后的地址是一个无效地址,则程序运行失败。如果覆盖返回地址的是恶意程序的入口地址,则源程序将转向去执行恶意程序。下面以一段程序为例说明通过栈溢出漏洞修改返回地址的原理。

void stack_overflow(char *argument)  
{
   
     
    char local[4];  
    for(int i = 0; argument[i]; i++)  
    {
   
     
        local[i] = argument[i];  
    }  
}  

  函数stack_overflow被调用时的堆栈布局如下图所示。图中local是栈中保存局部变量的缓冲区,根据char local[4]预先分配的大小为4个字节,当向local中写入超过4个字节的字符时,就会发生溢出。
在这里插入图片描述

  如果溢出的值为攻击代码的入口地址,即CCCC地址为攻击代码的入口地址,就会调用攻击代码。

2.2.2、利用方法二:覆盖临接变量

  我们知道,函数的局部变量在栈中一个挨着一个排列。如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的EBP值、返回地址等重要数据,以此为基础,就可以通过覆盖临近变量的值,以更改程序的执行流程。下面用一个例子来说明破坏栈内局部变量对程序的安全性有什么影响。

#include <stdio.h>    
#include <iostream>    
#include <string.h>     
    
#define PASSWORD "1234567"    
    
int verify_password(char *password)    
{
   
       
    int authenticated;    
    // add local buff to be overflowed    
    char buffer[8];    
    authenticated = strcmp(password, PASSWORD);    
    strcpy(buffer, password);  
    return authenticated;    
}    
    
int main()    
{
   
       
    int valid_flag = 0;    
    char password[1024];    
    while(1)    
    {
   
       
        printf("please input password: ");    
        scanf("%s", password);    
        valid_flag = verify_password(password);    
        if(valid_flag)    
        {
   
       
            printf("incorrect password!\n\n");    
        }    
        else    
        {
   
       
            printf("Congratulation! You have passed the verification!\n\n");    
        }    
    }    
    return 0;    
}    

  这段代码并不是很复杂,通过观察源代码不难发现,authenticated变量的值来源于strcmp函数的返回值,之后会返回给main函数作为密码验证成功与否的标志变量,即当authenticated为0时,表示验证成功,反之,验证不成功。

  此时我们注意,如果我们输入的密码超过了7个字符(注意:字符串截断符NULL将占用一个字节),则越界字符的ASCII码会修改掉authenticated的值。如果这段溢出数据恰好把authenticated改为0,则程序执行流程将会被改变,即构造一个精心设计的字符串(不需要这个字符串是真正的密码)使authenticated为0,然后返回给main函数就会将这个精心设计的字符串判断为真正的密码,从而实现密码破解的目的。

  不过问题是,我们该如何将authenticated覆写为0呢?其实要成功覆盖临近变量authenticated并使其为0,需要满足两个条件:

  • 输入一个8位的字符串的时候,比如“22334455”,此时,字符串的结束符恰好是0,则覆盖变量authenticated的高字节并使其为0
    解释:如果字符串的结束符不为0,那么就无法覆盖变量authenticated的高位字节使其为0,故无法实现密码的破解,因为只有变量authenticated的值为0时,程序才会认为密码输入正确
  • 输入的字符串应该大于“12345678”(即真正的密码),因为执行strcpy之后要确保变量authenticated的值为1,也就是只有高字节是1,其他字节为0(小端存储,即Little Endian)
    解释:因为变量authenticated为int类型,占4个字节,如果其值不为1(当然,如果变量authenticated为0,就说明密码正确,这种情况我们不考虑),就会导致其在内存中存储的其它位也不为0,故就算将其高位字节覆写为1,最终变量authenticated的值也不为0,也就无法成功破解密码。所以我们要保证变量authenticated的值为1,这样将其高位字节覆写后,其余位也都为0,最终变量authenticated的值就为0,从而成功破解密码

  当满足以上两点要求后,继续执行验证密码的程序,首先通过strcmp函数得到authenticated的值为1,然后通过strcpy函数实现缓冲区溢出,将authenticated在内存中的高位字节的1覆写为0(使用值为0的字符串的结束符覆写),最终authenticated的值就被设置为0,然后将其返回给main函数进行验证,因为authenticated已经被设计为0,所以就算我们输入一个错误的密码,仍然会通过密码检查,从而实现密码的破解。

  理论知识我们已经讲述清楚了,我们现在可以在Visual C++ 6.0上对此程序进行我们介绍过的验证,看看是否可以按照我们所述成功破解密码。
在这里插入图片描述
  可以发现,正如我们所介绍的一样,我进行了三组测试:

  • 输入“7654321”:这是一个错误密码,没有通过密码检查,故密码破解失败
  • 输入“1234567”:这是一个正确密码,通过了密码检查,这并不是一次密码破解
  • 输入“22334455”:这是一个错误密码,通过了密码检查,故密码破解成功

  所以说,我们成功通过栈溢出漏洞,并利用其覆盖临接变量的栈溢出方法,成功破解了密码,达到了我们的目的。

3、堆溢出漏洞

3.1、堆溢出漏洞概述

  堆溢出是指在堆中发生的缓冲区溢出。堆溢出后,数据可以覆盖堆区的不同堆块的数据,带来安全威胁。我们将通过下面一个简单例子,来演示一个简单的堆溢出漏洞:该漏洞在产生溢出的时候,将覆盖一个目标堆块的块身数据。

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
  
#define FILENAME "myoutfile"    
    
int main(int argc, char *argv[])    
{
   
       
    FILE *fd;    
    char bufchar[100];    
    
    char* buf1 = (char*)malloc(20);    
    char* buf2 = (char*)malloc(20);    
    
    ptrdiff_t diff = buf2 - buf1;    
    
    strcpy(buf2, FILENAME);    
      
    printf("-----------打印信息-----------\n");  
    
    printf("buf1存储地址:%p\n", buf1);    
    printf("buf2存储地址:%p, 存储内容:%s\n", buf2, buf2);    
    printf("两个存储地址之间的距离:%d个字节\n\n", diff);    
    
    
    printf("-----------打印信息-----------\n");  
      
    if(argc < 2)    
    {
   
       
        printf
<think>好的,用户遇到了Git合并时本地更改会被覆盖的问题,需要解决方案。根据之前的引用资料,解决方法主要有commit、stash和revert三种。首先,我得确认用户的具体情况,比如他们的本地更改是否已经完成,是否需要保留,或者是否可以丢弃。用户可能不太清楚这三种方法的适用场景,需要详细解释每个选项的步骤和优缺点。比如,commit适用于需要保留更改的情况,stash适合临时保存以便后续恢复,而revert则是直接撤销更改,适用于不需要保留的情况。需要结构清晰,分点说明,每个方法都给出操作步骤和适用场景,帮助用户理解哪种情况最适合他们。还要注意使用中文,避免技术术语过于复杂,让用户容易理解。同时,生成相关问题,可能涉及如何恢复stash的内容,或者如何避免频繁冲突等,扩展用户的知识点。另外,要确保LaTeX格式正确,但在这个问题中可能用不上数学公式,所以主要关注代码块和引用标识。检查引用是否正确,比如引用[^1]和[^2]的来源是否相关,用户提供的引用内容是否准确对应解决方案。 最后,确保回答真实可靠,操作步骤准确无误,避免误导用户。可能还需要提醒用户根据具体情况选择方法,比如如果有未完成的实验性代码,stash可能是更好的选择,而如果是确定要保留的修改,则应该commit。</think>要解决Git合并时本地更改被覆盖的问题,可根据不同场景选择以下三种方法: ### 一、提交本地更改(commit) **适用场景**:当本地更改已测试通过且需要保留到版本历史中。 **操作步骤**: 1. 将改动添加到暂存区: ```bash git add . # 添加所有文件 # 或指定文件 git add file1.txt file2.py ``` 2. 提交更改并添加描述: ```bash git commit -m "完成用户登录模块优化" ``` 3. 执行合并操作: ```bash git merge origin/main ``` **优点**:保留完整的开发记录,便于后续追溯[^1]。 ### 二、暂存更改(stash) **适用场景**:需要临时保存未完成的修改(如调试代码),优先处理合并操作。 **操作步骤**: 1. 保存当前工作状态: ```bash git stash # 可选添加备注 git stash save "用户登录模块调试代码" ``` 2. 查看暂存记录列表: ```bash git stash list ``` 3. 合并远程分支: ```bash git merge origin/main ``` 4. 恢复暂存内容: ```bash git stash pop # 恢复并删除最近一次暂存 # 或指定特定暂存 git stash apply stash@{1} ``` **优点**:保持工作目录整洁,特别适合处理紧急合并任务[^2]。 ### 三、撤销本地更改(revert) **适用场景**:本地修改可丢弃(如实验性代码或错误修改)。 **操作步骤**: 1. 撤销所有未提交的修改: ```bash git checkout -- . # 全部文件 # 或指定文件 git checkout -- file1.txt ``` 2. 执行合并操作: ```bash git merge origin/main ``` **风险提示**:此操作不可逆,需确认文件无需保留。 --- ### 对比决策表 | 方法 | 保留记录 | 可恢复性 | 适用阶段 | |---------|----------|----------|------------------| | commit | ✅ | ✅ | 功能开发完成时 | | stash | ❌ | ✅ | 开发中途临时切换 | | revert | ❌ | ❌ | 放弃无效修改 | ### 扩展应用场景 - 当存在多个暂存时,可用`git stash list`查看记录,通过`git stash apply stash@{n}`精准恢复 - 合并后出现冲突时,可使用`git mergetool`可视化工具解决冲突 - 长期分支开发推荐使用`git rebase`保持提交历史线性
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IronmanJay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值