Try   HackMD

透過 Netfilter 自動過濾廣告

儘管我們可在網頁瀏覽器中透過像是 AdBlock 這類的 extension 來過濾廣告,但需要額外的設定和佔用更多系統資源,倘若我們能透過 netfilter,直接在核心層級過濾網路廣告,那所有應用程式都有機會受益。

TODO

實作

手動更改 iptable 以過濾廣告

實驗前清除所有規則

$ sudo iptables -F

使用命令查找目標網頁的資訊

$ nslookup youtube.com
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
Name:	youtube.com
Address: 142.251.42.238
Name:	youtube.com
Address: 2404:6800:4012:2::200e

youtube.com加入到 iptable 的 reject 名單

$ sudo iptables -A OUTPUT -d 142.250.0.0/15 -j REJECT

使用以下指令查看 iptable 內容

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
REJECT     all  --  anywhere             cg-in-f0.1e100.net/15  reject-with icmp-port-unreachable

REJECT 中指定了目標是cg-in-f0.1e100.net/15 ,這可能不是 YouTube 所有的 IP 地址,因此瀏覽器還可以順利打開

拒絕所有可能是 YouTube 的 IP 地址

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
REJECT     all  --  anywhere             cg-in-f0.1e100.net/15  reject-with icmp-port-unreachable
REJECT     all  --  anywhere             ord38s04-in-f0.1e100.net/16  reject-with icmp-port-unreachable
REJECT     all  --  anywhere             216-58-0-0.cpe.distributel.net/16  reject-with icmp-port-unreachable

測試連接 : 連接至youtube.com失敗

aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~$ curl -v http://youtube.com
*   Trying 172.217.160.78:80...
* connect to 172.217.160.78 port 80 failed: Connection refused
*   Trying 2404:6800:4012::200e:80...
* Immediate connect fail for 2404:6800:4012::200e: Network is unreachable
* Failed to connect to youtube.com port 80 after 12 ms: Connection refused
* Closing connection 0
curl: (7) Failed to connect to youtube.com port 80 after 12 ms: Connection refused

使用核心模組以過濾廣告

根據 Use the iptables firewall to block ads on your Linux machine 可以使用上述方式手動更改 iptable,但如果使用核心模組來過濾廣告不僅更靈活,同時也可以提高瀏覽器性能,主要差別如下:

  • 使用核心模組過濾可以根據域名進行過濾,而不需要依賴固定的 IP 地址
  • 核心模組過濾可以在解析階段就阻止廣告域名,從而防止任何針對這些域名的連接嘗試,而 iptables 規則只能在連接建立後起作用
  • 在 DNS 層過濾,可以更早地阻止廣告內容的加載,可能提高瀏覽器性能。

Netfilter 是一個框架,嵌入在 Linux 核心中,用於執行各種封包的操作任務,包括網路地址轉換 (NAT) 和封包過濾。它通過幾個預定義的鉤子 (hook) 在封包傳輸過程中運行。以下是五個主要鉤子及其用途:

  1. NF_IP_PRE_ROUTING 在做任何路由決策之前觸發,當作檢查和修改封包的初始點,適用於路由前的目標 NAT 和封包處理
  2. NF_IP_LOCAL_IN 針對發往本地系統的封包, 允許檢查和修改那些要交給本機的入站封包
  3. NF_IP_FORWARD 用於在系統中從一個網路介面轉發到另一個網路介面的封包,對應用轉發規則和過濾不屬於本地系統的封包非常重要
  4. NF_IP_POST_ROUTING 在所有路由決策做出之後但在封包實際傳送之前觸發,通常用於源 NAT 和在封包傳送前的附加封包處理
  5. NF_IP_LOCAL_OUT 針對本地系統生成的封包,在它們實際發出之前,提供修改出站封包、設置源 NAT 或應用其他出站過濾規則的機會

image
圖片來源 Netfilter Linux netfilter Hacking HOWTO: Netfilter Architecture

參照 adriver,使用 Linux 核心模組,即可在核心裡頭註冊不同的 hook 來處理封包,為了在使用者瀏覽網站時屏蔽廣告,因此 hook 必須作用在進入本機之前,也就是 Local In 或 Pre Routing

static struct nf_hook_ops my_ops = {
    .hook = my_hook,
    .pf = NFPROTO_IPV4,
    .hooknum = NF_INET_PRE_ROUTING,
    .priority = NF_IP_PRI_FIRST,
};

static int mod_init(void)
{
    return nf_register_net_hook(&net, &my_ops);
}

static void mod_exit(void)
{
    nf_unregister_net_hook(&net, &my_ops);
}

而 hook 函式需接受三個參數

  • priv: 為一個私有指針,通常用於傳遞 hook 函式自身需要的私有資料
  • skb: 是一個指向 Linux 中網路封包(socket buffer)的指針。在 netfilter 中,封包通過系統中的不同階段(如 INPUT、OUTPUT 等)時,可以被 hook 函式捕獲和處理。skb 對象包含了網路封包的所有信息,包括協定頭、有效資料等。
  • state: 是一個指向 nf_hook_state 構體的指針,這個結構體提供了有關 hook 點和系統狀態的詳細信息
static unsigned int my_hook(void *priv, 
                            struct sk_buff *skb, 
                            const struct nf_hook_state *state)
{
    ...

my_hook 函式主要由 should_run_get_sfilter()should_run_dns_sfilter() 組成,兩個 funtion 的功用在於檢查並提取傳入的封包是否為 DNS 封包或 HTTP封包,並將其資料存儲到 buf 結構體中

run_get_sfilters(&buf)run_dns_sfilters(&buf) 則將 buf 與預先設置好的 DNS 過濾規則或 HTTP 過濾規則做比對,若比對成功說明其封包包含與廣告相關的內容,因此在函式中將 ret 設置為 NF_DROP,即丟棄該封包

hook function 的返回值共五個:

  • NF_DROP丟棄封包
  • NF_ACCEPT允許封包通過
  • NF_STOLEN表示封包已被當前 hook 函式"偷走"。封包的所有權已經轉移到當前 hook 函式中,不在繼續沿Netfilter 處理路徑傳輸
  • NF_QUEUE將封包送往 nfqueue
  • NF_REPEAT重新呼叫這個 hook function
should_run_dns_sfilter(skb, &buf);
if (buf.data)
{
    if (run_dns_sfilters(&buf))
    {
        printk(KERN_INFO "dns filter matched, dropping");

        // just drop here, don't REJECT
        ret = NF_DROP;
        goto done;
    }
    ret = NF_ACCEPT;
    goto done;
}
// buf.data is NULL, no need to free

should_run_get_sfilter(skb, &buf);
if (buf.data)
{
    if (run_get_sfilters(&buf))
    {   
        printk(KERN_INFO "get filter matched, dropping");

        ret = NF_DROP;
        goto done;
    }
}
done:

從 UDP 通訊協定中獲取 DNS 資訊

檢查傳入的網路封包(skb)是否為 DNS 封包需經過一連串條件審核,步驟如下:

  1. skb是否不為空
  2. skb是否包含 ip_header
  3. ip_header 裡的協定是否為 UDP 通訊協定
  4. udp_header 是否不為空
  5. 確認埠號為 53
static void should_run_dns_sfilter(struct sk_buff *skb, struct buf *buf)
{
    struct iphdr *ip_header = NULL;
    struct udphdr *udp_header = NULL;

    if(!skb || !(ip_header = ip_hdr(skb)))
    {
        printk(KERN_INFO "Not IP");
        return;
    }
    if(IPPROTO_UDP != ip_header->protocol)
    {
        printk(KERN_INFO "Not UDP");
        return;
    }
    udp_header = udp_hdr(skb);
    if(!udp_header)
    {
        printk(KERN_INFO "Failed to get UDP header");
        return;
    }
    if(ntohs(udp_header->dest) == 53)
    {
        dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr));
        dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr);
    }
    ...
}

埠號 53 是用於 DNS(域名系統)服務的預設埠,且許多廣告和跟蹤服務使用 DNS 請求來進行域名解析,過濾埠號 53 上的數據包可以有效地阻止這些廣告和跟蹤請求

在確認某網路封包就是目標封包後,提取其 DNS 資訊,及下圖中的 Data。

image

udp_header 指標位置 + udphdr 長度,即為 Data 位置

dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr));
dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr);

dns_data ,即 DNS Message Packet,用 16 進位法表示如下:

de b2 01 20 00 01 00 00 00 00 00 01 09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72 00 00 1c 00 01 00 00 29 04 b0 00 00

前 12 個 bytes 為 DNS Header,分別代表:

  • ID(2bytes): 識別碼
  • Flag(2bytes): 用來描述 DNS 訊息封包的功能
    • QR: 0 表示此訊息是查詢,1 為回應訊息封包
    • OP-code: 表示封包的類型。0 是標準查詢,1 是反向查詢,2 是伺服器狀態查詢,3-15 保留未用
    • AA: 1 表示回應中的資料是授權資料,來自管轄的名稱伺服器
    • TC: 1 表示資料過長被截短,因為超過了 UDP 封包的限制(512 Bytes)
    • RD: 1 表示遞迴查詢,0 表示反覆查詢。通常由查詢封包設定,回應封包會反映同樣的設定
    • RA: 由回應的伺服器設定,1 表示支援遞迴
    • R-code: 在回應封包中表示查詢結果。0 沒有錯誤,1 封包格式錯誤,2 伺服器錯誤,3 名稱錯誤,4 不支援此查詢類型,5 拒絕查詢
  • Question Count(2bytes): 表示後面緊接著問題區段的數量
  • Answer RR Count(2bytes): 表示後面緊接著答案區段中資源紀錄(Resource Record, RR)的數量
  • Authority RR Count(2bytes): 權威區段的紀錄數量
  • Addition RR Count(2bytes):此欄位為增加權威區段中紀錄的數量

image

Question Name 欄位存放所欲查詢的 FQDN 名稱,每一名稱長度不定,因此,此欄位的長度也不定。網域名稱的存放是以 ASCII 字元表示,最長限制在 64 個字元之內,名稱中的『.』(dot),並不表示出來,而以字元計數取代。譬如:
09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72

  • Length: 9, Label: googleads
  • Length: 1, Label: g
  • Length: 11, Label: doubleclick
  • Length: 3, Label: net
  • Length: 11, Label: dlinkrouter
  • Null terminator: 00

Fully Qualified Domain Name: googleads.g.doubleclick.net.dlinkrouter

參考資料: DNS 訊息格式

封包在執行完 should_run_dns_sfilter 之後,若包含 DNS 資訊的話會將其存入 buf->data 裡,用來與 dns_sfilters[] 裡的字串做比對,若比對成功則丟棄封包,dns_sfilters[] 包含許多過濾規則,內容如下:

const struct sfilter dns_sfilters[] = {
	{3, {"media","admob","com" } }, 
 	{3, {"tpc","googlesyndication","com" } }, 
 	{3, {"pagead2","googlesyndication","com" } }, 
 	{3, {"xads","zedo","com" } }, 
 	{3, {"xads","zedo","com" } }, 
 	{3, {"ad","doubleclick","net" } }, 
 	{4, {"cm","g","doubleclick","net" } }
}

使用 sudo dmesg 查看核心訊息

[  702.524716] buf data: googleadsgdoubleclicknet
[  702.524718] DNS filter matched

[  703.106169] buf data: lexicon33acrosscom
[  703.106172] DNS filter matched

[  702.973604] buf data: securepubadsgdoubleclicknet
[  702.973606] DNS filter matched

從 TCP 通訊協定中獲取 HTTP 資訊

檢查傳入的網路封包(skb)是否為 DNS 封包需經過一連串條件審核,步驟如下:

  1. skb是否不為空
  2. skb是否包含 ip_header
  3. ip_header 裡的協定是否為 TCP 通訊協定
  4. tcp_header 是否不為空
static void should_run_get_sfilter(struct sk_buff *skb, struct buf *buf)
{
    struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
    struct tcphdr *tcp_header;
    
    if(!skb){
        return;
    }
    if (!iph || iph->protocol != IPPROTO_TCP) {
        printk(KERN_INFO "Not a TCP packet\n");
        return;
    }
    tcp_header = (struct tcphdr *) skb_transport_header(skb);
    if (!tcp_header) {
        printk(KERN_INFO "Failed to get TCP header\n");
        return;
    }
    ...
}

我在解析 TCP 通訊協定的資料時,獲取 payload 的方式與 UDP 一樣,但印出來的都是以 16 03 0317 03 03 為開頭且無法被閱讀的資料。我嘗試理解格式訊息發現資料使用 TLS 進行加密,因此無法被解析

16 03 03

  • 16 TLS Handshake protocol
  • 03 03 SSL 3.3 (TLS 1.2)
  • 00 7a Length of handshake message (122 bytes)

17 03 03

  • 17 TLS Application Data
  • 03 03 version 1.2
  • 00 45 length of encrypted data (69 bytes)

得到 tcp_header 後,嘗試讀取其 Source port 及 Destination port

[18156.615918] Source port: 443
[18156.615922] Destination port: 43550
[18156.615926] payload = 17 03 03 00 6a 85 d5 73 14 a2 23 72 ae be 80 82 ef 62 ae e9 27 f8 8b 03 38 b3 0e a5 a8 2b 59 e9 1c 74 6b d8 c7 a0 a8 0b 75 f2 6c 9b b3 fe 75 5f 57 ef 87 9a 19 cb 46 19 4d 73 ae 77 e8 75 8c 0d

埠號為 443 說明這個 TCP 連接是用於 HTTPS 通訊,是 HTTP 協定的安全版本,通過 TLS/SSL 進行加密,確保資料在傳輸過程中受到保護,這也解釋上述 payload 無法被解析的原因

透過掛載 module 實作

$ sudo insmod adblock.ko
$ lsmod | grep adblock

瀏覽網頁會發現許多廣告被屏蔽了

image

使用ifconfig查詢你的系統中有哪些網卡,並確定你要監聽的網卡名稱,須先下載套件net-tools

~~aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~/adriver$ ifconfig
eno2: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 04:42:1a:d1:d5:20  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 5280  bytes 495925 (495.9 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5280  bytes 495925 (495.9 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlo1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.100.110  netmask 255.255.255.0  broadcast 192.168.100.255
        inet6 fe80::9ff8:2596:4921:4e8f  prefixlen 64  scopeid 0x20<link>
        ether 20:1e:88:e9:bb:1e  txqueuelen 1000  (Ethernet)
        RX packets 140233  bytes 201059706 (201.0 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 56952  bytes 6138159 (6.1 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0~~

lo顯示 running 使用此網路介面

sudo tcpdump -ni lo -c 10 -t udp and dst host 127.0.0.1