当前位置: 首页 > 科技观察

缓冲区溢出漏洞那些事:C-gets函数

时间:2023-03-18 16:12:53 科技观察

基本概念缓冲区是内存存储的一块区域,在数据从一个位置传输到另一个位置时临时保存数据。当数据量超过内存缓冲区的存储容量时,就会发生缓冲区溢出(或缓冲区溢出)。因此,试图将数据写入缓冲区的程序会覆盖相邻的内存位置。缓冲区溢出最初是指当某个数据超出处理程序返回的堆栈地址限制范围时,程序运行异常。造成这种现象的原因是:有缺陷的编程,尤其是C语言,不像其他一些高级语言会自动检查数组或指针的堆栈块边界,增加了溢出的风险。C语言中的C标准库也有一些非常危险的操作函数,使用不当也为溢出创造了条件。什么是缓冲区溢出攻击攻击者通过覆盖应用程序的内存来利用缓冲区溢出问题。这会改变程序的执行路径,触发损坏文件或暴露私人信息的响应。例如,攻击者可能会引入额外的代码,向应用程序发送新指令以获得对IT系统的访问权限。如果攻击者知道程序的内存布局,他们可以故意提供缓冲区无法存储的输入,并覆盖保存可执行代码的区域,将其替换为自己的代码。例如,攻击者可以覆盖一个指针(指向内存中另一个区域的对象)并将其指向一个漏洞利用负载,从而获得对程序的控制。缓冲区溢出攻击的类型基于堆栈的缓冲区溢出更为常见,并且利用仅在函数执行期间存在的堆栈内存。基于堆的攻击更难执行,并且涉及淹没分配给程序的内存空间,超出当前运行时操作所使用的内存空间。哪些编程语言更容易受到攻击?C和C++是两种极易受到缓冲区溢出攻击的语言,因为它们没有内置的保护措施来防止覆盖或访问内存中的数据。MacOSX、Windows和Linux都使用用C和C++编写的代码。PERL、Java、JavaScript和C#等语言使用内置的安全机制来最大限度地减少缓冲区溢出的可能性。如何防止缓冲区溢出开发人员可以通过代码中的安全措施或使用提供内置保护的语言来防止缓冲区溢出漏洞。此外,现代操作系统具有运行时保护。三种常见的保护措施是:地址空间随机化(ASLR)——打乱数据区域的地址空间位置。通常,缓冲区溢出攻击需要知道可执行代码的位置,而随机化地址空间使这几乎不可能。DataExecutionPrevention-将某些内存区域标记为不可执行或可执行,防止攻击在不可执行区域中运行代码。结构化异常处理程序覆盖保护(SEHOP)-帮助防止恶意代码攻击结构化异常处理(SEH),这是一个用于管理硬件和软件异常的内置系统。因此,它可以防止攻击者利用SEH覆盖技术。在功能层面,SEH覆盖是通过基于栈的缓冲区溢出覆盖线程栈中存储的异常注册记录来实现的。代码和操作系统保护方面的安全措施还不够。当组织发现缓冲区溢出漏洞时,它必须快速响应以修补受影响的软件并确保软件用户能够访问补丁。示例代码根据STACK1_VS_2017.cpp代码修改;#include#include#include"Windows.h"intmain(intargc,char**argv){MessageBoxA((HWND)-0,(LPCSTR)"缓冲区溢出测试\n",(LPCSTR)"函数",(UINT)0);intcookie;charbuf[2];int*a=&cookie;char*b=buf;printf("buf:%08xcookie:%08x\n",b,a);u_int64p=(u_int64)a-(u_int64)b;printf("两个变量的内存地址差=%d\n",p);gets(buf);if(cookie==0x41424344)printf("缓冲区溢出成功!\n");}显示运行效果;使用MessageBoxA函数检测程序是否正常运行,点击确定开始测试并使用printf()函数输出提示信息,使用gets()函数获取用户输入信息;任意输入两个值,如果不满足条件,程序运行完成;代码分析漏洞原因:charbuf[2];代码部分分析---用char声明变量buf变成了一个有2个元素的数组,元素类型为character。buf有自己的两个长度,提示:u_int64p=(u_int64)a-(u_int64)b;代码部分减少程序中涉及到的变量的内存地址计算并赋值给变量p,(为了使运算有效,也是类型转义),结果可以看出内存之间的距离两个变量的地址,方便溢出利用的隐患:使用gets()函数获取输入数据,因为gets()函数是无限读取数据,不检查buffer的大小限制,会不断的读取将超出缓冲区的数据写入堆栈,导致潜在的溢出。这里为了方便理解代码demo:#include#include#include"Windows.h"intmain(){chartest[]="test1";printf("test1初始值清除%s的输入st值\n:",test);字符st[2];得到(st);printf("输出测试:%s\n",test);printf("outputst:%s\n",st);}运行效果可以看出超出栈空间的值继续写入栈,导致覆盖了test中对应的值栈,导致其值发生变化:test1-3456反汇编其运行过程分析栈是如何变化的?运行(加源码)程序可知initial关键字:test1的初始值为test1。该关键字可用于快速定位反汇编程序中与程序相关的函数运行区。在入口命令处设置断点,方便分析。并且在实际操作中发现,这里的操作是显示相关的特征字符信息,初步判断是正确的。反编译这个区域字符串,并与源代码进行对比,进一步验证。如果没有汇编说明,比较起来不是很直观。换一个插件和工具来展示。当不输入st值时,测试对应数据栈情况。000000000061FDE8000000000061FE0A“测试1”。输入后查看。000000000061FDE8000000000061FE08“123456”。000000000061FDF0000000000061FE0A"3456"//之前是test1。按照这个思路分析前面的示例程序。拆解分析按照之前的思路定位封闭区域。查找特征码:根据特征码查找;在入口命令处设置断点;运行查看某个变量对应的地址;继续运行可以看到另一个变量的地址;因为剩下的只是运行,就不显示了。输入值后,可以看到之前空白的数据已经被输入的数据覆盖了;扩展知识:发现栈中对应的值和内存中对应的值是相反的,因为栈中的值遵循先进后出的原则,所以是相反的。扩展分析,这个区域有一个je跳转指令,在它上面有一个cmp指令(函数用来比较),其中一个值固定为:41424344。可以看出跳转显然不满足,所以不会执行跳转,隐藏信息也不会显示。按照这个思路:输入两个值填满缓冲区,然后数据溢出覆盖思路,计算两个变量之间的距离为2,堆栈数据按照先进后出的原则提示,所以可以通过输入以下条件来建立跳转,然后输出隐藏信息12DCBA;跳跃建立;可以看到隐藏的信息已经显示出来了;扩展知识RSP+2C=rsp+2c对应的地址,即000000000061FDE0+2C=61FE0C;十六进制转十进制计算;IDA调试分析使用Itfollows可以清楚的查看变化;为了分析方便,可以在关键函数处设置断点,一步步执行,看执行效果;已知MesssageBoxA在调用之后才会开始运行,所以可以在本次调用之后设置断点;运行直到到达断点暂停;运行后发现rsi对应的值没有变化,这是正常的,因为除了初始赋值外,没有其他指令赋值给rsi;在执行gets函数之前,rsi对应的数据没有变化;gets函数执行后,rsi对应的数据发生了变化;USHRSI.TEXT:000000004078D1按下RBX.TEXT:00000000004078D2SUBRSP,38H.TEXT:00000000004078D6致电__main.text:000000000000000000000000004078DBLEARRSI,[RSSI,[RSSS]uType.text:00000000004078E3xorecx,ecx;hWnd.text:00000000004078E5learbx,[rsp+48h+var_1C].text:00000000004078EAlear8,Caption;lpCaption.text:00000000004078F1leardx,文本;lpText.text:00000000004078F8调用cs:__imp_MessageBoxA。文本:00000000004078FEmovr8,rbx.text:0000000000407901movrdx,rsi.text:0000000000407904subrbx,rsi.text:0000000000407907learcx,aBuf08xCookie08;"buf:%08xcookie:%08x\n".text:000000000040790Ecall_Z6printfPKcz;printf(charconst*,...).text:00000000004074913char*.text:000000000040791Amovrdx,rbx.text:000000000040791Dcall_Z6printfPKcz;printf(charconst*,...)。文本:00000000407922MOVRCX,rsi.text:00000000407925呼叫gets.text:000000000040792acmp[RSP+48H+VAR_1C],41424444H.TET:4142444H.TEX:414244H.TEXT:000000000009933333333333393392该区域的汇编代码,除了初始操作外,没有给rsi赋值的指令,因此可知存在溢出漏洞,覆盖了rsi的原始数据,导致其值发生变化。根据单步trace执行过程,gets函数执行后值发生变化,因此可以判断是使用gets函数获取的数据导致栈数据溢出。要修复它,请使用fgets或gets_s函数替换gets函数。函数分析:fgets()函数的第二个参数指定了读取的最大字符数。如果此参数的值为n,则fgets()将读取n-1个字符,或者读取直到遇到第一个换行符;这里是3,所以得到的值为12,一共输入6个数字读取第二个,结束IDA追加流程,一步步执行,查看执行过程;fgets执行前rsi对应的数据;fgets执行后rsi对应的数据可以看到数据没有溢出,分析到此结束。