diff --git a/README.md b/README.md index 007c8c87..08b942d0 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,40 @@ ![language](https://shields.io/github/languages/top/chriskalix/HIDS-Linux) -Hades 是一款运行在 Linux 下的 HIDS,目前还在开发中。支持内核态(ebpf)以及用户态(cn_proc)的事件进程采集。其中借鉴了非常多的代码和思想(from meituan, Elkeid, tracee) +Hades 是一个基于 eBPF 的主机入侵检测系统,同时兼容低版本下通过 Netlink 进行事件审计。 -## 架构设计以及引擎 +项目借鉴了 [Tracee](https://github.com/aquasecurity/tracee) 以及 [Elkeid](https://github.com/bytedance/Elkeid) 中的代码以及思想 -> 注: Agent 部分基本参照 Elkeid 1.7 部分重构, eBPF 部分借鉴 Elkeid 思路,tracee 等 +## Hades 架构图 -### 架构图 +> 注: Agent 部分基本参照 Elkeid 1.7 部分重构。后续考虑插件全部能兼容至 `Elkeid` 项目下 -![data](https://github.com/chriskaliX/HIDS-Linux/blob/main/imgs/agent.png) +### Agent 部分 -### 数据处理 +![data](https://github.com/chriskaliX/HIDS-Linux/blob/main/imgs/agent.png) -> Agent 字段连接公司对应的 cmdb,做初步扩展。之后走入 Flink CEP 做初步的节点数据清洗。打入 HIVE 时根据情况,也可再做一次清洗减小性能消耗。清洗过后的数据走入第二个 Flink CEP 以及规则引擎,HIDS 的规则部分其实较为头疼,是一个 HIDS 能否用好的关键所在,后续会把自己的想法逐步开源 +### 数据处理流程 ![data](https://github.com/chriskaliX/HIDS-Linux/blob/main/imgs/data_analyze.png) -## 目前阶段 +## 插件列表 -用户态基本完成,eBPF 进行中, 目前 execve 字段全部采集完毕, 包括进程树, envp, cwd... +- [Driver-eBPF](https://github.com/chriskaliX/Hades/tree/main/plugin/driver/eBPF) +- [Collector](https://github.com/chriskaliX/Hades/tree/main/plugin/collector) +- HoneyPot +- Monitor +- Scanner +- Logger -目前在重要的字段下先对齐 Elkeid, 还有一些纰漏, 慢慢的修复 +## 目前进展 -![data](https://github.com/chriskaliX/HIDS-Linux/blob/main/imgs/examples.png) +支持 `13` 种 Hook,涵盖大部分安全审计检测需求 ## 开发计划 > 记录一些方案选择, 目前进度等,另外 golang 1.18 上线啦~ [官方 Tutorial](https://golang.google.cn/doc/tutorial/generics),后续会开始多试试 Generics -### 插件交互 +### Agent-插件 交互 > Linux 进程间通信的[方式](https://www.linuxprobe.com/linux-process-method.html) @@ -76,15 +81,14 @@ Hades 是一款运行在 Linux 下的 HIDS,目前还在开发中。支持内 - [x] channel 消费无上限, 过多会导致 ringbuffer full, 自带 drop - [x] 过 Prctl 部分, 字节只 hook PR_SET_NAME,考虑添加 PR_SET_MM - [x] (100%)第一轮 review 修改进行中. 使用 ebpfmanager 重构了一下. memfd_create 添加, LSM bind 函数 ipv6 添加, 有个小的问题: json 效率和 inline - - [ ] eBPF uprobe(openjdk/readline)... + - [x] eBPF uprobe(openjdk/readline)... (对于OpenJDK)的观测稍微延后, 需要作为一个可配置模块 - [x] 面向对象, ebpfmanager review 使用 - [x] eBPF 进程监控 - [x] socket 下完全支持 ipv6, 字段丰富 EXE 完成(跟之前一样, 无 lock 操作, 可能有读错的问题) - [ ] 整理 ebpf 初版, 预备 release version - [ ] (20%)code review tracee 函数 get_path_str, 本周完成与 fsprobe 的方式对比以及原理, 更新在 private repo, 到时候写个小文章 - [x] 目前非 CO-RE, 后续支持 - - [ ] ehids 下有个 JVM Hook 的文章, 2022 年 3 月份内 go through , 最好能实现 rmi 等 hook - - [ ] 在 4 月份左右会完成 CO-RE 的兼容, 同时会开始编写配套的 BPF Rootkit(读 cfc4n 师傅有感, 另外盘古实验室的[文章](https://www.pangulab.cn/post/the_bvp47_a_top-tier_backdoor_of_us_nsa_equation_group/)好像提到了 BPF 作用于通信隐藏, 改正:内核版本太低了, 不会是 XDP... 新的任务是稍微看一下 Linux 网络协议这一块(源码级别)后续会有笔记放开) + - [x] ehids 下有个 JVM Hook 的文章, 2022 年 3 月份内 go through , 最好能实现 rmi 等 hook - [x] [cd00r.c](https://github.com/ehids/rootkit-sample)这个 2000 的 backdoor 稍微看了一下,以及对应的 pdf。本质上新颖的地方在于不会暴露端口,libpcap 的模式来监听 knock, 看起来和 tcpdump 一样, 上述盘古文章里的后门里,这个应该是很小的一环。cd00r.c 是用户态的一个 demo,如果整合进来做成 rootkit 也挺好。 - [ ] 完成轮询交互 - [x] Agent 端 HTTPS 心跳 & 配置检测 @@ -99,10 +103,9 @@ Hades 是一款运行在 Linux 下的 HIDS,目前还在开发中。支持内 ## Other +- [Linux RootKit初窥(一)IDT](https://chriskalix.github.io/2022/03/19/linux-rootkit%E5%88%9D%E7%AA%A5-%E4%B8%80-idt) - [阿里云 Rootkit 检测产品 Simple Doc](https://help.aliyun.com/document_detail/194087.html?spm=5176.24320532.content1.3.7389ece6Exy34X) ## 交流群 - - diff --git a/plugin/driver/eBPF/kernel/include/define.h b/plugin/driver/eBPF/kernel/include/define.h index 7d57dcc7..cbaf38f4 100644 --- a/plugin/driver/eBPF/kernel/include/define.h +++ b/plugin/driver/eBPF/kernel/include/define.h @@ -1,5 +1,6 @@ #ifndef __DEFINE_H #define __DEFINE_H +#ifndef CORE #include #include #include @@ -17,6 +18,10 @@ #include #include #include +#else +#include +#endif + #include "bpf_helpers.h" #include "bpf_core_read.h" @@ -25,7 +30,6 @@ #define MAX_PERCPU_BUFSIZE (1 << 14) #define MAX_STRING_SIZE 256 // Same with Elkeid, but it's larger in tracee or other project #define MAX_STR_ARR_ELEM 32 -#define MAX_PID_LEN 7 // (up to 2^22 = 4194304) #define MAX_PATH_COMPONENTS 20 #define MAX_BUFFERS 3 @@ -33,7 +37,7 @@ #define SUBMIT_BUF_IDX 0 #define STRING_BUF_IDX 1 -#define EXECVE_GET_SOCK_FD_LIMIT 12 +#define EXECVE_GET_SOCK_FD_LIMIT 8 #define EXECVE_GET_SOCK_PID_LIMIT 4 /* ========== MAP MICRO DEFINATION ========== */ @@ -69,12 +73,11 @@ typedef struct data_context { u64 ts; // timestamp u64 cgroup_id; // cgroup_id - u32 uts_inum; // in Elkeid, they use pid_inum and root_pid_inum. TODO: go through this + u32 pns; // in Elkeid, they use pid_inum and root_pid_inum. TODO: go through this u32 type; // type of struct u32 pid; // processid u32 tid; // thread id u32 uid; // user id - u32 euid; // effective user id u32 gid; // group id u32 ppid; // parent pid => which is tpid, pid is for the kernel space. In user space, it's tgid actually u32 sessionid; @@ -155,7 +158,8 @@ BPF_PERF_OUTPUT(net_events, 1024); BPF_PERCPU_ARRAY(bufs, buf_t, 3); BPF_PERCPU_ARRAY(bufs_off, u32, MAX_BUFFERS); -/* read micro */ +// CORE, just like in tracee +#ifndef CORE #define READ_KERN(ptr) \ ({ \ typeof(ptr) _val; \ @@ -163,6 +167,17 @@ BPF_PERCPU_ARRAY(bufs_off, u32, MAX_BUFFERS); bpf_probe_read((void *)&_val, sizeof(_val), &ptr); \ _val; \ }) +#define GET_FIELD_ADDR(field) &field +#else +#define READ_KERN(ptr) \ + ({ \ + typeof(ptr) _val; \ + __builtin_memset((void *)&_val, 0, sizeof(_val)); \ + bpf_core_read((void *)&_val, sizeof(_val), &ptr); \ + _val; \ + }) +#define GET_FIELD_ADDR(field) __builtin_preserve_access_index(&field) +#endif /* array related function */ static __always_inline buf_t *get_buf(int idx) @@ -189,8 +204,6 @@ static inline struct mount *real_mount(struct vfsmount *mnt) return container_of(mnt, struct mount, mnt); } -#define GET_FIELD_ADDR(field) &field - /* hook point id */ // TODO: gather all and update #define SYS_ENTER_PTRACE 164 diff --git a/plugin/driver/eBPF/kernel/include/hades_net.h b/plugin/driver/eBPF/kernel/include/hades_net.h index 8e5020d9..6db4e491 100644 --- a/plugin/driver/eBPF/kernel/include/hades_net.h +++ b/plugin/driver/eBPF/kernel/include/hades_net.h @@ -17,7 +17,6 @@ int kprobe_security_socket_connect(struct pt_regs *ctx) struct sockaddr *address = (struct sockaddr *)PT_REGS_PARM2(ctx); if (!address) return 0; - sa_family_t sa_fam = READ_KERN(address->sa_family); if ((sa_fam != AF_INET) && (sa_fam != AF_INET6)) { diff --git a/plugin/driver/eBPF/kernel/include/utils.h b/plugin/driver/eBPF/kernel/include/utils.h index cdd1c64a..274c67d4 100644 --- a/plugin/driver/eBPF/kernel/include/utils.h +++ b/plugin/driver/eBPF/kernel/include/utils.h @@ -1,14 +1,20 @@ #ifndef __UTILS_H #define __UTILS_H -#include "bpf_helpers.h" -#include "bpf_core_read.h" -#include "bpf_endian.h" +#ifndef CORE #include #include #include #include #include #include +#include +#else +#include +#endif + +#include "bpf_helpers.h" +#include "bpf_core_read.h" +#include "bpf_endian.h" // TODO: 后期改成动态的 /* R3 max value is outside of the array range */ @@ -21,10 +27,8 @@ /* init_context */ static __always_inline int init_context(context_t *context, struct task_struct *task) { - // 获取 timestamp - struct task_struct *realparent; - bpf_probe_read(&realparent, sizeof(realparent), &task->real_parent); - bpf_probe_read(&context->ppid, sizeof(context->ppid), &realparent->tgid); + struct task_struct *realparent = READ_KERN(task->real_parent); + context->ppid = READ_KERN(realparent->tgid); context->ts = bpf_ktime_get_ns(); // 填充 id 相关信息 u64 id = bpf_get_current_uid_gid(); @@ -34,24 +38,25 @@ static __always_inline int init_context(context_t *context, struct task_struct * context->pid = id; context->tid = id >> 32; context->cgroup_id = bpf_get_current_cgroup_id(); - // 读取 cred 下, 填充 id - struct cred *cred; - // 有三个 ptracer_cred, real_cred, cred, 看kernel代码即可 - bpf_probe_read(&cred, sizeof(cred), &task->real_cred); - bpf_probe_read(&context->euid, sizeof(context->euid), &cred->euid); // 容器相关信息 // Elkeid - ROOT_PID_NS_INUM = task->nsproxy->pid_ns_for_children->ns.inum; // namespace: https://zhuanlan.zhihu.com/p/307864233 - struct nsproxy *nsp; - struct uts_namespace *uts_ns; + struct nsproxy *nsp = READ_KERN(task->nsproxy); + struct uts_namespace *uts_ns = READ_KERN(nsp->uts_ns); + struct pid_namespace *pid_ns = READ_KERN(nsp->pid_ns_for_children); // nodename - bpf_probe_read(&nsp, sizeof(nsp), &task->nsproxy); - bpf_probe_read(&uts_ns, sizeof(uts_ns), &nsp->uts_ns); bpf_probe_read_str(&context->nodename, sizeof(context->nodename), &uts_ns->name.nodename); // pid_namespace - bpf_probe_read(&context->uts_inum, sizeof(context->uts_inum), &uts_ns->ns.inum); + context->pns = READ_KERN(pid_ns->ns.inum); + // For root pid_namespace, it's not that easy in eBPF. The way that we can get pid=1 task + // is to lookup (task->parent) recursively. And We should not do this for every time. (cache) + // Also, we should considered the situation that we can not get in the very first time + // since we used a bounded loop to get the root pid (The NUM 1 pid) + // Still, I think it's fine if we just get the root pid_namespace from usersapce. I do not + // catch the reason that Elkeid get this in root. // sessionid bpf_probe_read(&context->sessionid, sizeof(context->sessionid), &task->sessionid); + // This may changed since struct pid_cache_t *parent = bpf_map_lookup_elem(&pid_cache_lru, &context->pid); if (parent) bpf_probe_read_str(&context->pcomm, sizeof(context->pcomm), &parent->pcomm); diff --git a/plugin/driver/eBPF/userspace/decoder/decoder.go b/plugin/driver/eBPF/userspace/decoder/decoder.go index 5405f6ae..957f25dc 100644 --- a/plugin/driver/eBPF/userspace/decoder/decoder.go +++ b/plugin/driver/eBPF/userspace/decoder/decoder.go @@ -54,27 +54,26 @@ func (decoder *EbpfDecoder) ReadAmountBytes() int { func (decoder *EbpfDecoder) DecodeContext() (c *Context, err error) { offset := decoder.cursor - if len(decoder.buffer[offset:]) < 168 { + if len(decoder.buffer[offset:]) < 160 { err = fmt.Errorf("can't read context from buffer: buffer too short") return } ctx := NewContext() ctx.Ts = binary.LittleEndian.Uint64(decoder.buffer[offset : offset+8]) ctx.CgroupID = binary.LittleEndian.Uint64(decoder.buffer[offset+8 : offset+16]) - ctx.UtsInum = binary.LittleEndian.Uint32(decoder.buffer[offset+16 : offset+20]) + ctx.Pns = binary.LittleEndian.Uint32(decoder.buffer[offset+16 : offset+20]) ctx.Type = binary.LittleEndian.Uint32(decoder.buffer[offset+20 : offset+24]) ctx.Pid = binary.LittleEndian.Uint32(decoder.buffer[offset+24 : offset+28]) ctx.Tid = binary.LittleEndian.Uint32(decoder.buffer[offset+28 : offset+32]) ctx.Uid = binary.LittleEndian.Uint32(decoder.buffer[offset+32 : offset+36]) - ctx.EUid = binary.LittleEndian.Uint32(decoder.buffer[offset+36 : offset+40]) - ctx.Gid = binary.LittleEndian.Uint32(decoder.buffer[offset+40 : offset+44]) - ctx.Ppid = binary.LittleEndian.Uint32(decoder.buffer[offset+44 : offset+48]) - ctx.Sessionid = binary.LittleEndian.Uint32(decoder.buffer[offset+48 : offset+52]) - ctx.Comm = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+52:offset+68], "\x00")) - ctx.PComm = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+68:offset+84], "\x00")) - ctx.Nodename = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+84:offset+148], "\x00")) - ctx.RetVal = uint64(binary.LittleEndian.Uint64(decoder.buffer[offset+148 : offset+156])) - ctx.Argnum = uint8(binary.LittleEndian.Uint16(decoder.buffer[offset+156 : offset+168])) + ctx.Gid = binary.LittleEndian.Uint32(decoder.buffer[offset+36 : offset+40]) + ctx.Ppid = binary.LittleEndian.Uint32(decoder.buffer[offset+40 : offset+44]) + ctx.Sessionid = binary.LittleEndian.Uint32(decoder.buffer[offset+44 : offset+48]) + ctx.Comm = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+48:offset+64], "\x00")) + ctx.PComm = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+64:offset+80], "\x00")) + ctx.Nodename = helper.ZeroCopyString(bytes.Trim(decoder.buffer[offset+80:offset+144], "\x00")) + ctx.RetVal = uint64(binary.LittleEndian.Uint64(decoder.buffer[offset+144 : offset+152])) + ctx.Argnum = uint8(binary.LittleEndian.Uint16(decoder.buffer[offset+152 : offset+160])) decoder.cursor += int(ctx.GetSizeBytes()) c = ctx return diff --git a/plugin/driver/eBPF/userspace/decoder/event.go b/plugin/driver/eBPF/userspace/decoder/event.go index 9f212a0a..e24f9296 100644 --- a/plugin/driver/eBPF/userspace/decoder/event.go +++ b/plugin/driver/eBPF/userspace/decoder/event.go @@ -16,10 +16,6 @@ type Event interface { var eventMap map[uint32]Event = make(map[uint32]Event) func Regist(event Event) { - // if event.ID() != 2001 { - // return - // } - // fmt.Println(event.String(), " loaded!") eventMap[event.ID()] = event } diff --git a/plugin/driver/eBPF/userspace/decoder/protocol.go b/plugin/driver/eBPF/userspace/decoder/protocol.go index ec74de8c..613d70c9 100644 --- a/plugin/driver/eBPF/userspace/decoder/protocol.go +++ b/plugin/driver/eBPF/userspace/decoder/protocol.go @@ -15,23 +15,22 @@ func init() { } type Context struct { - Ts uint64 `json:"timestamp"` - CgroupID uint64 `json:"cgroupid"` - UtsInum uint32 `json:"utsinum"` - Type uint32 `json:"type"` - Pid uint32 `json:"pid"` - Tid uint32 `json:"tid"` - Uid uint32 `json:"uid"` - EUid uint32 `json:"euid"` - Gid uint32 `json:"gid"` - Ppid uint32 `json:"ppid"` - Sessionid uint32 `json:"sessionid"` - Comm string `json:"comm"` - PComm string `json:"pomm"` - Nodename string `json:"nodename"` - RetVal uint64 `json:"retval"` - Argnum uint8 `json:"argnum"` - _ [11]byte `json:"-"` + Ts uint64 `json:"timestamp"` + CgroupID uint64 `json:"cgroupid"` + Pns uint32 `json:"pns"` + Type uint32 `json:"type"` + Pid uint32 `json:"pid"` + Tid uint32 `json:"tid"` + Uid uint32 `json:"uid"` + Gid uint32 `json:"gid"` + Ppid uint32 `json:"ppid"` + Sessionid uint32 `json:"sessionid"` + Comm string `json:"comm"` + PComm string `json:"pomm"` + Nodename string `json:"nodename"` + RetVal uint64 `json:"retval"` + Argnum uint8 `json:"-"` + _ [7]byte `json:"-"` // added Sha256 string `json:"sha256"` Username string `json:"username"` @@ -42,7 +41,7 @@ type Context struct { } func (Context) GetSizeBytes() uint32 { - return 168 + return 160 } func (c *Context) SetEvent(event Event) {