2.替换任意程序读取或写入的文本

1. 内核态 eBPF 代码

1.1. 源代码

代码 replace.bpf.c

// SPDX-License-Identifier: BSD-3-Clause
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "replace.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, size_t);
    __type(value, unsigned int);
} map_fds SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, size_t);
    __type(value, long unsigned int);
} map_buff_addrs SEC(".maps");

#define MAX_POSSIBLE_ADDRS 500
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, MAX_POSSIBLE_ADDRS);
    __type(key, unsigned int);
    __type(value, long unsigned int);
} map_name_addrs SEC(".maps");
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, MAX_POSSIBLE_ADDRS);
    __type(key, unsigned int);
    __type(value, long unsigned int);
} map_to_replace_addrs SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 5);
    __type(key, __u32);
    __type(value, __u32);
} map_prog_array SEC(".maps");

const volatile int target_ppid = 0;

const volatile int filename_len = 0;
const volatile char filename[50];

const volatile  unsigned int text_len = 0;
const volatile char text_find[FILENAME_LEN_MAX];
const volatile char text_replace[FILENAME_LEN_MAX];

SEC("tp/syscalls/sys_exit_close")
int handle_close_exit(struct trace_event_raw_sys_exit *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (check == 0) {
        return 0;
    }

    bpf_map_delete_elem(&map_fds, &pid_tgid);
    bpf_map_delete_elem(&map_buff_addrs, &pid_tgid);

    return 0;
}

SEC("tp/syscalls/sys_enter_openat")
int handle_openat_enter(struct trace_event_raw_sys_enter *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    if (target_ppid != 0) {
        struct task_struct *task = (struct task_struct *)bpf_get_current_task();
        int ppid = BPF_CORE_READ(task, real_parent, tgid);
        if (ppid != target_ppid) {
            return 0;
        }
    }

    char check_filename[FILENAME_LEN_MAX];
    bpf_probe_read_user(&check_filename, filename_len, (char*)ctx->args[1]);

    for (int i = 0; i < filename_len; i++) {
        if (filename[i] != check_filename[i]) {
            return 0;
        }
    }

    unsigned int zero = 0;
    bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY);

    bpf_printk("[TEXT_REPLACE] PID %d Filename %s\n", pid, filename);
    return 0;
}

SEC("tp/syscalls/sys_exit_openat")
int handle_openat_exit(struct trace_event_raw_sys_exit *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (check == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;

    unsigned int fd = (unsigned int)ctx->ret;
    bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY);

    return 0;
}

SEC("tp/syscalls/sys_enter_read")
int handle_read_enter(struct trace_event_raw_sys_enter *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    unsigned int* pfd = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (pfd == 0) {
        return 0;
    }

    unsigned int map_fd = *pfd;
    unsigned int fd = (unsigned int)ctx->args[0];
    if (map_fd != fd) {
        return 0;
    }

    long unsigned int buff_addr = ctx->args[1];
    bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &buff_addr, BPF_ANY);

    size_t buff_size = (size_t)ctx->args[2];
    bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_addr 0x%lx\n", pid, fd, buff_addr);
    bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_size %lu\n", pid, fd, buff_size);
    return 0;
}

SEC("tp/syscalls/sys_exit_read")
int find_possible_addrs(struct trace_event_raw_sys_exit *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int buff_addr = *pbuff_addr;
    long unsigned int name_addr = 0;
    if (buff_addr <= 0) {
        return 0;
    }

    if (ctx->ret <= 0) {
        return 0;
    }
    long int buff_size = ctx->ret;
    unsigned long int read_size = buff_size;

    bpf_printk("[TEXT_REPLACE] PID %d | read_size %lu | buff_addr 0x%lx\n", pid, read_size, buff_addr);
    char local_buff[LOCAL_BUFF_SIZE] = { 0x00 };

    if (read_size > (LOCAL_BUFF_SIZE+1)) {
        read_size = LOCAL_BUFF_SIZE;
    }

    unsigned int tofind_counter = 0;
    for (unsigned int i = 0; i < loop_size; i++) {
        bpf_probe_read(&local_buff, read_size, (void*)buff_addr);
        for (unsigned int j = 0; j < LOCAL_BUFF_SIZE; j++) {
            if (local_buff[j] == text_find[0]) {
                name_addr = buff_addr+j;
                bpf_map_update_elem(&map_name_addrs, &tofind_counter, &name_addr, BPF_ANY);
                tofind_counter++;
            }
        }

        buff_addr += LOCAL_BUFF_SIZE;
    }

    bpf_printk("[TEXT_REPLACE] PID %d | tofind_counter %d \n", pid, tofind_counter);

    bpf_tail_call(ctx, &map_prog_array, PROG_01);
    return 0;
}

size_t my_strlen(const char *str) {
    size_t len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

static inline int my_bpf_strncmp(const char *s1, size_t len, const char *s2)
{
    for (size_t i = 0; i < 3; i++)
    {
        if (s1[i] != s2[i])
            return 1;
    }

    return 0;
}
SEC("tp/syscalls/sys_exit_read")
int check_possible_addresses(struct trace_event_raw_sys_exit *ctx) {
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int* pName_addr = 0;
    long unsigned int name_addr = 0;
    unsigned int newline_counter = 0;
    unsigned int match_counter = 0;

    char name[text_len_max+1];
    unsigned int j = 0;
    char old = 0;
    const unsigned int name_len = text_len;
    if (name_len < 0) {
        return 0;
    }
    if (name_len > text_len_max) {
        return 0;
    }
    for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) {
        newline_counter = i;
        pName_addr = bpf_map_lookup_elem(&map_name_addrs, &newline_counter);
        if (pName_addr == 0) {
            break;
        }
        name_addr = *pName_addr;
        if (name_addr == 0) {
            break;
        }
        bpf_probe_read_user(&name, text_len_max, (char*)name_addr);
        int key = 0;
        for (j = 0; j < text_len_max && text_find[j] != '\0' && name[j] != '\0'; j++) {
            if (name[j] != text_find[j]) {
                key = 1;
                break;
           }
        }
        if (key==0){
            bpf_map_update_elem(&map_to_replace_addrs, &match_counter, &name_addr, BPF_ANY);
            match_counter++;
        }
        bpf_map_delete_elem(&map_name_addrs, &newline_counter);
        bpf_printk("[TEXT_REPLACE] PID %d | [*] name: %s\n", pid, name);
    }

    if (match_counter > 0) {
           bpf_printk("[TEXT_REPLACE] PID %d | match_counter %d \n", pid, match_counter);
        bpf_tail_call(ctx, &map_prog_array, PROG_02);
    }
    return 0;
}

SEC("tp/syscalls/sys_exit_read")
int overwrite_addresses(struct trace_event_raw_sys_exit *ctx) {
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, &pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int* pName_addr = 0;
    long unsigned int name_addr = 0;
    unsigned int match_counter = 0;

    for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) {
        match_counter = i;
        pName_addr = bpf_map_lookup_elem(&map_to_replace_addrs, &match_counter);
        if (pName_addr == 0) {
            break;
        }
        name_addr = *pName_addr;
        if (name_addr == 0) {
            break;
        }

        long ret = bpf_probe_write_user((void*)name_addr, (void*)text_replace, text_len);
        struct event *e;
        e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
        if (e) {
            e->success = (ret == 0);
            e->pid = pid;
            bpf_get_current_comm(&e->comm, sizeof(e->comm));
            bpf_ringbuf_submit(e, 0);
        }
        bpf_printk("[TEXT_REPLACE] PID %d | [*] replaced: %s\n", pid, text_find);
        
        bpf_map_delete_elem(&map_to_replace_addrs, &match_counter);
    }

    return 0;
}

1.2. 代码分析

1.2.1. 代码段1

// Map 保存 openat 系统调用中的文件描述符
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, size_t);
    __type(value, unsigned int);
} map_fds SEC(".maps");

// Map 保存 read 系统调用中的缓冲区地址
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, size_t);
    __type(value, long unsigned int);
} map_buff_addrs SEC(".maps");

SEC("tp/syscalls/sys_exit_close")
int handle_close_exit(struct trace_event_raw_sys_exit *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    // 没在Map中找到说明不是我们要关注的进程,就放行
    unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (check == 0) {
        return 0;
    }

    bpf_map_delete_elem(&map_fds, &pid_tgid);
    bpf_map_delete_elem(&map_buff_addrs, &pid_tgid);

    return 0;
}
Info

该 eBPF 程序在系统调用退出时执行,很适合做收尾、清理数据工作的, map_fds 中存储着 进程ID 对 文件描述符 的映射, map_buff_addrs 中存储着 进程ID 对 数据缓冲区地址 的映射,该进程打开文件-读取文件 完成后就会触发 handle_close_exit 该 eBPF 程序,清除该进程相关信息(文件描述符/文件内容)。

1.2.2. 代码段2

#define FILENAME_LEN_MAX 50

SEC("tp/syscalls/sys_enter_openat")
int handle_openat_enter(struct trace_event_raw_sys_enter *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    // 如果指定了目标进程ID,就只关注该进程,否则就关注所有进程
    if (target_ppid != 0) {
        struct task_struct *task = (struct task_struct *)bpf_get_current_task();
        int ppid = BPF_CORE_READ(task, real_parent, tgid);
        if (ppid != target_ppid) {
            return 0;
        }
    }

    // 从参数中获取文件名
    char check_filename[FILENAME_LEN_MAX];
    bpf_probe_read_user(&check_filename, filename_len, (char*)ctx->args[1]);

    // 检查文件名是不是我们的目标
    for (int i = 0; i < filename_len; i++) {
        if (filename[i] != check_filename[i]) {
            return 0;
        }
    }

    // 将这个我们关注的进程ID(键)存储在 map_fds 映射中,值暂时赋值为0
    unsigned int zero = 0;
    bpf_map_update_elem(&map_fds, &pid_tgid, &zero, BPF_ANY);

    bpf_printk("[TEXT_REPLACE] PID %d Filename %s\n", pid, filename);
    return 0;
}

handle_openat_enter 该 eBPF 程序是在某系统调用(进程打开文件)发生时执行,作用就是记录哪个进程打开了我们的目标文件

1.2.3. 代码段3

SEC("tp/syscalls/sys_exit_openat")
int handle_openat_exit(struct trace_event_raw_sys_exit *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    // 没在Map中找到说明不是我们要关注的进程,就放行
    unsigned int* check = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (check == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;

    // 将映射值设置为返回的文件描述符
    unsigned int fd = (unsigned int)ctx->ret;
    bpf_map_update_elem(&map_fds, &pid_tgid, &fd, BPF_ANY);

    return 0;
}

handle_openat_exit 该 eBPF 程序在进程执行完系统调用 openat 后,将返回的文件描述符更新到一个名为 map_fds 的 eBPF Map 中,以进程的 PID 作为键。如果在 Map 中找不到对应的 PID,则说明该进程不是我们关注的进程,直接返回。如果找到了,则将返回的文件描述符更新到 Map 中。

1.2.4. 代码段4

SEC("tp/syscalls/sys_enter_read")
int handle_read_enter(struct trace_event_raw_sys_enter *ctx)
{
    // 没在Map中找到说明不是我们要关注的进程,就放行
    size_t pid_tgid = bpf_get_current_pid_tgid();
    int pid = pid_tgid >> 32;
    unsigned int* pfd = bpf_map_lookup_elem(&map_fds, &pid_tgid);
    if (pfd == 0) {
        return 0;
    }

    // 检查上面查找获取的文件描述符是不是正确的
    unsigned int map_fd = *pfd;
    unsigned int fd = (unsigned int)ctx->args[0];
    if (map_fd != fd) {
        return 0;
    }

    // 进程ID为键,将缓冲区地址为值存储在 Map 中
    long unsigned int buff_addr = ctx->args[1];
    bpf_map_update_elem(&map_buff_addrs, &pid_tgid, &buff_addr, BPF_ANY);

    size_t buff_size = (size_t)ctx->args[2];
    bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_addr 0x%lx\n", pid, fd,buff_addr);
    bpf_printk("[TEXT_REPLACE] PID %d | fd %d | buff_size %lu\n", pid, fd,buff_size);
    return 0;
}

handle_read_enter 该 eBPF 程序是在某系统调用(进程读取文件)发生时执行。在系统调用 read 中,数据会被读取到应用程序提供的缓冲区中,缓冲区地址表示文件读取的数据存储的内存地址

1.2.5. 代码段5

#define LOCAL_BUFF_SIZE 64
#define loop_size 64
#define FILENAME_LEN_MAX 50
const volatile char text_find[FILENAME_LEN_MAX];

SEC("tp/syscalls/sys_exit_read")
int find_possible_addrs(struct trace_event_raw_sys_exit *ctx)
{
    // 检查是不是我们关注的进程(是的话在handle_read_enter中存储了对应的缓冲区地址)
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs, 
&pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int buff_addr = *pbuff_addr;
    long unsigned int name_addr = 0;
    if (buff_addr <= 0) {
        return 0;
    }

    // 从读取系统调用返回的数据量
    if (ctx->ret <= 0) {
        return 0;
    }
    long int buff_size = ctx->ret;
    unsigned long int read_size = buff_size;

    bpf_printk("[TEXT_REPLACE] PID %d | read_size %lu | buff_addr 0x%lx\n", pid,read_size, buff_addr);
    char local_buff[LOCAL_BUFF_SIZE] = { 0x00 };
    // 将读取每一块的数据大小限制在LOCAL_BUFF_SIZE内
    if (read_size > (LOCAL_BUFF_SIZE+1)) {
        read_size = LOCAL_BUFF_SIZE;
    }

    // 读取以块为单位返回的数据,并记下"查找"文本的第一个字符的每个实例
    unsigned int tofind_counter = 0;
    // 第一层循环指定了迭代次数。loop_size是一个常量,定义了外部循环的次数
    for (unsigned int i = 0; i < loop_size; i++) {
        // 从缓冲区读取数据块,读取大小为read_size
        bpf_probe_read(&local_buff, read_size, (void*)buff_addr);
        // 内部循环,遍历local_buff数组中的每个字符
        for (unsigned int j = 0; j < LOCAL_BUFF_SIZE; j++) {
            // 检查local_buff数组中的每个字符是否与要查找的文本的第一个字符相匹配
            if (local_buff[j] == text_find[0]) {
                // 如果匹配成功,将当前字符的地址记录下来,并将该地址更新到一个eBPF映射中,以便后续处理
                name_addr = buff_addr+j;
                bpf_map_update_elem(&map_name_addrs, &tofind_counter, &name_addr,BPF_ANY);
                tofind_counter++;
            }
        }
        // 下一次迭代中读取下一个数据块
        buff_addr += LOCAL_BUFF_SIZE;
    }
    bpf_printk("[TEXT_REPLACE] PID %d | tofind_counter %d \n", pid,tofind_counter);
    // 尾调用 check_possible_addrs 以循环访问可能的地址
    // map_prog_array 是编号与eBPF 程序的指针的映射
    // 其中 PROG_01 对应着 check_possible_addresses
    bpf_tail_call(ctx, &map_prog_array, PROG_01);
    return 0;
}

find_possible_addrs 该 eBPF 程序是在某系统调用(进程读取文件)结束时执行。从系统调用返回的数据分块,并循环(第一层循环)读取每一块数据块,在某个数据块中再次循环(第二层循环)搜索要查找的文本的第一个字符的位置,并记录该位置保存在 Map 中

1.2.6. 代码段6

const volatile  unsigned int text_len = 0;
#define text_len_max 20
#define MAX_POSSIBLE_ADDRS 500

SEC("tp/syscalls/sys_exit_read")
int check_possible_addresses(struct trace_event_raw_sys_exit *ctx) {
    // 检查是不是我们关注的进程(是的话在handle_read_enter中存储了对应的缓冲区地址)
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs,&pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int* pName_addr = 0;
    long unsigned int name_addr = 0;
    unsigned int newline_counter = 0;
    unsigned int match_counter = 0;

    char name[text_len_max+1];
    unsigned int j = 0;
    char old = 0;
    const unsigned int name_len = text_len;
    if (name_len < 0) {
        return 0;
    }
    if (name_len > text_len_max) {
        return 0;
    }
    // 仔细检查每一个可能的位置,检查它是否真的与要查找的文本匹配
    for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) {
        newline_counter = i;
        pName_addr = bpf_map_lookup_elem(&map_name_addrs, &newline_counter);
        if (pName_addr == 0) {
            break;
        }
        name_addr = *pName_addr;
        if (name_addr == 0) {
            break;
        }
        bpf_probe_read_user(&name, text_len_max, (char*)name_addr);
        int key = 0;
        for (j = 0; j < text_len_max && text_find[j] != '\0' && name[j] != '\0';j++) {
            if (name[j] != text_find[j]) {
                key = 1;
                break;
           }
        }
        if (key==0){
            bpf_map_update_elem(&map_to_replace_addrs, &match_counter,&name_addr, BPF_ANY);
            match_counter++;
        }
        bpf_map_delete_elem(&map_name_addrs, &newline_counter);
    }

    // 找到至少一个与要查找的文本的匹配项,就跳到 PROG_02 对应的eBPF程序中覆盖文本
    if (match_counter > 0) {
        bpf_tail_call(ctx, &map_prog_array, PROG_02);
    }
    return 0;
}

overwrite_addresses 该 eBPF 程序是在某系统调用(进程读取文件)结束时执行,也就是上一个eBPF 程序最后进行尾调用跳转的地方。和上一个 eBPF 程序相辅相成,上一个程序细致的匹配某一个字符片段和要查找的文本是否匹配,本程序则是将那个字符片段替换成我们想要替换的字符串。

1.2.7. 代码段7

SEC("tp/syscalls/sys_exit_read")
int overwrite_addresses(struct trace_event_raw_sys_exit *ctx) {
    // 检查是不是我们关注的进程(是的话在handle_read_enter中存储了对应的缓冲区地址)
    size_t pid_tgid = bpf_get_current_pid_tgid();
    long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buff_addrs,&pid_tgid);
    if (pbuff_addr == 0) {
        return 0;
    }
    int pid = pid_tgid >> 32;
    long unsigned int* pName_addr = 0;
    long unsigned int name_addr = 0;
    unsigned int match_counter = 0;

    // 在每个地址上循环以将查找的文本替换
    for (unsigned int i = 0; i < MAX_POSSIBLE_ADDRS; i++) {
        match_counter = i;
        pName_addr = bpf_map_lookup_elem(&map_to_replace_addrs, &match_counter);
        if (pName_addr == 0) {
            break;
        }
        name_addr = *pName_addr;
        if (name_addr == 0) {
            break;
        }
        long ret = bpf_probe_write_user((void*)name_addr, (void*)text_replace,text_len);
        struct event *e;
        e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
        if (e) {
            e->success = (ret == 0);
            e->pid = pid;
            bpf_get_current_comm(&e->comm, sizeof(e->comm));
            bpf_ringbuf_submit(e, 0);
        }
        bpf_printk("[TEXT_REPLACE] PID %d | [*] replaced: %s\n", pid, text_find);
        
        bpf_map_delete_elem(&map_to_replace_addrs, &match_counter);
    }

    return 0;
}

overwrite_addresses 该 eBPF 程序是在某系统调用(进程读取文件)结束时执行,也就是上一个eBPF 程序昀后进行尾调用跳转的地方。和上一个 eBPF 程序相辅相成,上一个程序细致的匹配某一个字符片段和要查找的文本是否匹配,本程序则是将那个字符片段替换成我们想要替换的字符串

1.3. 运行展示

我们执行命令

./replace -f /proc/net/tcp -i"0100007F" -r "08080808"

任何读取 /proc/net/tcp 文件的返回内容中的 0100007F 替换成 08080808
netstat 命令也是读取这个文件用户态代码,这就实现了一个网络伪造的功能

Pasted image 20250420130926

2. 用户态代码

和上面的几个用户态代码其实是一模一样的,特别标准化,这也是用 libbpf-bootstrap 脚手架的好处