ida插件——kaypatch与findcrypt安装
安装python3
更换pip源
以管理员身份运行cmd
pip install pip
升级pip版本:>>> pip install --user --upgrade pip
提示 No module named pip 解决方法:>>> python -m ensurepip
(好像不进行这一步也问题不大
pip install keystone-engine
pip install six
pip install yara-python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 > 工具地址:https://github.com/keystone-engine/keypatch > > https://github.com/polymorf/findcrypt-yara 下载工具,并将`keypatch.py`、`findcrypt3.py`、 `findcrypt3.rules`放到ida的plugins目录,重启ida即可## 使用### keypatchedit - keypatch ,或者control+alt+k即可修改后可以看到旁边的注释,或者ctrl+alt+p 可以看到曾经的修改如果需要保存,edit - patch program - apply patches to input file ,然后ok即可### findcryptEdit->Plugins->Findcrypt,具体的等遇到再补(摸了# 自动patch(限制execve等危险函数参考:[homura_pwn_waf](https://github.com/wjbsyc/homura_pwn_waf/tree/ed27c8dbdb48d08f858f150fb6ffdcc3ee14ee7a)可能的坑点:我在ubuntu20和mac本机上死活跑不通,缺一个keystone,即使我 `pip install keystone-engine`也不好使,但是我在裸ubuntu18上虽然也缺,但是`pip install keystone-engine`后就好了,可能是我之前尝试的地方都有python3的问题。结果我整了一天,装成功之后patch了程序跑不了。可能是程序本身调用了这些玩意?行吧,三点了,该睡了。# 手动patch## 栈溢出32位:更改参数大小即可64位:找到对应的寄存器## 格式化字符串### 方法一 更改函数如果有puts函数可以更改成函数的地址,即call _puts首先找到puts函数的plt表地址,然后找到call命令的下一条命令地址,相减得到偏移,如果是负数记得使用补码形式E8 是操作码 后四位是偏移,注意x86是小端序 所以改成 E8 46 FE FF FF 即可但是这种方式有一个问题,因为puts函数是自动在输出的字符串尾部加入一个回车符,如果check脚本中是比较两次输入与输出是否全等,这种patch方法就不能过关。### 方法二 加入一个%s参数需要有汇编指令可供修改,参考 [pwn之简单patch](http://baijiahao.baidu.com/s?id=1680405668162970131&wfr=spider&for=pc)## uaf### lieflief是一个开源的跨平台的可执行文件修改工具,链接: https://github.com/lief-project/LIEF提供了python、C、C++等接口,用于解析/修改各种不同平台/格式的文件。python安装:`sudo pip install lief`它最新的安装包只支持python3.6以上,如果要用python2.7需要安装以前的包python2安装`sudo pip install lief==0.8.3.post3`api文档:https://lief.quarkslab.com/doc/latest/index.html### 增加segment这个方法的目的是增加一个程序段,在这个程序段中加入一个修复漏洞的程序代码,一般程序会在call某个函数时触发漏洞,一般语句为call 0x8041234,可以劫持这句话的逻辑,改成call我们定义的修复函数。示例代码如下:```c#include
如果我们想把第一个printf改成我们自己的逻辑,首先需要编译一个包含实现patch函数的静态库,比如:
123456789101112131415161718192021222324void myprintf(char *a,int b){ asm( "mov %rdi,%rsi\n" "mov $0,%rdi\n" "mov $0x20,%rdx\n" "mov $0x1,%rax\n" "syscall\n" );}void myputs(char *a){ asm( "push $0x41414141\n" "push $0x42424242\n" "push %rsp\n" "pop %rsi\n" "mov $0,%rdi\n" "mov $0x20,%rdx\n" "mov $0x1,%rax\n" "syscall\n" "pop %rax\n" "pop %rax\n" );}//gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
如上,将printf改成了write(0,”/bin/sh%d”,0x20),利用注释的gcc命令将其编译。
patch程序的流程是首先将代码段加入到binary程序中,然后修改跳转逻辑,将call printf@plt,改成call myprintf。
lief中提供了add参数可以用于为二进制文件增加段:
123binary = lief.parse(binary_name)lib = lief.parse(lib_name) segment_add = binary.add(lib.segments[0])
在修改跳转语句部分,由程序的call执行寻址方法是相对寻址的,即call addr = EIP + addr
因此需要计算写入的新函数距离要修改指令的偏移,计算方法如下:
1call xxx =(addr of new segment + offset function ) - (addr of order + 5 /*length of call xx*/)
由于偏移地址是补码表示的,因此在用python计算时需要对结果异或0xffffffff,最终patch脚本如下:
12345678910111213141516171819202122232425262728import lieffrom 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 segmentbinary = lief.parse("a")hook = lief.parse("hook") segment_added = binary.add(hook.segments[0])hook_fun = hook.get_symbol("myprintf")# hook call delete_notedstaddr = segment_added.virtual_address + hook_fun.valuesrcaddr = 0x400557patch_call(binary,srcaddr,dstaddr)binary.write("patch_add_segment")
可以看到确实patch成功了
但是中间的hook编译时有点问题,我在不同的虚拟机上跑的hook,并不能成功,a.c 和 hook.c 在同一个地方编译,才成功,之后patch可能也需要注意这个问题。
不过缺点可以看到,文件大小变化很大,可能会被判定为通防或者宕机。
增加library当程序中加载两个库时,在调用某一函数在两个库内同名存在时,是有一定查找顺序的,也就是可以实现,在不修改程序正常代码的前提下,对全部libc函数进行hook。
优势很明显,可以执行任意libc内函数代码,让编程更容易。
不过缺点也很明显,首先程序变得巨大,并且当不存在这个静态链接库的时候,程序跑不起来…
修改程序.eh_frame段——重写函数调用感觉上改.en_frame是目前对程序改动最小的通用方案,.eh_frame段会加载到程序中来,并且自身带有可执行权限,如果我们把patch的放到这里,首先我们没有附加任何东西,所以对程序的大小影响不大,其次他自带可执行权限,非常方便,缺点可能就是.en_frame的大小有限。
脚本如下:
123456789101112131415161718192021222324252627282930313233343536import lieffrom 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 segmentbinary = lief.parse("./a")hook = lief.parse("./hook")hook_sec = hook.get_section(".text")bin_eh_frame = binary.get_section(".eh_frame")print hook_sec.contentprint bin_eh_frame.contentbin_eh_frame.content = hook_sec.contentprint bin_eh_frame.content# hook calldstaddr = bin_eh_frame.virtual_addresssrcaddr = 0x400557patch_call(binary,srcaddr,dstaddr)binary.write("patch_md_ehframe")
可以看到先跳到 .eh_frame 然后回来
修改程序.eh_frame段——指针清零pwn中的堆题最常见的漏洞就是free后指针未清零,我们尝试patch一下
我们以ciscn2021的loneywolf为例
123456789101112unsigned __int64 sub_C60(){ __int64 v1; // [rsp+0h] [rbp-18h] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-10h] v2 = __readfsqword(0x28u); __printf_chk(1LL, "Index: "); __isoc99_scanf(&unk_F44, &v1); if ( !v1 && buf ) free(buf); return __readfsqword(0x28u) ^ v2;}
可以很明显的看到,存在free后未清零的问题
1234567891011121314151617181920212223242526.text:0000000000000C60 ; __unwind {.text:0000000000000C60 sub rsp, 18h.text:0000000000000C64 lea rsi, aIndex ; "Index: ".text:0000000000000C6B mov edi, 1.text:0000000000000C70 mov rax, fs:28h.text:0000000000000C79 mov [rsp+18h+var_10], rax.text:0000000000000C7E xor eax, eax.text:0000000000000C80 call ___printf_chk.text:0000000000000C85 lea rdi, unk_F44.text:0000000000000C8C xor eax, eax.text:0000000000000C8E mov rsi, rsp.text:0000000000000C91 call ___isoc99_scanf.text:0000000000000C96 cmp [rsp+18h+var_18], 0.text:0000000000000C9B jnz short loc_CAE.text:0000000000000C9D mov rdi, cs:buf ; ptr.text:0000000000000CA4 test rdi, rdi.text:0000000000000CA7 jz short loc_CAE.text:0000000000000CA9 call _free.text:0000000000000CAE.text:0000000000000CAE loc_CAE: ; CODE XREF: sub_C60+3B↑j.text:0000000000000CAE ; sub_C60+47↑j.text:0000000000000CAE mov rax, [rsp+18h+var_10].text:0000000000000CB3 xor rax, fs:28h.text:0000000000000CBC jnz short loc_CC3.text:0000000000000CBE add rsp, 18h.text:0000000000000CC2 retn
我们在.en_frame上随便找个地,然后开始对着改,需要注意的是字符串是通过计算偏移来实现的,更换地址后需要重新计算偏移。
old: arm_addr = 0xC64 + 0x7 = 0xc6b str_addr = 0xf48 0xf48 - 0xc6b = 0x2dd
new: arm_addr = 0x1074 + 0x7 = 0x107b str_addr = 0xf48 0xf48 - 0x107b = 0xfffffecd
但是写着写着我发现有点不对劲,这一个个偏移算的很烦,而且有的指令还看起来不太对劲,比如mov [rsp+18h+var_10], rax,我还没办法用keypatch直接改,仔细想想,我为什么要重复写这么一大坨玩意呢,我需要的只是加上几行命令而已,直接把一行改成跳转然后执行完了再跳回去不就得了。
这里我偷了个懒,直接把最后的canary检查给关了,其实不关也行,或者是干脆用这几行的空闲来加上patch,我这里主要是想试试jmp,keypatch会直接帮你计算偏移,确实好用
修改之后如下:
反汇编如下:
参考:
CISCN2020-CTF-半决赛赛前patch学习
CTF线下赛中常用的PWN题patch方法
记fast_bin attack到patch的三种手法