无需Ptrace就能实现Linux进程间代码注入

2023-11-28 12:10

本文主要是介绍无需Ptrace就能实现Linux进程间代码注入,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文讲的是 无需Ptrace就能实现Linux进程间代码注入

无需Ptrace就能实现Linux进程间代码注入

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代码。




原文发布时间为:2017年9月11日
本文作者:xiaohui
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。
原文链接

这篇关于无需Ptrace就能实现Linux进程间代码注入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/429675

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

Java Lettuce 客户端入门到生产的实现步骤

《JavaLettuce客户端入门到生产的实现步骤》本文主要介绍了JavaLettuce客户端入门到生产的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 目录1 安装依赖MavenGradle2 最小化连接示例3 核心特性速览4 生产环境配置建议5 常见问题

linux ssh如何实现增加访问端口

《linuxssh如何实现增加访问端口》Linux中SSH默认使用22端口,为了增强安全性或满足特定需求,可以通过修改SSH配置来增加或更改SSH访问端口,具体步骤包括修改SSH配置文件、增加或修改... 目录1. 修改 SSH 配置文件2. 增加或修改端口3. 保存并退出编辑器4. 更新防火墙规则使用uf

Java 的ArrayList集合底层实现与最佳实践

《Java的ArrayList集合底层实现与最佳实践》本文主要介绍了Java的ArrayList集合类的核心概念、底层实现、关键成员变量、初始化机制、容量演变、扩容机制、性能分析、核心方法源码解析、... 目录1. 核心概念与底层实现1.1 ArrayList 的本质1.1.1 底层数据结构JDK 1.7

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代