PWN Patch defense skill

环境安装

  • keypatch

https://github.com/keystone-engine/keypatch

将keypatch.py复制到 IDA 7.0\plugins\

安装依赖Consolas

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

例如这里存在off-by-null,直接将该指令nop掉即可:

img

如下图所示: img

此时查看反编译的结果可以发现已经没有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

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

修改指定的函数调用,使其跳转到我们修改后的.eh_frame段来执行,效果如下图所示:

img

示例

容易造成栈溢出的函数

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

将修改保存到一个新的二进制文件

1687963196606

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

所以,这里的目标就是在调用该函数之前,把它的第二个参数再次减去1,这样就不存在off-by-null漏洞了,调用该函数的地方有好几个,这里仅对其中的一个进行hook,就选取edit功能中调用该函数的地方进行hook

hook的call指令地址为0xF2B

img

要将第二个参数减一,而第二个参数是存放在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

是怎么回事呢?对比一下两个程序的段信息,发现patch之后的程序第一个段的大小增加了0x1000,导致后面的地址都增加了0x1000

img

所以在增加了段之后我们需要修改的call指令地址已经不再是0xf2b,而变成了0x1f2b,所以对脚本稍作修改,把脚本中的srcaddr = 0xf2b改成srcaddr = 0x1f2b,再次查看patch的结果:

img

可以看到该出的函数调用确实指向了我们的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

所以直接如下设置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

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

可以看到这种方式对程序的影响确实很小,在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表

增加代码,在读的字节中过滤一些特殊的字符