PWN Patch defense skill

环境安装

pip install keystone-engine
pip install six

Edit -> Keypatch -> Patcher 选中一行指令patch,可以输入汇编代码 。 Edit -> Patch program -> Apply patches to input file 将修改保存到一个新的二进制文件(这是ida原本就有的功能) Edit -> Keypatch -> Search 可以搜索汇编指令,不能直接搜索16进制,多条指令用 ; 分隔

  • lief
#python3
pip3 install lief
#python2
wget https://github.com/lief-project/LIEF/releases/download/0.9.0/lief-0.9.0-py2.7-linux.egg
cp ./lief-0.9.0-py2.7-linux.egg ~
pip install lief==0.9.0

API文档:https://lief-project.github.io/doc/latest/api/python/index.html

patch技巧

IDA直接patch

这种方式适合于较简单的修改,不能修改文件结构,直接使用**keypatch(快捷键Ctrl+Alt+k)**修改汇编代码,或者在Edit–>Patch program–>Assemble中进行修改: img](../../img_list/20190411094250.png) 例如这里存在off-by-null,直接将该指令nop掉即可: img](../../img_list/20190411094555.png) 如下图所示: img](../../img_list/20190411094831.png) 此时查看反编译的结果可以发现已经没有off-by-null漏洞了,最终还需要将修改的结果保存至文件中: Edit–>Patch program–>Apply patches to input file

使用LIEF

项目的地址: https://github.com/lief-project/LIEF

LIEF增加段来patch

程序的源代码如下:

#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函数为我们自己的函数

hook程序中的导入函数

编写hook函数

首先要先编写我们的hook函数,编写hook函数有几个要求:

  • 汇编代码必须是位置独立的(也就是要使用-fPIC或-pie / -fPIE标志编译)
  • 不要使用libc.so等外部库(使用:-nostdlib -nodefaultlibs flags) 根据上面的限制条件,我们编译hook程序时使用的编译指令如下所示:

gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook 我们编写的hook函数my_printf如下:

void myprintf(char *a,int b){
//AT&T汇编格式
asm(
"mov %rdi,%rsi\n"
"mov $0,%rdi\n"
"mov $0x20,%rdx\n"
"mov $0x1,%rax\n"
"syscall\n"
);
}
//gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
// Intel汇编格式
// void myprintf(char *a,int b){
// 	asm(
// 		"mov rsi, rdi;\n"
// 		"mov rdi, 0;\n"
// 		"mov rdx, 0x20;\n"
// 		"mov rax, 0x1;\n"
// 		"syscall;\n"
// 		);
// }
//gcc -masm=intel -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook

将hook函数注入到程序并修改got表

import lief
binary = lief.parse("./vulner")
hook = lief.parse('./hook')
# inject hook program to binary
segment_added  = binary.add(hook.segments[0])
# hook got
my_printf      = hook.get_symbol("myprintf")
my_printf_addr = segment_added.virtual_address + my_printf.value
binary.patch_pltgot('printf', my_printf_addr)
binary.write('vulner.patched')

运行patch后的程序可以发现patch成功: img](../../img_list/20190411190529.png)

hook指定地址的函数调用

使用下面的代码可以完成hook程序中指定地址的call函数调用:

import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./vulner")
hook = lief.parse('./hook')
# inject hook program to binary
segment_added  = binary.add(hook.segments[0])
hook_fun      = hook.get_symbol("myprintf")
dstaddr = segment_added.virtual_address + hook_fun.value
srcaddr = 0x400584
patch_call(binary,srcaddr,dstaddr)
binary.write('vulner.patched')

修改.eh_frame段实现patch

eh_frame段在执行的时候对程序的影响不大,所以可以把hook代码添加到该段中,通过修改函数跳转的方式来执行hook代码 对section的操作参考官方文档-section部分 section对象中的content属性就是该section中的内容,所以要对待patch程序的.eh_frame段进行修改,直接将hook程序中的.text段的内容赋值到.eh_frame段的内容即可。赋值完成之后,在通过与前面一致的方法修改函数跳转地址,使其跳转到.eh_frame段来执行我们的hook代码 具体的代码如下:

import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./vulner")
hook = lief.parse('./hook')
# write hook's .text content to binary's .eh_frame content
sec_ehrame = binary.get_section('.eh_frame')
print sec_ehrame.content
sec_text = hook.get_section('.text')
print sec_text.content
sec_ehrame.content = sec_text.content
print binary.get_section('.eh_frame').content
# hook target call
dstaddr = sec_ehrame.virtual_address
srcaddr = 0x400584
patch_call(binary,srcaddr,dstaddr)
binary.write('vulner.patched')

直接将hook程序中的.text段的content赋值到binary程序中的.eh_frame段的content段得到的效果如下图所示,内容确实是我们的hook函数: img](../../img_list/20190411220505.png) 修改指定的函数调用,使其跳转到我们修改后的.eh_frame段来执行,效果如下图所示: img](../../img_list/20190411223153.png)

示例

容易造成栈溢出的函数

void * memcpy ( void * destination, const void * source, size_t num );
char * strcpy ( char * destination, const char * source );
char * strncpy ( char * destination, const char * source, size_t num );
char * gets(char*str);
ssize_t read(int fd, void *buf, size_t count);
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
char * fgets ( char * str, int num, FILE * stream );
char * strcat ( char * destination, const char * source );
scanf(“%20c”, &s);

read patch

read造成的栈溢出,直接修改第三个参数,限制输入大小。 others_babystack中,read存在栈溢出。 使用keypatch将第三个参数改为0x80即可。 Edit -> Patch program -> Apply patches to input file 将修改保存到一个新的二进制文件

gets patch

ret2text为例,32位程序。 gets造成栈溢出,程序没有read函数,构造syscall系统调用,注意32位程序系统调用为int 0x80;,64为程序为syscall ; 先从call gets跳到eh_frame构造系统调用处的。

void mygets(char *a,int b){
// 32bits int 0x80;   64bits syscall;
asm(
"mov $0x3,%eax\n"
"mov $0, %ebx\n"
"mov %eax, %ecx\n"
"mov $0x20,%edx\n"
"int $0x80\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
);
}
//gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook_gets.c -o hook_gets
// 64bits
// void mygets(char *a,int b){
// 	// 32bits int 0x80;   64bits syscall;
// 	asm(
// 		"mov $0x0,%rax\n"
// 		"mov %rdi, %rsi\n"
// 		"mov $0, %rdi\n"
// 		"mov $0x20,%rdx\n"
// 		"syscall\n"
// 		"nop\n"
// 		"nop\n"
// 		"nop\n"
// 		"nop\n"
// 		"nop\n"

// 		);
// }
import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "i386"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./ret2text")
hook = lief.parse('./hook_gets')
# write hook's .text content to binary's .eh_frame content
sec_ehrame = binary.get_section('.eh_frame')
print sec_ehrame.content
sec_text = hook.get_section('.text')
print sec_text.content
sec_ehrame.content = sec_text.content
print binary.get_section('.eh_frame').content
# hook target call
dstaddr = sec_ehrame.virtual_address
srcaddr = 0x080486AE
print("dstaddr ==>", hex(dstaddr))
patch_call(binary,srcaddr,dstaddr)
binary.write('ret2text-patched')

堆溢出

以0ctf_2017_babyheap为例,程序edit功能处,输入大小又用户重新输入,造成任意大小写。

__int64 __fastcall sub_E7F(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = sub_138C();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = sub_138C();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3); //v3为用户输入任意大小
}
}
}
return result;
}

这个我们把v3参数修改为堆块本身大小即可,根据堆块结构(24LL * v2 + a1 + 8)为堆块的size。 那么把v3修改为(24LL * v2 + a1 + 8) 对应汇编代码如下: 需要修改rsi为(24LL * v2 + a1 + 8) ,即mov rsi, [rax+8]

.text:0000000000000F39                 mov     rax, [rax+10h]
.text:0000000000000F3D                 mov     rsi, rcx
.text:0000000000000F40                 mov     rdi, rax
.text:0000000000000F43                 call    sub_11B2
.text:0000000000000F48                 jmp     short locret_F4E

mov rsi, [rax+8]占四字节,本身 mov rsi, rcx指令占3字节,长度不够,所以跳转到.eh_frame段执行再跳转回来。 又由于mov rax, [rax+10h]修改了rax的值,所以mov rax, [rax+10h]mov rsi, [rax+8]调换一下位置 patch方法:edit->Patch program->change byte输入十六进制机器码

48 8B 70 08 48 8B 40 10 48 89 C7 E9 A3 F9 FF

修改如下:

.eh_frame:0000000000001590               loc_1590:
.eh_frame:0000000000001590 48 8B 70 08            mov     rsi, [rax+8]
.eh_frame:0000000000001594 48 8B 40 10            mov     rax, [rax+10h]
.eh_frame:0000000000001598 48 89 C7               mov     rdi, rax
.eh_frame:000000000000159B E9 A3 F9 FF FF         jmp     loc_F43
.text:0000000000000F39 E9 52 06 00 00             jmp     loc_1590
.text:0000000000000F3E 90                         nop
.text:0000000000000F3F 90                         nop
.text:0000000000000F40 90                         nop
.text:0000000000000F41 90                         nop
.text:0000000000000F42 90                         nop
.text:0000000000000F43
.text:0000000000000F43      loc_F43:                         ; CODE XREF: sub_E7F+71C↓j
.text:0000000000000F43 E8 6A 02 00 00             call    sub_11B2

查看伪代码

....
.....
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = sub_138C();
if ( (int)result > 0 )
{
printf("Content: ");
return sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
.....
....

off-by-null/one patch

b00ks为例,题目中读取数据的函数会多读入2个字节的数据,程序在调用该函数时都将size减去了1,所以这里仍然会多读入一个字节,导致off-by-null img](../../img_list/20190413184702.png) 所以,这里的目标就是在调用该函数之前,把它的第二个参数再次减去1,这样就不存在off-by-null漏洞了,调用该函数的地方有好几个,这里仅对其中的一个进行hook,就选取edit功能中调用该函数的地方进行hookhook的call指令地址为0xF2B img](../../img_list/20190413185106.png) 要将第二个参数减一,而第二个参数是存放在rsi寄存器中的,所以只需要将rsi的值减去一,接着直接调用函数read_ndata即可,而我们在hook.c中写hook函数时是不知道增加了segment之后函数read_ndata的地址是多少的,所以这里先采取用5个nop指令来占位置的方法为call指令占位

int myread(char *ptr,int num){
asm(
"sub $0x1,%rsi\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
);
return 0;
}

将上述的hook.c进行编译,得到hook文件:

gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook 接着使用如下脚本将0xF2B处的call指令进行hook

import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./b00ks")
hook = lief.parse('./hook')
print hook.get_section('.text').content
# inject hook program to binary
segment_added  = binary.add(hook.segments[0])
hook_fun      = hook.get_symbol("myread")
# hook b00k's call
dstaddr = segment_added.virtual_address + hook_fun.value
srcaddr = 0xf2b
patch_call(binary,srcaddr,dstaddr)
binary.write('b00ks-patched')

patch之后,把得到的b00ks-patched文件拖到IDA中进行分析,发现并没有将edit功能中的call指令指向我们的hook函数,而且该call指令的地址也由原来的0xf2b变成了0x1f2b img](../../img_list/20190413190056.png) 是怎么回事呢?对比一下两个程序的段信息,发现patch之后的程序第一个段的大小增加了0x1000,导致后面的地址都增加了0x1000img](../../img_list/20190413190609.png) 所以在增加了段之后我们需要修改的call指令地址已经不再是0xf2b,而变成了0x1f2b,所以对脚本稍作修改,把脚本中的srcaddr = 0xf2b改成srcaddr = 0x1f2b,再次查看patch的结果: img](../../img_list/20190413190956.png) 可以看到该出的函数调用确实指向了我们的hook函数sub_4042d8

LOAD:00000000004042D8 sub_4042D8      proc near               ; CODE XREF: sub_1E17+114↑p
LOAD:00000000004042D8
LOAD:00000000004042D8 var_C           = dword ptr -0Ch
LOAD:00000000004042D8 var_8           = qword ptr -8
LOAD:00000000004042D8
LOAD:00000000004042D8                 push    rbp
LOAD:00000000004042D9                 mov     rbp, rsp
LOAD:00000000004042DC                 mov     [rbp+var_8], rdi
LOAD:00000000004042E0                 mov     [rbp+var_C], esi
LOAD:00000000004042E3                 sub     rsi, 1
LOAD:00000000004042E7                 nop
LOAD:00000000004042E8                 nop
LOAD:00000000004042E9                 nop
LOAD:00000000004042EA                 nop
LOAD:00000000004042EB                 nop
LOAD:00000000004042EC                 mov     eax, 0
LOAD:00000000004042F1                 pop     rbp
LOAD:00000000004042F2                 retn
LOAD:00000000004042F2 sub_4042D8      endp
  • 继续完善 经过上面的操作,我们已经能够将call劫持到我们的hook函数来执行了,还差的就是把hook函数中占位的nop指令修改成call read_ndata函数,所以接下来将对其进行修改 观察上面patch后的结果,可以知道nop指令的起始地址为0x4042E7,我们要调用的函数read_ndata地址则变成了0x19f5 img](../../img_list/20190413191719.png) 所以直接如下设置patch_call的参数就能实现最终的patch:
dstaddr = 0x19f5
srcaddr = 0x4042e7

完整的脚本如下:

import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./b00ks")
hook = lief.parse('./hook')
print hook.get_section('.text').content
# inject hook program to binary
segment_added  = binary.add(hook.segments[0])
hook_fun      = hook.get_symbol("myread")
# hook b00k's call
dstaddr = segment_added.virtual_address + hook_fun.value
srcaddr = 0x1f2b
patch_call(binary,srcaddr,dstaddr)
dstaddr = 0x19f5
srcaddr = 0x4042e7
patch_call(binary,srcaddr,dstaddr)
binary.write('b00ks-patched')

patch的效果如下所示: img](../../img_list/20190413192115.png)

LOAD:00000000004042D8 ; Attributes: bp-based frame
LOAD:00000000004042D8
LOAD:00000000004042D8 sub_4042D8      proc near               ; CODE XREF: sub_1E17+114↑p
LOAD:00000000004042D8
LOAD:00000000004042D8 var_C           = dword ptr -0Ch
LOAD:00000000004042D8 var_8           = qword ptr -8
LOAD:00000000004042D8
LOAD:00000000004042D8                 push    rbp
LOAD:00000000004042D9                 mov     rbp, rsp
LOAD:00000000004042DC                 mov     [rbp+var_8], rdi
LOAD:00000000004042E0                 mov     [rbp+var_C], esi
LOAD:00000000004042E3                 sub     rsi, 1
LOAD:00000000004042E7                 call    sub_19F5
LOAD:00000000004042EC                 mov     eax, 0
LOAD:00000000004042F1                 pop     rbp
LOAD:00000000004042F2                 retn
LOAD:00000000004042F2 sub_4042D8      endp
__int64 __fastcall sub_4042D8(__int64 a1, __int64 a2)
{
sub_19F5(a1, a2 - 1);
return 0LL;
}
  • 通过.eh_frame段实现patch 前面介绍的方法是通过在程序中增加一个段的方式来实现patch的,经过这种方法patch后虽然正常的执行都没有问题,但是程序的第一个段的大小增加了0x1000,这导致了程序中各个函数的地址也都增加了0x1000,对程序的改动较大,这里可以通过往.eh_frame段写入hook代码,然后跳转到这里执行的方式 过程和前面介绍的差不多,这里直接贴patch成功的脚本了
import lief
from pwn import *
def patch_call(file,srcaddr,dstaddr,arch = "amd64"):
print hex(dstaddr)
length = p32((dstaddr - (srcaddr + 5 )) & 0xffffffff)
order = '\xe8'+length
print disasm(order,arch=arch)
file.patch_address(srcaddr,[ord(i) for i in order])
binary = lief.parse("./b00ks")
hook = lief.parse('./hook')
# write hook's .text content to binary's .eh_frame content
sec_ehrame = binary.get_section('.eh_frame')
# print sec_ehrame.content
sec_text = hook.get_section('.text')
sec_ehrame.content = sec_text.content
# hook target call
dstaddr = sec_ehrame.virtual_address
srcaddr = binary.get_section('.text').virtual_address+(0xf2b-0x8e0)
print 'srcaddr:'+hex(srcaddr)
print 'dstaddr:'+hex(dstaddr)
patch_call(binary,srcaddr,dstaddr)
# modify nop to call
dstaddr = binary.get_section('.text').virtual_address+(0x9f5-0x8e0)
srcaddr = sec_ehrame.virtual_address+0xf
patch_call(binary,srcaddr,dstaddr)
binary.write('b00ks-patched-frame')

patch的效果如下: img](../../img_list/20190413210506.png) 可以看到这种方式对程序的影响确实很小,在hook的代码很少的情况下,可以首选这种方法

整数溢出

有符号跳转改为无符号。

无符号跳转:
JA ;无符号大于则跳转
JNA ;无符号不大于则跳转
JAE ;无符号大于等于则跳转 同JNB
JNAE ;无符号不大于等于则跳转 同JB
JB ;无符号小于则跳转
JNB ;无符号不小于则跳转
JBE ;无符号小于等于则跳转 同JNA
JNBE ;无符号不小于等于则跳转 同JA
有符号跳转:
JG ;有符号大于则跳转
JNG ;有符号不大于则跳转
JGE ;有符号大于等于则跳转 同JNL
JNGE ;有符号不大于等于则跳转 同JL
JL ;有符号小于则跳转
JNL ;有符号不小于则跳转
JLE ;有符号小于等于则跳转 同JNG
JNLE ;有符号不小于等于则跳转 同JG

格式化字符串

程序里有puts的话把call printf改成call puts 增加代码,添加合适的参数,将printf(xxx)改为printf(“%s”,xxxxx) 增加代码,把printf改成write,没有write可以通过系统调用的形式。 容易造成格式化字符串漏洞的函数

int printf ( const char * format, ... );
int fprintf ( FILE * stream, const char * format, ... );
int sprintf ( char * str, const char * format, ... );

命令执行

把命令执行函数nop掉 容易造成命令执行的函数

FILE *popen(const char *command, const char *type);
int system(const char *command);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execveat(int dirfd, const char *pathname, char *const argv[], char *const  envp[], int flags);

不太优雅的方法

check脚本的时候有可能会检测 把free的plt表改成ret~~ nop 掉 malloc nop 掉 free 打乱got表 增加代码,在读的字节中过滤一些特殊的字符