MacOS再次出現漏洞,號稱牢不可破的系統也有弱點
*為了方便表達,本文將採用第一人稱的方式敘述。
本文講述了我在蘋果的macOS系統內核中發現的幾個堆棧和緩衝區溢出漏洞,蘋果官方將這幾個漏洞歸類為內核中的遠程代碼執行漏洞,因此這些漏洞的威脅級別非常高。攻擊者可以通過這些漏洞遠程入侵Mac,也可以在通過物理的方式訪問計算機時,僅需要以訪客身份登錄(無需密碼),就可以通過這些漏洞從而獲取許可權並控制計算機。
這些漏洞基本都存在於NFS協議中,就是用來將網路驅動安裝至Mac的文件系統時使用的,類似於NAS。
漏洞相關內容
蘋果公司在2018年7月9日發布的MacOS 10.13.6版本更新中修復了這些漏洞。但是當時他們要求我們先不要公布這些漏洞,因為他們需要再做一些調查,看看其他平台上有沒有類似的問題或者是否需要解決。當然,現在我們可以敞開說了。
NFS這個東西,現在的使用範圍已經非常廣,特別是在一些大型企業中,它主要用於共享驅動或聯網設備的主目錄等內容。當然,也可以在家庭設備中使用,通常會用作媒體伺服器。
在macos中,安裝NFS不需要特殊的許可權,因此任何級別的用戶都可以觸發這些漏洞,甚至是不需要密碼的訪客賬戶。此外許多計算機(尤其是企業環境中的)會配置為在啟動時自動掛載NFS共享。
這意味著這些漏洞至少存在兩個攻擊媒介:
1.可能被用於在使用NFS文件管理器的企業中快速傳播病毒。
想要做到這一點,攻擊者只需要在其文件管理器上安裝具備惡意代碼的NFS伺服器軟體即可,或者通過將惡意文件管理器放在網路上去攔截NFS流量來達成目的。
2.用於提權。
比如,有些人會使用Guest身份登錄,然後發出一系列命令連接到NFS伺服器(可能存在於網路的任何位置),就可能在計算機上獲取內核級別的訪問許可權。
蘋果公司為這五個漏洞分配了CVE分別是:CVE-2018-4259,CVE-2018-4286,CVE-2018-4287,CVE-2018-4288和CVE-2018-4291。我在2018年5月21日發給蘋果的漏洞報告中,分別在源碼中列出了14條不確定的點。但由於蘋果最近才發布了更新,所以我也沒有來得及完成對全部源代碼的審核。因此,為避免意外泄露任何可能未修復的錯誤,本文中我只談及其中兩個已經得到驗證和修復的漏洞。
漏洞復現
我編寫了一個PoC去驗證漏洞的可用性,可以使用0覆蓋4096個位元組的堆內存從而導致內核崩潰。我做了一個簡短的視頻來證明這一點。
4096是一個隨機選擇,事實上我可以隨意修改來發送儘可能多的數據,任何大於128位元組的數都會觸發緩衝區溢出,我也能夠完全控制寫入的位元組值。因此,儘管這些動作只破壞了內核,但是實際上是可以通過這些緩衝區溢出來實現遠程代碼執行以及本地提權的操作。
在我第一次發現這個漏洞時,幾乎不敢想像我會為了PoC去自己編寫NFS伺服器。但是在我學了一些NFS相關知識以及了解到如何使用rpcgen之後,我就發現其實想要實現也非常簡單。用來驗證這個漏洞的PoC,僅包含46行C語言以及63行 RPC語言代碼。當然,這些源代碼會在蘋果官方完成全部修復之後再放出。
漏洞詳解
在我寫的PoC中,這兩個漏洞都需要通過這一行看似無害的代碼來實現:
nfsm_chain_get_fh(error, &nmrep, nfsvers, fh);
這行代碼的作用是讀取NFS伺服器發送回Mac的回復消息(nmrep)中的文件句柄(fh)。這個文件句柄是NFS共享的文件或目錄中的不透明標識符。NFSv3中的文件句柄最多64個位元組,NFSv4中最多128個位元組,XNU中的fhandle_t類型則有足夠的空間容納128位元組的文件句柄,但是他們卻忽略了去檢查nfsm_chain_get_fh宏中的緩衝區溢出情況:
/* get the size of and data for a file handle in an mbuf chain */
#define nfsm_chain_get_fh(E, NMC, VERS, FHP)
do {
if ((VERS) != NFS_VER2)
nfsm_chain_get_32((E), (NMC), (FHP)->fh_len);
else
(FHP)->fh_len = NFSX_V2FH;
nfsm_chain_get_opaque((E), (NMC), (uint32_t)(FHP)->fh_len, (FHP)->fh_data);
if (E)
(FHP)->fh_len = 0;
} while (0)
由於宏命令的大量使用,想理解這段代碼可能有些難,但它的實際作用卻非常簡單:它能夠從消息中讀取一個32為無符號整數到(FHP)->fh_len,然後讀取該位元組數,從消息直接進入(FHP)->fh_data。由於沒有邊界檢查,因此攻擊者可以選擇任何位元組序列覆蓋任意數量的內核堆。被覆蓋的文件句柄在內存中的nfs_socket.c:1401中分配。
這個PoC中,第二個bug是nfsm_chain_get_opaque中的整數溢出:
/* copy the next consecutive bytes of opaque data from an mbuf chain */
#define nfsm_chain_get_opaque(E, NMC, LEN, PTR)
do {
uint32_t rndlen;
if (E) break;
rndlen = nfsm_rndup(LEN);
if ((NMC)->nmc_left >= rndlen) {
u_char *__tmpptr = (u_char*)(NMC)->nmc_ptr;
(NMC)->nmc_left -= rndlen;
(NMC)->nmc_ptr += rndlen;
bcopy(__tmpptr, (PTR), (LEN));
} else {
(E) = nfsm_chain_get_opaque_f((NMC), (LEN), (u_char*)(PTR));
}
} while (0)
這段代碼使用bfsn_rndup將LEN移動至4的下一個倍數處。但它在調用bcopy時會使用LEN的原始值。如果其初值為0xFFFFFFFF,則nfsm_rndup中將出現加法溢出,renlen的值為0,這意味著能夠與(NMC)->nmc_left比較成功,並且使用0xFFFFFFFF作為size參數調用bcopy。這便會導致內核崩潰,因此它被用作拒絕服務攻擊。
使用QL查找錯誤
QL的一大優勢是能夠找到已知錯誤的變種。今年早些時候,我的同事Jonas Jensen在蘋果的NFS啟動中發現了兩個漏洞:CVE-2018-4136和CVE-2018-4160。我們當時也發布了一篇關於這些漏洞的文章,主要就是針對對bcopy的調用,這個調用可能存在為負的用戶控制的大小參數。最簡單的方法就是查找用戶控制源緩衝區中對bcopy的調用。這很有趣,因為它們可以將用戶的數據複製到內核中。
/**
* @name bcopy of network data
* @description Copying a variable-sized network buffer into kernel memory
* @kind path-problem
* @problem.severity warning
* @id apple-xnu/cpp/bcopy-negative-size
*/
import cpp
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import DataFlow::PathGraph
class MyCfg extends DataFlow::Configuration {
MyCfg() {
this = "MyCfg"
}
override predicate isSink(DataFlow::Node sink) {
exists (FunctionCall call
| sink.asExpr() = call.getArgument(1) and
call.getTarget().getName() = "__builtin___memmove_chk" and
not call.getArgument(2).isConstant())
}
override predicate isSource(DataFlow::Node source) {
source.asExpr().(FunctionCall).getTarget().getName() = "mbuf_data"
}
}
from DataFlow::PathNode sink, DataFlow::PathNode source, MyCfg dataFlow
where dataFlow.hasFlowPath(source, sink)
select sink, source, sink, "bcopy of network data"
上面這條查詢相當簡單,因為它查找的是對bcopy的任何調用,該調用過程是將數據從mbuf複製到內核中。只要正確檢查size參數的邊界,這樣的調用就沒有錯誤。然而事實證明,很大一部分使用nfsm_chain_get_fh的情況中,不會進行任何邊界檢查。因此,儘管查詢方式很簡單,但卻很有效,發現了很多重要的漏洞。
實現邊界檢查的常用方法,一般是:
if (n < limit) {
bcopy(src, dst, n);
}
我又寫了一段來對上面這步進行檢測:
/**
* Holds if `guard` is a bounds check which ensures that `size` is less than
* `limit`. For example:
*
* if (size < limit) {
* ... size ...
* }
*/
predicate guardedSize(GuardCondition guard, Expr size, Expr limit,
RelationStrictness strict) {
exists (boolean branch, Expr sz, BasicBlock block
| guard.controls(block, branch) and
block.contains(size) and
globalValueNumber(size) = globalValueNumber(sz) and
relOpWithSwapAndNegate(guard, sz, limit, Lesser(), strict, branch))
}
這段代碼使用Guards庫來查找在guard控制的控制流位置中使用的大小表達式,然後使用globalValueNumber庫來檢查條件本身是否出現相同大小的表達式。GlobalValueNumbering庫可以提供預測功能,以檢測有效短語的權重:
if (packet.data.size < limit) {
... packet.data.size ...
}
最後,它使用名為relOpWithSwapAndNegate的程序來檢查size表達式是否小於限制:
if (packet.data.size >= limit) {
return -1;
} else {
... packet.data.size ...
}
當然,有時候也可以通過另一種方式實現邊界檢查,例如調用min:
/**
* Holds if `size` is bounds checked with a call to `min`:
*
* size = min(n, limit);
*
* ... size ...
*/
predicate minSize(Expr size) {
exists (DataFlow::Node source, DataFlow::Node sink
| DataFlow::localFlow(source, sink) and
source.asExpr().(FunctionCall).getTarget().getName() = "min" and
size = sink.asExpr())
}
我簡單的組合了一下:
/**
* Holds if `size` has been bounds checked.
*/
predicate checkedSize(Expr size) {
lowerBound(size) >= 0 and
(guardedSize(_, size, _, _) or minSize(size))
}
注意一點,我使用了lowerBound來確保不出現負整數溢出的情況。需要做的就是在isSink方法中使用checkedSize,以減少誤報的數量。語句如下:
/**
* @name bcopy of network data
* @description Copying a variable-sized network buffer into kernel memory
* @kind path-problem
* @problem.severity warning
* @id apple-xnu/cpp/bcopy-negative-size
*/
import cpp
import semmle.code.cpp.valuenumbering.GlobalValueNumbering
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import DataFlow::PathGraph
/**
* Holds if `guard` is a bounds check which ensures that `size` is less than
* `limit`. For example:
*
* if (size < limit) {
* ... size ...
* }
*/
predicate guardedSize(GuardCondition guard, Expr size, Expr limit,
RelationStrictness strict) {
exists (boolean branch, Expr sz, BasicBlock block
| guard.controls(block, branch) and
block.contains(size) and
globalValueNumber(size) = globalValueNumber(sz) and
relOpWithSwapAndNegate(guard, sz, limit, Lesser(), strict, branch))
}
/**
* Holds if `size` is bounds checked with a call to `min`:
*
* size = min(n, limit);
*
* ... size ...
*/
predicate minSize(Expr size) {
exists (DataFlow::Node source, DataFlow::Node sink
| DataFlow::localFlow(source, sink) and
source.asExpr().(FunctionCall).getTarget().getName() = "min" and
size = sink.asExpr())
}
/**
* Holds if `size` has been bounds checked.
*/
predicate checkedSize(Expr size) {
lowerBound(size) >= 0 and
(guardedSize(_, size, _, _) or minSize(size))
}
class MyCfg extends DataFlow::Configuration {
MyCfg() {
this = "MyCfg"
}
override predicate isSink(DataFlow::Node sink) {
exists (FunctionCall call
| sink.asExpr() = call.getArgument(1) and
call.getTarget().getName() = "__builtin___memmove_chk" and
not checkedSize(call.getArgument(2)))
}
override predicate isSource(DataFlow::Node source) {
source.asExpr().(FunctionCall).getTarget().getName() = "mbuf_data"
}
}
from DataFlow::PathNode sink, DataFlow::PathNode source, MyCfg dataFlow
where dataFlow.hasFlowPath(source, sink)
select sink, source, sink, "bcopy of network data"
以上就是關於漏洞的所有內容,在這個測試過程中,要多虧Jeremy Andrus的文章,給予了我莫大的幫助。
漏洞時間線
2018-05-21:將漏洞信息及驗證過程私信給蘋果。
2018-05-22:得到蘋果官方的確認。
2018-07-09:收到蘋果的通知表示他們需要在其他平台上解決相似問題,要求我不要透露漏洞相關信息。
2018-07-09:蘋果官方發布MacOS 10.13.6版本,漏洞被修復。
2018-09-13:向蘋果官方諮詢能否發布漏洞相關信息。
2018-09-13:蘋果方面表示漏洞細節信息會在11月發出。
2018-10-30:漏洞發布。
*參考來源:lgtm,Karunesh91編譯,轉載請註明來自FreeBuf.COM
※信息安全領域中機器學習入門淺談
※Android數據存儲安全實踐
TAG:FreeBuf |