一、函数调用栈
- 函数调用栈在虚拟内存中用户空间的最高地址,是指程序运行时内存的一段连续区域。
- 用来保存函数运行时的状态信息,包括函数参数和局部变量等
- 称之为”栈“是因为发生函数调用时,调用函数的状态被保存在栈内,被调用函数的状态被压入调用栈的栈顶
- 当函数调用结束时,栈顶的函数状态被弹出,栈顶恢复到调用函数的状态
- 函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大
- 栈就是先入后出,先入栈的在底部
- 一个栈帧包含一个函数的状态信息,父函数每调用一个子函数就会在函数调用栈中新增一个栈帧,栈帧的栈底是用ebp或者rbp寄存器进行保存,栈底为esp(32位x86cpu)或者rsp(64位x86cpu)寄存器进行保存
如果在一个函数中调用另一个函数,编译器就会对应生成一条call
指令,当程序执行到这条call指令时,就会跳到对应的函数入口处开始执行,而每一个函数的最后,都有一条return
指令,负责在函数结束后跳回到调用处继续执行。
1.栈帧结构
- previous stack frame pointer:父函数的栈底指针
- stack frame pointer:父函数的栈顶指针
- return address:返回地址,返回地址会被存储在栈中,以便函数执行完毕后能够返回到调用该函数的地址。
- ebp-esp为当前桢
- local variables:局部变量区
- arguments:函数实参的集合
2.函数调用栈过程
函数状态主要涉及三个寄存器——esp,ebp,eip。
- esp:用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
- ebp:用来存储当前函数的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。
- eip:用来存储即将执行的程序指令的地址,cpu依照eip的存储内容读取指令并执行,eip随之指向相邻的下一条指令,如此往复使得程序连续执行。
1.函数调用的过程
首先,被调用函数的参数按照逆序依次压入栈内。如果被调用函数不需要参数,则不需要此步骤。这些参数仍会保存在调用函数的函数状态内,之后压入栈内的数据都会作为被调用函数的函数状态来保存。
然后将调用函数进行调用后的下一条指令地址作为返回地址压入栈内。这样函数的eip信息得以保存。
再将当前ebp寄存器的值(也就是调用函数的基地址)压入栈内,并将ebp寄存器的值更新为当前栈顶的地址。这样调用函数的ebp信息得以保存。同时ebp被更新为被调用函数的基地址。
在之后将被调用函数的局部变量等数据压入栈内。
2.函数调用结束的过程
首先,被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数的基地址
然后将基地址内存储的调用函数的基地址从栈内弹出,并存到ebp寄存器内。这样调用函数的ebp信息得以恢复,此时栈顶会指向返回地址
二、攻击
控制程序执行指令最关键的寄存器就是eip,所以我们的目标就是让eip载入攻击指令的地址。
1.缓冲区溢出
本质是向定长的缓冲区中写出了超长的数据,造成超出的数据覆写看合法内存区域。
- 栈溢出(Stack overflow): 最常见、漏洞比例最高、危害最大的二进制漏洞 在 CTF PWN 中往往是漏洞利用的基础
- 堆溢出(Heap overflow) :堆管理器复杂,利用花样繁多 CTF PWN 中的常见题型
- Data段溢出: 攻击效果依赖于 Data段 上存放了何种控制数
2.ret2text
篡改栈帧上的返回地址为程序已有的后门函数
3.ret2shellcode
劫持程序控制流,使得程序返回执行自己写的恶意代码。
过程如下:
- 构造shellcode通过溢出放到程序某片存储空间上——为了能够执行这段代码,所存储的区域需要有可执行的权限
- 将返回地址劫持到构造的shellcode地址使得程序开始执行恶意代码
shellcode的获取方法:
- 先指定context.arch="i386/amd64" (注意具体架构)
- asm(自定义shellcode)
- asm(shellcraft.sh())
自动生成shellcode
三、内存保护措施
1.ASLR(地址空间布局随机化)
- 系统的防护措施,装载时生效
- ASLR的目标是通过随机化内存布局,使得每次程序加载时,各个段的起始地址都发生变化。
- /proc/sys/kernel/randomize_va_space = 0:没有随机化。即关闭 ASLR
- /proc/sys/kernel/randomize_va_space = 1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化
- /proc/sys/kernel/randomize_va_space = 2:完全的随机化。在randomize_va_space = 1的基础上,通过 brk() 分配的内存空间也将被随机化
2.pie(地址无关可执行文件)
- 程序的防护措施,编译时生效
- 随机化ELF文件的映射地址
- 开启ASLR之后,PIE才会生效
3.The NX bits
- 程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现
- 通过内存页的标识中增加“执行”位,可以表示该内存页是否可以执行,若程序代码的EIP执行至不可运行的内存页,则CPU将直接拒绝执行“指令”造成程序崩溃
4.Canary(金丝雀)
- 程序的防护措施,编译时生效
- 在刚进入函数时,在栈上放置一个标志canary,在函数返回时检测其是否被干煸,以达到防护栈溢出的目的
- canary长度为1字长,其位置不一定与ebp/rbp存储的位置相邻,具体得看程序的汇编操作
5.RELRO
- 程序的防护措施,编译时失效
- 部分RELRO,在程序装入后,将其中一些段(如.dynamic)标记为只读,防止程序的一些重定位信息修改
- 完全RELRO,在部分RELRO的基础上,在程序装入时,直接解析完所有符号并填入对应的值,此时所有的GOT表项都已初始化,且不装入link_map与di_runtime_resolve的地址