安全的程序必須最小化特權,以降低 bug 轉化為安全缺陷的可能性。本文討論了如何通過最小化有特權的模塊、授與的特權以及特權的有效時間來最小化特權。文章不僅討論了一些傳統的類 UNIX 特權機制,還討論了較新的機制,如 FreeBSD 的 jail(),Linux 安全模塊(Linux Security Modules,LSM)框架,以及 Security-Enhanced Linux(SELinux)。
2003 年 3 月 3 日,Internet Security Systems 對 Sendmail 中的一個嚴重的漏洞提出了警告。所有的電子郵件都通過郵件傳輸代理(mail transfer agent,MTA)來傳輸,Sendmail 則是最流行的 MTA,所以這個警告影響了世界范圍內的很多組織。問題在於,按通常的配置,精心設置了“from”、 “to”或者“cc”域的電子郵件消息可以讓發送者完全(root)控制任何一台運行著 Sendmail 的機器。更嚴重的是,一般的防火牆將 不能保護其內部的機器免受這種攻擊。
造成這一漏洞的直接原因是,Sendmail 的一個安全檢測是有缺陷的,可以發生緩沖區溢出。不過,一個重要的作用因素是,Sendmail 經常被安裝為一個單一的“setuid root”程序,對運行它的系統有完全的控制權限。這樣,Sendmail 中的任何缺陷都可以讓攻擊者直接控制整個系統。
這個設計是必須的嗎?不是;Wietse Venema 的 Postfix 是一個常見的可以與之匹敵的 MTA。類似於 Sendmail,Postfix 會去做很多安全檢測,不過,為了 最小化特權,Postfix 設計為一組模塊。結果,Postfix 通常被認為是比 Sendmail 更安全的程序。本文討論了如何最小化特權,您可以將同樣的思想應用到您的程序中。
最小化特權的基礎
實際應用的程序會有缺陷。不是我們希望那樣,但是確實是有。復雜的需求、日程的壓力和環境的變化使得不太可能得到實用的無缺陷的程序。甚至那些通過復雜而且精確的技術正式地證明是正確的程序,也會有缺陷。為什麼?其中一個原因是,驗證必須做很多假設,而且通常這些假設並不是完全正確。無論如何,出於種種原因,大部分程序沒有得到嚴格的檢驗。而且,即使今天沒有任何缺陷(不太可能),日後維護的改變或者環境的改變都可能會引入缺陷。所以,要處理實際的問題,我們不得不以某種方式來開發安全的程序, 盡管 我們的程序中有缺陷。
盡管有這些缺陷,對安全編程來說最重要的方法是 最小化特權。特權只是允許去做並不是 每個人 都可以做的事情。在類 UNIX 系統中,擁有“root”用戶、其他用戶或者一個組的成員的特權是最常見的特權種類。一些系統讓您可以授與讀或寫特定文件的特權。不過,不管怎麼樣,要最小化特權:
只為程序中需要特權的部分授與特權
只授與部分絕對需要的具體特權
將特權的有效時間或者可以有效的時間限制到絕對最小
這些其實是目標,不是絕對的。您的基礎組織(比如您的操作系統或者虛擬機)可能使得嚴格完成這些並不容易,或者嚴格完成這些可能會很復雜,而導致在嘗試嚴格完成時引入更多缺陷。但是,你距離這些目標越近,缺陷導致安全問題的可能性就越低。即使缺陷導致了安全問題,它導致的安全問題的嚴重性可能會更低。而且,如果您可以確保只有小部分程序擁有特定的特權,您就可以用大量額外的時間來確保 那 一部分能抵御攻擊。這個思想並不新; Saltzer 和 Schroeder 的優秀的 1975 論文討論了安全的原理,明確地將最小化特權作為一個原則(查看 參考資料)。有一些思想是永恆的,比如最小化特權。
接下來的三節將依次討論這些目標,包括如何在類 UNIX 系統中實現他們。然後,我們將討論 FreeBSD 和 Linux 中可用的一些特別的機制,包括對 NSA 的 Security-Enhanced Linux(SELinux)的討論。
最小化有特權的模塊
如前所述,只有需要特權的部分程序才應用擁有特權。這就是說,當您設計您的程序時,盡量將程序分解為獨立的部分,以使得只有小而獨立的部分需要特定的特權。
如果不同的部分必須同時運行,那麼在類 UNIX 系統中使用進程(不是線程)。線程共享它們的安全特權,有問題的線程可能會干擾進程中所有其他線程。編寫有特權的部分時,就當作其它的程序正在攻擊它:某一天會可能!確保有特權的部分只去做盡可能少的事情;受限的功能意味著更不易被利用。
一個通常的方法是,創建功能極度受限的擁有特定特權(比如是 setuid 或者 setgid)命令行工具。UNIX 的 passwd 命令就是一個例子;它是一個具有特定特權的命令行工具,用於修改密碼(setuid root),但是它所能做的只是修改密碼。於是各種 GUI 工具可以要求 passwd 來做實際的更改。如果有可能,盡量完全避免創建 setuid 或 setgid 程序,因為很難確保您正在真正保護所有輸入。不過,有時您需要創建 setuid/setgid 程序,所以,當需要時,盡可能使程序最小且最受限制。
有很多其他的方法。例如,您可以有一個具有特定特權的小的“服務器(server)”進程;那個服務器只允許特定的請求,而且只是在確認請求者被允許發出請求之後。另一個常見的方法是,使用特權啟動一個程序,這個程序然後派生放棄所有特權的第二個進程,而由這個進程來做大部分工作。
要當心這些模塊彼此之間如何通信。在很多類 UNIX 系統上,命令行值和環境可以被其他用戶看到,所以不是在進程間保密地發送數據的好辦法。管道可以勝任,但是要細心地避免死鎖(一個兩端都可以刷新的簡單的請求/響應協議就可以勝任)。
最小化授與的特權
確保您只授與特權給確實需要的程序——到此為止。UNIX 進行獲得特權的主要途徑是它們以哪個用戶或組的身份運行。通常,進程以使用它們的用戶和組身份運行,不過,“setuid” 或 “setgid” 的程序會獲得擁有這個程序的用戶或組的特權。
悲哀的是,還是有一些不自主地給予程序“setuid root”特權的類 UNIX 系統上的開發者。這些開發者認為他們使得事情對自己來說變得“容易”,因為他們不必再去深入考慮他們的程序確切需要什麼特權。問題是,由於這些程序程序可以在大部分類 UNIX 系統上做差不多所有的事情,所以任何一個缺陷都可以很快成為一個安全災難。
不要只是因為您需要完成一個簡單的任務就給出所有可能的特權。而應該只給予程序它們所需要的特權。如果您可以,以 setgid 來運行它們,而不要用 setuid——setgid 給予的特權更少。創建特定的用戶和組(不要使用 root),並根據您的需要使用它們。確保 root 所擁有的那些可執行程序只能由 root 來寫,這樣其他人就不能修改它們。設置非常嚴格的文件權限——如果不是絕對需要,不要讓所有人都可以讀或寫文件,並且使用那些特定的用戶和組。能說明所有這些的一個例子可能是游戲“top ten”分數的標准慣例。很多程序都是“setgid games”,以使得只有游戲程序可以修改“top ten”分數,而且存儲這些分數的文件的主人是 games 組(而且只有這個組可以寫)。即使攻擊者攻擊並進入了一個游戲程序,所有他能做的事情將是修改分數文件。無論如何,游戲開發者還是需要編寫他們的程序來防止惡意的分數文件。
chroot() 系統調用是一個實用的工具——不幸的是有一些難用。當進程查看文件系統的 “root”時,這個系統調用會修改進程所看到的內容。如果您計劃用它——而且它可能是實用的——要准備好花一些時間來用好它。必須精心准備 “new root”,這是復雜的,因為確切的應用程序依賴於平台和應用程序的特性。您 必須 以 root 身份來進行 chroot() 調用,而且您 應該快速地 改變為非 root 身份(root 用戶可以脫離 chroot 環境,所以如果它要生效,您需要解除那個特權)。而且 chroot 不會改變網絡訪問。這可以是一個實用的系統調用,所有有時候需要考慮它,但是要做好付出努力的准備。
限制資源是一個經常被遺忘的工具,這既包括存儲的資源也包括進程的資源。這些限制拒絕服務攻擊尤其有用:
對存儲來說,您可以為每個用戶或組設置每個掛載的文件系統的存儲量或文件數的配額(限定)。在 GNU/Linux 系統中查看 quota(1)、quotactl(2) 和 quotaon(8) 來深入了解這一功能,不過,盡管它們不是哪裡都能用,大部分類 UNIX 系統都包含了 quota 系統。在 GNU/Linux 和很多其他系統中,您可以設置“硬”界限(永遠不能超出)和“軟”界限(可以臨時超出)。
對進程來說,您可以設置很多限定,比如打開文件的數目、進程的數目,等等。這種能力實際上是標准的一部分(比如單一 UNIX 規范(Single UNIX Specification)),所有它們在類 UNIX 系統上幾乎普遍存在;要深入了解,請查看 getrlimit(2)、 setrlimit(2) 以及 getrusage(2)、sysconf(3) 和 ulimit(1)。進程永遠不能超出“當前界限”,但是它們可以將當前界限一路上升到“上限”。不幸的是,這裡有一個不合常理的術語問題可能會使您迷惑。“當前界限”也被稱為“軟”界限,上限也稱為 “硬”界限。這樣,您就會處在一個異乎尋常的情形,進程 永遠 不能超出進程界限的軟(當前)界限——而對於 quota 來說您 可以 超出軟界限。我建議為進程界限使用術語“當前界限”和“上限”(永遠不要使用術語“軟”和“硬”),那樣就沒有任何迷惑了。
最小化特權的時間[更多精采技術文章-★編程入門★網]
只是當需要的時候才給予特權——片刻也不要多給。
只要可能,使用無論什麼您立即需要的特權,然後 永久地 放棄它們。一旦它們被永久放棄,後來的攻擊者就不能以其他方式利用那些特權。例如,需要個別的 root 特權的程序可能以 root 身份啟動(比如說,通過成為 setuid root)然後切換到以較少特權用戶身份運行。這是很多 Internet 服務器(包括 Apache Web 服務器)所采用的方法。類 UNIX 系統不允許任何程序打開 0 到 1024 TCP/IP 端口;您必須擁有 root 特權。但是大部分服務器只是在啟動的時候需要打開端口,以後就再也不需要特權了。一個方法是以 root 身份運行,盡可能快地打開需要特權的端口,然後永久去除 root 特權(包括進程所屬的任何有特權的組)。也要嘗試去除所有其他繼承而來的特權;例如,盡快關閉需要特定的特權才能打開的文件。
如果您不能永久地放棄特權,那麼您至少可以盡可能經常臨時去除特權。這不如永久地去除特權好,因為如果攻擊者可以控制您的程序,攻擊者就可以重新啟用特權並利用它。盡管如此,還是值得去做。很多攻擊只有在它們欺騙有特權的程序做一些計劃外的事情而且程序的特權被啟用時才會成功(例如,通過創建不合常理的符號鏈接和硬鏈接)。如果程序通常不啟用它的特權,那麼攻擊者想利用這個程序就會更困難。
較新的機制
到目前為止,我們所討論的原則實際上適用於幾乎所有操作系統,而且,自 19 世紀 70 年代以來,幾乎所有的類 UNIX 系統的常規機制都是類似的。那並不是說它們沒有用處;簡單和時間的檢驗是它們本身的優勢。不過,一些較新的類 UNIX 系統已經增加了支持最少權限的機制,值得去了解。雖然很容易找出經過時間檢驗的機制,可是關於較新的機制的資料還沒有廣為人知。所以,在這裡我將討論選出的一些有價值的機制:FreeBSD jail() 、Linux 安全模塊(LSM)框架和 Security-Enhanced Linux(SELinux)。
FreeBSD jail()
chroot() 系統調用有很多問題,如前所述。例如,它難以正確使用,root 用戶還是可以從中脫離,而且它根本不去控制網絡訪問。FreeBSD 開發者決定增加一個新的系統調用來解決這些問題,這個新的系統調用叫做 jail() 。這個調用類似於 chroot() ,不過盡力更易用且更用效。在一個 jail 中,所有的請求(即使是 root 的)都被 jail 所限,進程只能與 jail 中的其他進程通信,而且系統封鎖了 root 用戶試圖從 jail 中脫離的典型途徑。jail 會被分配一個特定的 IP 地址,不能使用任何其他地址作為它自己的地址。
jail() 調用是 FreeBSD 所獨有的,這就限制了它的效用。不過,各個 OSS/FS 內核之間有很多交叉影響(cross-pollination)。例如,已經使用 Linux 安全框架為 Linux 開發了一個 jail 版本。而且, FreeBSD 5 已經添加了一個靈活的 MAC 框架(來自 TrustedBSD 項目),包括一個具有類似 SELinux 基本功能的模塊。所有,將來看到更多這種情況不要感到奇怪。
Linux 安全模塊(LSM)
在 2001 年的 Linux Kernel Summit 上,Linus Torvalds 遇到了一個問題。一些不同的安全項目,包括 Security-Enhanced Linux(SELinux)項目,要求他將他們的安全方法添加到 Linux 內核中。問題是,這些不同的方法常常是不兼容的。 Torvalds 沒有簡單的方法可以判定哪個是最好的,所以他要求那些項目為 Linux 合作創建某種通用的安全框架。那樣,管理員就可以給他們特別的系統安裝任意他們想要的安全方法。與 Torvalds 討論了幾次以後,Crispin Cowan 建立了一個小組來創建這個通用的安全框架。這個框架被命名為 Linux 安全模塊(LSM)框架,現在是標准 Linux 內核的一部分(如 2.6 版本內核)。
概念上講,LSM 框架特別簡單。Linux 內核仍去做它常規的安全檢測;例如,如果您要寫一個文件,您仍需要對其有寫權限。不過,不論何時如果 Linux 內核需要判定是否應該准許訪問,它還要進行核對——通過一個“book”去要求一個安全模塊來進行—— 來確定動作是否得到准許。這樣,管理員可以簡單地選出他想要使用的安全模塊,並像其他 Linux 內核模塊一樣將其插入。從那時以後,那個安全模塊將判定什麼是允許的。
LSM 框架設計得如此靈活,它可以實現很多不同種類的安全策略。實際上,一些不同的項目進行合作以確保 LSM 框架足以勝任真正的工作。例如,當內部對象被創建或被刪除時 LSM 引入一些調用——不是因為那些操作可能會中止,而是讓安全模塊可以保持對重要數據的追蹤。使用了一些不同的分析工具來確保 LSM 框架不會遺漏其目標的任何重要異常分支。結果證明,這個項目比很多人想像的要困難,它的成功是來之不易的。
有必要理解 LSM 所做的基本的設計決定。基本上,LSM 框架故意設計為幾乎所有異常分支都是受限的,而不是可信的(authoritative)。一個 可信的 異常分支做出絕對最終的決定:如果異常分支認為一個請求應該被准許,那麼它就會被無條件准許。相反, 可信的 異常分支只能增加另外的限制;它不能授與新權限。理論上,如果所有 LSM 異常分支都是可信的,LSM 框架將會更加靈活。有一個名為 capable() 的異常分支是可信任的——但是只是因為它不得不支持常規的 POSIX 能力。不過,要讓 所有 異常分支都可信,就要對 Linux 內核進行很多根本的改變,還說不准這種改變是不是會被接受。
有很多人擔心,如果大部分異常分支都可信,即使是最小的缺陷也將成為災難;而讓異常分支受限意味著用戶將不會感到意外(不管怎樣,原來的 UNIX 權限仍正常工作)。所以 LSM 框架開發者有意選擇了限制方法,而且它的大部分開發者自信他們可以在框架內工作。
理解 LSM 框架的其他限制也是重要的。LSM 框架設計只是用來支持訪問控制,不是審計等其他安全問題。 LSM 模塊本身不能記錄所有請求或它們的結果,因為它們不能看到全部。為什麼?一個原因是,內核可能沒有調用 LSM 模塊就拒絕了請求;如果您想審計這個拒絕就會有問題。還有,出於性能的考慮,有一些提議的用於網絡的 LSM 異常分支和數據域沒有被主線內核所采用。它可以控制一些網絡訪問,但不足以支持“labelled”網絡數據流(在這種情況下,不同的數據包有不同的由操作系統處理的安全標簽)。這些都是不合適的限制,也不符合一般思想的基本原則;LSM 框架有希望終有一天得到擴展以破除這些限制。
盡管如此,即使有這些限制,LSM 框架對給特權添加限制來說仍是非常實用。Torvalds 的目標由 LSM 框架根本上實現了:“我不喜歡在不同的安全人群之間斗爭。我希望是間接的,讓 我跳出這場斗爭,然後市場斗爭就可以決定哪個策略和實現最終得到 應用。”
所以,如果您想在 Linux 上限制授與您的程序的特權,您可以創建完全您自己的安全模塊。如果您利用真正外來的限制,可能會需要那樣做——幸好這是可能的。不過,這很重要;不管怎樣,您還是要編寫內核代碼。如果可能,您最好不要使用已有的 Linux 安全模塊,而是嘗試去編寫自己安全模塊。有一些可用的 LSM 模塊,不過,Security-Enhanced Linux (SELinux)是最成熟的 Linux 安全模塊之一,所以讓我們來研究這個模塊。
Security-Enhanced Linux(SELinux)的歷史
一個小歷史將有助於幫助您理解 Security-Enhanced Linux(SELinux)——而且它本身也是段有趣的歷史。美國國家安全局(National Security Agency,NSA)長時間以來就關注大部分操作系統中受限的安全能力。畢竟,他們的工作之一就是要確保美國國防部使用的計算機在面臨沒完沒了的攻擊時保持安全。NSA 發現大部分操作系統的安全機制,包括 Windows 和大部分 UNIX 和 Linux 系統,只實現了“選擇性訪問控制(discretionary access control)”(DAC)機制。DAC 機制只是根據運行程序的用戶的身份和文件等對象的所有者來決定程序可以做什麼。NSA 認為這是一個嚴重的問題,因為 DAC 本身對脆弱的或惡意的程序來說是一個不合格的防護者。取而代之的,NSA 長期以來一直希望操作系統同樣能支持“強制訪問控制(mandatory access control)”(MAC)機制。
MAC 機制使得系統管理員可以定義整個系統的安全策略,這個策略可以基於其他因素,像是用戶的角色、程序的可信性及預期使用、程序將要使用的數據的類型等等,來限制程序可以做哪些事情。一個小例子,有了 MAC 後用戶不能輕易地將“保密的(Secret)”數據轉化為“不保密的(Unclassified)”的數據。不過,MAC 實際上可以做的比那要多得多。
NSA 已經與操作系統提供商合作了多年,但是很多占有最大市場的提供商對於將 MAC 集成進來沒有興趣。即使是那些集成了 MAC 的提供商也通常是將其做為“單獨的產品”,而不是常規產品。一部分原因只是因為舊式的 MAC 不夠靈活。
於是 NSA 的研究力量盡力去使 MAC 更靈活並且並容易被包含在操作系統中。他們使用 Mach 操作系統開發了他們的思想的原型,後來發起的工作擴展了“Fluke”研究操作系統。不過,難以讓人們信服這些思想可以適用於 “真實的”操作系統 ,因為所有這些工作都基於微型的“玩具級的”研究項目。極少可以在原型之外進行嘗試以查看這些思想在真實的應用程序中工作得如何。NSA 不能說服具有所有權的提供商來添加這些思想,而且 NSA 也沒有權利去修改私有的操作系統。這不是個新問題;多年前 DARPA 試圖強制它的操作系統研究人員使用私有的操作系統 Windows,但遇到了很多問題(參見下面的 參考資料)。
於是,NSA 偶然發現了一個回想起來似乎顯而易見的想法:使用一個 不是 玩具的開放源代碼操作系統,並實現他們的安全思想,以顯示(1)它可以工作,(2)它具體如何工作(通過為所有人提供源代碼)。他們選擇了主導市場的開放源代碼內核(Linux)並在其中實現了他們的思想,即“security-enhanced Linux”(SELinux)。毫無意外,使用真正的系統(Linux)讓 NSA 研究人員可以處理他們在玩具中無法處理的問題。例如,在大部分基於 Linux 的系統中,幾乎所有都是動態鏈接的,所以他們不得不做一些關於程序如何執行的深入分析(查閱他們關於“entrypoint”和 “execute”權限的文檔以獲得更多資料)。這是一個更為成功的方法;正在使用 SELinux 的人比使用先前的原型的人多得多。
SELinux 如何工作
那麼,SELinux 如何工作呢?SELinux 的方法實際上非常普通。每一個重要的內核對象,比如每個文件系統對象和每個進程,都有一個關聯到它們的“安全上下文(security context)”。安全上下文可以基於軍事安全層級(如不保密的、保密的和高度保密的)、基於用戶角色、基於應用程序(這樣,一個 Web 服務器可以擁有它自己的安全上下文),或者基於很多其他內容。當它執行另一個程序時,進程的安全上下文可以改變。甚至,取決於調用它的程序,一個給定的程序可以在不同的安全上下文中運行,即使是同一個用戶啟動了所有程序。
然後系統管理員就可以創建一個指定哪些特權授與哪個安全上下文的“安全策略(security policy)”。當發生系統調用時,SELinux 去檢查是否所有需要的特權都已經授與了——如果沒有,它就拒絕那個請求。
例如,要創建一個文件,當前進程的安全上下文必須對父目錄的安全上下文的“搜索(search)”和“add_name”特權,而且它需要有對於(要創建的)文件的安全上下文的“創建(create)”特權。同樣,那個文件的安全上下文必須有特權與文件系統“關聯(associated)”(所以,舉例來說,“高度保密”的文件不能寫到一個“不保密”的磁盤)。還有用於套接字、網絡接口、主機和端口的網絡訪問控制。如果安全策略為那些全部授與了權限,那麼請求就會被 SELinux 所允許。否則,就會被禁止。如果按部就班地去做所有這些檢查將會較慢,不過有很多優化方案(基於多年的研究)使其變得很迅速。
這一檢查完全獨立於類 UNIX 系統中的通常的權限位;在 SELinux 系統中,您必須 既 有標准的類 UNIX 權限, 又 有 SELinux 權限才能去做一些事情。不過,SELinux 檢查可以做很多對傳統的類 UNIX 權限來說難以完成的事情。使用 SELinux,您可以方便地創建一個只能運行特定程序並且只能在特定的上下文中寫文件的 Web 服務器。更有趣的是,如果一個攻擊者攻入了 Web 服務器並成為 root,攻擊者不會獲得整個系統的控制權——如果有一個好的安全策略的話。
那就有了困難:為了使 SELinux 有效,您需要有一個好的安全策略來由 SELinux 執行。大部分用戶將需要一個他們容易修改的實用的初始策略。幾年前我開始體驗 SELinux;那時,初始策略還不成熟,有很多問題。例如,在那些以前的日子中我發現早期的樣例策略不允許系統更新硬件時鐘(最後我提交了一個補丁以解決這一問題)。設計好的初始安全策略類似對產品分類, NSA 希望由商業界來做,而且看起來是要這樣做。Red Hat、一些 Debian 開發者、Gentoo 以及其他人正在使用基本的 SELinux 框架,並且正在創建初始安全策略,這樣用戶可以馬上開始使用它。的確,Red Hat 計劃為所有用戶在他們的 Fedora 內核中都啟用 SELinux,並提供簡單的工具來使得非專業用戶可以通過選擇一些常見選項來修改他們的安全策略。Gentoo 有一個可引導的 SELinux LiveCD。這些團體將使得最小化程序特權變得更簡單,而不需要大量代碼。
在這裡我們又回到了原處。SELinux 只有當程序執行時才允許發生安全傳輸,它控制進程的權限(不是一個進程的一部分)。所以,為了充分發揮 SELinux 的潛力,您需要將您的應用程序分解為獨立的進程和程序,只有一些小的有特權的組件—— 這恰恰如同如何在沒有 SELinux 的情況開發安全的程序。像 SELinux 這樣的工具讓您可以更好地控制授與的權限,並這樣創建一個更強有力的防御,但是,您仍需要將您的程序拆分為更小的組件,以使得那些控制能發揮最大的效用。
結束語
最小化特權是對各種安全問題的最重要防御。由於缺陷是不可避免的,您會希望大大降低缺陷導致安全問題的可能性。不過,至少一個安全的程序的 一些 部分必須有涉及安全的代碼,所以您不能只是最小化特權而忽視所有其他。甚至在您已經最小化了那些涉及安全的部分以後,那些部分還是必須是正確的。為了是正確的,您需要避免常見的錯誤。