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的话就更简略了,如下图所示:

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

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…