栈溢出和ROP
gets_plt = 0x08048460 |
栈溢出和ROP
栈
:一种先进后出的数据结构。常见操作有两种,进栈(PUSH) 和弹栈(POP),用于标识栈的属性有两个,一个是栈顶(TOP),一个是栈底(BASE)
程序中的栈:
内存中的一块区域,用栈的结构来管理,从高地址向低地址增长
寄存器esp代表栈顶(即最低栈地址)
栈操作
压栈(入栈)push sth-> [esp]=sth,esp=esp-4
弹栈(出栈)pop sth-> sth=[esp],esp=esp+4
栈用于保存函数调用信息和局部变量
函数调用
如何通过系统栈进行函数的调用和递归
函数调用指令: call ret
函数调用时栈的变化
大致过程:
参数入栈
返回地址入栈
代码区块跳转
栈帧调整:
保存当前栈帧的状态值,为了后面恢复本栈帧时使用(EBP入栈);
将当前的栈帧切换到新栈帧(ESP值装入EBP,更新栈帧底部)
给新栈帧分配空间(ESP减去所需要空间的大小,抬高栈顶)
push 参数 3 #参数由右向左入栈 |
参数传参:取决于调用约定,一般情况下:
X86 从右向左入栈,X64 优先寄存器,参数过多(6个以上)时才入栈
手把手教你栈溢出从入门到放弃(上)讲栈的调用非常好
调用时主要变化要点就是保存调用函数,同时创建被调用函数的状态
1.函数调用开始时,将被调用函数(callee)的参数逆序保存在栈中,如果没有就参数就不需要,这些参数仍需保存在调用函数(caller)的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存。将被调用参数压栈
2.,然后将调用函数(caller)进行调用之后的下一条指令地址作为返回地址压入栈内。这样调用函数(caller)的 eip(指令)信息得以保存。将被调用函数的返回地址压入栈内
3.再将当前的ebp 寄存器的值(也就是调用函数的基地址)压入栈内,并将 ebp 寄存器的值更新为当前栈顶的地址。这样调用函数(caller)的 ebp(基地址)信息得以保存。同时,ebp 被更新为被调用函数(callee)的基地址。
4.将调用函数的基地址(ebp)压入栈内,并将当前栈顶地址传到 ebp 寄存器内,再之后是将被调用函数(callee)的局部变量等数据压入栈内。
寄存器
重要的寄存器:rsp/esp, pc, rbp/ebp, rax/eax, rdi, rsi, rdx, rcx
ESP: 栈指针寄存器,内存存放着一个指针,指针指向系统栈最上面一个栈帧的底部
EBP:基址指针寄存器,存放着一个指针,指针指向系统栈最上面的一个栈帧底部
栈溢出
的原理就是不顾堆栈中分配的局部数据块大小,向该数据快写入了过多的数据,导致数据越界,结果覆盖来看老的堆栈数据。
栈溢出的保护机制
栈上的数据无法被当作指令来执行
数据执行保护(NX/DEP)
绕过方法ROP
难以找到想要找的地址
地址空间布局随机化(ASLR)
绕过方法:infoleak 、retdlresolve 、ROP
检测栈数据是否被修改
Stack Canary/ Cookie
绕过方法: infoleak
CTF 常用套路: 栈溢出的利用方法
现代栈溢出利用技术基础:ROP
利用signal机制的ROP技术:SROP
没有binary怎么办:BROP 、dump bin
劫持栈指针:stack pivot
利用动态链接绕过ASLR:ret2dl resolve、fake linkmap
利用地址低12bit绕过ASLR:Partial Overwrite
绕过stack canary:改写指针与局部变量、leak canary、overwrite canary
溢出位数不够怎么办:覆盖ebp,Partial Overwrite
现代栈溢出利用技术基础:ROP
ctf中的ROP的套路
说了这么多,rop是什么?
ROP是(Return Oriented Programming),主要是在栈溢出的基础上,利用程序中已有的小片段(gadgets),来改变某些寄存器或者变量的值,从而控制程序的执行流,gadgets就是以ret结尾的指令序列,通过这些指令序列进行修改程序。
ROP攻击满足条件:
- 程序可能存在溢出,并且可以控制返回地址。
- 可以找到满足条件的gadgets以及对应的地址
ret2text
// jarvisoj_level2 |
read存在栈溢出,且存在/bin/sh字符串
思路
:覆盖返回地址为system_plt,并根据32位的参数传参数binsh
payload:'a'*0x88+'b'*4+system_plt+'c'*4+binsh_addr
解释:padding+覆盖ebp+覆盖返回地址+函数调用返回地址+system函数参数
该c*4可以改为main函数的地址,这样就可以反复触发漏洞
如果是64位呢?
64位首先要先使用寄存器(依次位rdi,rsi,rdx,rcx,r8,r9),当参数超出6个时才会用栈.
思路
:覆盖返回地址为system_plt,并根据64位的参数传递规则设置参数.
我们需要rdi指向binsh,所以我们需要找到pop rdi的地址(可以使用ropper或Ropgadget)
payload:'a'*0x80+'b'*8+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)
ret2shellcode
即控制程序执行shellcode代码,shellcode用于完成某些功能的汇编代码,常见的就是获取shell,一般shellcode需要我们自己填充,注意,shellcode所在区域需要有可执行权限.
// jarvisoj_level1 |
程序栈可执行,泄露buf地址,read函数存在栈溢出.
思路
:在buf出写shellcode,覆盖ret地址为buf
ret2syscall
ret2libc
ret2cus
第一次触发漏洞
,通过ROP泄漏libc的address(如puts_got),计算system地址,然后返回到一个可以重现触发漏洞的位置(如main),再次触发漏洞,通过ROP调用system(“/bin/sh”)
第二个pwn的特点是
,我们需要去info leak 得到信息,然后计算system 的地址。
ROP
对抗DEP/NX保护技术
因为核心在于利用了指令集中的ret指令,改变了指令流的执行顺序,并且利用gadgets片段的ret,可以实现连续控制。
ROP攻击需要满足
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的gadgets以及相应gadgets的地址。
ROP ret2libc
过程:
- 构造栈结构
- 利用返回地址ret的跳转特点
- 不在栈中或bss端执行代码,而是在程序的可执行段寻找可以执行的小组件(gadget)
- 把小组件串起来,构造而成的就叫ROP链
对抗ASLR/PIE保护技术
如果需要跳转的函数没有在程序里,libc里有。
但ASLR/PIE保护技术使得程序基地址和libc基地址每次加载不一样
思路
- 泄露GOT表中某个函数的libc地址
- 在libc找到system和binsh的相对偏移
- 得到system和binsh的地址
- 构造ROP
在执行了一次某函数之后,GOT表中就会把一个函数在程序中的终极偏移存起来
最终偏移 = libc基址 + 库捏函数的相对偏移
脚本
from pwn import * |
from pwn import * |