原文:https://blog.rapid7.com/2019/01/03/santas-elfs-running-linux-executables-without-execve/

这篇博客是年度的12 Days of HaXmas系列博客中的第11篇。

Executable and Linkable Format ( ELF ) 是许多类Unix操作系统(如Linux,大多数现代BSD和Solaris)的基础。ELF文件有许多技巧,比如我们在our *nix Meterpreter implementation中使用的那些,但那些技巧需要使用我们的特殊 toolchain 或带有-static-pie标志的GCC 8+来构建每个可执行文件。如果环境不同怎么办呢?内核加载和运行代码时并不需要磁盘上的文件,下面我们看下如何在没有execve的情况下运行Linux可执行文件。

手工制作镜像

也许最有效的可执行格式技巧是反射加载(reflective loading)反射加载是后渗透阶段的一种重要技术,用于逃避检测和在受限制的环境中执行复杂命令。 它通常有三个广泛的步骤:

  1. 使代码执行(如:利用漏洞或网络钓鱼)
  2. 从某个地方抓取自己的代码
  3. 使操作系统运行你自己的代码(但是不像普通进程那样加载)

最后一步是反射加载的关键。 环境限制越来越多,普通的进程启动方式目标非常明显。 传统的防病毒会扫描磁盘上的文件,新进程启动时会进行代码签名完整性检查,行为监控会不断检查以确保进程没有恶意行为。 背后的思想是,如果攻击者无法运行任何程序,他们就无法做任何事情,系统也是安全的。这不完全正确,阻挡常见的攻击路径只是让事情变得更加困难。

特别是,对于使用可移植可执行(PE)格式的Windows环境,我们已经看到过很多关于这个主题的研究,因为Windows使用广泛,而且操作系统中内置了非常有用的反射构造模块。 事实上,它具有这类工作的黄金标准API:CreateRemoteThread及其他相关API,这不仅仅允许攻击者加载代码,而且可以将代码注入其他正在运行的进程。

尽管缺乏如Windows中的CreateRemoteThread这样有趣的反射注入API,最近几年我们依然看到了一些有趣的研究,通过非常规的方法来使以开发者中心的Linux特性为安全所用。比如这是一个很好的命令行途径的案例:https://blog.sektor7.net/#!res/2018/pure-in-memory-linux.md。以及其他一些技术,这些技术需要工具中所带的辅助文件,如:https://github.com/gaffe23/linux-inject。更现代的技术是可以利用一些新的syscalls从一个辅助二进制或者脚本语言来执行代码:https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html。

Linux上的这些方法可以分为五类:
· 写入临时文件: 这与典型代码没有太大区别,但它不会留下磁盘成品文件。
· 注入ptrace这需要一些精巧的控制,比如经典的process-hopping
· 自修改可执行文件:dd是经典之作,它需要一些比较小的定制shellcode。
· 脚本语言中集成的FFI:Python和Ruby都可以,当前技术只能加载shellcode。
· 创建非文件系统的临时文件:这使用2014年添加的系统调用,因此它即将广泛使用。

严格的工作条件

很少有人密切关注他们的Linux机箱(如果你这样做了,那么恭喜!),但这些技术风格除了上面提到的各自的技术挑战外,还有安全/操作方面的考虑。 如果您处于安全对抗中的防守方,可以考虑用以下方式阻止远程攻击者:

临时文件

越来越常见的是查找/tmp/dev/shm挂载了noexec(即,该树中没有文件可以执行),特别是在移动和嵌入式系统上。 说到嵌入式系统,那些嵌入式系统通常也只有只读的持久文件存储,所以你甚至无法回退来阻止别人查看磁盘。

自修改可执行文件和ptrace

访问ptrace/proc/<PID>中的大多数有趣的自检由kernel.yama.ptrace_scope sysctl变量控制。在未用于开发的盒子上,应将其设置至少为2以删除非特权用户的访问权限。 这在许多移动和嵌入式系统上是默认的,而现代桌面/服务器发行版默认至少为1,这降低了它在跨进程中疯狂跳跃的实用性。 此外,它是特定用于Linux,所以没有可爱的BSD shells。

FFI(Foreign Function Interface)集成

RubyfiddlePythonctypes特别灵活,并且很多小东西都可以灵活的像解释型的C语言一样运行。但是,它们没有汇编程序,所以任何使用寄存器或引导到不同可执行文件的细节都需要使用shellcode来完成。 您也永远不知道目标系统会安装哪个版本的环境。

非文件系统的临时文件

同样是Linux特有的技巧。我能调用一对新的syscalls,它们组合起来可以绕过任何noexec标志(内核4.19.10测试通过)。 第一个系统调用是memfd_create(2),在内核3.17版本中添加,它分配一个具有默认权限的新临时文件系统,并在其中创建一个文件,除/proc目录,该文件不会显示在其他任何已挂载的文件系统中。第二个系统调用是execveat(2),在内核版本3.19中添加。它可以采用文件描述符并将其传递给内核以供执行。缺点是创建的文件可以很容易地通过find /proc/*/fd -lname '/memfd:*'找到,因为所有的memfd_create(2)文件都表示为具有常量前缀的符号链接。这个功能在普通软件中很少使用,我仅找到过一个这种用法的普通程序,那是在2016年被添加到PulseAudio中的。

回到过去,一个(Runtime)链接

不过这里有一个很大的瓶颈:要运行其他程序,所有这些技术都使用标准的execve(2)(最后一种情况调用的也是相关的execveat(2))。定制的SELinux配置文件或syscall审计的存在可以很容易进行阻拦,并且在最近的强制启用SELinux的Android版本中,确实是这样。
还有另一种叫做用户空间exec或ul_exec的技术; 然而它模仿了内核在执行execve期间是如何初始化进程的,并将控制权移交给运行时链接器:ld.so(8)。 这是该领域最早的技术之一,由grugq开创,它因为与以上技术相比较为繁琐而从未被推行过。 针对x86_64有一个优化和重写的版本,但是它实现了自己的标准库,因此很难扩展并且无法在现代的堆溢出保护环境下编译。

Linux的世界与15年前这种技术首次发布时的情况截然不同了。 现在,默认有40多位地址空间和位置独立的可执行文件,将execve进程拼凑在一起更为直接。 当然,我们对堆栈地址进行硬编码并不能保证90%以上的可用率,但程序也不再依赖于它的常量,所以我们几乎可以把它放在任何地方并且少用内存地址。 Linux环境现在也比2004年更加普遍、有价值并且受到更受保护,因此在某些情况下,这种努力还是值得的。

过程

根据执行方法和环境,模拟execve的工作可能有两个要求。 首先,我们需要页面对齐的内存分配以及在填充后使内存可执行的能力。 因为它需要JIT、内置的library加载器dlopen(3)以及一些DRM实现,所以很难从系统中完全删除。 然而,SELinux可以限制可执行内存的分配,而一些像Android这样的自包含平台使用这些限制可以很好地控制未经批准的浏览器或DRM库。 接下来,我们要求能够任意跳入所述内存。 例如在C或shellcode中,它确实需要脚本语言中的一个完整的外部函数调用接口(FFI),排除非XS的Perl实现。

grugq详述的过程以一种微妙而有趣的方式发生了改变。 即使开发已经全部完成,但整体的步骤还是一样的。 现代GNU / Linux用户空间的一个安全特性是它不太关心特定的内存地址,这使我们可以更灵活地实现我们的其他类型的安全特性。内核在辅助向量中传递给Runtime还有更多的提示,可以查找到原始数据,但大多数程序在大多数操作系统架构中都可以使用简单的方法。

工具

许多工具的没落都是因为过时了,特别是在开源和安全领域。 execve模拟器没有其他方法了出现了,大家对两个ul_exec的实现也几乎没有兴趣,(据我所知)也很少有这方面的更新。

Linux Meterpreter的实现目前不支持execute命令中常见的-m选项,这个选项在Windows上以合法的程序作为幌子,并完全在内存中执行恶意命令。使用这个和一个回退技术或者以上两个技术将为我们提供在内存运行上传的文件的功能,以及一些小窍门。例如:映射额外的合法文件,或在在将控制权交给上传的可执行文件之前更改进程名称,可以完全复制-m。一个很好的副作用是,这也将使构建和分发插件更容易,因为它们将不再需要在构建时制作成内存映像。

为了实现这一点,我正在创建一个与我们的Linux Meterpreter共存的共享链接库mettle。它不依赖于任何Meterpreter代码,但默认情况下,它将使用其toolchain进行构建。它是免费的,可以打包成你想要的任何后渗透手法。如果您有任何问题或建议,请查看pull request

这里有一个示例,可以通过strace进行跟踪分析。使用该库。 为了减少普通跟踪调试相关输出的干扰,我们只使用%process跟踪表达式来仅查看与进程生命周期相关的调用,如forkexecveexit。 在x86_64上,%process表达式还获取了arch_prctl系统调用,该调用仅用于x86_64,仅用于设置线程本地存储。 execve来自strace启动可执行文件,第一对arch_prctl调用来自库初始化。直到目标库启动自己的一对arch_prctl调用并打印出我们的消息。

$ strace -e trace=%process ./noexec $(which cat) haxmas.txt
execve("./noexec", ["./noexec", "/usr/bin/cat", "haxmas.txt"], 0x7ffdcbdf0bc0 /* 23 vars */) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffa750dd20) = -1 EINVAL (Invalid argument)
arch_prctl(ARCH_SET_FS, 0x7f17ca7db540) = 0
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffa750dd20) = -1 EINVAL (Invalid argument)
arch_prctl(ARCH_SET_FS, 0x7f17ca7f3540) = 0
Merry, HaXmas!
exit_group(0)                           = ?
+++ exited with 0 +++

结束

有了Mettle的这个新库,我们希望提供一种长期,隐秘的方法,可以在攻陷的Linux机器上可靠地加载程序。 如果您有任何问题或建议,请查看我的pull request的过程,我们* nix的payload,或在Slack添上加我们

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖