利用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 argcchar *argv[], char *envp[])
{
   pid_t  child = 0;
   long   addr  = 0argaddr = 0;
   int    status = 0i = 0arc = 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 == -10"fork");
   IF(child == 0proc_child(args[0], args));
   MSG("child pid = %d\r\n"child);
   while(1)
  {
       wait(&status);
       if(WIFEXITED(status))
           break;
       // 获取寄存器中的值,并将其保存在regs中
       ptrace(PTRACE_GETREGSchildNULL®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_PEEKTEXTchildaddrNULL);
           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_PEEKTEXTchildaddr + (i * sizeof(void*)), NULL);
               data.val = ptrace(PTRACE_PEEKTEXTchildargaddrNULL);
               MSG("src: ubp_av[%d]: %s\r\n"idata.chars);
               MSG("dst: upb_av[%d]: %s\r\n"iargs[i]);
               //修改参数指针指向的内容,demo暂时不支持超过7个字符的参数
               strncpy(data.charsargs[i], sizeof(long- 1);
               ptrace(PTRACE_POKETEXTchildargaddrdata.val);
          }
           ptrace(PTRACE_CONTchildNULLNULL);
           ptrace(PTRACE_DETACHchildNULLNULL);
           break;
      }
       //调用一下 ptrace(PTRACE_SINGLESTEP) 就能完成这样的事情,这个调用会告诉内核,在子进程每执行完一条子令之后,就停一下
       ptrace(PTRACE_SINGLESTEPchildNULLNULL);
  }
   return 0;
}

static char *encryptedarg = "3abb6677af34ac57c0ca5828fd94f9d886c"
"26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523"
"eed7511e5a9e4b8ccb3a4686";

int proc_child(const char *pathchar *argv[])
{
   int i = 1;
   ptrace(PTRACE_TRACEME0NULLNULL);
   for(i = 1;argv[i!= NULL;i ++)
       argv[i= encryptedarg;
   anonyexec(pathargv);
   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=59success=yes exit=0 a0=7fff5c378750 a1=7fff5c3788d0 a2=0 a3=7fff5c3781a0items=2 ppid=58893 pid=58894 auid=1000 uid=1000 gid=1000 euid=1000suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts3 ses=1 comm="3"exe=2F6D656D66643A656C66202864656C6574656429subj=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:00obj=unconfined_u:object_r:user_tmp_t:s0 objtype=NORMALcap_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:00obj=system_u:object_r:ld_so_t:s0 objtype=NORMAL cap_fp=0000000000000000cap_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 == -10"fork");
IF(child == 0proc_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 *pathchar *argv[])
{
   int i = 1;
   ptrace(PTRACE_TRACEME0NULLNULL);
   for(i = 1;argv[i!= NULL;i ++)
       argv[i= encryptedarg;
   anonyexec(pathargv);
   return 0;
}

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

1.path:/bin/ls

2.*argv[]:char *args[] = { "/bin/ls", "-a", "-l", NULL };经过for循环之后,path和argv变为了:

3.path:/bin/ls

4.*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_GETREGSchildNULL&regs);
if(regs.rip == entry)
{
  .....
  • uint64_t entry = elfentry(args[0]); 其中的args[0]就是/bin/ls,所以entry其实就是得到/bin/lsentry

  • ptrace(PTRACE_GETREGS, child, NULL, &regs); 获取child进程的寄存器的值,并将其保存到&regs中,&regs是一个user_regs_struct类型的结构体

  • if(regs.rip == entry)这个的含义就是判断如果判断当前的执行的进程如果是正在执行/bin/ls,则进入到下面的处理流程中

获取参数个数

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

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_PEEKTEXTchildaddr + (i * sizeof(void*)), NULL);
   data.val = ptrace(PTRACE_PEEKTEXTchildargaddrNULL);
   MSG("src: ubp_av[%d]: %s\r\n"idata.chars);
   MSG("dst: upb_av[%d]: %s\r\n"iargs[i]);
   //修改参数指针指向的内容,demo暂时不支持超过7个字符的参数
   strncpy(data.charsargs[i], sizeof(long- 1);
   ptrace(PTRACE_POKETEXTchildargaddrdata.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是不是一个唯一解呢?

以上就是蓝域白帽破解网为您分享的内容,如果您喜欢 记得转发+关注!

版权保护: 本文由admin所发布,转载请保留本文链接: http://www.yemogege.cn/nxxg-jswz/408.html

免责声明:蓝域安全网所发布的一切渗透技术视频文章,逆向脱壳病毒分析教程,以及相关实用源码仅限用于学习和研究目的
请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除! 

侵权删帖/违法举报/投稿等事物联系邮箱:yemogege@vip.qq.com 网站地图