eBPF Talk: 手撕 XDP 程序

前段時間,老闆讓我 手撕 分析 一個陌生的 XDP 程序。

責任重過山頭,莫敢推辭,只能硬着頭皮上了。

陌生的 XDP 程序

沒有源代碼,且發現該 XDP 程序還沒帶上 debug 信息;分析的難度急劇飆升。

不過,根據 XDP 程序開發的套路,還是能夠從 bpf 彙編分析一下 XDP 程序大概的處理邏輯。以下爲部分 bpf 彙編:

# bpftool p d x i 21712
   0: (bf) r3 = r1
   1: (b7) r0 = 1
   2: (79) r1 = *(u64 *)(r3 +8)
   3: (79) r4 = *(u64 *)(r3 +0)
   4: (bf) r2 = r4
   5: (07) r2 += 14
   6: (2d) if r2 > r1 goto pc+144
   7: (71) r6 = *(u8 *)(r4 +13)
   8: (67) r6 <<= 8
   9: (71) r2 = *(u8 *)(r4 +12)
  10: (4f) r6 |= r2
  11: (15) if r6 == 0xa888 goto pc+3
  12: (b7) r2 = 14
  13: (b7) r5 = 0
  14: (55) if r6 != 0x81 goto pc+7
  15: (bf) r2 = r4
  16: (07) r2 += 18
  17: (b7) r0 = 1
  18: (2d) if r2 > r1 goto pc+132
  19: (b7) r2 = 18
  20: (b7) r5 = 4
  21: (69) r6 = *(u16 *)(r4 +16)
  22: (bf) r0 = r6
  23: (57) r0 &= 65535
  24: (15) if r0 == 0x81 goto pc+2
  25: (15) if r0 == 0xa888 goto pc+1
  26: (05) goto pc+12
  27: (bf) r7 = r2
  28: (07) r7 += 4
  29: (bf) r6 = r4
  30: (0f) r6 += r7
  31: (b7) r0 = 1
  32: (2d) if r6 > r1 goto pc+118
  33: (bf) r0 = r4
  34: (07) r0 += 2
  35: (0f) r0 += r2
  36: (07) r5 += 4
  37: (69) r6 = *(u16 *)(r0 +0)
  38: (bf) r2 = r7
  39: (57) r6 &= 65535
  40: (b7) r0 = 2
  41: (15) if r6 == 0x8 goto pc+1
  42: (05) goto pc+108
  43: (bf) r7 = r4
  44: (0f) r7 += r2
  45: (bf) r6 = r7
  46: (07) r6 += 20
  47: (b7) r0 = 1
  48: (2d) if r6 > r1 goto pc+102
  49: (71) r2 = *(u8 *)(r7 +9)
  50: (b7) r0 = 2
  51: (55) if r2 != 0x6 goto pc+99
  52: (bf) r2 = r6
  53: (07) r2 += 20
  54: (b7) r0 = 1
  55: (2d) if r2 > r1 goto pc+95
  56: (69) r1 = *(u16 *)(r6 +2)
  57: (b7) r0 = 2
  58: (55) if r1 != 0xfcff goto pc+92
  59: (69) r1 = *(u16 *)(r6 +12)
  60: (57) r1 &= 512
  61: (b7) r0 = 2
  62: (15) if r1 == 0x0 goto pc+88
  63: ...
 131: (b7) r8 = 0
 132: (79) r1 = *(u64 *)(r10 -48)
 133: (05) goto pc+29
 134: (18) r1 = 0xa726f72726520
 136: (7b) *(u64 *)(r10 -8) = r1
 137: (18) r1 = 0x67616c665f706374
 139: (7b) *(u64 *)(r10 -16) = r1
 140: (18) r1 = 0x206563616c706572
 142: (7b) *(u64 *)(r10 -24) = r1
 143: (18) r1 = 0x5f6d7573635f346c
 145: (7b) *(u64 *)(r10 -32) = r1
 146: (bf) r1 = r10
 147: (07) r1 += -32
 148: (b7) r2 = 32
 149: (85) call bpf_trace_printk#-55408
 150: (b7) r0 = 1
 151: (95) exit
 152: ...
 196: (b7) r1 = 0
 197: (73) *(u8 *)(r10 -2) = r1
 198: (b7) r1 = 2674
 199: (6b) *(u16 *)(r10 -4) = r1
 200: (b7) r1 = 1869771365
 201: (63) *(u32 *)(r10 -8) = r1
 202: (18) r1 = 0x207165735f6b6361
 204: (7b) *(u64 *)(r10 -16) = r1
 205: (18) r1 = 0x206563616c706572
 207: (7b) *(u64 *)(r10 -24) = r1
 208: (18) r1 = 0x5f6d7573635f346c
 210: (7b) *(u64 *)(r10 -32) = r1
 211: (bf) r1 = r10
 212: (07) r1 += -32
 213: (b7) r2 = 31
 214: (05) goto pc-66
 215: ...

參考

enum xdp_action {
    XDP_ABORTED = 0,
    XDP_DROP,
    XDP_PASS,
    XDP_TX,
    XDP_REDIRECT,
};

給 bpf 彙編加上一些註釋:

# bpftool p d x i 21712
   0: (bf) r3 = r1
   1: (b7) r0 = 1                       // XDP_DROP
   2: (79) r1 = *(u64 *)(r3 +8)
   3: (79) r4 = *(u64 *)(r3 +0)
   4: (bf) r2 = r4
   5: (07) r2 += 14
   6: (2d) if r2 > r1 goto pc+144
   7: (71) r6 = *(u8 *)(r4 +13)
   8: (67) r6 <<= 8
   9: (71) r2 = *(u8 *)(r4 +12)
  10: (4f) r6 |= r2
  11: (15) if r6 == 0xa888 goto pc+3    // VLAN 8021AD
  12: (b7) r2 = 14
  13: (b7) r5 = 0
  14: (55) if r6 != 0x81 goto pc+7      // VLAN 8021Q
  15: (bf) r2 = r4
  16: (07) r2 += 18
  17: (b7) r0 = 1                       // XDP_DROP
  18: (2d) if r2 > r1 goto pc+132
  19: (b7) r2 = 18
  20: (b7) r5 = 4
  21: (69) r6 = *(u16 *)(r4 +16)
  22: (bf) r0 = r6
  23: (57) r0 &= 65535
  24: (15) if r0 == 0x81 goto pc+2      // VLAN 8021Q
  25: (15) if r0 == 0xa888 goto pc+1    // VLAN 8021AD
  26: (05) goto pc+12
  27: (bf) r7 = r2
  28: (07) r7 += 4
  29: (bf) r6 = r4
  30: (0f) r6 += r7
  31: (b7) r0 = 1                       // XDP_DROP
  32: (2d) if r6 > r1 goto pc+118
  33: (bf) r0 = r4
  34: (07) r0 += 2
  35: (0f) r0 += r2
  36: (07) r5 += 4
  37: (69) r6 = *(u16 *)(r0 +0)
  38: (bf) r2 = r7
  39: (57) r6 &= 65535
  40: (b7) r0 = 2                       // XDP_PASS
  41: (15) if r6 == 0x8 goto pc+1       // ETH_P_IP
  42: (05) goto pc+108
  43: (bf) r7 = r4
  44: (0f) r7 += r2
  45: (bf) r6 = r7
  46: (07) r6 += 20                     // iphdr
  47: (b7) r0 = 1                       // XDP_DROP
  48: (2d) if r6 > r1 goto pc+102
  49: (71) r2 = *(u8 *)(r7 +9)
  50: (b7) r0 = 2                       // XDP_PASS
  51: (55) if r2 != 0x6 goto pc+99      // IPPROTO_TCP
  52: (bf) r2 = r6
  53: (07) r2 += 20                     // tcphdr
  54: (b7) r0 = 1                       // XDP_DROP
  55: (2d) if r2 > r1 goto pc+95
  56: (69) r1 = *(u16 *)(r6 +2)
  57: (b7) r0 = 2                       // XDP_PASS
  58: (55) if r1 != 0xfcff goto pc+92   // dport 65532
  59: (69) r1 = *(u16 *)(r6 +12)
  60: (57) r1 &= 512
  61: (b7) r0 = 2                       // XDP_PASS
  62: (15) if r1 == 0x0 goto pc+88      // tcp flags, not SYN
  63: ...
 131: (b7) r8 = 0
 132: (79) r1 = *(u64 *)(r10 -48)
 133: (05) goto pc+29
 134: (18) r1 = 0xa726f72726520
 136: (7b) *(u64 *)(r10 -8) = r1
 137: (18) r1 = 0x67616c665f706374
 139: (7b) *(u64 *)(r10 -16) = r1
 140: (18) r1 = 0x206563616c706572
 142: (7b) *(u64 *)(r10 -24) = r1
 143: (18) r1 = 0x5f6d7573635f346c
 145: (7b) *(u64 *)(r10 -32) = r1
 146: (bf) r1 = r10
 147: (07) r1 += -32
 148: (b7) r2 = 32
 149: (85) call bpf_trace_printk#-55408     // print "l4_csum_replace tcp_flag error\n"
 150: (b7) r0 = 1                           // XDP_DROP
 151: (95) exit
 152: ...
 196: (b7) r1 = 0
 197: (73) *(u8 *)(r10 -2) = r1
 198: (b7) r1 = 2674
 199: (6b) *(u16 *)(r10 -4) = r1
 200: (b7) r1 = 1869771365
 201: (63) *(u32 *)(r10 -8) = r1
 202: (18) r1 = 0x207165735f6b6361
 204: (7b) *(u64 *)(r10 -16) = r1
 205: (18) r1 = 0x206563616c706572
 207: (7b) *(u64 *)(r10 -24) = r1
 208: (18) r1 = 0x5f6d7573635f346c          // l4_csum_replace ack_seq erro
 210: (7b) *(u64 *)(r10 -32) = r1
 211: (bf) r1 = r10
 212: (07) r1 += -32
 213: (b7) r2 = 31
 214: (05) goto pc-66
 215: ...

結合兩個 bpf_trace_printk() 可知,這個 XDP 程序對目的端口爲 65532 的 TCP SYN 包進行了網絡包修改。

儘管分析的結果還不夠透徹,但已滿足老闆的預期,就不再繼續分析了。

分析過程

XDP 套路一:判斷協議

在 XDP 程序裏處理網絡包,首先需要判斷的就是當前網絡包是不是預期的那個;如果不是,則 XDP_PASS

而在,判斷的過程中,如果發現是畸形包,則直接 XDP_DROP

上面的 XDP 程序就判斷了 ETH_P_8021Q、ETH_P_8021AD、ETH_P_IPIPPROTO_TCP 等協議。

比如:

eth = (typeof(eth)) xdp->data;
if (eth->h_proto == ETH_P_8021Q || eth->h_proto == ETH_P_8021AD)
    eth = (void *) (eth + 1) + sizeof(struct vlan_hdr);

XDP 套路二:判斷範圍

在讀取網絡包內容前,必須先判斷讀取的內存有沒有超出網絡包內容的範圍了。

上面的 XDP 程序裏,如果超出網絡包內容的範圍,就 XDP_PASS。

比如:

eth = (typeof(eth)) xdp->data;
if ((void *) (eth + 1) > xdp->data_end)
    return XDP_PASS;

XDP 套路三:bpf_trace_printk() 的使用

其實,這是大部分 bpf 程序的套路了。

這是因爲 bpf_trace_printk() 所使用的字符串都保存到棧上,如:

 134: (18) r1 = 0xa726f72726520
 136: (7b) *(u64 *)(r10 -8) = r1
 137: (18) r1 = 0x67616c665f706374
 139: (7b) *(u64 *)(r10 -16) = r1
 140: (18) r1 = 0x206563616c706572
 142: (7b) *(u64 *)(r10 -24) = r1
 143: (18) r1 = 0x5f6d7573635f346c
 145: (7b) *(u64 *)(r10 -32) = r1
 146: (bf) r1 = r10
 147: (07) r1 += -32
 148: (b7) r2 = 32
 149: (85) call bpf_trace_printk#-55408     // print "l4_csum_replace tcp_flag error\n"

通過 r1 = 0xa726f72726520; *(u64 *)(r10 -8) = r1 的方式,將字符串保存到棧上,然後準備好 bpf_trace_printk() 的參數 R1R2,最後 call bpf_trace_printk#-55408;這裏,R1 是指向字符串保存的棧空間的地址的指針,R2 是字符串大小 32 字節。

所以,可以將保存到棧上的字符串給翻譯出來:

s := []uint64{0x5f6d7573635f346c, 0x206563616c706572, 0x67616c665f706374, 0xa726f72726520}
b := (*byte)(unsafe.Pointer(&s[0]))
fmt.Println(unsafe.String(b, len(s)*8))
// l4_csum_replace tcp_flag error

這裏就是 bpf_printk("l4_csum_replace tcp_flag error\n") 了。

小結

只要掌握了那些 XDP(bpf)程序的開發套路,分析陌生的 XDP 程序就不在話下。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Vr7At9-wG3p6mfY6L49nyg