LWN: 對kernel memcpy() 進(jìn)行更嚴(yán)格的越界檢查!
關(guān)注了就能看到更多這么棒的文章哦~
Strict memcpy() bounds checking for the kernel
By Jonathan Corbet
July 30, 2021
DeepL assisted translation
https://lwn.net/Articles/864521/
大家都知道 C 語言容易出現(xiàn)內(nèi)存安全問題(memory-safety),也就是會(huì)容易引發(fā)緩沖區(qū)溢出(buffer overflow)等似乎無窮無盡的安全漏洞。但是在 C 語言中,很多情況下其實(shí)是可以改善這方面的表現(xiàn)的。比如說用來高效地復(fù)制或者覆蓋一塊內(nèi)存區(qū)域的 memcpy() 系列函數(shù)。在編譯器的幫助下,這些函數(shù)就可以避免在寫入時(shí)超過目標(biāo)對象的末尾。不過,在內(nèi)核中要做到這一點(diǎn)比起人們想象得要更加難,這一點(diǎn)可以從 Kees Cook 的一組大型 patch set 中看出來。
緩沖區(qū)溢出問題似乎永遠(yuǎn)不會(huì)消失,一直在內(nèi)核中導(dǎo)致一些 bug 以及安全問題。據(jù)說目前的加固(hardening)技術(shù)已經(jīng)足夠好了,許多類型的堆棧溢出問題都可以被檢測出來加以防御(如果沒有其他辦法的話,關(guān)閉系統(tǒng)也是一種防御)。如果無法越過邊界(并且這里有個(gè)重要數(shù)據(jù)),那么就很難改寫 stack 的內(nèi)容,也就無法暴露出這種問題。然而,heap 中的數(shù)據(jù)則并沒有這種邊界,這使得 heap (堆)空間的溢出問題更難被發(fā)現(xiàn)。因此,攻擊者就越來越喜歡這方面的漏洞了。
memset_after()
一個(gè)常見的對多個(gè)字段進(jìn)行 copy 的使用場景是 "從這里開始一直到結(jié)構(gòu)末尾都寫成 0"。例如 AR9170 無線網(wǎng)絡(luò)驅(qū)動(dòng)中的這段代碼:
memset(&txinfo->status.ack_signal, 0,
sizeof(struct ieee80211_tx_info) -
offsetof(struct ieee80211_tx_info, status.ack_signal));
這段代碼就是在進(jìn)行對結(jié)構(gòu)的后半部分進(jìn)行清零的操作。它也很好地展示了這種代碼多么笨拙。這種對長度的計(jì)算很容易出錯(cuò),而且如果 struct 的布局因?yàn)槟承┰蚨l(fā)生了變化,那么這段代碼就會(huì)受到影響。事實(shí)上,上面這段代碼之前就有這么一行保護(hù)代碼:
BUILD_BUG_ON(offsetof(struct ieee80211_tx_info, status.ack_signal) !=20);
如果打算要寫入 0 的第一個(gè)字段的偏移量不符合預(yù)期的話,這一行就會(huì)導(dǎo)致編譯失敗,但這種做法無法發(fā)現(xiàn)該字段之后的任何變動(dòng)。ack_signal 之后添加的結(jié)構(gòu)成員也會(huì)被這個(gè) memset() 調(diào)用清零,在寫這段代碼的時(shí)候可能并不容易意識(shí)到這一點(diǎn)。
為了讓這類代碼更準(zhǔn)確,并且避免對 memset() 進(jìn)行的更嚴(yán)格的檢測產(chǎn)生誤報(bào),patch set 中針對該操作引入了一個(gè)新的宏:
memset_after(object, value, member);
這個(gè)宏會(huì)讓位于 member 之后的 object 的每個(gè)字節(jié)都被設(shè)置為 value 值。下面這個(gè)宏就可以代替上面的代碼:
memset_after(&txinfo->status, 0, rates);
這里 ack_signal 字段就是第一個(gè)被清零的字段,在這個(gè)結(jié)構(gòu)中是緊隨 rates 之后的。在 Cook 的 patch set 中,許多類似這樣的問題都得到了 fix。
Grouped structure fields
不過還有一種更復(fù)雜的情況,即一個(gè)結(jié)構(gòu)中的一系列字段都在一次調(diào)用中被改寫了。內(nèi)核中已經(jīng)使用了一些方法來針對這種情況進(jìn)行安全的復(fù)制操作。方法之一就是在上面的例子中看到的那種 offsetof() 計(jì)算方式。但也還有其他的方法。例如在用來代表網(wǎng)絡(luò)數(shù)據(jù)包的 sk_buff 結(jié)構(gòu)的深處就有這樣一個(gè)字段:
__u32 headers_start[0];
整整 120 行之后是另一個(gè)名為 headers_end 的長度為零的數(shù)組。這種數(shù)組顯然無法容納任何有意義的數(shù)據(jù),相反,它們是被用來配合類似的 offset 算法,從而能夠在一次操作中復(fù)制若干個(gè) packet header 的。同樣這里也有一組在 build 時(shí)進(jìn)行的檢查,用來確保所有相關(guān)的 header field 都位于兩個(gè) marker 之間。
一些開發(fā)者會(huì)簡單地把要寫的字段的長度加起來,然后用這個(gè)結(jié)果作為內(nèi)存操作的長度。其實(shí)有另一種方法,就是定義一個(gè)嵌套結(jié)構(gòu)(nested structure)來保存將要復(fù)制的這些字段。這種做法比較安全,但是它使得這些字段的用法變得復(fù)雜了(因?yàn)楸仨氁ㄟ^一個(gè) intermediate structure 來訪問),并且這里加入宏來讓這個(gè)操作容易使用的話,又容易導(dǎo)致命名空間污染。
總之,內(nèi)核開發(fā)者已經(jīng)想出了許多方法用來處理跨字段的內(nèi)存操作,但沒有一個(gè)特別令人滿意的。Cook 的 patch set 以 struct_group() macro 的形式帶來了一個(gè)新的解決方案(Keith Packard 是他的共同作者)。以這組 patch 中的例子為例,如果有下面這樣的結(jié)構(gòu):
struct foo {
int one;
int two;
int three;
int four;
};
如果開發(fā)者想通過一次 memcpy()調(diào)用就復(fù)制字段 two 和 three 的話,可以通過下面這樣來聲明該結(jié)構(gòu)從而給出正式一些的方案:
struct foo {
int one;
struct_group(thing,
int two,
int three,
);
int four;
};
這個(gè)宏的作用是創(chuàng)建一個(gè)名為 thing 的嵌套結(jié)構(gòu),此結(jié)構(gòu)可以與 memcpy() 等函數(shù)配合使用,并啟用嚴(yán)格的邊界檢查。單個(gè)字段仍然可以用 two 和 three 來引用,不需要使用這個(gè)嵌套結(jié)構(gòu)的名字,并且也不用加什么難看的宏。底層是通過這種方式實(shí)現(xiàn)的:
#define struct_group_attr(NAME, ATTRS, MEMBERS) \
union { \
struct { MEMBERS } ATTRS; \
struct { MEMBERS } ATTRS NAME; \
}
#define struct_group(NAME, MEMBERS) \
struct_group_attr(NAME, /* no attrs */, MEMBERS)
這個(gè)宏兩次定義了一個(gè) intermediate structure 來保存這些被 group 起來的字段。其中一次是匿名的,而另一個(gè)則有明確指定的 NAME。然后,這些重復(fù)結(jié)構(gòu)就會(huì)在一個(gè)匿名 union 中 overlay 起來。這個(gè)小技巧使得我們可以直接使用字段的名字了,同時(shí)也提供了整個(gè) structure 的名字供 memory function 使用。
Toward a harder kernel
patch set 中的大部分內(nèi)容都是在整個(gè)內(nèi)核中各處的結(jié)構(gòu)里定義這些 group,然后使用這些 group 來進(jìn)行 memory operation。這樣一來就有可能(某種程度上)對這些操作進(jìn)行更嚴(yán)格的邊界檢查了。剩下的問題是這種跨 field 的操作實(shí)際上很難在代碼中找全。沒有統(tǒng)一的模式,也就無法輕易搜索出來。因此,很有可能在內(nèi)核中還有其他一些尚未被發(fā)現(xiàn)的地方。正如 Cook patch 修改了若干版本之后時(shí)指出的:在內(nèi)核中有超過 25000 個(gè) memcpy() 調(diào)用。如果對一個(gè)尚未修改好的對多個(gè) field 的操作(這個(gè)操作本身其實(shí)可能是正確的)就觸發(fā)系統(tǒng) crash,最起碼也會(huì)被人們抱怨說這個(gè)做法太粗暴了。所以在可以遇見的未來內(nèi),我們將不得不僅僅使用 warning 來提醒用戶。
不過,未來的某一天,應(yīng)該 warning 已經(jīng)很罕見了,社區(qū)也有了足夠的信心可以在檢測到越界 copy 時(shí)就讓系統(tǒng) halt。這樣做的好處很可能非常大。上述 patch 中就指出:
有了這個(gè)功能之后,我們就可以將已知的 11 個(gè) memcpy() 相關(guān)漏洞位置跟新檢查出來的潛在越界訪問的事件進(jìn)行比較,從而衡量這些加強(qiáng)措施的潛在效果如何。令我非常驚訝、恐懼以及高興的是,所有這 11 個(gè)缺陷都會(huì)被新增加的 run-time bounds check 功能檢測出來,這說明它明顯是一個(gè)重要的改進(jìn)措施。
這種改進(jìn)措施看起來很值得擁有,但第一步需要先把這些 patch 合入 mainline kernel。安全相關(guān)的工作往往總是很難合入內(nèi)核,盡管這種情況在過去幾年中已經(jīng)有了不少改善。至少針對這個(gè)具體 patch set 來說,人們經(jīng)常針對安全 patch 提出的一個(gè)抱怨(對性能的影響)并不是一個(gè)問題,只有那些在編譯時(shí)不知道 size 的情況下才會(huì)引入很小的 run-time length check。但是,這個(gè) patch set 實(shí)在太大了,修改的范圍也太廣。在合并之前很可能要進(jìn)行不少討論。這個(gè)過程一旦完成,預(yù)示著我們又一次終結(jié)了一類安全漏洞。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
