android ebpf捕获https数据包

ecapture

ebpf利用uprobe/uretprobe可以hook用户层函数,通过对https SSL层的SSL_writeSSL_read进行hook可以拦截明文数据信息。大佬的开源项目:ecapturehttps://github.com/gojue/ecapture,编译参考:https://blog.seeflower.dev/archives/172/

ecapture默认只对使用了系统路径中的ssl库(libssl.so,libgnutls.so等)的进行hook,如果软件使用自带的ssl库就需要通过--libssl指定库路径。这里自编译openssl和curl静态库并链接到自己的so中进行https请求,这里编译的时候保留openssl中的符号。使用ecapture指定so库路径可以正常抓取https明文信息。

但是如果在编译的时候去除openssl中的符号程序是无法自动找到SSL_writeSSL_read等函数的偏移的,需要自己找到对应的符号偏移并指定UAddress

BCC脚本

这里在bcc环境下编写脚本尝试hookssl_write_internalssl_read_internal,首先找到openssl去除了符号后的SSL_writeSSL_read。查看openssl源码,SSL_write在函数头部会设置错误码,参数的值是ERR_raise(20,271)

ERR_raise(20,271)对应的函数调用指令为MOV W0, #0x14 MOV W1, #0x10F,对应的机器码为80 02 80 52 E1 21 80 52

对应的so中搜索二进制特征码,找到了几个函数。

经过筛选后得到ssl_write_internalssl_read_internal的偏移分别为0x1ADC380x1AD5C0

编写bcc脚本对ssl_write_internalssl_read_internal进行hook

import argparse
from bcc import BPF
from bcc.utils import printb

bpf_text='''
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#define MAX_SSL_PACKAGE_SIZE 16384
struct ssl_write_entry_info {
    u32 pid;
    u64 time;
    u32 size;
    char comm[TASK_COMM_LEN];
    char buf[MAX_SSL_PACKAGE_SIZE];
};

struct ssl_read_entry_info {
    u32 pid;
    u64 time;
    u64 buf_address;
    u64 readBytes_address;
};

struct ssl_read_ret_info {
    u32 pid;
    u64 time;
    u32 size;
    char comm[TASK_COMM_LEN];
    char buf[MAX_SSL_PACKAGE_SIZE];
};

BPF_ARRAY(write_entry_info, struct ssl_write_entry_info, 1);
BPF_PERF_OUTPUT(ssl_write_events);
//int ssl_write_internal(SSL *s, const void *buf, size_t num, size_t *written)
int pre_ssl_write_internal(struct pt_regs *ctx) {
    u32 zero = 0;
    struct ssl_write_entry_info *p_info = write_entry_info.lookup(&zero);
    if(p_info == 0){
        return 0;
    }
    
    u64 pid_tid = bpf_get_current_pid_tgid();
    p_info->pid = pid_tid >> 32;
    p_info->time = bpf_ktime_get_ns();

    if(bpf_get_current_comm(&p_info->comm, sizeof(p_info->comm)) != 0){
        return 0;
    }

    p_info->size = PT_REGS_PARM3(ctx);
    if(bpf_probe_read_user(&p_info->buf, min((size_t)p_info->size, (size_t)sizeof(p_info->buf)), (void*)PT_REGS_PARM2(ctx)) < 0){
        return 0;
    }
    ssl_write_events.perf_submit(ctx, p_info, sizeof(struct ssl_write_entry_info));
    return 0;
}

//key : pid_tid
BPF_HASH(read_entry_info, u64, struct ssl_read_entry_info);
int pre_ssl_read_internal(struct pt_regs *ctx) {
    struct ssl_read_entry_info _info = {};
    u64 pid_tid = bpf_get_current_pid_tgid();
    _info.pid = pid_tid >> 32;
    _info.time = bpf_ktime_get_ns();
    _info.buf_address = PT_REGS_PARM2(ctx);
    _info.readBytes_address = PT_REGS_PARM4(ctx);
    read_entry_info.update(&pid_tid, &_info);
    return 0;
}

BPF_ARRAY(read_ret_info, struct ssl_read_ret_info, 1);
BPF_PERF_OUTPUT(ssl_read_events);
//int ssl_read_internal(SSL *s, void *buf, size_t num, size_t *readbytes)
int post_ssl_read_internal(struct pt_regs *ctx) {
    if(PT_REGS_RC(ctx) == 0xffffffff){
        return 0;
    }

    u32 zero = 0;
    struct ssl_read_ret_info *p_ret_info = read_ret_info.lookup(&zero);
    if(p_ret_info == 0){
        return 0;
    }

    u64 pid_tid = bpf_get_current_pid_tgid();
    struct ssl_read_entry_info* p_entry_info;
    p_entry_info = read_entry_info.lookup(&pid_tid);
    if(p_entry_info == 0){
        return 0;
    }
    
    u32 readBytes;
    if(bpf_probe_read_user(&readBytes, sizeof(readBytes), (void*)p_entry_info->readBytes_address) < 0){
        return 0;
    }

    p_ret_info->pid = p_entry_info->pid;
    p_ret_info->time = p_entry_info->time;
    p_ret_info->size = readBytes;
    if(bpf_probe_read_user(&p_ret_info->buf, min((size_t)readBytes, (size_t)sizeof(p_ret_info->buf)), (void*)p_entry_info->buf_address) < 0){
        return 0;
    }

    if(bpf_get_current_comm(&p_ret_info->comm, sizeof(p_ret_info->comm)) != 0){
        return 0;
    }

    ssl_read_events.perf_submit(ctx, p_ret_info, sizeof(struct ssl_read_ret_info));
    read_entry_info.delete(&pid_tid);
    return 0;
}
'''
# Get args
class HexIntAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, int(values, 16))

parser = argparse.ArgumentParser(description="file relocate by bcc")
parser.add_argument("--pid", required=True, type=int, help="process pid")
parser.add_argument("--libssl", required=True, type=str, help="ssl so path")
parser.add_argument("--write", required=True, type=str, action=HexIntAction, help="ssl_write_internal offset")
parser.add_argument("--read", required=True, type=str, action=HexIntAction, help="ssl_read_internal offset")
args = parser.parse_args()

# load BPF program
b = BPF(text=bpf_text)
b.attach_uprobe(
    name=args.libssl,
    addr=args.write,
    fn_name='pre_ssl_write_internal',
    pid=args.pid,
)

b.attach_uprobe(
    name=args.libssl,
    addr=args.read,
    fn_name='pre_ssl_read_internal',
    pid=args.pid
)

b.attach_uretprobe(
    name=args.libssl,
    addr=args.read,
    fn_name='post_ssl_read_internal',
    pid=args.pid
)


g_data = {"pid_tid" : ['buf', 0]}
# read events
def print_ssl_write(cpu, data, size):
    event = b["ssl_write_events"].event(data)
    print("\033[36m")
    print("SSL_write------------------------------------------------------")
    print("%-10d %-32s %-10d" % (event.pid, event.comm, event.size))
    printb(event.buf)
    print("\033[0m")
def print_ssl_read(cpu, data, size):
    event = b["ssl_read_events"].event(data)
    print("\033[32m")
    print("SSL_read------------------------------------------------------")
    print("%-10d %-32s %-10d" % (event.pid, event.comm, event.size))
    printb(event.buf)
    print("\033[0m")

b["ssl_write_events"].open_perf_buffer(print_ssl_write,page_cnt=32)
b["ssl_read_events"].open_perf_buffer(print_ssl_read,page_cnt=32)
while True:
    try:
        #b.trace_print()
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

运行脚本python ssl_hook.py --libssl /data/app/~~GWzeE8KPBIy0kjA1H2YxPQ==/com.reverccqin.logintest--SlKqgwTivBrodcAIX72vw==/lib/arm64/liblogintest.so --write 0x1ADC38 --read 0x1AD5C0 --pid 22226 捕获https明文。

libbpf

CO-RE

不同linux内核版本的内部结构体会有差异,如果想要编写支持CO-RE(一次编译,到处运行)的程序需要对不同的内核版本的结构体差异做兼容。这就和windows平台编写一些内核rookit驱动或者ark工具时需要访问一些例如KPROCESS这样的结构体一样,不同的windows版本其内核对应的结构体都可能有差异,大多数ark工具都是通过从微软的符号服务器中获取对应内核版本的符号去兼容不同的内核版本。而linux上的符号文件就是BTF,BTF是一个描述内核数据结构的元数据格式,只有在编译打开配置CONFIG_DEBUG_INFO_BTF=y后才能生成对应的BTF文件。libbpf就是通过在BPF运行的时候去解析BTF文件来兼容各个linux内核版本结构体的差异,而BCC则是在编译的时候获取此BTF文件去解析内核结构,所以不支持一次编译到处运行。编写程序的时候需要vmlinux.h头文件,此文件包含了所有内核结构的定义,可以通过bpftool工具从BTF文件中提取对应内核版本的头文件bpftool btf dump file /sys/kernel/btf/vmlinux format c

编写libbpf程序

这里通过wsl + vscode + clangd编写并交叉编译能够在android平台运行的ebpf程序(支持代码提示和跳转),因为交叉编译使用的编译工具是gcc-aarch64-linux-gnu,其会依赖并静态链接linux平台的libc标准库,所以在编写ebpf用户态程序的时候小心调用具有平台差异的libc函数(例如:localtime函数linux平台会去访问/etc/localtime文件,而android并不存在此文件),大多数libc函数都是没有关系的。具体请参考:https://github.com/revercc/libbpf-bootstrap-for-android.githttps://blog.csdn.net/youzhangjing_/article/details/132671989

编写libbpf程序利用uprobe实现ssl_write_internal/ssl_read_internal的hook,内核层代码如下

#include <vmlinux.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "sslfilter.h"

// BPF_MAP_TYPE_ARRAY类型map是数组类型
// max_entries相当于数组长度(元素个数)
// key是数组索引(8个字节)
// value是数组元素
// 因为数组map创建后,相当于所有的key(索引)都是默认存在的,和hash map类型不一样
struct{
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, struct SSL_FILTER_INFO);
} ssl_filter_info SEC(".maps");

struct READ_ENTRY_ARGS{
    u64 buf_address;
    u64 readbytes_address;
};

struct{
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 100);
    __type(key, u64);
    __type(value, struct READ_ENTRY_ARGS);
}read_entry_args SEC(".maps");

// BPF_MAP_TYPE_PERF_EVENT_ARRAY类型map的max_entries等与cpu数量
// value是perfbuf map的文件句柄
struct{
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));    
} events SEC(".maps");


//int ssl_write_internal(SSL *s, const void *buf, size_t num, size_t *written)
SEC("uprobe")
int BPF_KPROBE(ssl_write_internal_entry, void *SSL, void *buf, size_t num, size_t *written)
{
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u32 pid = pid_tgid;
    u64 index = 0;
    struct SSL_FILTER_INFO *p_filter_info = bpf_map_lookup_elem(&ssl_filter_info, &index);
    if(p_filter_info == NULL){
        return 0;
    }
    
    p_filter_info->bWrite = true;
    p_filter_info->pid = pid;
    p_filter_info->size = num;
    bpf_get_current_comm(&p_filter_info->comm, sizeof(p_filter_info->comm));
    if(bpf_probe_read_user(&p_filter_info->buf, num < sizeof(p_filter_info->buf) ? num : sizeof(p_filter_info->buf), buf) < 0){
        return 0;
    }

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
        p_filter_info, sizeof(*p_filter_info));
    return 0;
}

//int ssl_read_internal(SSL *s, void *buf, size_t num, size_t *readbytes)
SEC("uprobe")
int BPF_KPROBE(ssl_read_internal_entry, void *SSL, void *buf, size_t num, size_t *readbytes)
{
    struct READ_ENTRY_ARGS args = {};
    args.readbytes_address = (u64)readbytes;
    args.buf_address = (u64)buf;
    u64 pid_tgid = bpf_get_current_pid_tgid();
    bpf_map_update_elem(&read_entry_args, &pid_tgid, &args, BPF_ANY);
    return 0;
}

SEC("uretprobe")
int BPF_KRETPROBE(ssl_read_internal_exit, int ret)
{
    if(ret == -1){
        return 0;
    }

    u64 index = 0;
    struct SSL_FILTER_INFO *p_filter_info = bpf_map_lookup_elem(&ssl_filter_info, &index);
    if(p_filter_info == NULL){
        return 0;
    }

    u64 pid_tgid = bpf_get_current_pid_tgid();
    u32 pid = pid_tgid;
    struct READ_ENTRY_ARGS *p_args = bpf_map_lookup_elem(&read_entry_args, &pid_tgid);
    if(p_args == NULL){
        return 0;
    }

    size_t readbytes;
    if(bpf_probe_read_user(&readbytes, sizeof(readbytes), (size_t*)p_args->readbytes_address) < 0){
        return 0;
    }

    p_filter_info->pid = pid;
    p_filter_info->size = readbytes;
    p_filter_info->bWrite = false;
    bpf_get_current_comm(&p_filter_info->comm, sizeof(p_filter_info->comm));
    if(bpf_probe_read_user(&p_filter_info->buf, 
        readbytes < sizeof(p_filter_info->buf) ? readbytes : sizeof(p_filter_info->buf), 
        (void*)p_args->buf_address) < 0){
        return 0;
    }

    bpf_printk("read : %d", readbytes);
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, 
        p_filter_info, sizeof(*p_filter_info));
    return 0;
}
char LICENSE[] SEC("license") = "GPL";

用户层核心代码如下:加载并验证bpf程序--->附加uprobe/uretprobe

运行./sslfilter -l /data/app/~~jHc9IVYjkaSDTGeAKNfakQ==/com.reverccqin.logintest-HoULxuV9FDpqJa8D2ZWmEA==/lib/arm64/liblogintest.so -w 0x22AC38 -r 0x22A5C0 -p 23396测试效果:

-h参数支持输出hex格式,./sslfilter -h -l /data/app/~~jHc9IVYjkaSDTGeAKNfakQ==/com.reverccqin.logintest-HoULxuV9FDpqJa8D2ZWmEA==/lib/arm64/liblogintest.so -w 0x22AC38 -r 0x22A5C0 -p 23396

代码和配置已上传github:https://github.com/revercc/libbpf-bootstrap-for-android.git

参考:
http://blog.silence.pink/p/create-btf-from-debuginfo/
https://blog.csdn.net/youzhangjing_/article/details/132671989
https://nakryiko.com/posts/libbpf-bootstrap/
https://github.com/iovisor/bcc/tree/3162551b0b93ac9b663c14fdaae5c1503b68fc65/libbpf-tools
https://www.kernel.org/doc/html/latest/bpf/libbpf/libbpf_overview.html
https://github.com/libbpf/libbpf-bootstrap.git

posted @ 2023-10-24 10:58  怎么可以吃突突  阅读(943)  评论(0编辑  收藏  举报