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中进行修改:
例如这里存在off-by-null
,直接将该指令nop掉即可:
此时查看反编译的结果可以发现已经没有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成功:
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函数:
修改指定的函数调用,使其跳转到我们修改后的.eh_frame
段来执行,效果如下图所示:
示例
容易造成栈溢出的函数
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
所以,这里的目标就是在调用该函数之前,把它的第二个参数再次减去1
,这样就不存在off-by-null
漏洞了,调用该函数的地方有好几个,这里仅对其中的一个进行hook
,就选取edit
功能中调用该函数的地方进行hook
待hook
的call指令地址为0xF2B
要将第二个参数减一,而第二个参数是存放在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
是怎么回事呢?对比一下两个程序的段信息,发现patch之后的程序第一个段的大小增加了0x1000
,导致后面的地址都增加了0x1000
:
所以在增加了段之后我们需要修改的call
指令地址已经不再是0xf2b
,而变成了0x1f2b
,所以对脚本稍作修改,把脚本中的srcaddr = 0xf2b
改成srcaddr = 0x1f2b
,再次查看patch的结果:
可以看到该出的函数调用确实指向了我们的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
所以直接如下设置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的效果如下所示:
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的效果如下:
可以看到这种方式对程序的影响确实很小,在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表
增加代码,在读的字节中过滤一些特殊的字符