1.基础知识
函数与函数栈
栈是一种先进后出的特殊数据结构,用于存储 程序在运行时的临时数据和地址,用于支撑函 数的运行和嵌套调用。
栈的分配是由程序编译时确定下来的,无法由 程序员控制。
栈中存储着线程或者进程的局部变量
不同的进程或线程的栈处于不同的位置,在程 序正常运行时不同线程和进程之间不能互相访 问彼此的栈地址
函数传递参数:
函数都要通过传递进去的参数来确定其具体的 工作内容,而函数间的参数也是通过栈来传递 的 。我们看如下例子:
32位操作系统,32位的程序反汇编结果
函数需要传递的参数是直接被压入栈中的的方式,是直接 写在栈上的。
这时ebp的下方依此是父函数的栈底地址,程序返回地址,传递的 参数1,传递的参数2,…。
由于在函数调用前通过push指令向栈中压入了数据,使得栈顶被 抬高了
所以在函数调用结束以后,将通过add esp 0x10这条指令,即增 加esp来恢复函数调用前的esp。
64位操作系统,64位的程序反汇编结果
传递的参数不再直接写在栈上,而是通过寄存器传递
这些寄存器分别是rdi, rsi, rdx, rcx, r8, r9,超出寄 存器存放数量的数据才继续在栈上存储。
64位程序反汇编结果如下:
函数传递参数不再向栈中压入,而是将参数赋值给edi 和esi。由于没有压栈的操作,所以函数执行结束以后 也就没有相应的恢复栈顶的过程了
栈中数据是如何被调用的
函数可以调用的栈上的参数主要分为两部分:传递 的参数和程序里的局部变量
这里的printf函数的两个参数分别是父函数传递过去的第 一个参数和第二个参数 最上边的mov指令的参数就是局部变量:代码中的数组a
不论是函数的参数还是局部变量,都是通过统一的 方式:栈底指针+偏移量来读取的
2.漏洞原理
由于程序员在编写代码的时候,对于数组和字 符串等变量的边界没有进行足够的检测
栈溢出漏洞的产生总会伴随着以下两个事件 :
1. 程序向栈上写入一组数据 2. 并且写入的数据总长度没有进行有效的检测。
导致攻击者可以通过向其中写入过多的数据,从 而覆盖掉栈上的其他变量和原本不能访问到的 地址,如返回地址等。
栈溢出的危害 :
修改父函数栈底地址 修改返回地址 修改SEH链表指针
3. ROP基础知识
GOT与PLT
GOT(Global Offset Table,全局偏移表) :存放程序要引用的全局变量或函数的地址
PLT (Procedure Linkage Table,过程链接表):
Linux ELF文件中用于延迟绑定的表 每一项都是一小段代码,对应于程序要引用的一个 全局函数。
程序首次调用函数时,会先从plt表中查找这个位置。 第二次调用时,因为第一次已经写入到plt表中了,所以会直接从plt表中调用。
4. ROP漏洞原理
ROP的全称为Return-oriented Programming(返回导向编程)
计算机安全漏洞利用技术:
1. 绕过可执行空间保护、代码签名等安全保护机制执 行恶意代码
2. 通过控制被调用的堆栈对程序的控制流进行劫持, 完成某些特定功能 。
多与栈溢出漏洞结合利用
覆盖栈帧的返回地址和其他变量,将控制流转移到 期望的地址中
ret2libc
加入了DEP(Data Execution Prevention)和NX(No execute)保护之后,拒绝执行堆栈上的任何代码。 ret2libc是ROP技术的一种,通过将返回地址覆写为libc中的函 数绕过NX保护。
gadget
64位处理器的发展,改变了函数的调用约定,要求 函数的前6个参数保存在寄存器中,如果还有更多 的参数才会保存在栈中。 想继续给函数传递参数将不能通过简单操作栈来操 作函数,还需要操作寄存器。由此造成ret2libc也 变的难以成功。 gadget是从可执行文件或共享库中获取的以ret为 结尾的指令序列。这种ROP技术寻找能够将栈中的 值pop到寄存器的指令片段,由此构造函数参数。
5. ROP漏洞实验
一、程序流的劫持
下面这个函数,我们关掉了DEP和NX来编译。我们使用直接覆盖返回地址,来进行劫持程序执行流程。
vuln( )函数中,buf数组的大小为128字节,但是在 read时最多能够读入256字节,容易造成缓冲区溢出, 利用这个漏洞对程序流进行劫持,执行构造好的 payload。
那么我们的具体的思路是,把payload写入buf数组中,并利用缓 冲区漏洞将返回地址修改为buf数组的地址,vuln( )函 数返回之后,就会到buf数组中执行恶意代码。
payload中除了shellcode外,要填充足够长度以 覆盖返回地址。Buf数组的地址为ebp-0x88,即buf 距离ebp有0x88字节,ebp距离返回地址又0x4字节 (32位时),覆盖返回地址前先填充0x8c个字节。
Shellcode中要填充的长度解决了,要确定覆盖返 回地址的内容,即将要覆盖为的buf数组的地址 ,在脚本中进行gdb调试获取buf数组真实地址。
我们的payload如下:图中的返回地址我们是乱写的。。。
二、 ROP绕过NX实验(32位)
本实验在实验一的基础上开启了NX保护,分别分析32 位和64位程序。
只开启Canary栈保护:gcc rop1.c -o rop2 -m32 fno-stack-protector 查看rop2进程栈的权限为rw,不可执行,如下图:
不能在栈上执行shellcode,但程序中用到了libc库 中的read和printf函数。libc.so中保存了大量的可 用函数,考虑调用system(‘/bin/sh’)来获取shell。
获取system函数地址和字符串’/bin/sh’地址。 由于关闭了ASLR,system函数在内存中地址不会 发生变化;libc.so中也包含了’/bin/sh’字符串。 用gdb查找这两个地址。第一张图是查找system()函数的地址
第二张图是查找 /bin/sh 字符串的地址:
接下来,我们就要来构造我们的ROP:
最终的payload就是’a’*0x8c+rop。其中0xdeadbeef是 system函数的返回地址,因为获取shell后没有别的操作了, 就随意写一个0xdeadbeef作为返回地址。返回地址后面是 system函数的参数,‘/bin/sh’的地址。
三、gadgetROP实验(64位)
不同于32位程序,参数存放在寄存器中,多于6个参数 才会放在栈上。
由于参数不会直接放在栈上,需要寻找类似于pop rdi;ret的gadget,将参数从栈中弹出到rdi寄存器 后,返回到返回地址处继续执行。本题在栈中事先 压入参数’/bin/sh’地址和system地址。
查找gadget:借助ROPgadgets工具,在libc.so 中查找可用的gadgets。先确定rop3程序使用的共享库 。ROPgadgets查找结果如下,其中0x21102是相 对于libc的偏移。我 们用之前vmmap获取的libc.so首地址加上这个偏 移,就得到了最终的地址。 需要注意的是,64位系统编译出的buf地址也发生了改变 ,需要再次查看;sys_addr和binsh_addr地址的获取和 32位系统下方法一样,但是值发生了变化 。
我们可以从上图中看到我们的ROP的构造思路:首先使用 pop rdi + /bin/sh 这一组指令,我们将/bin/sh这个字符串的地址弹出到rdi中,然后再将ret的返回地址覆盖位 system()函数的地址,这样system()执行时,他的参数就会从rdi中找到,也就是\bin\sh。这样就能执行system()函数。