二进制文件应急修复

Catalpa 网络安全爱好者

漏洞修复概述

在具体分析一个漏洞之前,我先将漏洞简单的分一下类,根据漏洞修复的难度,可以把漏洞分为以下四类

  1. 后门函数、危险的字符串(/bin/sh)、输入函数长度溢出(硬编码)等
  2. 格式化字符串等
  3. 指针悬挂、堆栈溢出(动态长度)等
  4. 逻辑漏洞

这几类漏洞在 CTF、AWD 比赛中很常见,也是二进制漏洞利用的主要考察点,按照不同的漏洞又可以总结出几种修复方式

  1. 暴力 nop、修改硬编码数据
  2. 替换 GOT 表条目、符号解析信息
  3. 第三方工具替换系统函数、添加代码
  4. 手动添加代码

实际上,无论是何种修复手段,无非是对程序的代码进行添加、删除或者修改,虽然没有源代码,但是开发者们制作出了很多实用工具,灵活实用这些工具,就算没有源代码也可以实现对 binary 的 patch。

Patch 的核心思想:在不破坏程序原有功能的情况下,加入或者删除部分代码,修复程序的漏洞。

删除代码容易实现,但是插入代码难度就比较高了,具体我们在下面讨论。

工具简介

在 patch 二进制文件时几个常用工具:

  1. IDA
  2. keypatch
  3. LIEF

IDA 无需多言,keypatch 是 IDA 的一个插件,项目地址:https://github.com/keystone-engine/keypatch ,虽然 IDA 自带了一个 patch 工具,但是 keypatch 的功能要远远强于它。

LIEF 是一个支持多个平台的二进制工具,通过 LIEF 可以实现对 binary 的 patch、hook、以及导入、导出函数等操作, 灵活使用能够达到意想不到的效果。

LIEF 有详细的官方教程以及 API 文档,大家可以自行了解用法。

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. 

简单来讲,这个段的是编译器自己添加进去的,当代码中包含异常处理操作时就会生成,它的主要作用时描述如何卸载 stack。

一般情况下,程序正常运行的时候是不会触发异常处理代码的,于是这个段就可以作为保存 patch 代码的空间。

一个 binary 的漏洞通常出现在某个函数调用前后,例如指针悬挂漏洞是由于 free 一个 chunk 没有清空它的指针导致的,再如格式化字符串漏洞是参数问题导致的。patch 漏洞的时候一个核心思路是保持程序正常功能的同时,加入检测、修复代码,而最适于实现这个操作的位置就是 call 指令。第一,call 指令长度为 5 个字节,空间充裕,第二,通过 call 跳转的功能可以劫持程序的控制流到我们的 patch 代码上,完成修复之后再通过强制跳转指令回到正确的控制流,对程序的修改很小,相对于增加一个段或者是 libc 的方法来说更加稳定。

Patch 1:

image-20190614185335210

很明显的格式化字符串漏洞,造成漏洞的主要原因是 printf 函数参数用户可控。

patch 格式化字符串类的漏洞比较简单,通常直接使用 keypatch 修改 call printf 前后代码,满足 puts(buf) 即可。

patch前:

1
2
3
4
5
6
7
.text:0000000000000DEF call    read
.text:0000000000000DF4 lea rax, [rbp+buf]
.text:0000000000000DF8 mov rsi, rax
.text:0000000000000DFB mov edi, offset unk_1 ; s
.text:0000000000000E00 mov eax, 0
.text:0000000000000E05 call __printf_chk
.text:0000000000000E0A lea rdi, aPleaseInputYou ; "Please input your

patch后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0000000000000DEF call    read
.text:0000000000000DF4 lea rax, [rbp+buf]
.text:0000000000000DF8 mov rdi, rax ; Keypatch modified this from:
.text:0000000000000DF8 ; mov rsi, rax
.text:0000000000000DFB nop ; s
.text:0000000000000DFB ; Keypatch modified this from:
.text:0000000000000DFB ; mov edi, offset unk_1
.text:0000000000000DFB ; Keypatch padded NOP to next boundary: 4 bytes
.text:0000000000000DFC nop
.text:0000000000000DFD nop
.text:0000000000000DFE nop
.text:0000000000000DFF nop
.text:0000000000000E00 mov eax, 0
.text:0000000000000E05 call puts ; Keypatch modified this from:
.text:0000000000000E05 ; call __printf_chk
.text:0000000000000E0A lea rdi, aPleaseInputYou ; "Please input you

反编译结果:

image-20190614190117757

ps:某些情况下程序中可能没有 puts 函数,此时需要对 printf 函数进行一定程度的修改,在真正输出字符串之前先过滤,去掉非法字符串(例如 %p 等),如何 patch 参考下面的内容。

Patch2:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 delete()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("Please input the index:");
_isoc99_scanf("%d", &v1);
free(chunk_list[2 * v1]); // uaf
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}

这是一个 UAF 漏洞,堆块指针保存在全局数组中,但是 free 的时候没有清除指针,导致 UAF。

patch 这种漏洞就不能直接在原来的代码基础上修改了,因为直接添加代码会导致指令被覆盖,破坏程序的正常功能。

这里用到上面提到的技巧,通过修改 call 指令劫持程序的控制流到 .eh_frame 段即添加的 fix 代码处,对 chunk_list 执行清空操作,然后正常调用 free 函数完成程序功能,最后通过强制跳转指令回到正常的控制流。

patch前:

1
2
3
4
5
6
.text:0000000000000D79 lea     rax, chunk_list
.text:0000000000000D80 mov rax, [rdx+rax]
.text:0000000000000D84 mov rdi, rax ; ptr
.text:0000000000000D87 call free
.text:0000000000000D8C lea rdi, aDone ; "Done!"
.text:0000000000000D93 call p

Patch后(在 .eh_frame 段添加了代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.eh_frame:00000000000011F9 ; START OF FUNCTION CHUNK FOR delete
.eh_frame:00000000000011F9
.eh_frame:00000000000011F9 loc_11F9: ; CODE XREF: delete+55↑j
.eh_frame:00000000000011F9 lea rax, chunk_list
.eh_frame:0000000000001200 mov rax, [rdx+rax] ; Keypatch modified this from:
.eh_frame:0000000000001200 ; add [rdx+52h], edi
.eh_frame:0000000000001200 ; Keypatch padded NOP to next boundary: 2 bytes
.eh_frame:0000000000001200 ; Keypatch modified this from:
.eh_frame:0000000000001200 ; nop
.eh_frame:0000000000001200 ; nop
.eh_frame:0000000000001200 ; nop
.eh_frame:0000000000001200 ; nop
.eh_frame:0000000000001204 mov qword ptr [rax], 0 ; Keypatch modified this from:
.eh_frame:0000000000001204 ; nop
.eh_frame:0000000000001204 ; js short loc_1217
.eh_frame:0000000000001204 ; Keypatch modified this from:
.eh_frame:0000000000001204 ; mov r9, rax
.eh_frame:0000000000001204 ; Keypatch padded NOP to next boundary: 2 bytes
.eh_frame:0000000000001204 ; Keypatch modified this from:
.eh_frame:0000000000001204 ; nop
.eh_frame:0000000000001204 ; nop
.eh_frame:0000000000001204 ; nop
.eh_frame:0000000000001204 ; add [rbx], ebx
.eh_frame:0000000000001204 ; or al, 7
.eh_frame:000000000000120B call free ; Keypatch modified this from:
.eh_frame:000000000000120B ; or [rax+24000001h], dl
.eh_frame:000000000000120B ; Keypatch padded NOP to next boundary: 1 bytes
.eh_frame:0000000000001210 jmp loc_D8C ; Keypatch modified this from:

patch 后反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 delete()
{
signed __int64 v0; // rdx
void *v1; // rdi
int v3; // [rsp+4h] [rbp-Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("Please input the index:");
_isoc99_scanf("%d", &v3);
v0 = 2LL * v3;
v1 = chunk_list[v0];
*chunk_list[v0] = 0LL;
free(v1);
puts("Done!");
return __readfsqword(0x28u) ^ v4;
}

Patch3:

1
2
puts("Input your Plaintext to be encrypted");
gets(s);

gets 栈溢出,如法炮制,修改 call gets 指令劫持控制流到 .eh_frame 段。但是这里有一个问题,此程序中只有 gets 函数能够接受用户输入,难道就无法 patch 了吗?

其实不然,因为还有 syscall 可以使用,通过 syscall 构造 read 函数,就能控制输入数据的长度,完成修复。

patch 前:

1
2
3
4
5
.text:00000000004009D1 lea     rax, [rbp+s]
.text:00000000004009D5 mov rdi, rax
.text:00000000004009D8 mov eax, 0
.text:00000000004009DD call _gets
.text:00000000004009E2 jmp loc_40

patch 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.eh_frame:0000000000400F7D ; START OF FUNCTION CHUNK FOR encrypt
.eh_frame:0000000000400F7D
.eh_frame:0000000000400F7D loc_400F7D: ; Keypatch modified this from:
.eh_frame:0000000000400F7D mov rax, 3 ; db 0
.eh_frame:0000000000400F7D ; db 0
.eh_frame:0000000000400F7D ; db 0
.eh_frame:0000000000400F7D ; db 0
.eh_frame:0000000000400F7D ; db 0
.eh_frame:0000000000400F7D ; Keypatch modified this from:
.eh_frame:0000000000400F7D ; mov eax, 3
.eh_frame:0000000000400F7D ; db 0BBh
.eh_frame:0000000000400F7D ; db 0
.eh_frame:0000000000400F84 mov rbx, 0 ; Keypatch modified this from:
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F84 ; db 14h
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F84 ; db 0
.eh_frame:0000000000400F8B mov rcx, rdi ; Keypatch modified this from:
.eh_frame:0000000000400F8B ; db 0
.eh_frame:0000000000400F8B ; db 0
.eh_frame:0000000000400F8B ; db 0
.eh_frame:0000000000400F8E mov rdx, 40h ; Keypatch modified this from:
.eh_frame:0000000000400F8E ; db 0
.eh_frame:0000000000400F8E ; db 0
.eh_frame:0000000000400F8E ; db 1
.eh_frame:0000000000400F8E ; db 7Ah
.eh_frame:0000000000400F8E ; db 52h
.eh_frame:0000000000400F8E ; db 0
.eh_frame:0000000000400F8E ; db 1
.eh_frame:0000000000400F95 syscall ; Keypatch modified this from:
.eh_frame:0000000000400F95 ; js short loc_400FA7
.eh_frame:0000000000400F95 ; add [rbx], ebx
.eh_frame:0000000000400F95 ; or al, 7
.eh_frame:0000000000400F95 ; Keypatch padded NOP to next boundary: 1 bytes
.eh_frame:0000000000400F95 ; Keypatch modified this from:
.eh_frame:0000000000400F95 ; jmp loc_400AB4
.eh_frame:0000000000400F95 ; Keypatch padded NOP to next boundary: 3 bytes
.eh_frame:0000000000400F97 jmp loc_400AB4 ; Keypatch modified this from:
.eh_frame:0000000000400F97 ; END OF FUNCTION CHUNK FOR encrypt ; nop

反编译代码:

1
2
puts("Input your Plaintext to be encrypted");
__asm { syscall; Keypatch modified this from: } // read

总结

Patch binary 的时候要注意不能使文件体积变动过大,否则很容易被判宕机,另外,针对不同的漏洞有不同的修复手段,但是总体上来看就是添加、删除代码的过程。patch 过程中注意不要破坏原有的功能(特别是堆栈、寄存器等运行环境),防止 check 不过,当遇到比较复杂的漏洞或者文件空间不足等情况,应该考虑使用如 LIEF 等工具直接替换整个函数。

  • Title: 二进制文件应急修复
  • Author: Catalpa
  • Created at : 2019-06-16 00:00:00
  • Updated at : 2024-10-17 08:23:45
  • Link: https://wzt.ac.cn/2019/06/16/binary_patch/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
二进制文件应急修复