We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ptrace 系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像 (包括寄存器) 的值。其基本原理是: 当使用了 ptrace 跟踪后,所有发送给被跟踪的子进程的信号 (除了 SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为 TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
ptrace 是如此的强大,以至于有很多大家所常用的工具都基于 ptrace 来实现。
ptrace 可以实时监测和修改另一个进程的运行,它是如此的强大以至于曾经因为它在 unix-like 平台 (如 Linux, *BSD) 上产生了各种漏洞。
所以,今天我要跟大家介绍的是在不使用 ptrace 的情况下获得代码注入。由于使用此方法不需要任何系统调用 (sys call),因此使用一种简单且无所不在的语言来完成代码注入是可能的。
在不使用 ptrace 的情况下获得代码注入,就允许用户执行任意的本机代码,当只有标准的 Bash shell 和 coreutils 可用时,就可以制作一个从内存中执行二进制的有效载荷绕过 noexecmountflag(挂载命令)。
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 代码。
本文翻译自https://blog.gdssecurity.com/labs/2017/9/5/linux-based-inter-process-code-injection-without-ptrace2.html,如若转载,请注明原文地址: http://www.4hou.com/technology/7614.html
https://zhuanlan.zhihu.com/p/29264608
The text was updated successfully, but these errors were encountered:
No branches or pull requests
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 文件看到:
其中,栈包含返回地址 (在不使用 “链接寄存器” 存储返回地址的架构上,例如 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上找到,不过,为了限制这个脚本的外部依赖,我做出了一些努力,因为在一些非常受限制的环境中,实用程序二进制文件可能不可用。当前的依赖性列表是:
该脚本的一般流程是在后台启动 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 代码。
https://zhuanlan.zhihu.com/p/29264608
The text was updated successfully, but these errors were encountered: