无需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

本文来自合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。

原文链接

时间: 2024-08-17 14:47:01

无需Ptrace就能实现Linux进程间代码注入的相关文章

Linux进程间通讯-IPC详解

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的.而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同.前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了"system V IPC",通信进程局限在单个计算机内:后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制.Linux则把两者继承了下来,如图示: 其中,最初Unix IPC包括:

Linux进程间的关系

Linux的进程相互之间有一定的关系.比如说,在Linux进程基础中,我们看到,每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构.我们在这里讲解进程组和会话,以便以更加丰富的方式了管理进程. 1. 进程组 (process group) 每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程.进程组会有一个进程组领导进程 (process group leader),领导进程的PID (PID见Linux进程基础)成为进程组的ID (proce

Linux 进程间通讯共享内存方式

共享内存方式:从物理内存里面拿出来一部分作为多个进程共享. 共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入数据,共享这个内存的所有进程都可以立即看到其中内容. 共享内存实现步骤: 一.创建共享内存,使用shmget函数. 二.映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数. 创建共享内存shmget: intshmget(key_t key, size_t size, int shmflg) 功能:得到一个共享内存标识符或创建一个共享内存对象并

关于linux使用动态库进行进程间通讯

问题描述 关于linux使用动态库进行进程间通讯 各位: 两个进程间通过动态库的方式如何进行参数的传递? 我首先在一个库中做了如下的内容: #include ""caculate.h""#include ""stdio.h""int iShare; #pragma data_seg (""shareddate"")int iShareInSeg = 1;#pragma data_seg#

Linux进程及进程通讯

一.进程特点及关键字. 1. 基础 [概念]一个具有一定独立功能的程序的一次运行. [特点]动态性,并发性,独立性,异步性. [状态]就绪,执行,阻塞. 2. 几个关键字 [PID/PPID]标示进程的唯一数字,PPID特指父进程ID. [UID]启动进程用户. 3. 临界资源与临界区 [临界资源]资源只允许指定数量的进程同时访问. [临界区]进程中访问临界资源的代码. [进程同步]并发进程按照一定顺序执行的过程. 4. 进程调度--按照一定的算法从一组待运行的进程中选出一个来占有CPU运行.

php进程间通讯实例分析_php技巧

本文实例讲述了php进程间通讯的方法.分享给大家供大家参考,具体如下: php单进程单线程处理批量任务太慢了,受不鸟了,但是php不能多线程,最终选择了多进程处理批量任务. php多进程主要使用for进行分裂,然后利用的unix/linux的信号量进行进程间通讯. 本例使用的是:生产者=>消费者=>收集器,的模式. <?php // ===== 全局变量 ===== // ipc进程间通讯 $key = ftok(__FILE__, "a"); $queue = ms

Linux 基于IPC机制实现进程间的共享内存处理

今天学习了相关于IPC(InterProcess Communication ,进程间通信)的相关知识.就做个笔记,一来让大家检查一下我的理解方面是不是有错误,二来也为了能让更多的博友们了解到相关的知识吧. IPC的种类 IPC 的种类,一般来说下面两种使用的较多: - 共享"内存" - 消息传递 下来我们就分别的介绍一下相关的信息吧. 共享内存 字面意思的理解是采用共享一块计算机中的内存空间来实现的进程之间的通信的一种方式.也就是说这块内存区域驻留在生成共享内存段进程的地址空间.(是

Linux进程通信(IPC)方式简介

linux下进程间通信的几种主要方式:管道(pipe)和有名管道(FIFO).信号(signal).消息队列.共享内存(shared memory).信号量(semaphore).套接字(socket),本文对这些做简单介绍 进程间通信的目的 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间. 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(

线程及 进程间的通信问题! .

一个很好的编程随想的博客http://program-think.blogspot.com/2009/03/producer-consumer-pattern-0-overview.html 架构设计:生产者/消费者模式[0]:概述  1.如何确定数据单元2.队列缓冲区3.环形缓冲区4.双缓冲区 生产 消费 2010-06-01 10:13   #include <boost/thread/thread.hpp> #include <boost/thread/mutex.hpp>