iT邦幫忙

2022 iThome 鐵人賽

DAY 20
0
Software Development

教練我想玩eBPF系列 第 20

Day20 - 外傳 - Socket filter 底層探索 (下)

  • 分享至 

  • xImage
  •  

接旭昨天,我們可以更深入的了解一下eBPF對BPF_ABS做了什麼事情,在verifier這個神奇的地方搜尋BPF_ABS這個instruction,會找到下面這段內容(簡化版)

/* Implement LD_ABS and LD_IND with a rewrite, if supported by the program type. */
if (BPF_CLASS(insn->code) == BPF_LD &&
	(BPF_MODE(insn->code) == BPF_ABS ||
	 BPF_MODE(insn->code) == BPF_IND)) {
	
	cnt = env->ops->gen_ld_abs(insn, insn_buf);
	new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);

首先執行條件是BPF_LDBPF_ABS,我們的code剛好符合這個條件,接著會呼叫env->ops->gen_ld_abs,根據原本的instrunction insn,生成新的instruction寫入insn_buf,接著呼叫bpf_patch_insn_data將原本的指令取代為新的指令。

接著我們要找一下gen_ld_abs,跟day 11介紹map的情況類似,verifier定義了bpf_verifier_ops 結構,讓不同的program type根據需要,實作bpf_verifier_ops 定義的function來提供不同的功能和行為。

socket filter的定義如下

const struct bpf_verifier_ops sk_filter_verifier_ops = {
	.get_func_proto		= sk_filter_func_proto,
	.is_valid_access	= sk_filter_is_valid_access,
	.convert_ctx_access	= bpf_convert_ctx_access,
	.gen_ld_abs		= bpf_gen_ld_abs,
};

所以讓我們看到bpf_gen_ld_abs (一樣經過簡化只看我們需要的部分)

static int bpf_gen_ld_abs(const struct bpf_insn *insn,
			  struct bpf_insn *insn_buf)
{
	*insn++ = BPF_MOV64_REG(BPF_REG_2, orig->src_reg);

/* We're guaranteed here that CTX is in R6. */
	*insn++ = BPF_MOV64_REG(BPF_REG_1, BPF_REG_CTX);

	*insn++ = BPF_EMIT_CALL(bpf_skb_load_helper_16_no_cache);
}

看到最後一行就很清晰了,最後其實等於調用了內部使用的helper function來存取資料。eBPF也提提供了類似的helper function bpf_skb_load_bytes,來提供存取封包內容的功能。

BPF_CALL_2(bpf_skb_load_helper_16_no_cache, const struct sk_buff *, skb,
	   int, offset)
{
	return ____bpf_skb_load_helper_16(skb, skb->data, skb->len - skb->data_len,
					  offset);
}

而bpf_skb_load_helper_16_no_cache其實就是直接從sk_buff->data的位置取得資料,data是sk_buff用來指到封包開頭的指標。

既然整個指令的本質是從sk_buff->data拿取資料,那我們是不是能夠直接從__sk_buff裡面拿到資料呢?

在socket program type下program context是__sk_buff,他其實本質是對sk_buff的多一層封裝(原因參見),在執行的時候,verifier換將其取代回sk_buff,因此__sk_buff等於是sk_buff暴露出來的介面。

struct __sk_buff {
	...
	__u32 data;
	__u32 data_end;
	__u32 napi_id;
	...

參考__sk_buff的定義,__sk_buff是有定義將datadata_end,那我們原始的eBPF程式是不是可以改成

void *cursor = (void*)(long)(__sk_buff->data);
struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet));
if (!(ethernet->type == 0x0800)) {
		goto DROP;
}

如果完成這樣的修改,重新跑一遍http-parse-simple.py,你會得到

python3 http-parse-simple.py -i eno0
binding socket to 'enp0s3'
bpf: Failed to load program: Permission denied
; int http_filter(struct __sk_buff *skb) {
0: (bf) r6 = r1
; void *cursor = (void*)(long) skb->data;
1: (61) r7 = *(u32 *)(r6 +76)
invalid bpf_context access off=76 size=4
processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Traceback (most recent call last):
  File "http-parse-simple.py", line 69, in <module>
    function_http_filter = bpf.load_func("http_filter", BPF.SOCKET_FILTER)
  File "/usr/lib/python3/dist-packages/bcc/__init__.py", line 526, in load_func
    raise Exception("Failed to load BPF program %s: %s" %
Exception: Failed to load BPF program b'http_filter': Permission denied

可以看到程式碼被verifier拒絕,並拿到了一個invalid bpf_context access off=76 size=4的錯誤,表示存取__sk_buff->data是非法的。

回去追蹤程式碼的話,會看到在verifier裡面會用env->ops->is_valid_access來檢查該存取是否有效,這同樣定義在bpf_verifier_ops結構內。

其中socket filter program的實作是

static bool sk_filter_is_valid_access(int off, int size,
				      enum bpf_access_type type,
				      const struct bpf_prog *prog,
				      struct bpf_insn_access_aux *info)
{
	switch (off) {
	case bpf_ctx_range(struct __sk_buff, tc_classid):
	case bpf_ctx_range(struct __sk_buff, data):
	case bpf_ctx_range(struct __sk_buff, data_meta):
	case bpf_ctx_range(struct __sk_buff, data_end):
	case bpf_ctx_range_till(struct __sk_buff, family, local_port):
	case bpf_ctx_range(struct __sk_buff, tstamp):
	case bpf_ctx_range(struct __sk_buff, wire_len):
	case bpf_ctx_range(struct __sk_buff, hwtstamp):
		return false;
	}
	...

可以很直接看到拒絕了data的存取。

從linux kernel的變更紀錄來推測,data欄位好像本來就不是給socket filter使用的,只是單純因為cls_bpf和socker filter可能共用了這部分的程式碼,因此要額外阻擋這部分的code不讓使用。

最後還有一個沒解決的問題,u8 *cursor = 0;,為甚麼空指標經過LLVM編譯後會編譯成對skb的存取還是未知的,看起來像是BCC特別的機制,但是找不太到相關資料,只好保留這個問題。

參考資料

本系列30天鐵人文章同步發表在我的個人部落格


上一篇
Day19 - 外傳 - Socket filter 底層摸索 (上)
下一篇
Day21 - XDP概念
系列文
教練我想玩eBPF30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言