-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy path1.html
More file actions
295 lines (225 loc) · 15.9 KB
/
1.html
File metadata and controls
295 lines (225 loc) · 15.9 KB
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
<html>
<head>
<title>tmp.0ut</title>
<meta charset="utf-8">
<style>
body {
color: #FEFEFE;
background-color: #0c0d10;
margin: 0 auto;
padding: 1em 0 1em 0;
}
@font-face { font-family: "gohu"; src: url("../gohu.woff") format('woff'); }
pre { font-family: "gohu", "Lucida Console", monospace, Monaco; font-size: 14px; line-height: 1.0; }
a { color: #93ffd7; text-decoration: none; }
</style>
</head>
<body>
<center><div style="display: inline-block; text-align: left;"><pre>
_ .-') _ ('-. ('-. _ .-') _ .-. .-') .-') _ ('-. .-')
( ( OO) ) _( OO) ( OO ).-.( ( OO) ) \ ( OO ) ( OO) ) _( OO) ( OO ).
\ .'_ (,------./ . --. / \ .'_ ;-----.\ ,--. ,--./ '._(,------.(_)---\_)
,`'--..._) | .---'| \-. \ ,`'--..._) | .-. | \ `.' / |'--...__)| .---'/ _ |
| | \ ' | | .-'-' | | | | \ ' | '-' /_).-') / '--. .--'| | \ :` `.
| | ' |(| '--.\| |_.' | | | ' | | .-. `.(OO \ / | | (| '--. '..`''.)
| | / : | .--' | .-. | | | / : | | \ || / /\_ | | | .--' .-._) \
| '--' / | `---.| | | | | '--' / | '--' /`-./ /.__) | | | `---.\ /
`-------' `------'`--' `--' `-------' `------' `--' `--' `------' `-----'
~ xcellerator
你好,ELF爱好者们!在这篇文章中,我想介绍一个我一直在开发的小型库,名为LibGolf。它最初只是
为了更好地理解ELF和程序头而创建的工具,但后来发展成了一个相当实用的项目。它可以非常容易地
生成一个由ELF头、单个程序头和单个可加载段组成的二进制文件。默认情况下,头部中的所有字段都
设置为合理的值,但有一个简单的方法可以修改这些默认值 - 这就是本文要讨论的内容!我将演示如何
使用LibGolf来精确枚举哪些字节是必需的,哪些字节会被Linux加载器忽略。幸运的是,事实证明,
加载器是标准Linux工具包中最不挑剔的解析器之一。在我们完成之前,我们将看到几个流行的静态分析
工具在我们损坏的ELF面前崩溃,而加载器继续愉快地加载并跳转到我们选择的字节。
+---------------------------+
|--[ 介绍LibGolf ]--|
+---------------------------+
不久前,我一直在用NASM手写ELF文件。虽然这很有趣(当然也有它的好处),但我意识到我错过了
C结构体所能提供的所有乐趣。特别是,我相信许多读者都知道,<linux/elf.h>中充满了像
`Elf64_Ehdr`和`Elf32_Phdr`这样可以声明的有趣东西。
不想让这些有用的头文件浪费掉,我决定把它们利用起来。经过这些努力,产生了libgolf.h,这是一个
可以轻松将shellcode注入到可执行文件中的库。我知道你在想什么 - "这听起来就像一个糟糕的链接器!",
你可能是对的。但是,这里的好处是你可以在二进制文件构建之前轻松修改头部。
让我们看看它是如何工作的。如果你想在家跟着做,你可以在[0]找到所有这些的源代码。你可以在
'examples/01_dead_bytes'下找到本文中的代码。基本设置需要两个文件:一个C源文件和一个
shellcode.h。说到shellcode,我喜欢使用老朋友'b0 3c 48 31 ff 0f 05',它反汇编为:
mov al, 0x3c @ b0 3c
xor rdi, rdi @ 48 31 ff
syscall @ 0f 05
(是的 - 把这个称为"shellcode"有点牵强!)
本质上,它只是调用exit(0)。这很好,因为我们可以通过shell扩展$?轻松检查这些字节是否成功执行。
将这个或其他shellcode(但要确保它是PIC - 还不支持可重定位符号!)放入shellcode.h中的buf[]
缓冲区,然后回到C文件。如果你只是想得到一个执行你的shellcode的二进制文件,那么你只需要这些:
#include "libgolf.h"
#include "shellcode.h"
int main(int argc, char **argv)
{
INIT_ELF(X86_64,64);
GEN_ELF();
return 0;
}
编译并运行生成的可执行文件会给你一个.bin文件 - 这就是你闪亮的新ELF!很简单,对吧?简单往往
伴随着枯燥,这里也是如此,所以让我们做些更有趣的事情!
在继续之前,值得解释一下这两个宏在幕后做了什么。首先,INIT_ELF()接受两个参数,ISA和架构。
目前,LibGolf支持X86_64、ARM32和AARCH64作为有效的ISA,以及32或64作为架构。它首先设置一些
内部簿记结构,并决定是使用Elf32_*还是Elf64_*对象作为头部。它还自动分配指向ELF和程序头的
指针,分别称为ehdr和phdr。我们将使用这些来轻松修改字段。除此之外,它还复制shellcode缓冲区,
并在计算合理的入口点之前填充ELF和程序头。接下来是GEN_ELF(),它只是将一些漂亮的统计信息
打印到stdout,然后将适当的结构写入.bin文件。.bin的名称由argv[0]决定。
所以,在我们使用INIT_ELF()宏之后,我们可以解引用ehdr和phdr。假设我们想修改ELF头的e_version
字段。我们只需要添加一行:
#include "libgolf.h"
#include "shellcode.h"
int main(int argc, char **argv)
{
INIT_ELF(X86_64);
// 将e_version设置为12345678
ehdr->e_version = 0x78563412;
GEN_ELF();
return 0;
}
再次快速编译和执行,你就会得到另一个.bin文件。在xxd、hexyl或你喜欢的二进制操作工具中查看
这个文件,你会看到一个漂亮的'12 34 56 78'从偏移量0x14开始。很容易,不是吗?
为了让事情进展得更快,我喜欢使用以下Makefile:
.PHONY golf clean
CC=gcc
CFLAGS=-I.
PROG=golf
golf:
@$(CC) -o $(PROG) $(PROG).c
@./$(PROG)
@chmod +x $(PROG).bin
@rm $(PROG) $(PROG).bin
(这是你在仓库[0]中找到的Makefile)
+-----------------------------------+
|--[ 第一个障碍 ]--|
+-----------------------------------+
众所周知,文件解析器是可怕的东西。虽然规范通常有诚恳的目标,但它们很少被那些应该更了解的人
所尊重。这些亵渎者中最主要的就是Linux ELF加载器本身。LibGolf使我们很容易调查这些违反elf.h
的行为的程度。
一个好的开始是从头开始,也就是ELF头。在任何ELF文件的开始,当然是熟悉的0x7f后跟ELF,对它的
朋友来说被称为EI_MAG0到EI_MAG3。不出所料,修改这四个字节中的任何一个都会导致Linux加载器
拒绝该文件。谢天谢地!
那么字节0x5呢?我们可靠的规范告诉我们,这是EI_CLASS字节,表示目标架构。可接受的值是0x01和
0x02,分别用于32位和64位。我再说一遍:可接受的值是0x01和0x02。如果我们将其设置为0x58
(或对ASCII爱好者来说是"X")会怎样?我们可以通过添加以下内容来实现:
(ehdr->e_ident)[EI_CLASS] = 0x58;
到我们的生成C文件中。(为什么是0x58?它在xxd/hexyl输出中显示得很清楚!)
一旦我们得到了.bin文件可以玩,在尝试执行它之前,让我们试试其他几个熟悉的ELF解析工具,看看
还有哪些罪魁祸首。列表中的第一个是gdb。去试试吧,我等着。看看会发生什么?
"not in executable format: file format not recognized"
同样,objdump也会给你类似的答案。看来这些解析器在正确地完成它们的工作。现在,让我们尝试
正常运行二进制文件。
<spoiler>它完美运行。</spoiler>
如果你使用我的示例shellcode,那么查询$?会遗憾地告诉你二进制文件成功退出。当设置EI_DATA和
EI_VERSION为非法值时,也会发生同样的罪行。
+---------------------------------------+
|--[ 将损坏程度提升到11 ]--|
+---------------------------------------+
那么,我们能走多远?Linux加载器会忽略多少ELF和程序头?我们已经讨论了EI_CLASS、EI_DATA和
EI_VERSION,但事实证明EI_OSABI也可以安全地被忽略。这带我们到了偏移量0x8。根据规范,接下来
是EI_ABIVERSION和EI_PAD,它们一起带我们到字节0xf。看来没人关心它们,所以我们可以毫无顾虑
地将它们全部设置为0x58。
继续前进,我们遇到了一个似乎不能被破坏的字段:e_type。可以理解,如果我们不告诉Linux加载器
我们提供的是什么类型的ELF文件,它就不会喜欢(很高兴知道它确实有*一些*标准!- 双关语)。
我们需要这两个字节保持为0x0002(或对elf.h信徒来说是ET_EXEC)。接下来是另一个挑剔的字节,
在熟悉的0x12偏移处:e_machine,它指定目标ISA。就我们而言,通过将X86_64指定为INIT_ELF()的
第一个参数,LibGolf已经为我们将这个字节填充为0x3e。
突然,出现了一个野生的e_version!我们面对另一个异端,它理应总是字节0x00000001。然而,在
实践中,似乎没有人关心,所以让我们用0x58585858填充它。
在这串异教徒之后,我们有几个似乎不能被滥用的重要字段:e_entry和e_phoff。我想我不需要详细
解释e_entry;它是二进制文件的入口点,一旦可加载段被加载到内存中,执行就会在这里开始。虽然
人们可能期望加载器能够在不知道程序头偏移量的情况下管理,但似乎它不够聪明,需要被喂食。
最好让这两个保持原样。
LibGolf还不支持节头(考虑到它专注于生产*小型*二进制文件,将来可能也不太可能支持它们)。
这意味着,面对与它们相关的任何头部,我们可以随心所欲地修改。这包括e_shoff、e_shentsize、
eh_shnum甚至e_shstrndx。如果我们没有任何节头,我们就不用对破坏它们负责!
剩下的对Linux加载器似乎有一些重要性的字段是e_ehsize、e_phentsize和e_phnum。这也不足为奇,
因为它们与将唯一的可加载段加载到内存中并移交控制权有关。如果你需要复习,e_ehsize是ELF头的
大小(对于32位和64位分别是0x34或0x40),eh_phentsize是即将到来的程序头的大小(同样,对于
32位和64位架构硬编码为0x20或0x38)。如果加载器对EI_CLASS更挑剔一点,它就不需要这两个字段。
最后,e_phnum只是程序头中的条目数 - 对我们来说总是0x1。毫无疑问,这用于内存加载例程中的
某个循环,但我还没有进一步调查。
ELF头中还有一个我没有提到的字段,就是e_flags。原因很简单,因为它是架构相关的。对于x86_64,
它完全无关紧要,因为它是未定义的(尽管对某些ARM平台来说它*确实*很重要!看看[0]中的arm32
示例)。
这就到了ELF头的结尾。对于那些没有计数的人来说,超过50%的ELF头被加载器忽略。但是程序头
呢?事实证明,程序头的可操作空间要少得多,但不是因为人们可能期望的原因。实际上,程序头的
*任何*损坏都不会真正影响Linux加载器。我们可以用我们信任的0x58填充整个东西,加载器一点也
不会在意。但要小心,大胆的冒险者,摆弄错误的字节,你就会被扔进段错误的地牢!
那么,在程序头中有什么可以被强制修改的吗?事实证明,有两个字段由于自身的原因,现在已经
不再相关了:p_paddr和p_align。前者在虚拟内存之前的辉煌时代很重要,那时4GB RAM只是孩子的
白日梦,因此告诉加载器在物理内存中的哪里加载段是很重要的。
内存对齐是一个有趣的问题。据说,p_vaddr应该等于p_offset模除p_align。"正常的"ELF文件
(至少是用GCC编译的)似乎只是将p_offset设置为等于p_vaddr然后继续。这也是LibGolf默认做的,
这使得p_align完全多余!
总的来说,不如ELF头那么有趣,但仍然有一些小收获。现在二进制生成的C文件看起来是这样的:
#include "libgolf.h"
#include "shellcode.h"
int main(int argc, char **argv)
{
INIT_ELF(X86_64,64);
/* 让我们破坏一些字段! */
(ehdr->e_ident)[EI_CLASS] = 0x58;
(ehdr->e_ident)[EI_DATA] = 0x58;
(ehdr->e_ident)[EI_VERSION] = 0x58;
(ehdr->e_ident)[EI_OSABI] = 0x58;
(ehdr->e_ident)[EI_ABIVERSION] = 0x58;
memset(&((ehdr->e_ident)[EI_PAD]), 0x58, 7);
ehdr->e_version = 0x58585858;
ehdr->e_shoff = 0x58585858;
ehdr->e_flags = 0x58585858;
ehdr->e_shentsize = 0x5858;
ehdr->e_shnum = 0x5858;
ehdr->e_shstrndx = 0x5858;
/* 程序头也可以被破坏 */
phdr->p_paddr = 0x5858585858585858;
phdr->p_align = 0x5858585858585858;
GEN_ELF();
return 0;
}
如果你编译并运行这个程序,你会得到以下二进制文件:
00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858 .ELFXXXXXXXXXXXX
00000010: 0200 3e00 5858 5858 7800 4000 0000 0000 ..>.XXXXx.@.....
00000020: 4000 0000 0000 0000 5858 5858 5858 5858 @.......XXXXXXXX
00000030: 5858 5858 4000 3800 0100 5858 5858 5858 XXXX@.8...XXXXXX
00000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................
00000050: 0000 4000 0000 0000 5858 5858 5858 5858 ..@.....XXXXXXXX
00000060: 0700 0000 0000 0000 0700 0000 0000 0000 ................
00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05 XXXXXXXX.<H1...
这个文件大小为127字节,但我们能够用'X'替换总共50个字节,这意味着这个二进制文件中有将近40%
的内容被Linux ELF加载器忽略!谁知道你能用这50个字节做什么?
事实证明 - 可以做很多事。几年前netspooky的一些令人惊叹的研究展示了如何将程序头的部分内容
堆叠到ELF头中。结合将你的shellcode存储在这些死字节区域之一中,以及其他一些巧妙的技巧,
可以将ELF缩小到仅84字节 - 比LibGolf目前最好的成果还要减少34%。我建议你去看看他在[1]上
发表的精彩的"ELF Mangling"系列文章。
这些技术的另一个有趣方面很容易被忽视。虽然Linux加载器似乎很少关心ELF的结构,除了它需要
获取机器码的部分之外,但其他工具要挑剔得多。我们已经看过objdump和gdb,但很多防病毒解决
方案在面对格式错误的ELF时也会崩溃。在我的研究中,唯一(某种程度上)做对的产品是ClamAV,
它会给出"Heuristics.Broken.Executable"的正面结果。当然,动态分析仍然是任何人的赌注。
+----------------------+
|--[ 未来展望 ]--|
+----------------------+
x86_64不是LibGolf支持的唯一ISA!你也可以用它为ARM32和AARCH64平台构建小型可执行文件。
在GitHub[0]的仓库中,你会找到两个ARM平台的一些示例(包括本文中的死字节示例)。
但是示例算什么!希望大多数读到这里的人都想看看libgolf.h本身。正如我在开始时提到的,
这整个项目最初是作为一个学习练习,所以我特别注意尽可能详细地注释所有内容。
+---------------------------------+
|--[ 关于可重现性的说明 ]--|
+---------------------------------+
在整个研究过程中,我主要在内核版本为5.4.0-65-generic的Ubuntu 20.04上进行测试,但也
验证了在5.11.11-arch1-1上可以获得相同的结果。我听说在WSL内核上有时会发生奇怪的事情,
但我还没有调查过 - 也许你可以试试!
+----------------+
|--[ 致谢 ]--|
+----------------+
特别向Thugcrowd、Symbolcrash和Mental ELF Support Group的所有人问好!
+------------------+
|--[ 参考文献 ]--|
+------------------+
[0] https://www.github.com/xcellerator/libgolf
[1] https://n0.lol/ebm/1.html
~ xcellerator
</pre></div></center></body></html>