likes
comments
collection
share

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

作者站长头像
站长
· 阅读数 3

0x01 背景概要

在做FlowGod之前,我曾在日常工作以及企业实习的过程多次面对这些问题场景:

  • 应急响应时,终端edr设备告警有恶意外联,但edr不提供发起外连的进程名,可能会给外连地址,但是地址也在不断地变化。在这样的条件下,如何“快速”地定位到恶意进程?
  • 甲方客户反映,某应用业务接口存在fastjson反序列化漏洞/log4j远程代码执行探测行为(dnslog),要求确认漏洞存在的真实性以及影响范围(host->pod->process)。(应用都是通过k8s部署的,提供生产机器的权限,没有CI系统的权限
  • 在安全设备上告警内网DNS解析记录有主机外联恶意域名(由于内网很大,无DNS解析日志),如何定位是哪台主机的哪个进程?
  • .....

这些问题场景最终都归类到一个核心的问题,便是如何做到进程级别的流量监控

在接触ebpf之前,对于上述的部分问题,我习惯性地会采用轮询、正则匹配等方法去定位到进程。譬如若已知恶意外联的地址,我会采用 netstat进行轮询:

#! /bin/bash 
while [[ true ]] do 
if [ -n "$(netstat -atnpl | grep xxx.xxx.xxx.xxx)" ]; 
then echo "异常外连及进程已找到:" netstat -atnpl | grep xxx.xxx.xxx.xxx 
break 
else echo "waiting...." 
sleep 0.5 
fi 
done

再譬如面对确认漏洞影响范围的问题时,我习惯性地先复现漏洞,然后在复现结果的基础之上去获取一些系统信息或者更直接点反弹个shell 过来去查看线索,之后我会根据前者反馈的线索进入到目标pod中(如果是k8s部署的话),再去 grep一些依赖信息(log4j-2.x.jar;fastjson-4.x-jar)或者通过解压war包查看内容,大致便可以确定应用位置和漏洞成因了。

但在实际操作的过程中,我发现这样去处理问题太过于被动了,虽然最终问题也能够被解决,但对我而言,这个过程只是为了解决当时场景下的问题。如果条件再受限一点。譬如外联的频率很低、时间很短, 并且netstattop等命令工具也只是在间断性地采样,并不是一个连续的监控过程;一个pod部署多个应用.......在这样的条件下用轮询、用正向追踪漏洞的思路真的能解决问题吗?

所以在面对传统Linux-edr、NIDS或者流量监控设备的缺憾时,我便有了实现一个进程级别的流量监控工具的想法,并且在接触了ebpf技术之后,很快就有了FlowGod的demo。

0x02 eBPF

eBPF全称为extended Berkeley Packet Filter,即拓展的BPF。熟悉tcpdump工具的应该知道,tcpdump的底层核心技术就是通过BPF来实现的,具体可参考下面这张图:

FlowGod——一款基于eBPF的流量捕获工具(进程级别) tcpdump会根据用户的输入参数生成对应的字节码,然后加载进内核中的BPF虚拟机,再根据规则对数据包进行过滤,最后复制符合规则的数据包到用户态程序中。在BPF技术出现之前,如果需要对指定的数据包进行过滤必须从内核中复制全部的数据包到用户态程序中,然后在用户态程序中进行规则匹配,这样极大的消耗了系统资源(尤其是全部数据包通过协议栈进行解包的资源),并且效率低下,所以BPF的出现很好的改善了这个问题。

而eBPF便是BPF加强版,eBPF的出现不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储,还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件(perf_events)以及安全控制等。

概括来说,eBPF使得 BPF 不再仅限于网络栈,而是成为内核的一个顶级子系统。下图便是eBPF程序运行的大致流程:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

具体解释来说:我们可以在用户态编写eBPF程序,然后借助LLVM将编写好的eBPF程序转换为BPF字节码,然后再通过bpf系统调用提交给内核执行。但内核并不是执行任意的eBPF程序(这必然会引发安全问题,譬如任意读取内核内存信息等),内核在接受 BPF 字节码之前,会首先通过验证器对字节码进行校验,只有校验通过的 BPF 字节码才会提交到即时编译器执行。校验的过程十分严格,校验规则以及限制也有很多(譬如禁止随意调用内核函数,只能调用API中定义的辅助函数;栈空间最多只有512字节...),目的就是为了保证eBPF程序的安全和稳定。

再有,BPF 程序可以利用 BPF 映射(map)进行存储,而用户程序通常也需要通过 BPF 映射同运行在内核中的 BPF 程序进行交互。如下图所示,BPF 程序收集内核运行状态存储在映射中,用户程序再从映射中读出这些状态:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

可以看到,eBPF 程序的运行需要历经编译、加载、验证和内核态执行等过程,而用户态程序则需要借助 BPF 映射来获取内核态 eBPF 程序的运行状态。

关于eBPF字节码以及eBPF辅助函数等内容不在本章作过多介绍,具体的会在第三章描述实现过程的内容里说明。

0x03 实现思路

在设计FlowGod之前,首先还是需要先明确下FlowGod的预期目标:

获取到流量信息以及流量所关联的进程信息。

说到流量,那第一时间便想到使用套接字eBPF:监听套接字,根据自己的规则过滤每一个数据包,然后从套接字的缓冲区中复制过滤后的包数据到用户程序(这就是tcpdump的思路)。

那么该如何获取数据所对应的进程信息呢?

在之前对eBPF相关技术的研究工作中可知,在eBPF程序中可以通过 bpf_get_current_pid_tgid()bpf_get_current_comm()等bpf辅助函数去获取进程信息,但是这些辅助函数只有在特定的情况下才能使用,譬如只有在对内核函数、用户态程序函数等hook时才能使用,而在套接字类型的程序中是无法使用这些辅助函数的。但既然是发包,那么其对应的系统调用(或者说内核函数)必然是可以跟踪的,譬如 tcp_sendmsg()内核函数(其系统调用一般为 send()),通过跟踪 tcp_sendmsg(),便可以拿到进程信息,并且不管是HTTP还是HTTPS请求,最终也都会落在tcp_sendmsg()上(因为这俩应用层协议就是基于TCP协议的)。

但是又该如何将数据包和获取到的进程信息相关联呢?

一个是socket套接字钩子,一个是tcp_sendmsg()内核函数。在解决这个问题之前,我最初的思路想法是:既然可以通过tcp_sendmsg()拿到进程信息,那么为什么不能通过tcp_sendmsg()再去获取数据包的信息呢?既然是 send()系统调用在内核中的实现,tcp_sendmsg()函数的入口参数中肯定有与网络数据传输相关的信息。

通过在内核源码中可以看到tcp_sendmsg()函数的入参是 sock结构体:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

sock 结构体的原子结构长这样:

struct sock {
		/*
		 * Now struct inet_timewait_sock also uses sock_common, so please just
		 * don't add nothing before this first member (__sk_common) --acme
		 */
		struct sock_common	__sk_common;
	#define sk_node			__sk_common.skc_node
	#define sk_nulls_node		__sk_common.skc_nulls_node
	#define sk_refcnt		__sk_common.skc_refcnt
	#define sk_tx_queue_mapping	__sk_common.skc_tx_queue_mapping
	#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
	#define sk_rx_queue_mapping	__sk_common.skc_rx_queue_mapping
	#endif
	
	#define sk_dontcopy_begin	__sk_common.skc_dontcopy_begin
	#define sk_dontcopy_end		__sk_common.skc_dontcopy_end
	#define sk_hash			__sk_common.skc_hash
	#define sk_portpair		__sk_common.skc_portpair
	#define sk_num			__sk_common.skc_num
	#define sk_dport		__sk_common.skc_dport
	#define sk_addrpair		__sk_common.skc_addrpair
	#define sk_daddr		__sk_common.skc_daddr
	#define sk_rcv_saddr		__sk_common.skc_rcv_saddr
	#define sk_family		__sk_common.skc_family
	#define sk_state		__sk_common.skc_state
	#define sk_reuse		__sk_common.skc_reuse
	#define sk_reuseport		__sk_common.skc_reuseport
	#define sk_ipv6only		__sk_common.skc_ipv6only
	#define sk_net_refcnt		__sk_common.skc_net_refcnt
	#define sk_bound_dev_if		__sk_common.skc_bound_dev_if
	#define sk_bind_node		__sk_common.skc_bind_node
	#define sk_prot			__sk_common.skc_prot
	#define sk_net			__sk_common.skc_net
	#define sk_v6_daddr		__sk_common.skc_v6_daddr
	#define sk_v6_rcv_saddr	__sk_common.skc_v6_rcv_saddr
	#define sk_cookie		__sk_common.skc_cookie
	#define sk_incoming_cpu		__sk_common.skc_incoming_cpu
	#define sk_flags		__sk_common.skc_flags
	#define sk_rxhash		__sk_common.skc_rxhash
    ......
}

除了五元组信息(源ip,源port,目的ip,目的port,协议)对程序有用以外,并没有找到存储原始数据包的字段(即sk_buffer)。

同理, udp_sendmsg() 也是一样:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

最终,这个问题是通过 BPF_TABLE_PUBLIC 来解决。BPF_TABLE_PUBLIC 也是一个映射存储结构(TABLE),顾名思义它是一个公共的表,可以被其他BPF程序访问到,那么关联的思路便有了:通过tcp_sendmsg() 钩子向公共表中存储以五元组信息为 key ,进程信息为 value 的数据,然后再通过监听原始套接字程序的每一个包,取出原始数据包的五元组信息查找公共表,以获取对应的进程信息,然后将原始数据包和进程信息一起发送给用户态程序。

在之前的ebpf编程练习中,可以通过定义PERF性能事件映射的方式将数据传递给用户态程序,这意味着我们需要将进程信息和流量信息整合成一个数据然后传递给用户态程序,因为我们事先是不知道包的大小的,所以这又意味着我们需要去定义一个足够大的变量去存储,这种方法显然是不太科学的,好在eBPF提供了一个比较优雅的辅助函数perf_submit_skb :

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

perf_submit_skb 函数可以同时传递数据和数据包,这正好适配我的需求。

所以,最后程序的流程设计是这样的(特别说明下,本次项目采用bcc作为实现、加载eBPF程序的框架,并且通过bcc框架读取映射表获取数据包和其他信息):

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

其中校验分包情况的流程图设计如下:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

下面就是对HTTPS明文请求捕获的需求进行分析,我们知道,用户应用发送HTTPS请求在调用 send() 等系统调用之前会经过一层SSL的处理,如下图所示:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

在经过SSL层的处理之后,通过套接字程序监听到的数据包在应用层即HTTPS协议包上的 data 值是加密过的,所以无法针对这部分的 data 值进行分析。

上图SSL层处理例子中所涉及到的函数链接库是 openssl.so ,下面在我的开发环境下(Linux kernel 5.15.0 & Ubuntu 22.04)看看常见的请求工具应用的是哪类函数链接库:

ldd `which curl` | grep -E "tls|ssl|nspr" //显示curl所依赖的相关动态链接库

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

接下来可以通过 readelf 查看 libssl.so 动态链接库的内容,譬如过滤SSL_read 或者 SSL_write 函数,并且通过参数 -Ws 输出符号表:

readelf -Ws /lib/x86_64-linux-gnu/libssl.so.3 |grep -i 'ssl_read'

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

eBPF程序除了可以给内核函数挂钩以外,也可以给用户态的程序函数挂钩,最传统的方式便是找到函数在用户程序中的指定偏移量,然后根据偏移量挂钩,虽然比较复杂但这种方式是最保险的。在本次实现的过程中,还是希望借助eBPF本身提供的钩子来完成对SSL_read 或者 SSL_write 函数的跟踪:

通过查询,eBPF的确已经实现了 libssl.so 函数库的一些uprobe,其中就有程序实现需要SSL_readSSL_write

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

下面可以通过 bpftrace 简单地测试下这些uprobe是否可用:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

实验证明这些uprobe是有效的,但是为了在eBPF中更好地掌握对这些uprobe的利用,需要去看下SSL_readSSL_write 的具体实现,了解它们的入参和返回值:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

SSL_read :用于从已经建立的SSL session中读取数据,放入到缓冲区中(HTTPS响应)

SSL_write :与 SSL_read 相反,在建立的SSL Session中写入数据到缓冲区,发送到远程服务器(HTTPS请求)

所以,在当前场景中,显然我们更应该关注SSL_write 这个函数。与 SSL_write 相关的函数有两个SSL_writeSSL_write_ex ,前者是将未加密的HTTPS请求写入到缓冲区,后者是前者完成后的返回,并且将已经写入数据的大小存储到某个地址中。只有SSL_writeSSL_write_ex 都完成了才算真正的将SSL Session中的数据传递给下面的流程中去。

这样的话,获取到HTTPS明文请求的思路就比较顺畅了,通过给SSL_write 挂uprobe,拿到进程信息(Key)以及HTTPS明文请求(Value),并存储到Map中,然后给SSL_write_exSSL_write 的返回函数挂钩,并通过进程信息(Key)拿到HTTPS明文请求(Value)。

需要特别说明的是,虽然可以通过上述的uprobe获取到HTTPS明文请求以及对应的进程信息,但是并不能就此发送给BBC前端处理和输出,因为BCC前端除了给 SSL_write 挂钩外,还在上文中对 tcp_sendmsg 挂了钩子,HTTPS是建立在TCP的基础之上的,所以获取到的数据便重复了,并且单靠tcp_sendmsg 获取到的仍然是加密后的HTTPS请求。

所以,我希望将HTTPS的请求最终也落在tcp_sendmsg 并发送给BCC前端。

解决方案是再加一个映射 BPF_TABLE_PUBLIC ,与之前BPF_TABLE_PUBLIC 不同的是,这个映射的键是进程信息,值是HTTPS明文请求信息。在完成SSL处理之后,应用程序必然会调用tcp_sendmsg ,通过在tcp_sendmsg 中获取进程信息,并查找上述的BPF_TABLE_PUBLIC 映射以获取HTTPS明文请求,最终在tcp_sendmsg 中以PERF_EVENT的方式将数据一并发送给BCC前端。具体的流程设计如下图所示:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

但是同样的方案实现在python程序通过requests库发送https请求时却出现了问题。

我在使用LD_DEBUG信息跟踪某发送HTTPS请求的python程序时,输出了该python程序引用到的链接库,如下所示:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

所以我使用bpftrace简单地hook了该链接库的SSL_write函数,在执行该python测试程序后并没有捕获到相关信息:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

这是一个有趣的现象,同样地我对curl进行了测试,发现curl也同样引用了该链接库,但是不同地是可以捕获到调用信息:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

于是我打印了一次https请求所调用的所有uprobe,试图找到python程序发送https请求所调用的链接库函数:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

我将结果整理如下:

Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_new
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_new_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_COMP_get_compression_methods
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_ex_data_X509_STORE_CTX_idx
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_ciphersuites
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_COMP_get_compression_methods
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_CTX_new
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_CTX_set_ssl_ctx
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_CTX_set_flags
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_cmd
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_cipher_list
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_CTX_finish
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CONF_CTX_free
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_callback
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_verify
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_options
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_cipher_list
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_ctrl
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_ctrl
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_session_id_context
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get0_param
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_post_handshake_auth
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_cipher_list
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_options
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_options
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_options
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_callback
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_verify
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_callback
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_verify
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_load_verify_locations
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_load_verify_file
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_alpn_protos
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_set_alpn_select_cb
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_new
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_up_ref
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_up_ref
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_clear
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_SESSION_free
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set_ct_validation_callback
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get0_param
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set_ex_data
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set_fd
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set_bio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set0_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set0_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_ctrl
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_set_connect_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_do_handshake
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_before
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_clear
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_SESSION_free
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_before
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_SESSION_new
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_SESSION_free
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_ciphers
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
....
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_SESSION_free
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get0_CA_list
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_version
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_version
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_options
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_ex_data_X509_STORE_CTX_idx
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_security_level
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_is_init_finished
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_version
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_mode
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_is_init_finished
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get1_peer_certificate
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get0_peer_certificate
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_SSL_CTX
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_mode
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_CTX_get_verify_mode
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_write_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_write_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_state
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_read_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_read_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_read_ex
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_in_init
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_rbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_get_wbio
Get!uprobe:/lib/x86_64-linux-gnu/libssl.so.3:SSL_read_ex

最终,我找到了,实际向缓冲区写入https明文请求的是SSL_write_ex函数:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

同理,便可以对python程序发出的https请求进行捕获了。

对于go程序,同样可以通过读取编译后的go程序符号表,来选择需要hook的uprobe:

readelf -Ws ./hello | grep tls

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

同样,最终我找到了go程序发送HTTPS请求只要Hook writeRecordLocked 这个函数就可以了,其中writeRecordLocked 函数的入参 byte 记录的是明文的HTTPS请求:

// writeRecordLocked writes a TLS record with the given type and payload to the
// connection and updates the record layer state.


func (c *Conn) writeRecordLocked(typ recordType, data []byte) (int, error) {
   950  	outBufPtr := outBufPool.Get().(*[]byte)
   951  	outBuf := *outBufPtr
   952  	defer func() {
   953  		// You might be tempted to simplify this by just passing &outBuf to Put,
   954  		// but that would make the local copy of the outBuf slice header escape
   955  		// to the heap, causing an allocation. Instead, we keep around the
   956  		// pointer to the slice header returned by Get, which is already on the
   957  		// heap, and overwrite and return that.
   958  		*outBufPtr = outBuf
   959  		outBufPool.Put(outBufPtr)
   960  	}()
   961  
   962  	var n int
   963  	for len(data) > 0 {
   964  		m := len(data)
   965  		if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload {
   966  			m = maxPayload
   967  		}
   968  
   969  		_, outBuf = sliceForAppend(outBuf[:0], recordHeaderLen)
   970  		outBuf[0] = byte(typ)
   971  		vers := c.vers
   972  		if vers == 0 {
   973  			// Some TLS servers fail if the record version is
   974  			// greater than TLS 1.0 for the initial ClientHello.
   975  			vers = VersionTLS10
   976  		} else if vers == VersionTLS13 {
   977  			// TLS 1.3 froze the record layer version to 1.2.
   978  			// See RFC 8446, Section 5.1.
   979  			vers = VersionTLS12
   980  		}
   981  		outBuf[1] = byte(vers >> 8)
   982  		outBuf[2] = byte(vers)
   983  		outBuf[3] = byte(m >> 8)
   984  		outBuf[4] = byte(m)
   985  
   986  		var err error
   987  		outBuf, err = c.out.encrypt(outBuf, data[:m], c.config.rand())
   988  		if err != nil {
   989  			return n, err
   990  		}
   991  		if _, err := c.write(outBuf); err != nil {
   992  			return n, err
   993  		}
   994  		n += m
   995  		data = data[m:]
   996  	}
   997  
   998  	if typ == recordTypeChangeCipherSpec && c.vers != VersionTLS13 {
   999  		if err := c.out.changeCipherSpec(); err != nil {
  1000  			return n, c.sendAlertLocked(err.(alert))
  1001  		}
  1002  	}
  1003  
  1004  	return n, nil
  1005  }

但是对于go编译的程序而言,go程序不完全遵守ABI调用规定,这意味着在eBPF程序中无法简单地通过读取规定寄存器来获取函数的入参值,需要根据go的版本来选择不通的参数读取方法如寄存器读取和堆栈读取,其中的技术实现细节不再赘述了,以下是一个可供参考的Golang函数参数、返回值的寄存器传递布局:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

协议解包

这里我想简单记录下手工对eBPF获取到的原始数据包进行协议解析的过程,因为当程序的BCC前端获取到原始数据包以及所关联的进程信息时,下一个工作必然就是对原始数据包进行解包,因为最后程序希望输出的一定是高层协议的 data值,譬如HTTP、HTTPS、UDP等,虽然python提供了一些方便的函数库能够辅助完成这一步,但作为计网高分选手,我还是想回顾下计网中重要的包结构相关知识,并进行实践操作。

从数据帧的结构开始吧,如下图所示:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

数据链路层的数据帧结构相对比较简单,帧前面14个字节是固定的数据,分别是目的MAC地址、源MAC地址以及帧类型,然后便是IP层的数据了,如下图所示:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

P层的数据比较复杂,但是也不用太在意,因为对于FlowGod来说有用的字段就那么几个,首先还是需要计算下整个IP数据包 header 以及 data 的大小:

假设现在我们获取到原始数据包为 packet_raw ,这是一个字节数组,是经过协议栈层层封装起来的,并且我们已经知道了数据帧的 header 值为14,所以我们就可以通过偏移packet_raw 获取IP数据包,譬如packet_raw[14:]

有了IP数据包,那么就需要计算IP数据包的 header 大小,在IP数据包第一个字节的低4位代表首部长度,所以可以先获取到4位首部长度,然后乘4(因为IP数据包每一层是4个字节),如下编码所示:

ip_header_length = packet_raw[14]    # 获取IP数据包第一个字节
ip_header_length = ip_header_length & 0x0F     # 获取4位首部长度  
ip_header_length = ip_header_length << 2       # 乘4获取首部长度字节数   

有了IP数据包的 header 大小,那么就可以同理通过偏移获得TCP或UDP数据报了:packet_raw[14+ip_header_length:]

在到TCP/UDP数据报之前,在IP数据报中我们还知道,前20个字节是确定的,并且其中也有一些有用的字段信息,譬如32位的源IP地址和目的地址,同样我们可以通过偏移来获取这些字段的值:

ip_src = packet_str[ETH_hearder_Length + 12: ETH_HLEN + 16] # ETH_hearder_Length = 14
ip_dst = packet_str[ETH_hearder_Length + 16:ETH_HLEN + 20] 

但是上面代码片段中获取到的 ip_srcip_dst 都是字节码,并且还存在网络字节序和主机字节序之间的转换问题,根据之前的网络程序设计课程中学习到的知识,网络字节序是以大端的形式存储的,在C语言的socket编程中我们常用 htonshtonlntohsntohl 来进行网络字节序和主机字节序之间的转换,在python中,我们也可以通过 int.from_bytes(xxx,"big") 来进行转换(大端字节序):

ip_src = int.from_bytes(ip_src,"big")
ip_dst = int.from_bytes(ip_dst,"big")

然后再进行点分十进制的转换,譬如可以这样:

def int2ip(rawip):
    result = []
    for i in range(4):
        rawip, mod = divmod(rawip, 256)
        result.insert(0,mod)
    return '.'.join(map(str,result))


ip_src_str = int2ip(ip_src)
ip_dst_str = int2ip(ip_dst)

至此IP数据报就处理的差不多了,下面就进入TCP数据报:

FlowGod——一款基于eBPF的流量捕获工具(进程级别)

TCP数据报和IP数据报复杂程度差不多,按照之前的思路,我们首先还是需要确定下TCP数据报的 header 大小,可以看到在TCP数据报第13个字节的高4位代表“4位首部长度”,那么就可以根据这“4位首部长度“计算出TCP数据报的 header 大小,如下:

tcp_header_length = packet_bytearray[ETH_HLEN + ip_header_length + 12]
tcp_header_length = tcp_header_length & 0xF0
tcp_header_length = tcp_header_length >> 2 # 这是高4位,需要除以4

有了TCP数据报的 header 大小,那么就可以偏移到更上层应用的数据了(HTTP/HTTPS),在此之前因为TCP数据报前20个字节也是固定的,所以可以获取下TCP数据报中有用的信息,譬如16位的源端口号和目的端口号:

port_src = packet_str[ETH_hearder_Length + ip_header_length:ETH_hearder_Length + ip_header_length + 2]
port_dst = packet_str[ETH_hearder_Length + ip_header_length + 2:ETH_hearder_Length+ ip_header_length + 4]

同样,这也涉及到字节序的转换问题,同理可以通过 int.from_bytes 来解决:

port_src = str(int.from_bytes(port_src,"big"))
port_dst = str(int.from_bytes(port_dst,"big"))

至于UDP的话就更简单了,如下图所示:

UDP数据报的 header 大小是确定的,因为UDP数据报没有包头的“选项”字段,所以就是固定的8个字节,那么通过这8个字节可以偏移得到更高层的协议数据报,也可以获取到UDP数据报中有用的信息,这里就不赘述了,方法和上面是一样的。

0x04 总结回顾

FlowGod这个项目我之前一直都在维护,算是以做带学,中间参考了来自ebpf和bcc社区很多优秀的开源项目,也包括美团研发大佬CFC4N的ecapture项目。因为FlowGod并不同于传统的流量嗅探工具,它可以作进程关联,可以解决HTTPS流量的问题,并且eBPF的强大之处也不仅仅在于上文实现的这些,eBPF在流量控制、微隔离等方面的应用也十分广泛。所以下一步,我希望在此项目的基础之上,能够将恶意流量检测、主动阻断等功能同样加入到FlowGod中,目前也已经形成了一些具体的想法。

最后还是希望给FlowGod打个小广告,希望这个项目能提供大家一些启发和思路。 也希望大家能够给予笔者一个宝贵的star ~

FlowGod项目链接:github.com/Your7Maxx/F…

0x05 参考链接

【1】www.cnxct.com/ecapture-su…

【2】www.zadmei.com/wlgzrhsy.ht…

【3】github.com/iovisor/bcc…

【4】github.com/gojue/ecapt…