patch

转自peda

IDA

IDA Pro是一个非常强大的工具,其中包含了对汇编指令修改的功能。

以国赛华北赛区的半决赛为例,其中有一道PWN2是一个栈溢出,代码是这样的。

img

很显然,在read这里有一个明显的栈溢出,修复漏洞的方法也和容易,将这个值改小成0x138就好了,下面的write也一样的改法。

这里使用IDA默认的修改插件来改,在Edit-Patch Program目录下,首先切换到IDA View-A这个汇编指令界面,并选中要改的汇编指令行:

img

选择Assemble/Change byte/Change word都可以,以Assemble为例在Instruction窗口,将mov edx, 1cch改为mov edx, 138h。

此时,切换到类C语言窗口可以看到该行已经被修改为了read(a1, &s, 0x138uLL);

img

但并没有完,这仅仅修改了IDA对于该文件的数据库,并没有应用到文件中去,同样在Edit-Patch Program目录下,选择Apply patches into file…,将修改写入文件,就完成了一道简单题目的patch。

img

这种方法完全依靠手动,而且不能修改文件结构,可以供手动修改的位置也很少,一旦出现如UAF等悬垂指针的问题基本就很难解决了,还得依靠其他更有力的方法来解决。

lief

lief是一个开源的跨平台的可执行文件修改工具,链接如下:

1
https://github.com/lief-project/LIEF

对外提供了Python、C++、C的接口。

对于Python库安装可以使用pip,如

1
sudo pip install lief

对于lief的API和用法就不介绍了,RTFM。

1
https://lief.quarkslab.com/doc/latest/api/python/index.html

以下是几种可行的patch方法

增加segment

这个方法的目的是增加一个程序段,在这个程序段中加入一个修复漏洞的程序代码,一般程序会在call某个函数时触发漏洞,一般语句为call 0x8041234,可以劫持这句话的逻辑,改成call我们定义的修复函数。

首先我们的代码程序如下:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv) {
printf("/bin/sh%d",102);
puts("let's go\n");
printf("/bin/sh%d",102);
puts("let's gogo\n");
return EXIT_SUCCESS;
}

我们想把第一处printf修改掉,改成我们自己的逻辑,首先需要编译一个包含实现patch函数的静态库,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void 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参数可以用于为二进制文件增加段:

1
2
3
binary    = lief.parse(binary_name)
lib = lief.parse(lib_name)
segment_add = binary.add(lib.segments[0])

在修改跳转语句部分,由程序的call执行寻址方法是相对寻址的,即call addr = EIP + addr

因此需要计算写入的新函数距离要修改指令的偏移,计算方法如下:

1
call xxx  =(addr of new segment + offset function ) -  (addr of order + 5 /*length of call xx*/)

由于偏移地址是补码表示的,因此在用python计算时需要对结果异或0xffffffff,最终patch计算函数如下:

1
2
3
4
5
6
7
def patch_call(file,where,end,arch = "amd64"):
print hex(end)

length = p32((end - (where + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(where,[ord(i) for i in order])

执行之后可以看到patch成功了,

img

img

但是一个重大的问题是patch前后文件大小改动很大:

1
2
3
4
5
6
7
┌─[p4nda@p4nda-virtual-machine] - [~/Desktop/pwn/patch] - [一 7月 02, 20:36]
└─[$] <> python 1.py
0x8022f9
0: e8 70 1d 40 00 call 0x401d75
[+] ori size 8656
[+] patch size 15885
[+] Seccessful patched in adding segment

这样在一些线下赛中很容易由于修改过大和被判定为通防或者宕机。

增加library

这是借鉴LD_preload的一种思路,当程序中加载两个库时,在调用某一函数在两个库内同名存在时,是有一定查找顺序的,也就是可以实现,在不修改程序正常代码的前提下,对全部libc函数进行hook。如下例:

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
printf("/bin/sh%d", 102LL, envp, argv);
return 0;
}

编译一个动态链接库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//#include "/home/p4nda/linux-4.17.3/lib/syscall.c"

#define _GNU_SOURCE
//#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dlfcn.h>
// gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared patch.c -o patch -ldl


int printf(char *a,int b) {
char str[] = "hacked by me\n ";
//puts(a);
if(strstr(a,"/bin/sh")){
puts("find dangerous str~");
}
int (*old_printf)(char *,int);
old_printf =(int (*)(char *,int)) dlsym(RTLD_NEXT, "printf");
old_printf(a,b);
puts("\n");

}

编译命令在注释中,则每次printf时都会先执行上述库中的函数,达到hook的目的。

img

优势很明显,可以执行任意libc内函数代码,让编程更容易。

不过缺点也很明显,首先程序变得巨大,并且当不存在这个静态链接库的时候,程序跑不起来… 有些线下赛都是本地check的,比如*网杯,很容易就判断宕机了…

img

修改程序.eh_frame段

在TSCTF 2018 Final时,我在NeSE战队的binary文件中找到了通防工具,但是程序改动并没有特别大,当时感觉很好奇,在赛后调试了一下,发现他们把通防的shellcode写在了一个叫.eh_frame的段中,这个段会加载到程序中来,并且自身带有可执行权限,在查找这个段用处时,发现该段对程序执行影响不大,故可以将patch代码写在这个段中,再用跳转的方法将程序逻辑劫持到这里来。

img

img

可以看到在patch前后,程序大小保持不变。

img

缺点同样明显,.eh_frame的大小是有限的…

综上,似乎没有比较简洁的通用方法,综合着来用吧….