利用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文件中.代码如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#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 上面测试运行时,出现了如下的问题:

1
2
3
4
./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及其以下的内核都能够运行成功,成功运行的结果如下:

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
$ ./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监控,得到的结果如下:

1
2
3
4
5
6
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获取到的结果是:

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

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

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

1
2
3
4
5
6
7
8
{
"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首先定义了自己需要执行的实际的命令:

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

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

fork创建子进程

1
2
3
4
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).

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
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, &regs); 获取child进程的寄存器的值,并将其保存到&regs中,&regs是一个user_regs_struct类型的结构体
  • if(regs.rip == entry) 这个的含义就是判断如果判断当前的执行的进程如果是正在执行/bin/ls,则进入到下面的处理流程中

获取参数个数

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

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

修改参数值

1
2
3
4
5
6
7
8
9
10
11
12
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是不是一个唯一解呢?

参考