文章目录
内存对齐
内存对齐系数
说到内存对齐,就不得不说内存对齐系数, 对齐系数最简单的设置方法是使用
#pragma pack(n)
进行设置
为什么需要内存对齐?
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度.
现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作.
段错误的原理
所谓的段错误就是指访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的。
前提知识还是见:深入理解计算机链接
本文的主要目的是在简单了解进程的内存布局的情况下,从装载的过程入手,深入了解一下Segmetation Fault在操作系统层面是如何产生的
,以及程序开发过程中应该如何避免这样的错误。
众所周知Linux中可执行文件的格式是ELF,其实编译过程中的中间文件*.o文件、动态共享库*.so文件也是ELF格式的。
-
在链接器看来,当它通过*.o或者配合*.so文件来生成可执行文件时,它对ELF格式的文件以链接视图(Linking View)进行看待。也就是说
链接器以Section(节)的形式来对待和处理ELF文件
,诸如我们常见说的代码段(.text)、数据段(.data和.bss)等概念。 -
当程序最终需要被装载成进程时,装载器就出场了,
装载器将可执行文件以装载视图(Executive View)进行看待。装载器将以Segment的形式来处理ELF文件
。
所以。我们可以得到这样一个结论:一个具体的ELF文件,其文件头部中的某些属性值,指明了它到底是可执行文件还是可重定位文件(o和.so的统称)。这样,链接器和装载器通过分析ELF文件头部就可以知道它该怎么处理该文件了。用比较直观的、方便理解的图来表示它们的区别就是:
也就是说链接的时候Program Header Table(段头部表)是可选的,但Section Header Table(节头部表)是必须有的。例如*.o就没有Program Header Table,而*.so就有。装载的时候Program Header Table必须有,但Section Header Table是可选的,但即使有Section Header Table,装载器也不会鸟它。
何谓之非法地址:
- 任何没有经过MMU映射过的虚拟空间的地址,不管进程是执行写操作还是读操作,操作系统都会捕捉到这个错误的非法访问,然后输出一个“Segmetation Fault”的错误提示信息并强行终止进程。
- 入口地址之前的地址以及内核空间的地址。由于权限的问题,不允许进程直接访问,操作系统对其进行保护。所以用户进程如何访问它们的话就会触发“Segmetation Fault”的错误。
问题比较多的是出在malloc()之类的动态内存申请函数申请完内存,释放后,没有将指针设置为NULL,而其他地方在继续用先前申请的那块内存时,由于内存管理系统已经将其收回,所以才会出现这样的问题。良好的关于指针的使用习惯是,使用之前先判断其是否为NULL,所有已经归还给操作系统的内存,其访问指针都要及时置为NULL,防止所谓的“野指针”到处飞的情况,不然在大型项目里,光是围剿“Segmetation Fault”就要耗费不少兵力。
摘自:http://blog.chinaunix.net/uid-23069658-id-3959636.html)
缓冲区溢出
原理
缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C/C++语言中,通常使用字符数组和malloc/new之类内存分配函数实现缓冲区。溢出指 数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷。
栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的 EBP 值的正确性,但 EBP 域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。
例如:
若将长度为16字节的字符串赋给acArrBuf数组,则系统会从acArrBuf[0]开始向高地址填充栈空间,导致覆盖EBP值和函数返回地址。若攻击者用一个有意义的地址(否则会出现段错误)覆盖返回地址的内容,函数返回时就会去执行该地址处事先安排好的攻击代码。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。
攻击手段
- (1)
修改保存在堆栈帧中的函数的返回地址,使返回地址指向一段精心安排好的恶意代码
。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。 - (2) smashing-stack(使堆栈上的缓冲区溢出),然后改写局部变量(尤其函数指针)以利用缓冲区溢出缺陷。
当然,缓冲区根本就不必在堆栈上 ―― 它可以是堆中动态分配的内存(也称为“malloc”或“new”区域),或者在某些静态分配的内存中(比如“global”或“static”内存)。基本上,如果攻击者能够溢出缓冲区的边界,麻烦或许就会找上你了。(具体关于安全可能会牵扯出更多出来,再次不再深究)
导致缓冲区溢出的常见 C 和 C++ 错误
- C 和 C++ 附带的大量危险函数(或普遍使用的库)
- C 和 C++ 对整数具有非常弱的类型检查,一般不会检测操作这些整数的问题。处理整数的方式会导致程序缺陷。(不深究)
防止缓冲区溢出的一些技术
- 基于探测方法(canary)的防御。这包括 StackGuard(由 Immunix 所使用)、ProPolice(由 OpenBSD 所使用)和 Microsoft 的 /GS 选项。
- 非执行的堆栈防御。这包括 Solar Designer 的 non-exec 补丁(由 OpenWall 所使用)和 exec shield(由 Red Hat/Fedora 所使用)。
- 其他方法。这包括 libsafe(由 Mandrake 所使用)和堆栈分割方法。
遗憾的是,迄今所见的所有方法都具有弱点,因此它们不是万能药,但是它们会提供一些帮助。
重要选择:静态和动态分配的缓冲区
缓冲区具有有限的空间。因此实际上存在处理缓冲区空间不足的两种可能方式。
- “静态分配的缓冲区”方法:也就是当缓冲区用完时,不为缓冲区再增加任何空间。
解决方式:比如:标准 C strncpy/strncat 和 OpenBSD 的 strlcpy/strlcat
- “动态分配的缓冲区”方法:也就是当缓冲区用完时,动态地将缓冲区大小调整到更大的尺寸,直至用完所有内存。
解决方式:SafeStr和std::string。所以在Effective C++中就有了尽量不要从string中提取一个普通 C 字符串(比如使用 data() 或 c_str() )的规则。
实例研究(来一个最经典的修改返回地址的程序)
#include <stdio.h>
//foo.c
void foo(void)
{
int a, *p;
p = (int *)((char *)&a + 12);
//让p指向main函数调用foo时入栈的返回地址,等效于p = (int*)(&a + 3);
*p += 12;
//修改该地址的值,使其指向一条指令的起始地址
}
int main(void)
{
foo();
printf("First printf call\n");
printf("Second printf call\n");
return 0;
}
这个玩意儿,你还别说,还真有点神奇!!!!!
具体分析留待以后解决吧!
代码摘自:
https://www.cnblogs.com/clover-toeic/p/3737011.html
参考:
https://www.ibm.com/developerworks/cn/linux/l-sp/part4/index.html
https://www.ibm.com/developerworks/cn/linux/l-overflow/index.html