写在前面

我们此处讨论的是无源码下的二进制文件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段,功能描述如下

  1. 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.
  2. 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方法 - 图1

程序Patch

这里是原来的Printf调用逻辑

lea     rax, [rbp+format]
mov     rdi, rax        ; format
mov     eax, 0
call    _printf

接下来窝们查看call _printf对应的十六进制

关于二进制文件的Patch方法 - 图2

再查看上面call _puts对应的十六进制

关于二进制文件的Patch方法 - 图3

于是我们修改一个单字节

关于二进制文件的Patch方法 - 图4

可以发现我们已经Patch成功,导出Patch之后的文件

关于二进制文件的Patch方法 - 图5

漏洞验证(Fix后)

关于二进制文件的Patch方法 - 图6

发现该漏洞已被修复

对于程序中不存在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方法 - 图7

程序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

关于二进制文件的Patch方法 - 图8

可以发现已经修复成功

关于二进制文件的Patch方法 - 图9

接下来我们再考虑如果程序中没有%s可供我们利用,我们可以把%s写到.rodata段,可以看到,修复成功。

关于二进制文件的Patch方法 - 图10

关于二进制文件的Patch方法 - 图11

实例:对于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方法 - 图12

程序Patch

jmp 0x4007B8

call    _free
mov qword ptr [rbp-8], 0
jmp 0x40062A

调试发现漏洞已经修复~

总结

我们在进行Patch的时候可以尝试使用jmp将EIP引导至.rodata段。然后将汇编语句逐行写入即可~

参考文章

二进制文件应急修复-CataLpa