ptrace系统调用
ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
ptrace是如此的强大,以至于有很多大家所常用的工具都基于ptrace来实现。
ptrace可以实时监测和修改另一个进程的运行,它是如此的强大以至于曾经因为它在unix-like平台(如Linux, *BSD)上产生了各种漏洞。
所以,今天我要跟大家介绍的是在不使用ptrace的情况下获得代码注入。由于使用此方法不需要任何系统调用(sys call),因此使用一种简单且无所不在的语言来完成代码注入是可能的。
在不使用ptrace的情况下获得代码注入,就允许用户执行任意的本机代码,当只有标准的Bash shell和coreutils可用时,就可以制作一个从内存中执行二进制的有效载荷绕过noexecmountflag(挂载命令)。
无需Ptrace的进程间代码注入
Linux上的/proc文件系统提供了对Linux系统运行的内省(Introspection),每个进程在文件系统中都有自己的目录,其中包含有关流程及其内部的详细信息。在这个目录中,有两个伪文件,分别是maps文件和mem文件。
maps文件包含分配给二进制文件的所有内存区域架构,以及所有包含的动态库。不过,这个信息现在相对敏感,因为每个库位置的偏移量是由ASLR随机化的。
mem文件提供了流程使用的完整内存空间的稀疏架构,结合从maps文件获得的偏移量,可以使用mem文件读取和写入进程的内存空间。如果偏移量是错误的,或者从开始位置按顺序读取文件,将返回读写错误,因为这相当于是读取不可访问的未分配内存。
译者注:内省(Introspection)是面向对象语言和环境的一个强大特性,它是对象揭示自己作为一个运行时对象的详细信息的一种能力。这些详细信息包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。
假定没有其他限制访问控制(如SELinux或AppArmor),这些目录中的文件的读写权限是由位于/proc/sys/kernel/yama中的ptrace_scope文件决定的。Linux内核提供了可以设置不同值的文档,比如,对于Linux进程间代码注入,就有两层设置。较低的安全性设置(0和1)允许在同一uid下的任何进程,或者只是父进程,分别写入进程/ proc/${PID}/ mem文件。这些设置中的任何一个都可以进行代码注入。而更安全的设置,2和3,将限制admin写入,或者完全禁止访问。目前,大多数主要操作系统默认设置为“1”,只允许进程的父进程写入其/ proc/${PID}/ mem文件。
这种代码注入方法要使用这些文件,并且这个过程的栈存储在一个标准内存区域内。这可以通过读取一个进程的maps文件看到:
$ grep stack /proc/self/maps 7ffd3574b000-7ffd3576c000 rw-p 00000000 00:00 0 [stack]
其中,栈包含返回地址(在不使用“链接寄存器”存储返回地址的架构上,例如ARM),因此函数知道返回地址后应在哪个位置继续执行。通常,在诸如缓冲区溢出之类的攻击中,栈是要被覆盖的,而ROP技术则会对目标过程进行控制。ROP技术是用攻击者控制的返回地址替换原始返回地址。这将允许攻击者在每次执行ret指令时通过控制执行流调用自定义函数或系统调用。
虽然此代码注入并不依赖于任何类型的缓冲区溢出,但我确实使用了一个ROP链。考虑到我获得的访问级别,我可以直接将栈写入/ proc/${PID}/ mem中。
因此,该方法使用/proc/self/maps文件来查找ASLR随机偏移量,从中我可以定位目标进程内的函数。使用这些函数地址,我可以替换当前栈上的正常返回地址,并获得进程的控制。为了确保在重写栈时,进程处于预期状态,我使用sleep命令作为被覆盖的从属进程。sleep指令会在系统调用中使用nanosleep,这意味着sleep指令将在几乎整个运行(不包括安装和拆卸)中使用相同的函数。这就使我有足够的机会在系统调用返回之前覆盖整个流程的栈,这样,我将控制我自定义的ROP指令片段(gadget)链。为了确保系统调用执行时栈指针的位置,我会将NOP sled作为载荷的前缀,这样,栈指针几乎就可以指向任何有效的位置,而这些位置在返回后,又会增加栈指针,直到它得到并执行我的有效载荷。
这些注入代码可以在https://github.com/GDSSecurity/Cexigua上找到,不过,为了限制这个脚本的外部依赖,我做出了一些努力,因为在一些非常受限制的环境中,实用程序二进制文件可能不可用。当前的依赖性列表是:
GNU grep(必须支持- fao -byte-offset)
dd(用于读取或写入到一个文件的绝对偏移量)
Bash(用于数学和其他高级脚本特性)
该脚本的一般流程是在后台启动sleep拷贝并记录其进程id(PID),如上所述,sleep命令是一个理想的注入对象,因为在整个运行期间它只执行一个函数,这意味着当覆盖栈时,我不会以意想不到的状态结束。使用这个进程,我就可以发现实例化时哪些库被加载。
使用/proc/${PID}/maps,我就可以尝试找到所有我需要的gadget。如果我在自动加载的库中找不到一个gadget,我将到/usr/lib的系统库中扩展我的搜索,如果我在其他库中找到该gadget,我就可以到下一个进程中使用LD_PRELOAD加载该库。这将使丢失的gadget用于我的载荷。除此之外,我还验证了我发现的gadget(使用一个纯粹的grep命令)也位于加载库的 .text部分。如果gadget不存在,那么它们就有可能在执行时未被加载到可执行内存中,当我试图返回到这个gadget时,就会导致运行崩溃。一句话,这个“预加载”阶段应该会导致包含从标准加载库中丢失的gadget的库的空列表。
一旦我确认所有的gadget都可以提供给我,那我就会启动另一个sleep进程。如果有必要的话,LD_PRELOAD额外的库。现在,我重新在库中找到这些gadget,然后将它们迁移到正确的ASLRbase,这样我就知道这些gadget在目标区域的内存空间中的位置,而不仅仅是在磁盘上的二进制文件。如上所述,我在提交使用它之前,会验证该gadget是否位于可执行内存区域。
我需要的gadget列表相对较短,对于以上的NOP sled,我需要一个NOP来填充所有要求函数调用的寄存器,以及一个用于调用标准函数的gadget。利用该函数组合,我就可以调用任何函数或系统调用,但不允许我执行任何类型的逻辑。一旦这些gadget被找到,我就可以将有效载荷描述文件中的伪指令转换成一个ROP有效载荷。例如,对于一个64位系统,line的“syscall 60 0”将转换为ROPgadget,将“60”加载到RAX寄存器、“0”到RDI,以及一个syscallgadget。这将产生40字节的数据,即3个地址和2个常量,总共8个字节。在执行时,这个系统调用将调用exit(0)。
我还可以调用PLT中的函数,包括从外部库导入的函数,例如glibc。为了定位这些函数的偏移量,它们是由指针而不是系统调用来调用的,所以我需要首先在目标库中解析ELF段头,以找到函数偏移量。一旦我有了偏移量,我就可以将这些设备重新定位,并将它们添加到我的载荷中。
除此之外,我还处理了字符串参数,因为我知道内存栈的位置,因此我可以将字符串附加到有效载荷,并在必要时添加指向它们的指针。例如,fexecvesyscall需要参数数组的char * *。在注入我的载荷之前,我可以生成指针数组,并在执行时将栈上的指针指向一个指针数组,以便将一个正常的栈分配char * *一起使用。
一旦有效载荷被完全序列化,我就可以使用dd在过程中覆盖栈,以及从/proc/${PID}/maps文件中获得栈的偏移量。为了确保我不会遇到任何权限问题,必须使用“exec dd”行来结束注入脚本,它用dd流程替换bash进程,因此将父进程的所有权限从bash转移到dd。
在栈被覆盖之后,我就可以等待sleep二进制程序返回的nanosleepsyscall,这时我的ROP链就获得了应用程序的控制权,载荷将被执行。
以ROP链被注入的特定载荷可以合理地避开一些运行时逻辑(runtime logic)。由于目前,我使用的有效载荷是一个简单的open/memfd_create/sendfile/fexecve程序。它将目标二进制文件与文件系统noexecmountflag分离,然后将二进制文件从内存中执行,绕过noexec限制。由于sleep二进制文件是由bash执行的,因此不可能与二进制文件交互,因为它在dd退出后没有父进程。为了绕过这个限制,可以使用在libfuse分布中存在的一个示例,假定fuse在目标系统上存在:passthrough二进制文件,那么将创建根文件系统的镜像挂载到目标目录。这个新的挂载不是挂载的noexec,因此可以到一个二进制文件浏览这个新的挂载,然后执行。
点此链接,你可以看到允许在当前目录中执行二进制文件是如何作为shell的标准子进程进行的有效载荷。
为了加快执行速度,在预加载和主运行之间缓存由其各自的ASLR base来缓存的gadget将是有用的。这可以通过使用声明-p向磁盘转储关联数组来实现,但是该方法不一定总是合适的。所以你还可以使用重新架构脚本,以在主bash进程的相同环境中执行有效载荷脚本,而不是使用$()执行的子进程。
通过取消对GNU grep的需求,可以进一步限制外部依赖关系。虽然在发现gadget时被认为太慢了,但是可能有更多的优化代码。
所以,这种技术的明显缓解策略是将ptrace_scope设置为一个更严格的值。虽然不能完全禁用系统上的ptrace,但是对于普通用户来说,是无法使用ptrace的,你可以通过向/etc/sysctl.conf添加kernel.yama.ptrace_scope=2来设置。
其他缓解策略包括Seccomp、SELinux或 Apparmor 的组合,以限制获取/proc/${PID}/map或/prop/${PID}/mem这样敏感文件的权限。另外,点击该链接获取Bash ROP和POC代码。