# Netfilter-Adblock ## Reference >[Linux 核心專題: 透過 Netfilter 自動過濾廣告 24/07/06](https://hackmd.io/@sysprog/SyKqH2ILA) >[Linux 專題: 透過 Netfilter 自動過濾廣告 23/06/24](https://hackmd.io/@sysprog/BJb0NRYH3) >[透過 Netfilter 自動過濾廣告 21/01/20](https://hackmd.io/@ZhuMon/2020q1_final_project) >[eBPF Practical Tutorial: Capturing SSL/TLS Plain Text Using uprobe](https://medium.com/@yunwei356/ebpf-practical-tutorial-capturing-ssl-tls-plain-text-using-uprobe-fccb010cfd64) >[github: bpf-developer-tutorial](https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff) >[Debugging with eBPF Part 3: Tracing SSL/TLS connections](https://blog.px.dev/ebpf-openssl-tracing/) ### 透過 mapping 方式阻擋廣告 嘗試透過 [ZhuMon](https://hackmd.io/@ZhuMon/2020q1_final_project#%E5%B0%87%E7%B6%B2%E5%9F%9F-mapping-%E5%88%B0-localhost) 先前有實作過的方式將網域 mapping 到 localhost 將要 block 的網域名稱寫入 `/etc/hosts` 格式為 ``` 0.0.0.0 ads.example.com ``` 我們將 [StevenBlack 專案](https://github.com/StevenBlack/hosts/blob/master/hosts)提供的檔案寫入 `/etc/hosts` 可以看到以下實驗效果,廣告處會無法正常顯示廣告 ![image](https://hackmd.io/_uploads/Bk81Lnysgg.png) #### 為甚麼這個方法有效 大多數 Linux 系統的名稱解析順序由 /etc/nsswitch.conf 控制 ```shell # /etc/nsswitch.conf ... hosts: files mdns4_minimal [NOTFOUND=return] dns ``` 這行是系統在做「主機名稱解析」時的查詢順序與規則: - files:先查本機檔案(/etc/hosts)。所以把廣告網域寫進 /etc/hosts,會優先被採用。 - mdns4_minimal:用 mDNS 查像是 xxx.local 這類區域網路名稱的最小解析器。 - [NOTFOUND=return]:如果上一個來源(mdns4_minimal)回報 NOTFOUND,就立刻停止,不要再往後(dns)查。這通常是為了避免把 .local 名稱外送到公網 DNS。 - dns:最後才問一般的 DNS 伺服器。 --- ```clike static atomic_t device_opened = ATOMIC_INIT(0); ``` 當使用者空間程式開啟 /dev/adbdev 裝置時,會把 device_opened 設為 1;當關閉裝置時,會設為 0 ## kadblock.c ### mod_init() 模組的啟動入口,負責註冊字元設備、初始化資料結構,並把 Netfilter 鉤子掛上去 ```clike static int mod_init(void) { major = register_chrdev(0, DEV_NAME, &fops); if (major < 0) { pr_alert("Registering char device failed with %d\n", major); return major; } cls = class_create(THIS_MODULE, DEV_NAME); device_create(cls, NULL, MKDEV(major, 0), NULL, DEV_NAME); init_verdict(); return nf_register_net_hook(&init_net, &blocker_ops); } ``` [register_chrdev()](https://github.com/huaweicloud/huaweicloud-sdk-c-obs/blob/dca94d46fca3071fbe4478552df2dc0b2ed628c5/CI/rule/pclint/pclint_include/include_linux/linux/fs.h#L2405) ```clike static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); } ``` - major : 要註冊的 major number,若傳 0,kernel 會自動分配一個 - name : 裝置名稱,用於 /proc/devices 與內部識別 - fops : 指向 struct file_operations 的指標,定義 open/read/write 等行為 return - ≥0 : 成功,回傳分配的 major number(若你傳入 0)或你指定的值 - <0 : 失敗,通常是 -EBUSY(已存在)、-ENOMEM(資源不足)等 `pr_alert("msg")` 等同 `printk(KERN_ALERT, "msg")` ```clike return nf_register_net_hook(&init_net, &blocker_ops); ``` 在 IPv4 的 NF_INET_LOCAL_OUT(本機端輸出)階段,掛載 blocker_ops 所指向的 blocker_hook,使所有本機發出的封包都能進入此 callback 進行廣告過濾或重置連線處理。 最後把 nf_register_net_hook 的回傳值直接 return 回 mod_init 是為了確保:若 hook 註冊失敗(如記憶體不足或參數不合法),mod_init 會回傳非零錯誤碼,導致 insmod 中止模組載入,避免模組載入後無法運作卻仍留在核心中。 ```clike int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops); ``` > 參考: https://blogs.oracle.com/linux/post/introduction-to-netfilter 用來在指定的 network namespace 中註冊一個 packet hook。註冊後,當封包在這個 namespace 中流經某個預定義的階段(如 NF_IP_PRE_ROUTING, NF_IP_LOCAL_IN 等)時,核心就會依序呼叫對應的 callback 函式,讓模組得以檢查、修改或丟棄封包。 [nf_hook_ops](https://github.com/torvalds/linux/blob/78f4e737a53e1163ded2687a922fce138aee73f5/include/linux/netfilter.h#L97) `struct net *net`:指定要掛鉤的 network namespace;一般核心模組都傳入全域的 &init_net 表示預設 IPv4 namespace。 `const struct nf_hook_ops *ops`:指向一個 nf_hook_ops 結構,內含: - hook:pointer to the hook function(nf_hookfn),回傳值須是 NF_DROP、NF_ACCEPT、NF_QUEUE 等。 - pf:protocol family(如 PF_INET 表示 IPv4)。 - hooknum:hook 類型(NF_INET_PRE_ROUTING、NF_INET_LOCAL_OUT、…)。 - priority:相同 hook 下多個 callback 的執行順序,由低到高依序呼叫。 [device class](https://docs.kernel.org/5.10/driver-api/driver-model/class.html) `mod_init` 流程: 呼叫 `register_chrdev(0, "adbdev", &fops)`,動態取得一個 major number,註冊字元設備,接著透過 `class_create("adbdev")` 先在 /sys/class 建 class adbdev,再用 `device_create `建立真正的 /dev/adbdev 節點,這個字元裝置就是作為 userspace 與 kernel module 之間的「單向訊息通道」,其透過 `adbdev_open/adbdev_release` 的 atomic 變數確保同一時間只有一個使用者程式能夠 open 這個裝置,避免多個程式同時送 verdict 衝突。 另外,該模組把 .llseek 這個檔案操作函式掛在裝置上,讓 user-space 只要對 /dev/adbdev 呼叫 `lseek(fd, offset, SEEK_SET)`,kernel 就會進到 adbdev_lseek,要注意的是 offset 參數在這裡 不是單純用來移動檔案指標,而是 攜帶「哪個 PID」要被封鎖的訊息。 >***為甚麼要這麼做 ?*** TLS 流量是加密的,內核無法直接在 hook 裡看明文,該專案裡用 BPF 程式+使用者空間程式去截 SSL_write 裡的明文、做正規比對後才知道要不要擋。一旦使用者空間程式(如 ssl_sniff.c)檢測到「是廣告請求」,就需要告訴 kernel:「接下來這個 PID 的所有封包都要 drop/reset」。這時就打開 /dev/adbdev,對它做 lseek(pid, …),kernel 在 adbdev_lseek 看到後,把這個 PID 記到 verdict_head,blocker_hook 以後看見相同 PID 就會丟棄封包 最後 `return nf_register_net_hook(&init_net, &blocker_ops)` 代表成功用 insmod adblock.ko 載入模組之後,在 init_net(即 global 而非自創 netns) 這個 namespace 中,所有從本機送出的 IPv4 封包(經過 Netfilter 的 NF_INET_LOCAL_OUT 階段)都會觸發在 blocker_ops 裡面註冊的 `blocker_hook` 函式。 `blocker_hook` 會先確認 /dev/adblock 是否被打開( `atomic_read(&device_opened)`) 及 封包是否為 tls 封包( `data[0] == 0x17`),若上述條件符合則會將該封包插入 `order_head` (根據時間戳由小到大) 及 container 為 queue_st,接著 `poll_verdict` 用 timestamp(剛才排入的 order)和 pid 作 key,到 verdict_head 中尋找 user-space 回報的結果,回傳 0(放行)或 1(封鎖),若還沒回報則回 -1 :::info 1. mod_init 及 mod_exit 可以分別加上 \_\_init 及 \_\_exit 嗎? 這樣是否會節省記憶體使用,\_\_init 及 \_\_exit 具體行為尚待釐清 https://fastbitlab.com/linux-device-driver-programming-lecture-18-__init-and-__exit-macros/ init_verdict() 在 verdict_ssl.c 中 ```clike void init_verdict(void) { INIT_LIST_HEAD(&order_head); INIT_LIST_HEAD(&verdict_head); } ``` 2. linux network namespace 是甚麼需要再了解 [network_namespaces(7) — Linux manual page](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) [Network Namespaces Basics Explained in 15 Minutes](https://www.youtube.com/watch?v=j_UUnlVC2Ss) 有圖文講解 !!! 3. 現在的 `mod_init` 是回傳 nf_register_net_hook(...),那如果 hook 註冊失敗,先前創造的 device 依然存在但功能會缺失,所以應該要自動清除,因此是否要改成 ```diff -static int mod_init(void) +static int __init mod_init(void) { + int ret; + major = register_chrdev(0, DEV_NAME, &fops); if (major < 0) { - pr_alert("Registering char device failed with %d\n", major); + pr_alert("Registering char device failed: %d\n", major); return major; } - cls = class_create(DEV_NAME); - device_create(cls, NULL, MKDEV(major, 0), NULL, DEV_NAME); + + cls = class_create(THIS_MODULE, DEV_NAME); + if (IS_ERR(cls)) { + ret = PTR_ERR(cls); + unregister_chrdev(major, DEV_NAME); + pr_alert("class_create failed: %d\n", ret); + return ret; + } + + dev = device_create(cls, NULL, MKDEV(major, 0), NULL, DEV_NAME); + if (IS_ERR(dev)) { + ret = PTR_ERR(dev); + class_destroy(cls); + unregister_chrdev(major, DEV_NAME); + pr_alert("device_create failed: %d\n", ret); + return ret; + } class_destroy(cls); unregister_chrdev(major, DEV_NAME); - nf_unregister_net_hook(&init_net, &blocker_ops); + pr_info("adblock module unloaded\n"); } ``` [Error Codes in Linux](https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html) ::: ### init_verdict() ```clike // /verdict_ssl.c void init_verdict(void) { INIT_LIST_HEAD(&order_head); INIT_LIST_HEAD(&verdict_head); } ``` - order_head:當核心模組在 Netfilter hook 偵測到一筆 TLS 封包(`data[0] == 0x17`)時,會呼叫 `insert_order(timestamp)`,在此串列中插入一個含有該次時間戳的節點,作為「等待 user-space 回報」的標記 - verdict_head:user-space 的 BPF 監聽程式(ssl_sniff.c)檢測到明文請求含廣告字串後,透過對 /dev/adbdev 的 `lseek(pid | result<<31, …)` 呼叫,把「PID + 決策結果」送回核心,核心模組在 `insert_verdict(pid_with_flag)` 中把這筆帶有結果標誌的 pid 節點插入 verdict_head。 ### extract_tcp_data() ```clike static int extract_tcp_data(struct sk_buff *skb, char **data) { struct iphdr *ip = NULL; struct tcphdr *tcp = NULL; size_t data_off, data_len; if (!skb || !(ip = ip_hdr(skb)) || IPPROTO_TCP != ip->protocol) return -1; // not ip - tcp if (!(tcp = tcp_hdr(skb))) return -1; // bad tcp /* data length = total length - ip header length - tcp header length */ data_off = ip->ihl * 4 + tcp->doff * 4; data_len = skb->len - data_off; if (data_len == 0) return -1; if (skb_linearize(skb)) return -1; *data = skb->data + data_off; return data_len; } ``` ```clike data_off = ip->ihl * 4 + tcp->doff * 4; data_len = skb->len - data_off; ``` `ip->ihl` 為 Header 長度(以 32-bit words 為單位),因此乘以 4 後即為 IP header 的實際 byte 數,`tcp->doff` 是 TCP Header Length(同樣單位為 32-bit words) `skb_linearize` 會嘗試將 fragmented 資料整併為線性資料區塊,回傳非 0 表示失敗 :::info 1. 為甚麼 data->off 只需要計算 IPheader, TCPheader 的偏移量? 因為在 Netfilter HOOK 階段,skb->data 已經指到 IP header,MAC header 已經於先前被跳過,具體來說如下:( gpt 說的 有待求證) eth_type_trans()(由 driver 呼叫)中透過 skb_pull() 從 MAC header 拉到 IP header 這是 在 Netfilter NF_INET_PRE_ROUTING 之前發生的事 所以其實在 hook 階段時 `ip = ip_hdr(skb)` 其實等價於 `ip = skb->data` 2. 為甚麼資料會有 fragmented 的情況? ::: `skb->len` = `skb->tail` - `skb->data` ### block_hook() ```clike char *data = NULL; char *host = NULL; int result = 0; int len; ``` - data:用來接收指向 payload 的指標。 - host:暫存 DNS 解析出的域名字串。 - result:最終封鎖決策;>0 表示此封包應被丟棄。 - len:payload 長度。 先確認是否為 ```clike if (len > 0) { ... if (atomic_read(&device_opened) && data[0] == 0x17) { /* TLS application */ ktime_t time = ktime_get(); struct queue_st *order = insert_order(time); result = -1; while (result == -1 && ktime_to_ms(ktime_sub(ktime_get(), time)) < 50) { result = poll_verdict(time, current->pid); } if (result == -1) { list_del(&order->head); kfree(order); } } ``` `atomic_read(&device_opened)`:只有當使用者態程式(如 ssl_sniff.c)已經以 open("/dev/adbdev") 啟動 `ktime_t time = ktime_get();` 取得當前的高精度時間(nanosecond 精度),作為該封包的唯一「時間戳記」,也是暫存隊列的識別碼 `struct queue_st *order = insert_order(time);` 新增一個封包處理單(order)到 order_head 鍊結串列中,order 結構儲存該封包的時間戳記與用來比對的狀態,目的是等一下讓 user-space 根據時間戳去回傳 verdict `result = -1;` 初始設為未決定狀態 `while(...){...}` 不斷輪詢 poll_verdict() 函式,查看有沒有 user-space 程式根據這個時間戳傳入 verdict,最多等 50ms。如果超過時間或得到結果就跳出。 ```clike static loff_t adbdev_lseek(struct file *file, loff_t offset, int orig) { pid_t pid = offset; insert_verdict(pid); return 0; } static struct file_operations fops = {.owner = THIS_MODULE, .open = adbdev_open, .release = adbdev_release, .llseek = adbdev_lseek}; ``` 當呼叫 ssl_sniff.c 中的 `handle_sniff()`->`lseek(fd, pid | (verdict << 31), 0);` 時,在 kernel 就會觸發 `adbdev_lseek()` ### verdict ## verdict_ssl.c 核心模組攔截到 TLS 封包時不立即決定是否放行,而是暫存該封包的處理單 queue_st 到 order_head 串列中,讓 user-space 來分析封包內容並做決定。 ### insert_order() ```clike int ret = mutex_trylock(&insert_mutex); if (ret != 0) { ... } return NULL; } ``` 嘗試取得互斥鎖,避免其他執行緒同時修改 order_head,若沒成功,就不插入任何東西,返回 NULL :::info 1. 甚麼時候會有其他執行緒? 2. mutex_trylock() return 甚麼? A: mutex_trylock() 與 mutex_lock() 差異: mutex_trylock() : 嘗試取得鎖,如果鎖已被持有,不會阻塞,而是立即回傳 0,取得成功則回傳 1 mutex_lock(): 嘗試取得鎖,如果鎖已經被其他 context 持有,會進入 blocking 直到可以取得鎖為止 ::: ```clike list_for_each (cur, &order_head) { order = list_entry(cur, struct queue_st, head); if (order->timestamp < timestamp) break; } ``` 為了讓 order_head 保持依照 timestamp 排序,找到 第一個 timestamp 小於新封包的 entry,然後用 list_add() 把新的 order 插在它前面order_head 會變成「由新到舊」(時間遞減)的順序 :::info 1. 為甚麼 order_head 順序是時間遞減,這樣舊的封包不就要等新進來的先處理完嗎? 舊的有可能都沒辦法被處理到 A: 因為 `poll_verdict()` 內的 `first = list_first_entry(&order_head, struct queue_st, head); if (!first || first->timestamp != timestamp) return -1;` 是將最新封包跟 linked list 第一筆比較 timestamp 那如果真是如此,為甚麼 `insert_order()` 中會使用 `list_add(&order->head, cur);` 這樣似乎會出錯。 例子: order_head → [A:300] → [B:200] → [C:100] → NULL ```clike list_for_each (cur, &order_head) { order = list_entry(cur, struct queue_st, head); if (order->timestamp < 250) break; } ``` 第一次:order = A,timestamp = 300 → 300 > 250,不 break 第二次:order = B,timestamp = 200 → 200 < 250 → break! 此時 cur 指到 B 節點 → 所以接下來執行`list_add(&new_order->head, cur);` 把新節點插入在 cur(B)後面 → 出錯 !!! order_head → [A:300] → [B:200]→ [new:250] → [C:100] 所以是否應將 `list_add(&order->head, cur);` 改為 `list_add_tail(&order->head, cur);` ?? [list_add source code](https://github.com/torvalds/linux/blob/master/include/linux/list.h#L146) ```clike /* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } /** * list_add - add a new entry * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. */ static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } ``` 經過測試(在 mod_init 時 加入幾筆 insert_order() 並印出排序結果)後,原先程式確實無法正確排序,因此改為: ```diff struct queue_st *insert_order(ktime_t timestamp) { - int ret = mutex_trylock(&insert_mutex); - if (ret != 0) { - struct list_head *cur; - struct queue_st *order; - list_for_each (cur, &order_head) { - order = list_entry(cur, struct queue_st, head); - if (order->timestamp < timestamp) - break; - } - order = kmalloc(sizeof(struct queue_st), GFP_KERNEL); - order->timestamp = timestamp; - list_add(&order->head, cur); - mutex_unlock(&insert_mutex); - return order; + struct list_head *pos; + struct queue_st *entry; + struct queue_st *new = kmalloc(sizeof(*new), GFP_KERNEL); + if (!new) return NULL; + new->timestamp = timestamp; + + mutex_lock(&insert_mutex); + /* 找到第一個 timestamp > new 的節點 */ + list_for_each(pos, &order_head) { + entry = list_entry(pos, struct queue_st, head); + if (entry->timestamp > timestamp) + break; } - return NULL; + + list_add_tail(&new->head, pos); + mutex_unlock(&insert_mutex); + return new; } ``` 2. 甚麼情況會有在 kernel-space 時,封包不按照 timestamp call inser_order() 的情況 ? 如果不會發生這種狀況,那是不是可以不用再走訪 order_head,而是直接插入 head 的下一個。 ::: ```clike order = kmalloc(sizeof(struct queue_st), GFP_KERNEL); order->timestamp = timestamp; list_add(&order->head, cur); ``` :::info 1. 為甚麼這邊 kmalloc 的 flag 是使用 GFP_KERNEL ? GFP_KERNEL 跟其他 flag 差異是甚麼 ? ::: ### insert_verdict() 這個函式的目的是:接收來自 user-space (ssl_sniff.c) 的判決資訊,並加到 verdict_head 鏈結串列中 ```clike void insert_verdict(pid_t pid) { struct queue_st *verdict; verdict = kmalloc(sizeof(struct queue_st), GFP_KERNEL); // 分配記憶體 verdict->pid = pid; // 儲存含判決的 pid list_add_tail(&verdict->head, &verdict_head); // 插入鏈結串列末端 } ``` pid 設計: - MSB:代表判決結果(1=block, 0=allow) - 其餘 31 位元:代表 pid_t,即使用者程式的 PID :::info 1. 為甚麼會想到 pid 這樣設計 ? 以及為甚麼會想到以 union 作為 queue_st 的 member ```clike struct queue_st { struct list_head head; union { ktime_t timestamp; pid_t pid; }; }; ``` 2. 為甚麼是使用 list_add_tail 插入末端而不是頭 ? 3. 為甚麼 order_head, verdict_head 都是使用 queue_st ? ::: ### poll_verdict() poll_verdict(timestamp, pid) 比對第一筆 order_head 的 timestamp 是否為我們要等的,再檢查 verdict_head 中有沒有這個 pid 的結果,如果有就回傳 0 或 1(NF_ACCEPT 或 NF_DROP),同時刪掉 queue 與 verdict ```clike= first = list_first_entry(&order_head, struct queue_st, head); if (!first || first->timestamp != timestamp) return -1; ``` :::info 為甚麼只跟第一個 element 比 ? ::: ```clike list_for_each_entry (verdict, &verdict_head, head) { pid_t cpid = verdict->pid & ((1U << 31) - 1); int result = (u32) verdict->pid >> 31; if (cpid == pid) { ret = result; break; } } ``` ```clike list_del(&verdict->head); list_del(&first->head); kfree(verdict); kfree(first); ``` 即使 ret = -1,也會清除該筆封包與 verdict。這設計是強制一筆封包最多等一次 | 函式 | 來源 | 作用 | | ------------------ | -------------------------- | -------------------------------- | | `insert_order()` | kernel | 暫存等待判決的封包(由 TLS 封包觸發) | | `insert_verdict()` | user-space (`ssl_sniff.c`) | 回傳對某筆封包的允許/封鎖判斷 | | `poll_verdict()` | kernel | 驗證是否已有 verdict 結果(最多等 50ms)並做出動作 | ``` 【封包進入】 ↓ blocker_hook() ↓ insert_order(time) ➜ 將封包 request 放入 order_head(kernel) ↓ 等待 user-space 判決(最多 50ms) ↓ ssl_sniff.c: - 收到 perf buffer 資料(TLS 明文) - 執行 regex 比對 - 組出 pid | (result << 31) - 執行:lseek(fd, verdict, 0); → 傳入 /dev/adbdev(寫入 verdict) ↓ insert_verdict() ➜ 把結果寫入 verdict_head(kernel) ↓ poll_verdict() ➜ 依 timestamp + pid 比對,回傳 NF_DROP / NF_ACCEPT ``` :::danger insert_verdict() 與 poll_verdict() 如何與 user-space 和 kernel-space 互動尚不了解 !! ::: ## ssl_sniff.c ssl_sniff.c 是使用者空間程式,它透過 eBPF perf buffer 監控 TLS 封包內容(在函式像是 SSL_write() 中攔截明文),再根據正規表示式比對是否為廣告,並透過寫入 /dev/adbdev 回報封包判決(允許或封鎖)給 kernel 模組 adblock.ko ``` ╔════════════════════════════╗ ║ TLS 封包送出 ║ ╚════════════════════════════╝ │ ▼ eBPF (ssl_sniff.bpf.c) 用 UProbe 攔截 SSL_write() │ ▼ perf buffer 傳送明文封包到 ssl_sniff 程式(user-space) │ ▼ regex 比對是否包含 ad 字樣 │ ▼ 組合判決結果 ➜ 寫入 /dev/adbdev → 通知 kernel 模組 │ ▼ kernel 的 blocker_hook ➜ poll_verdict() ➜ NF_DROP 或 NF_ACCEPT ``` ### handle_sniff() ```clike void handle_sniff(void *ctx, int cpu, void *data, unsigned int data_sz) { struct data_t *d = data; uint32_t result = 0; if (d->buf[0] == 'G' && d->buf[1] == 'E' && d->buf[2] == 'T') { int r = regexec(&preg, d->buf, 0, NULL, 0); if (!r) result = 1; } lseek(fd, d->pid | result << 31, 0); } ``` 透過 lseek() 寫入一個含 MSB 判斷結果的 pid,驅動 kernel /dev/adbdev 的 .llseek() 實作,其中.llseek() 中呼叫的是 `insert_verdict(pid);` :::info 1. 為什麼要用 lseek,而不是使用 ioctl ? 2. 兩者在效能上有什麼差異? ::: ### main ```clike int main(int argc, char *argv[]) { if (argc == 1 || !(argc & 1)) { printf("wrong argument count\n"); printf("Usage: %s <libpath1> <func1> <libpath2> <func2>\n", argv[0]); exit(0); } int ret = regcomp(&preg, regexp, REG_NOSUB | REG_ICASE); assert(ret == 0); struct ssl_sniff_bpf *skel; struct perf_buffer *pb = NULL; skel = ssl_sniff_bpf__open_and_load(); if (!skel) { fprintf(stderr, "Failed to open and load BPF skeleton\n"); return 1; } for (int i = 1; i < argc; i += 2) { printf("Attaching %s in %s\n", argv[i + 1], argv[i]); struct bpf_uprobe_opts *ops = malloc(sizeof(struct bpf_uprobe_opts)); ops->sz = sizeof(*ops); ops->ref_ctr_offset = 0x6; ops->retprobe = false; ops->func_name = argv[i + 1]; struct bpf_link *link = bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_write, -1, argv[i], 0, ops); if(!link) printf("Error attaching %s in %s\n", argv[i + 1], argv[i]); } pb = perf_buffer__new(bpf_map__fd(skel->maps.tls_event), 8, &handle_sniff, NULL, NULL, NULL); if (libbpf_get_error(pb)) { fprintf(stderr, "Failed to create perf buffer\n"); return 0; } printf("Opening adbdev...\n"); fd = open("/dev/adbdev", O_WRONLY); if (fd < 0) { printf( "Failed to open adbdev.\nIt could be due to another program" "using it or the kernel module not being loaded.\n"); exit(1); } printf("All ok. Sniffing plaintext now\n"); while (1) { int err = perf_buffer__poll(pb, 1); if (err < 0) { printf("Error polling perf buffer: %d\n", err); break; } } return 0; } ``` [libbpf](https://libbpf.readthedocs.io/en/latest/api.html) ```clike LIBBPF_API struct bpf_link * bpf_program__attach_uprobe ( const struct bpf_program *prog, bool retprobe, pid_t pid, const char *binary_path, size_t func_offset) ``` >bpf_program__attach_uprobe() attaches a BPF program to the userspace function which is found by binary path and offset. You can optionally specify a particular process to attach to. You can also optionally attach the program to the function exit instead of entry. >Parameters: >- prog – BPF program to attach >- retprobe – Attach to function exit >- pid – Process ID to attach the uprobe to, 0 for self (own process), -1 for all processes >- binary_path – Path to binary that contains the function symbol >- func_offset – Offset within the binary of the function symbol > >Returns: Reference to the newly created BPF link; or NULL is returned on error, error code is stored in errno ```clike LIBBPF_API struct bpf_link * bpf_program__attach_uprobe_opts ( const struct bpf_program *prog, pid_t pid, const char *binary_path, size_t func_offset, const struct bpf_uprobe_opts *opts) ``` >bpf_program__attach_uprobe_opts() is just like bpf_program__attach_uprobe() except with a options struct for various configurations. Parameters: >- prog – BPF program to attach >- pid – Process ID to attach the uprobe to, 0 for self (own process), -1 for all processes >- binary_path – Path to binary that contains provided USDT probe >- func_offset – Offset within the binary of the function symbol >- opts – Options for altering program attachment > >Returns: Reference to the newly created BPF link; or NULL is returned on error, error code is stored in errno ```clike LIBBPF_API struct perf_buffer * perf_buffer__new (int map_fd, size_t page_cnt, perf_buffer_sample_fn sample_cb, perf_buffer_lost_fn lost_cb, void *ctx, const struct perf_buffer_opts *opts) ``` >perf_buffer__new() creates BPF perfbuf manager for a specified BPF_PERF_EVENT_ARRAY map Parameters: >- map_fd – FD of BPF_PERF_EVENT_ARRAY BPF map that will be used by BPF code to send data over to user-space >- page_cnt – number of memory pages allocated for each per-CPU buffer 每個 CPU 要 mmap 幾頁資料區 >- sample_cb – function called on each received data record >- lost_cb – function called when record loss has occurred >- ctx – user-provided extra context passed into sample_cb and lost_cb > >Returns: a new instance of struct perf_buffer on success, NULL on error with errno containing an error code [BPF skeleton](https://hackmd.io/Sd2y0qOoRbeo6SXLV2sXkg?both#eBPF-skeleton) main() 流程: `regcomp` 將字串 `regexp` 編譯為 `regex_t` 結構,後續用於匹配 HTTP payload `ssl_sniff_bpf__open_and_load()` 會呼叫 `bpf_object__open()` 打開編譯後的 BPF ELF (例如 ssl_sniff.bpf.o),再呼叫 `bpf_object__load()` 載入所有 BPF 程式到 kernel `pb = perf_buffer__new(...);` [Perf ring buffer](https://docs.kernel.org/userspace-api/perf_ring_buffer.html) 讓核心 BPF 端用`bpf_perf_event_output()` 將 {pid,len,buf} 送到環形緩衝區;使用者端用 `perf_buffer__poll()` 取出,此外 `perf_buffer__new` 內部會把所有 perf FD 加進 epoll,之後 perf_buffer__poll() 只要 epoll_wait() 就能一次收所有 CPU 的事件 接者打開 /dev/adbdev (`fd = open("/dev/adbdev", O_WRONLY);`)作為核心 IPC 結論: perf_buffer__new() 解決 核心 ➜ 使用者 單向傳輸;而 /dev/adbdev 提供 使用者 ➜ 核心 回覆結果的另一條路。perf_buffer__poll() 把兩端銜接起來,讓 BPF 事件能被同步分析並立即把 verdict 寫回核心模組。 [Perf ring buffer](https://docs.kernel.org/userspace-api/perf_ring_buffer.html) >The control structure is named as perf_event_mmap_page, it contains a head pointer data_head and a tail pointer data_tail. When the kernel starts to fill records into the ring buffer, it updates the head pointer to reserve the memory so later it can safely store events into the buffer. On the other side, when the user page is a writable mapping, the perf tool has the permission to update the tail pointer after consuming data from the ring buffer. 每個 perf ring buffer 前面有一頁控制區 `perf_event_mmap_page`,裡面維護兩個指標:data_head(寫端,kernel 遞增)、data_tail(讀端,user 空間遞增)。實際資料緊跟在控制頁之後,兩指標彼此繞圈不斷前進,就能在零拷貝的狀態下把事件送到 user space :::info 1. 關於 perf event、watermark、mmap、perf_buffer__poll (epoll) 需再了解 >關於 epoll 可參考課程教材 : [I/O 模型演化: 事件驅動伺服器:原理和實例](https://hackmd.io/@sysprog/linux-io-model/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fevent-driven-server) ::: ## ssl_sniff.bpf.c 屬於 kernel-space 這支程式的功能是:攔截 user-space 中的 SSL_write() 呼叫,把 TLS 明文資料透過 perf buffer 傳到 user-space <bpf/bpf_tracing.h> 提供 tracing 語法糖,例如 BPF_KPROBE ```clike struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(int)); } tls_event SEC(".maps"); ``` 建立一個 perf event buffer map,類型為 [BPF_MAP_TYPE_PERF_EVENT_ARRAY](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_PERF_EVENT_ARRAY/?utm_source=chatgpt.com),可用來 即時將資料傳送給使用者空間程式,這種 map 是用於「事件推送」而非查詢(與 hash map 相對) ```clike #define BUF_MAX_LEN 256 struct data_t { unsigned int pid; int len; unsigned char buf[BUF_MAX_LEN]; }; ``` 要傳給使用者空間的資料格式: - pid:來源進程的 process ID - len:實際寫入的資料長度(上限 256) - buf:從 SSL_write() 中取出的明文資料 ```clike SEC("uprobe") int BPF_KPROBE(probe_SSL_write, void *ssl, char *buf, int num) { unsigned long long current_pid_tgid = bpf_get_current_pid_tgid(); unsigned int pid = current_pid_tgid >> 32; int len = num; if (len < 0) return 0; struct data_t data = {}; data.pid = pid; data.len = (len < BUF_MAX_LEN ? (len & BUF_MAX_LEN - 1) : BUF_MAX_LEN); bpf_probe_read_user(data.buf, data.len, buf); bpf_perf_event_output(ctx, &tls_event, BPF_F_CURRENT_CPU, &data, sizeof(struct data_t)); return 0; } ``` 定義了一個 hook 點,掛載在使用者空間的 SSL_write() 函式,攔截 SSL_write() 可以拿到明文 先透過 `bpf_get_current_pid_tgid()` 取得當前執行緒的 (tgid, pid) 組合,並取出高 32 bit 的 PID,再用 `bpf_probe_read_user()` 把使用者空間的 buf 內容複製到 eBPF 程式的 data.buf,接著呼叫 `bpf_perf_event_output()`,把剛剛填好的 data_t 結構透過 tls_event map 送到 userspace 的 perf buffer,之後 userspace 程式(ssl_sniff.c)就能讀到裡面封包的明文 ```clike #include <openssl/ssl.h> int SSL_write(SSL *ssl, const void *buf, int num); ``` [openSSL doc](https://docs.openssl.org/1.1.1/man3/SSL_write/) > SSL_write_ex() and SSL_write() write num bytes from the buffer buf into the specified ssl connection. On success SSL_write_ex() will store the number of bytes written in *written. BPF_KPROBE(...) 是語法糖,會展開為: ``` SEC("uprobe/SSL_write") int probe_SSL_write(struct pt_regs *ctx, ...) ``` BPF_F_CURRENT_CPU 表示推送到目前執行的 CPU 上對應的 perf buffer :::info 1. 在呼叫 ssl_write() 之前,訊息會是明文,但經過 ssl_write() 之後就會是密文,這部分在[openSSL doc](https://docs.openssl.org/1.1.1/man3/SSL_write/) 中並沒有明確提及,[stackoverflow](https://stackoverflow.com/questions/67587810/ssl-write-default-behavior) 有說到 ssl_write 及 ssl_read 會自動做加解密的動作,但他們是怎麼被實做的呢 ? 似乎要了解 BIO(Basic I/O)in OpenSSL 2. probe (kprobe, uprobe) 是怎麼運作的? 3. User-space 如何接收資料? user space 程式(ssl_sniff.c)會註冊 `perf_buffer__new(map_fd, ... , callback);` 然後透過 `perf_buffer__poll();` 不斷接收這些從 kernel 傳來的 data_t 結構資料。 ::: ## GPT :::spoiler 你上傳的 `kadblock.c` 和 `adblock.c` 是一套配合運作的 **kernel-space + user-space ad blocking 系統**。以下是兩者如何互相配合的詳細說明: --- ## 🧩 兩支程式的定位與關聯概念總覽: | 模組 | 運行位置 | 功能 | 特殊溝通方式 | | ------------ | ---------------- | -------------------------------------------------------------- | --------------------------------------- | | `kadblock.c` | **Kernel Space** | 透過 Netfilter 攔截封包,處理 HTTP/TLS/DNS 資料 | 建立 `/dev/adbdev`,等待 user-space 程式傳回判斷結果 | | `adblock.c` | **User Space** | 用 `libnetfilter_queue` 抓封包內容、解析 HTTP host / TLS SNI 並判斷是否是廣告主機 | 利用 verdict 送回 DROP / ACCEPT 給核心 | --- ## 🔹 兩者的交互流程圖(總覽) ``` [瀏覽器] --> [Linux Netfilter] ↓ (由 kadblock.c 攔截封包) ↓ 若為 TLS 封包 && /dev/adbdev 已被打開 ↓ ➤ 加入待決定封包的 Queue,等待 verdict ↓ [adblock.c 程式用 open("/dev/adbdev")] ↓ [lseek(offset=PID)] 將 verdict 傳給核心 ↓ 若是廣告主機 → kernel 模組 drop 並送 TCP RST ``` --- ## 🔍 核心細節說明 ### 🔸 `kadblock.c` — 核心模組功能 * 攔截所有本機送出的封包(Netfilter `LOCAL_OUT`)。 * 若是: * `HTTP GET`:使用 `glob_match("*ad*")` 判斷是否為廣告 URL。 * `DNS`: 從封包內抽出 host,與黑名單比對。 * `TLS` 封包(以 `0x17` 為開頭): * 若有使用者程式開啟 `/dev/adbdev`: * 建立一個封包分析請求(queue\_st 結構) * 等待 50ms 讓使用者空間程式下 verdict(判定這個封包是否是廣告) * 若符合封鎖條件 → 呼叫 `send_server_reset()` 發送 TCP RST 並 `NF_DROP`。 --- ### 🔸 `adblock.c` — 使用者空間程式功能 * 使用 iptables 把封包導到 `NFQUEUE`: ```bash iptables -A OUTPUT -j NFQUEUE --queue-num 0 iptables -A INPUT -j NFQUEUE --queue-num 0 ``` * 透過 libnetfilter\_queue 來接收這些封包: * 抽取 IP + TCP Header。 * 若為 TLS,透過 `tls_protocol->parse_packet()` 解析 SNI。 * 若為 HTTP,透過 `http_protocol->parse_packet()` 抽 Host 欄位。 * 再根據 `host_table.h` 提供的廣告黑名單進行比對。 * 判斷結果用 `nfq_set_verdict()` 回傳 DROP / ACCEPT。 --- ## 🔗 關聯機制重點整合: | 項目 | 關聯解釋 | | ------------------------------------ | -------------------------------------------------------------------- | | `/dev/adbdev` | `kadblock.c` 建立字元裝置,讓 `adblock.c` 使用 `open()` 開啟,代表「我已上線可以給 verdict」 | | `verdict_ssl.h` / `insert_verdict()` | `kadblock.c` 中等待 verdict 的機制,會根據 PID 或時間與使用者程式互動 | | `skb->data[0] == 0x17` | 檢查 TLS 封包是否為 Application Data,送入決策流程(queue\_st + poll) | | `lseek(fd, pid, SEEK_SET)` | `adblock.c` 可透過 `lseek` 將對應 PID 的 verdict 傳給 `kadblock.c` 判斷封包處理方式 | | `in_word_set(host)` | 用來對比是否是廣告網址的黑名單查詢函式,兩邊都會使用 | --- ## ✅ 結論 這整體系統是典型的 **協作式封包過濾架構**: * **`kadblock.c` 在核心層攔截封包**,並根據明文協議 (HTTP、DNS) 自行過濾; * **遇到無法直接判斷的 TLS 封包時**,會把封包暫存下來並等待 **`adblock.c` 使用者空間程式進行更深入解析與判斷**; * `adblock.c` 判斷完後透過 `lseek` 送回 verdict 給 `kadblock.c`,最終由 kernel 採取動作。 --- 如果你願意,我可以幫你補一個完整的使用者空間程式碼示範(包含 open `/dev/adbdev`、讀封包、下 verdict)。是否需要? ::: ## Linux source ### ip_hdr() https://github.com/torvalds/linux/blob/master/include/linux/ip.h ```clike static inline struct iphdr *ip_hdr(const struct sk_buff *skb) { return (struct iphdr *)skb_network_header(skb); } ``` https://github.com/torvalds/linux/blob/master/include/linux/skbuff.h ```clike static inline unsigned char *skb_network_header(const struct sk_buff *skb) { return skb->head + skb->network_header; } ``` https://github.com/torvalds/linux/blob/master/include/linux/skbuff.h#L883 ```clike ``` ### iphdr https://github.com/torvalds/linux/blob/master/include/uapi/linux/ip.h#L87 ```clike struct iphdr { #if defined(__LITTLE_ENDIAN_BITFIELD) __u8 ihl:4, version:4; #elif defined (__BIG_ENDIAN_BITFIELD) __u8 version:4, ihl:4; #else #error "Please fix <asm/byteorder.h>" #endif __u8 tos; __be16 tot_len; __be16 id; __be16 frag_off; __u8 ttl; __u8 protocol; __sum16 check; __struct_group(/* no tag */, addrs, /* no attrs */, __be32 saddr; __be32 daddr; ); /*The options start here. */ }; ``` | 欄位 | 說明 | | ---------- | ----------------------------- | | `version` | IP 版本,IPv4 固定為 4 | | `ihl` | Header 長度(以 32-bit words 計算) | | `tos` | 服務類型(Type of Service) | | `tot_len` | 封包總長度(標頭 + 資料) | | `id` | 封包識別碼,用於分段重組 | | `frag_off` | 分段資訊與 flags(如 Don't Fragment) | | `ttl` | 存活時間(Hop 數限制) | | `protocol` | 上層協定號碼(如 TCP=6、UDP=17) | | `check` | 標頭 checksum 檢查 | | `saddr` | Source IP(IPv4) | | `daddr` | Destination IP(IPv4) | ### tcphdr https://github.com/torvalds/linux/blob/master/include/uapi/linux/tcp.h#L25C1-L60C3 ```clike struct tcphdr { __be16 source; __be16 dest; __be32 seq; __be32 ack_seq; #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 ae:1, res1:3, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:3, ae:1, cwr:1, ece:1, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error "Adjust your <asm/byteorder.h> defines" #endif __be16 window; __sum16 check; __be16 urg_ptr; }; ``` | 欄位 | 說明 | | -------------- | ---------------------------- | | `source` | TCP 原始埠號 | | `dest` | TCP 目的埠號 | | `seq` | 封包序列號 | | `ack_seq` | 確認應答序號 | | `doff` | Header 長度(以 32-bit word 為單位) | | `fin` \~ `cwr` | TCP 控制旗標(如 SYN、ACK、FIN) | | `window` | 流量控制視窗大小 | | `check` | TCP 檢查碼 | | `urg_ptr` | 緊急指標 | ### atomic_read() Linux 提供 atomic_t 型別來做多核心安全的數值讀寫。它能保證在 SMP(多處理器)環境下: - 操作是 不可中斷的 - 不需要加鎖(比 mutex 更輕量) - 適合儲存計數器、旗標等數值 ### sk_buff https://amsekharkernel.blogspot.com/2014/08/what-is-skb-in-linux-kernel-what-are.html ![image](https://hackmd.io/_uploads/HJIOOX3R1g.png) ### reg_exec() [regular-expressions.info](https://www.regular-expressions.info/quickstart.html) [regex(3) — Linux manual page](https://man7.org/linux/man-pages/man3/regexec.3.html) >regexec() is used to match a null-terminated string against the compiled pattern buffer in *preg, which must have been initialised with regcomp(). eflags is the bitwise OR of zero or more of the following flags: > regcomp() returns zero for a successful compilation or an error code for failure. regexec() returns zero for a successful match or REG_NOMATCH for failure. regerror() returns the size of the buffer required to hold the string. 範例程式: ```clike #include <stdio.h> #include <stdlib.h> #include <regex.h> int main() { regex_t regex; // 存放編譯後的正規表示式 const char *pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; const char *test_str = "user@example.com"; int ret; // 編譯正規表示式 ret = regcomp(&regex, pattern, REG_EXTENDED); // 編譯成功會回傳 0,失敗回傳錯誤碼 if (ret != 0) { // 編譯失敗 char msgbuf[100]; // 存放錯誤訊息 regerror(ret, &regex, msgbuf, sizeof(msgbuf)); // 將錯誤碼轉為錯誤訊息,存進 msgbuf fprintf(stderr, "Regex compilation failed: %s\n", msgbuf); return 1; } // 使用 regexec() 比對字串 ret = regexec(&regex, test_str, 0, NULL, 0); if (ret == 0) { printf(" Match found: \"%s\"\n", test_str); } else if (ret == REG_NOMATCH) { printf(" No match: \"%s\"\n", test_str); } else { char msgbuf[100]; regerror(ret, &regex, msgbuf, sizeof(msgbuf)); fprintf(stderr, "Regex match error: %s\n", msgbuf); } // 清除 regex_t 資源 regfree(&regex); return 0; } ``` :::info 1. printf(),fprintf() 的差異 ? stdout,stderr 又有甚麼差異 ? 2. 上述程式要如何觸發 error windows 無法編譯該程式 ( #include <regex.h> ) ::: ### lseek() ```clike #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); ``` 用來在檔案中移動讀寫位址 > lseek() repositions the file offset of the open file description associated with the file descriptor fd to the argument offset according to the directive whence as follows: > - SEEK_SET The file offset is set to offset bytes. > - SEEK_CUR The file offset is set to its current location plus offset bytes. > - SEEK_END The file offset is set to the size of the file plus offset bytes. > >lseek() allows the file offset to be set beyond the end of the file (but this does not change the size of the file). If data is later written at this point, subsequent reads of the data in the gap (a "hole") return null bytes ('\0') until data is actually written into the gap. ## other ### queue_st (自定義) ```clike struct queue_st { struct list_head head; union { ktime_t timestamp; pid_t pid; }; }; ``` `mod_init` 流程: 呼叫 `register_chrdev(0, "adbdev", &fops)`,動態取得一個 major number,註冊字元設備,接著透過 `class_create("adbdev")` 先在 /sys/class 建 class adbdev,再用 `device_create `建立真正的 /dev/adbdev 節點,這個字元裝置就是作為 userspace 與 kernel module 之間的「單向訊息通道」,其透過 `adbdev_open/adbdev_release` 的 atomic 變數確保同一時間只有一個使用者程式能夠 open 這個裝置,避免多個程式同時送 verdict 衝突。 另外,該模組把 .llseek 這個檔案操作函式掛在裝置上,讓 user-space 只要對 /dev/adbdev 呼叫 `lseek(fd, offset, SEEK_SET)`,kernel 就會進到 adbdev_lseek,要注意的是 offset 參數在這裡 不是單純用來移動檔案指標,而是 攜帶「哪個 PID」要被封鎖的訊息。 >***為甚麼要這麼做 ?*** TLS 流量是加密的,內核無法直接在 hook 裡看明文,該專案裡用 BPF 程式+使用者空間程式去截 SSL_write 裡的明文、做正規比對後才知道要不要擋。一旦使用者空間程式(如 ssl_sniff.c)檢測到「是廣告請求」,就需要告訴 kernel:「接下來這個 PID 的所有封包都要 drop/reset」。這時就打開 /dev/adbdev,對它做 lseek(pid, …),kernel 在 adbdev_lseek 看到後,把這個 PID 記到 verdict_head,blocker_hook 以後看見相同 PID 就會丟棄封包 最後 `return nf_register_net_hook(&init_net, &blocker_ops)` 代表成功用 insmod adblock.ko 載入模組之後,在 init_net(即 global 而非自創 netns) 這個 namespace 中,所有從本機送出的 IPv4 封包(經過 Netfilter 的 NF_INET_LOCAL_OUT 階段)都會觸發在 blocker_ops 裡面註冊的 blocker_hook 函式。 進入 blocker_hook 函式後,先檢查 user-mode program(ssl_sniff) 是否有在運作及確認封包是否為 TLS 封包: - 若是,則根據 :::info 07/02 執行 `$ make` 時會出現 Error attaching libssl.so.3 in SSL_write 因此做了以下修改 ```diff diff --git a/bpf.py b/bpf.py index c7e9086..28686e5 100644 --- a/bpf.py +++ b/bpf.py @@ -1,7 +1,7 @@ import subprocess -libs = ["libssl.so.3","SSL_write", #OpenSSL - "libnspr4.so","PR_Write"] #NSS +libs = ["/lib/x86_64-linux-gnu/libssl.so.3","SSL_write", #OpenSSL + "/usr/lib/x86_64-linux-gnu/libnspr4.so","PR_Write"] #NSS diff --git a/ssl_sniff.c b/ssl_sniff.c index 8e78125..6721cd0 100644 --- a/ssl_sniff.c +++ b/ssl_sniff.c @@ -3,6 +3,7 @@ #include <stdio.h> #include <unistd.h> #include <fcntl.h> +#include <limits.h> #include "ssl_sniff.skel.h" @@ -53,17 +54,44 @@ int main(int argc, char *argv[]) for (int i = 1; i < argc; i += 2) { printf("Attaching %s in %s\n", argv[i + 1], argv[i]); - struct bpf_uprobe_opts *ops = malloc(sizeof(struct bpf_uprobe_opts)); - ops->sz = sizeof(*ops); - ops->ref_ctr_offset = 0x6; - ops->retprobe = false; - ops->func_name = argv[i + 1]; - struct bpf_link *link = bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_write, -1, - argv[i], 0, ops); - if(!link) - printf("Error attaching %s in %s\n", argv[i + 1], argv[i]); + const char *rawpath = argv[i]; // 參數帶進來的 so + const char *func = argv[i + 1]; // 對應函式名 + char so[PATH_MAX]; + + if (!realpath(rawpath, so)) { + perror(rawpath); + continue; + } + + size_t offset = 0; + if (!strcmp(func, "SSL_write")) + offset = 0x36b20; // readelf 算出 + /* PR_Write 也可以事先用 readelf 算 offset 放這裡 */ + + struct bpf_uprobe_opts opts = { + .sz = sizeof(opts), + .retprobe = false, + }; + + if (offset) + ; // 用 offset attach + else + opts.func_name = func; // 用符號名 attach + + struct bpf_link *link = + bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_write, + -1, so, offset, &opts); + if (!link) + fprintf(stderr, "Error attaching %s in %s: %s (errno=%d)\n", + func, so, strerror(errno), errno); } ::: :::info >參考 https://github.com/eunomia-bpf/bpf-developer-tutorial/blob/main/src/30-sslsniff/README.md > >Uprobe in kernel mode eBPF runtime may also cause relatively large performance overhead. In this case, you can also consider using user mode eBPF runtime, such as bpftime。 該專案是 kernel mode eBPF runtime 還是 user mode eBPF runtime :::