内存管理之:内存空间分布和栈攻击(黑客常用攻击手段)

目录

C语言内存管理及栈攻击

内存管理

Linux虚拟内存空间分布(重要)

栈溢出(栈攻击)

堆栈的特点

栈攻击

栈攻击的实现

原理

编译器选项

实现案例

linux修改栈空间大小方式

内存泄漏

如何避免野指针?

如何杜绝忘记释放内存?(分析诊断工具valgrind)

内存泄漏总结


C语言内存管理及栈攻击

内存管理

Linux虚拟内存空间分布(重要)

(1)bss段存放初始化的变量:

  • 1.未初始化的全局变量和静态局部变量
  • 初始值为0的全局变量和静态局部变量(依赖于编译器实现)

(2).data数据段存放初始化的变量:

数据段通常用于存放程序中已初始化且初值不为0的全局变量、静态全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。其中有一个.rodata段,一般用于存放常量字符串和只读变量。

(3).text(代码段)

可执行文件加载到内存中的只有数据和指令之分,而指令被存放在.text段中,一般是共享的,编译时确定,只读,不允许修改

(4).heap(堆)

用于存放进程运行时动态分配的内存,可动态扩张或缩减,这块内存由程序员自己管理,通过malloc/new可以申请内存,free/delete用来释放内存,heap的地址从低向高扩展,是不连续的空间

(5).stack(栈)

记录函数调用过程相关的维护性信息,栈的地址一般从高地址向低地址扩展,是连续的内存区域

(6)共享库(libc.so)

静态链接库和动态链接库的区别:

  • 不同操作系统下后缀不一样

windows

linux

静态库

.lib

.a

动态/共享库

.dll

.so

  • 加载方法的时间点不同

*.a 在程序生成链接的时候已经包含(拷贝)进来了

*.so 程序在运行的时候才加载使用

  • 静态库把包含调用函数的库是一次性全部加载进去的,动态库是在运行的时候,把用到的函数的定义加载进去,所以包含静态库的程序所以用静态库编译的文件比较大,如果静态库改变了,程序得重新编译,相反的,动态库编译的可执行文件较小,但.so改变了,不影响程序,动态库的开发很方便
  • 程序对静态库没有依赖性,对动态库有依赖性。

栈溢出(栈攻击)

堆栈的特点

  • 栈增长方向(现在的内核):高地址->低地址
  • 函数调用栈的示意图

栈攻击

在旧版本的内核,栈空间是向下增长的由于栈空间后面紧挨着内核空间,所有一旦栈溢出,就会对内核空间进行非法访问,这样就是所谓的栈攻击(黑客常用攻击手段)

因此Linux内核在2.0升级维护的时候,在系统内核和栈之间增加了一个保护区,大概2M。作用是一旦栈溢出到了保护区,系统就自动报错(栈溢出错误)

还要一种做法,就是改变栈的增长方向,改为向下增长(Linux内核5.0的做法)。

栈攻击的实现

原理

如果忘了压栈的具体过程,可参考:

C/C++函数调用的压栈模型

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、main函数中的变量、进行依次压栈;当main函数开始调用func1()函数时,编译器此时会将main函数的运行状态进行压栈,再将func1()函数的返回地址、func1函数的参数、func1定义变量依次压栈;当func1调用func2的时候,编译器此时会将func2函数的运行状态进行压栈,再将func2函数的返回地址、func2函数的参数、func2定义变量依次压栈。

以向上增长的栈为例:

我们可以很容易的想到一种攻击方式,比如我们想要获取func2的使用权,我们可以在func1中使用一个数组,通过增加数组的大小,让它能够触及的地址一直增加,最终就能够越界获取到func2的入口地址,这样就能够获取到func2的使用权限

编译器选项

gcc C文件名 -z execstack -fno-stack-protector

-z execstack开启堆栈可执行机制

-fno-stack-protector关闭堆栈保护机制

实现案例

(这个实例的实现过程其实还是不太明白:已解决!!!见后面)

如图所示,我们定义了两个函数,func1中定义了一个数组,通过赋值传入func2的函数名(即函数的入口地址)给数组成员(注意这里传入的不是func2(),不是调用func2函数);在func2中打印了一句话。最终我们在main函数中调用func1函数

通过逐渐增加数组a[3]=func2中数组成员的下标,最终在a[6]的时候,成功获取使用权

但通过在func1最后加了个while(1),发现func1退出不了,func2就不会被调用,所以最后得出结论:应该是func1的函数返回地址刚好是func2的入口地址

该问题已被我发布在CSDN,可以参考如下:没有被调用的函数其代码为什么会被执行?_xyz-x的博客-CSDN博客

(2023年9月3日)该问题已解决:

结合GPT的回答,可以总结出实现的原理是:

函数的定义通常存放在代码段中,而不是栈中。在程序运行时,代码段是用来存储程序的指令的内存区域,它通常是只读的。函数的定义在编译时就确定了,并且存放在代码段中,以便在程序执行过程中被调用和执行。栈则是用于存放局部变量、函数参数和临时数据等的内存区域,它在函数调用时动态分配和释放,具有先进后出的特性。

所以func1和func2的入口地址和返回地址在编译时已经确定好了,在代码段中。

func1函数中的数组越界操作a[6] = func2。因为在func1函数的栈帧上,局部变量a的下标为 6 的位置处超过了a的实际长度(大小为 4),所以会覆盖到func1函数的返回地址。

因此,func2函数的入口地址被写入到了func1函数的返回地址位置上。当func1函数执行完毕并返回时,CPU会根据返回地址跳转到该地址对应的代码,从而执行了func2函数。

linux修改栈空间大小方式

由于线程使用的线程函数,因此栈空间越大,就能支持越多的线程,常用于网络编程中一个服务类连接更多的客户端

对于堆空间的大小,一般是在创建系统时候决定,堆空间一旦被分配完,如通过malloc或者new的方式再次申请空间就会一直失败。

内存泄漏

内存泄漏有三种情况:越界访问、野指针和内存忘记释放

如何避免野指针?

明确指针的指向

如何杜绝忘记释放内存?(分析诊断工具valgrind)

人无完人,我们人是不可能完全记得释放自己开辟的内存的,尤其在进行大型项目开发时,往往会产生疏忽,因此可以通过内存检测分析工具在写完程序之后进行诊断分析

推荐使用内存分析诊断工具valgrind

安装和使用可参考:linux代码检测工具valgrind之内存检测memcheck_linux代码检查工具_夜雨听萧瑟的博客-CSDN博客

使用时,在编译完程序,使用命令valgrind --tool=memcheck --leak-check=full ./test即可

内存泄漏总结

C语言没有更好的方法杜绝malloc的导致的内存泄漏

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

竹烟淮雨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值