C语言常见漏洞-缓冲区溢出

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上。

理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。

缓冲区溢出有堆缓冲区和栈缓冲区溢出,二者有些不同,大部分情况下都是讨论栈溢出。

1.原理

1.1 函数调用栈情况

程序运行时,为了实现函数之间的相互隔离,需要在调用新函数时保存当前函数的状态,这些信息全在栈上,为此引入栈帧。每一个栈帧保存者一个未运行完的函数的信息,包括局部变量等等。栈帧的边界由ebp/rbp(栈底指针)和esp/rsp(栈顶指针)确定。

先看一看函数调用时的栈情况,以func1调用func2为例。假如func2有2个形参。

当func1调用 func2会执行如下汇编码

push arg2
push arg1
call func2
add esp, 8

一般一个函数(func2)的起始和终止汇编代码会有如下操作

push ebp
mov ebp, esp
sub esp, xxx
...
mov esp, ebp
pop ebp
retn

func1调用func2之前,栈中只有func1的局部变量。

  • 执行call指令前,func1func2的2个参数压栈,此时ebp在func1局部变量之下,esp指向func2的第一个参数arg1
  • 之后执行call指令,将程序下一条指令的eip(add esp,8)压栈,跳到func2。ebp不变,esp指向返回地址。
  • 之后跳到func2指向push ebpfunc1的ebp被压栈。ebp依旧不变。
  • 执行mov ebp, esp。此时func2的ebp指向的是func1的ebp。
  • 执行sub esp, xxx,扩充栈空间,给局部变量清出空间。
  • 执行mov esp, ebp,销毁func2栈帧,再执行pop ebp。恢复func1的ebp。此时retn弹出func1的 eip(func2的返回地址)并回到func1继续执行。
  • 回到add esp, 8指令。清除func2的2个参数,此时栈中只剩func1局部变量。

func2执行完sub esp, xxx后整个栈空间布局如下,此时ebp指向的是func1的ebp。从func2返回地址到func1的局部变量都属于func1的栈帧。

func2局部变量
func1的ebp
func2返回地址(func1某条指令)
func2 2个参数
func1局部变量

1.2 缓冲区溢出

void function(char *str) {    
	char buffer[16];   
	strcpy(buffer,str); 
}

上述代码从strbuffer复制数据,当str长度超过16时,就会溢出。问题根源在于strcpy没有限制复制数据长度,存在类似的问题还有strcat()sprintf()vsprintf()gets()scanf()等。

不过随便溢出并不能造成很大的危害,不能达到攻击目的。所以一般攻击者需要利用缓冲区溢出漏洞运行危险函数(比如system("/bin/sh");)获取对面shell。

2.攻击方式

2.1 利用shellcode

将希望执行的指令输入到栈空间中,利用跳板指令jmp esp执行。jmp espuser32.dll中可以找到。

正常情况下,栈空间内容为(从上到下增长)

栈空间
栈变量
ebp
返回地址
函数形参

现在存在溢出风险的缓冲区在栈变量中,那么想执行shellcode应该怎么做呢?如何利用jmp esp呢?

一般来说函数调用最后几句有

pop ebp
retn(或pop eip)

retn之后esp指向函数形参,而eip此时假如指向esp,那么cpu就会来函数形参区取指令。就可以通过 (栈变量 + ebp + jmp esp指令地址 + shellcode)这样的组合payload,一路覆盖栈变量,ebp,返回地址和函数形参。即返回地址用jmp esp的地址取代。 jmp esp在动态链接库里有出现,其地址需要搜索下。

2.2 跳到其它函数

假如源代码中存在可以利用的函数,比如如下代码

#include<stdio.h>
#include<string.h>

void copyout(const char *input)
{
    char buf[10];
    strcpy(buf, input);
    printf("%s \n", buf);
}

void bar()
{
    system("/bin/sh");
    printf("hacked done\n");
}

int main(int argc, char *argv[])
{
    copyout(argv[1]);
    return 0;
}

源代码会执行main->copyoutcopyout中存在缓冲区溢出漏洞,此时只要将copyout的返回地址替换成bar的起始地址就行。

执行copyout时,栈空间如下

栈空间
copyout局部变量buf
main函数ebp
返回地址,该地址为main函数一个指令的地址
函数形参input

输入的input只要能把ebp覆盖并把返回地址替换成bar的起始地址就行,不需要考虑参数。

2.3 其它情况

理想很丰满,现实很骨干,一般代码往往不会有system("/bin/sh");摆在一个函数内部,所以需要自己构造。

当代码中引入了system函数时,可以利用system函数来构造,比如攻防世界level2,plt表有system,这里system地址为0x08048320(是_system不是system),可以用来当返回地址,接下来还需要/bin/sh字符串。

在这里插入图片描述/bin/sh地址是,0804A024(data节)

在这里插入图片描述漏洞函数如下
在这里插入图片描述这个函数栈如下

栈空间
局部变量buf,0x88字节
main函数ebp,4字节
返回地址,该地址为main函数一个指令的地址,4字节

payload就是要将返回地址变成system的地址,并且还附上参数的地址。完整的payload应为('a' * (0x88 + 0x04) + p32(system) + p32(0) + p32(bin_sh)

  • 0x88和0x4覆盖buf和ebp,随意设置就行
  • p32(system)system函数地址,0x8048320
  • p32(0)system返回地址,随意设置就行
  • p32(system)/bin/sh地址,0804A024

当然,有时候system/bin/sh都不会出现。这个时候只能通过plt表构造了,一般可执行文件都会引入动态链接库,read,write,printf等系统函数都会引入,而这些函数在动态链接库里的相对位置不变,所以可以通过它们的地址获取system的地址。

获取system地址之后,/bin/sh一般也在动态链接库里有,此时就可以构造payload了。或者修改某个函数的got地址,比如把printf换成system,把printf输出的可控参数输入成/bin/sh。(这个不一定每次有用,首先printf调用和system调用的参数要一致,其次,参数属于输入参数)。当然,这些操作也更加复杂。

3.防护

转自:缓冲区溢出保护机制

3.1 canary(栈保护)

栈溢出保护是一种缓冲区溢出攻击的缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。

当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,该cookie往往放置在ebp/rbp的正上方,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。

攻击者在覆盖返回地址的时候也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。

添加canary后依旧有机会通过格式化字符串漏洞或者整数溢出修改返回地址。

3.2 NX

NX即No-eXecute(不可执行)的意思,NX的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,主要用来限制shellcode执行。

等同于Windows下的DEP。

gcc编译器默认开启NX选项,通过-z execstack可以关闭NX。

3.3 RELRO

在前面描述的漏洞攻击中曾多次引入了GOT覆盖方法,GOT覆盖之所以能成功是因为默认编译的应用程序的重定位表段对应数据区域是可写的(如got.plt),这与链接器和加载器的运行机制有关,默认情况下应用程序的导入函数只有在调用时才去执行加载(所谓的懒加载,非内联或显示通过dlxxx指定直接加载),如果让这样的数据区域属性变成只读将大大增加安全性。RELRO(read only relocation)是一种用于加强对 binary 数据段的保护的技术,大概实现由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读,设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO 分为 partial relro 和 full relro。

开启RELRO后不可修改got表

3.4 PIE

Position-Independent-Executable是Binutils,glibc和gcc的一个功能,能用来创建介于共享库和通常可执行代码之间的代码。

标准的可执行程序需要固定的地址,并且只有被装载到这个地址才能正确执行,PIE能使程序像共享库一样在主存任何位置装载,这需要将程序编译成位置无关,并链接为ELF共享对象。

引入PIE的原因就是让程序能装载在随机的地址,从而缓解缓冲区溢出攻击。

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
C语言缓冲区溢出是指当向一个固定长度的缓冲区中写入超过其容量的数据时,超出部分的数据会溢出至其他内存区域,可能导致数据损坏或系统崩溃。下面我将以一个简单的代码示例来说明。 ```c #include <stdio.h> #include <string.h> int main() { char buffer[10]; char password[10] = "password"; printf("请输入密码:"); scanf("%s", buffer); if (strcmp(buffer, password) == 0) { printf("密码正确!\n"); } else { printf("密码错误!\n"); } return 0; } ``` 在这个示例代码中,我们定义了一个长度为10的缓冲区`buffer`,用户输入的密码通过`scanf`函数读取到这个缓冲区中。然后我们将输入的密码与预设的密码比较,如果相等则输出密码正确,否则输出密码错误。 然而,该程序存在缓冲区溢出的风险。当用户输入的密码超过10个字符时,比如输入"1234567890A",会导致超出缓冲区容量。由于C语言中的字符串没有固定长度,导致缓冲区以外的数据被覆盖,可能引发不可预料的结果。 例如,如果我们输入"1234567890A",则在比较密码时由于缓冲区溢出,会将替换缓冲区后面的内存中存放的数据一同比较,可能会错误地判断密码正确。这是因为字符串以空字符`\0`结尾,而`password`的长度是10,如果输入的密码长度超过10时,\0将被覆盖,导致`strcmp`函数无法正确判断字符串是否相等。 因此,缓冲区溢出是一个常见安全漏洞,会导致代码执行异常甚至系统崩溃。为了避免此类问题,我们在编写程序时应该特别注意缓冲区的大小,并且使用安全的字符串处理函数来防止缓冲区溢出

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值