写在前面
我们此处讨论的是无源码下的二进制文件Patch,适用于CTF-A&D赛制下的应急修复。
概述
首先我们在程序中常见的漏洞点有以下几类
Backdoor Function(后门函数) Danger String(危险字符串,例如/bin/sh) Format String Vulnerability(格式化字符串漏洞) Stack Overflow(栈溢出) Wild Pointers(野指针,悬挂指针) Logical Vulnerability(逻辑漏洞)
那么我们常见的修复思路也就有以下几种
暴力 nop、修改硬编码数据 替换 GOT 表条目、符号解析信息 第三方工具替换系统函数、添加代码 手动添加代码并引导EIP至Patch代码
Patch 的核心思想:在不破坏程序原有功能的情况下,加入或者删除部分代码,修复程序的漏洞。
添加Patch代码
程序的每条指令都有一定的长度,指令与指令之间没有多余字节,当patch
代码时,可能会遇到添加或删除代码的情况,删除代码比较容易实现,直接使用nop
指令替代原始代码即可,但是当需要添加、修改代码的时候,经常会遇到字节数不够用的情况,为了保证程序正常运行,我们又不能修改掉正常代码,这时就需要寻找一个合适的空间来保存shellcode
,并且这块空间需要具有可执行权限。
大部分 ELF 程序都有一个.eh_frame
段,功能描述如下
When gcc generates code that handles exceptions, it produces tables that describe how to unwind the stack. These tables are found in the .eh_frame section.
当gcc生成处理异常的代码时,它会生成描述如何展开堆栈的表。 这些表位于.eh_frame部分中。
简单来讲,这个段的是编译器自己添加进去的,当代码中包含异常处理操作时就会生成,它的主要作用时描述如何卸载 stack。
一般情况下,程序正常运行的时候是不会触发异常处理代码的,于是这个段就可以作为保存 patch 代码的空间。
一个binary
的漏洞通常出现在某个函数调用前后,例如Wild Pointers(野指针,悬挂指针)
漏洞常见于于free
一个chunk
没有清空它的指针导致的,再如Format String Vulnerability(格式化字符串漏洞)
是参数问题导致的。patch
漏洞的时候一个核心思路是保持程序正常功能的同时,加入检测、修复代码,而最适于实现这个操作的位置就是call
指令。第一,call
指令长度为5
个字节,空间充裕,第二,通过call
跳转的功能可以劫持程序的控制流到我们的patch
代码上,完成修复之后再通过强制跳转指令回到正确的控制流,对程序的修改很小,相对于增加一个段或者是libc
的方法来说更加稳定。
实例:对于Printf所造成的格式化字符串漏洞的Patch
对于程序中同时存在puts函数时的情形
漏洞源码
#include<stdio.h>
int main(){
puts("Hello!");
char s[20];
scanf("%s",s);
printf(s);
return 0;
}
//gcc Printf_with_Put.c -o Printf_with_Put
漏洞验证
程序Patch
这里是原来的Printf调用逻辑
lea rax, [rbp+format]
mov rdi, rax ; format
mov eax, 0
call _printf
接下来窝们查看call _printf
对应的十六进制
再查看上面call _puts
对应的十六进制
于是我们修改一个单字节
可以发现我们已经Patch成功,导出Patch之后的文件
漏洞验证(Fix后)
发现该漏洞已被修复
对于程序中不存在puts函数时的情形
漏洞源码
#include<stdio.h>
int main(){
printf("Hello!\n");
char s[20];
scanf("%s",s);
printf(s);
return 0;
}
//gcc Printf_no_Put.c -o Printf_no_Put
漏洞验证
程序Patch
此时需要对 printf 函数进行一定程度的修改,在真正输出字符串之前先过滤,去掉非法字符串(例如 %p 等)。
因此我们先写一个用于过滤的Waf例程
#include<stdio.h>
void waf_1(char *s){
int i;
for(i=0;i<20;i++){
if(s[i]=='%')
s="You are hacker!";
}
printf(s);
}
void waf_2(char *s){
printf("%s",s);
}
int main(){
printf("Hello!\n");
char s[20];
scanf("%s",s);
waf_1(s);
waf_2(s);
return 0;
}
//gcc Printf_no_Put_with_waf.c -o Printf_no_Put_with_waf
注:此处尽管可以把%
替换为%%
达到转义的效果,但是字符串长度会递增1,容易引发off-by-one
漏洞,故不作考虑。
对其反汇编
/*waf_1*/
push rbp
mov rbp, rsp
sub rsp, 20h
mov [rbp+format], rdi
mov [rbp+var_4], 0
jmp short loc_40067B
mov eax, [rbp+var_4]
movsxd rdx, eax
mov rax, [rbp+format]
add rax, rdx
movzx eax, byte ptr [rax]
cmp al, 25h
jnz short loc_400677
mov [rbp+format], offset aYouAreHacker ; "You are hacker!"
add [rbp+var_4], 1
cmp [rbp+var_4], 13h
jle short loc_40065B
mov rax, [rbp+format]
mov rdi, rax ; format
mov eax, 0
call _printf
nop
leave
retn
这里很明显出现了问题,"You are hacker!"
将存储在.rodata
段,那么我们事实上可以将这个偏易修改为.rodata
段的任何非敏感数据。(前提是程序不能存在能任意修改.rodata
段的漏洞)
/*waf_2*/
push rbp
mov rbp, rsp
sub rsp, 10h
mov [rbp-8], rdi
mov rax, [rbp-8]
mov rsi, rax
mov edi, offset format ; "%s"40073B
mov eax, 0
call _printf
nop
leave
retn
可以看出,这段代码的局限性更大,这要求.rodata
段存在%s
我们先尝试用Keypatch修复。
我们最终确定的是使用以下的patch语句
jmp 0x400778 ; Patch call _printf
mov rsi, rdi
mov rdi, offset 0x40073B ; "%s"
call 0x400510 ; call _printf
jmp 0x40068E
可以发现已经修复成功
接下来我们再考虑如果程序中没有%s
可供我们利用,我们可以把%s
写到.rodata
段,可以看到,修复成功。
实例:对于Free后未置零所造成的Use-After-Free漏洞的Patch
漏洞源码
#include <stdio.h>
#include <stdlib.h>
typedef struct name {
char *myname;
void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
NAME *a;
a = (NAME *)malloc(sizeof(struct name));
a->func = myprint;
a->myname = "I can also use it";
a->func("this is my function");
// free without modify
free(a);
a->func("I can also use it");
// free with modify
a->func = printmyname;
a->func("this is my function");
// set NULL
a = NULL;
printf("this pogram will crash...\n");
a->func("can not be printed...");
}
//gcc Use_After_Free_Demo.c -o Use_After_Free_Demo
漏洞验证
程序Patch
jmp 0x4007B8
call _free
mov qword ptr [rbp-8], 0
jmp 0x40062A
调试发现漏洞已经修复~
总结
我们在进行Patch的时候可以尝试使用jmp将EIP引导至.rodata
段。然后将汇编语句逐行写入即可~