avatar

如何对pwn题进行patch

ida插件——kaypatch与findcrypt

安装

  1. 安装python3

  2. 更换pip源

  3. 以管理员身份运行cmd

  4. 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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87

    > 工具地址:https://github.com/keystone-engine/keypatch
    >
    > ​ https://github.com/polymorf/findcrypt-yara

    下载工具,并将`keypatch.py`、`findcrypt3.py`、 `findcrypt3.rules`放到ida的plugins目录,重启ida即可

    ## 使用

    ### keypatch

    edit - keypatch ,或者control+alt+k即可

    修改后可以看到旁边的注释,或者ctrl+alt+p 可以看到曾经的修改

    如果需要保存,edit - patch program - apply patches to input file ,然后ok即可

    ### findcrypt

    Edit->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命令的下一条命令地址,相减得到偏移,如果是负数记得使用补码形式

    ![](https://space.0bs3rver.workers.dev/0bs3rver/Picture/master//blogimg/pwn-patch-1.png)

    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

    ### lief

    lief是一个开源的跨平台的可执行文件修改工具,链接: 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 <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 0;
    }

如果我们想把第一个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
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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("a")
hook = lief.parse("hook")

segment_added = binary.add(hook.segments[0])
hook_fun = hook.get_symbol("myprintf")

# hook call delete_note
dstaddr = segment_added.virtual_address + hook_fun.value
srcaddr = 0x400557
patch_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的大小有限。

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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("./a")
hook = lief.parse("./hook")

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
dstaddr = bin_eh_frame.virtual_address
srcaddr = 0x400557
patch_call(binary,srcaddr,dstaddr)

binary.write("patch_md_ehframe")

可以看到先跳到 .eh_frame 然后回来

修改程序.eh_frame段——指针清零

pwn中的堆题最常见的漏洞就是free后指针未清零,我们尝试patch一下

我们以ciscn2021的loneywolf为例

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __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后未清零的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.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会直接帮你计算偏移,确实好用

修改之后如下:

反汇编如下:

参考:

文章作者: 0bs3rver
文章链接: http://yoursite.com/2021/05/30/%E5%A6%82%E4%BD%95%E5%AF%B9pwn%E9%A2%98%E8%BF%9B%E8%A1%8Cpatch/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 0bs3rver的小屋