如果您不控制,攻擊者就會來控制 安全的程序中第一道防線是檢查每一個不可信的輸入。但是這是什麼意思呢?可以歸結為以下三點: 限制程序暴露的部分。如果您的程序分為若干塊 —— 這通常是一個好主意 —— 那麼盡量設計得讓攻擊者根本不能與大多數塊通信。
如果您不控制,攻擊者就會來控制 安全的程序中第一道防線是檢查每一個不可信的輸入。但是這是什麼意思呢?可以歸結為以下三點:
限制程序暴露的部分。如果您的程序分為若干塊 —— 這通常是一個好主意 —— 那麼盡量設計得讓攻擊者根本不能與大多數塊通信。這包括不能讓他們利用各塊之間的通信路徑。如果攻擊者不能查看、修改或者插入他們自己的數據到那些通信路徑中(包括作為塊間的中間人潛入),那是最好了。如果那不可能 —— 比如當塊之間使用網絡通信時 —— 那麼使用加密等機制來防范攻擊者。後續的文章將更深入地討論這一問題。
限制暴露部分所允許的輸入類型。有時您可以修改設計以使只有少數的輸入可以接受 —— 如果可以,那麼就這樣做吧。
嚴格檢查不可信的輸入。真正“安全”的程序應該沒有任何輸入,但那種程序是沒有用處的。因而,您需要對來自於不可信源的輸入路徑的數據進行嚴格的檢查。前一期文章 論述了如何檢查各種不同類型的數據;本文將幫助您確定這些數據的來源。這並不是說您 只 需檢查進入您的程序的數據。通常明智的做法是檢查多個位置的數據,但是您必須至少檢查所有的數據一次,並且明智的做法是至少在數據第一次進入時進行一次檢查。
程序的類型決定一切 您必須檢查所有不可信的輸入 —— 但是什麼是不可信的輸入呢?其中一些取決於您的程序要做什麼。如果您的程序是數據的浏覽器或者編輯器(比如文字處理器或者圖像顯示器),而這些數據有可能來自攻擊者,所以那是不可信的輸入。如果您的程序響應網絡上的請求,那些請求可能正是來自攻擊者 —— 所以網絡連接是不可信的輸入。
另一個重要的因素是您的程序是如何設計的。如果您的程序運行時身份是“root”或者其他一些特權用戶,或者有對數據(比如
數據庫中的數據)的訪問特權,那麼從程序中沒有特權的部分到那些有特權的部分的輸入是不可信的。
尤其重要的情形是所有的“setuid”或者“setgid”的程序。只是運行一個 setuid/setgid 程序就會獲得特權,這些程序特別難以保證安全。為什麼呢?因為 setuid/setgid 程序有特別多的輸入 —— 它們中很多輸入多得驚人 —— 可以被攻擊者控制。
常見的輸入源 下面的章節將討論一些常見的輸入以及如何處理這些輸入。當您編寫程序的時候這些輸入每一個都應該考慮,如果它們不可信,一定要謹慎對它們進行過濾。
環境變量 環境變量可能令人難以置信地危險,尤其是對那些 setuid/setgid 程序及它們調用的程序。危險的原因在於以下三個方面:
許多庫和程序由環境變量以非常含糊的方式控制著 —— 實際上,很多都完全沒有文檔化。命令 shell /bin/sh 使用 PATH 和 IFS 等環境變量,程序加載器 ld.so (/lib/ld-
linux.so.2) 使用 LD_LIBRARY_PATH 和 LD_PRELOAD 等環境變量,很多程序使用 TERM、HOME 和 SHELL 環境變量 —— 所有 這些環境變量都可用於
開發程序。這樣的環境變量數不勝數;對調試來說它們很多都是晦澀的變量,並且將它們全部列出也是無濟於事的。實際上,您不可能了解全部環境變量,因為有一些並沒有文檔化。
環境變量是繼承而來的。如果程序 A 調用 B,而 B 調用 C,C 調用 D,那麼程序 D 將獲得的環境變量就是程序 A 所獲得的環境變量,除非有一些程序在這個過程中對其進行了改動。這就意味著,如果 A 是一個安全的程序,而 D 的開發者為了調試方便而增加了一個沒有文檔化的環境變量,那麼程序 D 的這個附加的環境變量就會成為程序 A 的一個漏洞!這種繼承不是偶然的 —— 這是為了使環境變量有用 —— 但是這也使之成為一個嚴重的安全問題。
環境變量可以被本地運行的攻擊者 完全 控制,而且攻擊者可以用不同尋常的方式來利用這一點。如 environ(5) 手冊頁(參閱 參考資料)中所描述,環境變量在內部作為字符指針數組來存儲(數組以一個 NULL 指針結束),每一個字符指針指向一個形式為 NAME=value(這裡 NAME 是環境變量名) 的以零字符結尾的(NIL-terminated)字符串。這一細節的重要性何在?這是因為攻擊者可能會做一些不合常理的事情,例如為同一個環境變量名創建多個值(比如兩個不同的 LD_LIBRARY_PATH 值)。這可以很容易地導致庫使用環境變量去做意想不到的事情,有可能被利用。GNU glibc 庫對此有防范的例程,但是使用環境變量的其他庫和任何例程可能很快陷入困境。
有一些情形下,程序經過了修改,以使得難以利用它們來使用環境變量。歷史上,很多攻擊利用的是命令 shell 處理 IFS 環境變量的方法,但是當今大部分的 shell(包括 GNU bash)已經經過了修改,從而使 IFS 難以利用。
不幸的是,盡管這一加固措施是一個好主意,但它還是不夠 —— 您還是需要謹慎地去處理環境變量。在 Unix 類系統上所有的程序如何運行,這是一個特別重要的(雖然難以理解)例子。Unix 類系統(包括 GNU/Linux)首先通過系統加載器來運行程序(在大部分 GNU/Linux 系統中這個加載器是 /lib/ld-linux.so.2),它可以定位並加載所需要的共享庫。這個加載器通常由環境變量來控制。
在大部分 Unix 類系統中,加載器通常在環境變量 LD_LIBRARY_PATH 中列出的目錄中開始搜索庫。我應該說明一下,LD_LIBRARY_PATH 被很多 Unix 類系統使用,但不是全部都用;HP-UX 用的是環境變量 SHLIB_PATH,AIX 用的是 LIBPATH。而且,在 GNU-based 系統(包括 GNU/Linux)中,環境變量 LD_PRELOAD 所列出的庫首先加載,並且優先於所有其他的庫。
問題是,如果攻擊者可以控制程序用到的底層庫,那麼攻擊者就可以控制整個程序。例如,假設攻擊者可以運行 /usr/bin/passwd(一個可以改變您的口令的特權程序),但卻用環境變量去改變這個程序用到的庫。攻擊者可以編寫自己的口令加密函數 crypt(3), 然後當特權程序嘗試調用這個庫時,攻擊者可以讓這個程序來做任何事情 —— 包括允許永久地、無限制地控制整個系統。當前,加載器通過檢測程序是否設置了 setuid/setgid 來防范這一問題,如果設置了,它們就會忽略 LD_PRELOAD 和 LD_LIBRARY_PATH 環境變量。
那麼,我們安全了嗎?沒有。如果惡意的 LD_PRELOAD 或者 LD_LIBRARY_PATH 值沒有被 setuid/setgid 程序清除,它將被傳遞到其他程序,並導致出現加載器試圖去防范的問題。因而,雖然加載器讓編寫安全的程序成為 可能,但您還不得不去防范惡意的環境變量。而且,這還不能處理那些沒有文檔化的環境變量的問題。
對於安全的 setudi/setgid 程序來說,惟一可靠的辦法是,始終在程序開始時“提取並清除”環境變量:
提取出您確實需要的環境變量(如果有)。
清除所有的環境變量。在 C/C++ 中,通過包含 <unis
td.h> 並將 environ 變量設為 NULL 可以清除環境變量(盡早做這些事情,特別是在創建線程之前)。
只將您所需要的環境變量設置為可靠的值。您幾乎肯定要重新添加的一個環境值是 PATH,它是搜索程序的目錄列表。典型的 PATH 應該只是設置為 /bin:/usr/bin,或者一些類似的值。不要向 PATH 中添加當前路徑“.”,或者甚至一個空條目(這樣在開始和結束的冒號可被利用)。典型的,您還需要設置 IFS(設置為它默認的“ \t\n” —— 空格、制表符和新行)和 TZ(時區)。其他您可能需要設置的是 HOME 和 SHELL。您的應用程序可能還需要更多,但是要限制它們 —— 除非是特別需要,否則不要接受潛在的攻擊者的數據。
文件 正如我在前一期文章中提到的,不要信任可以被攻擊者設置的文件名。Linux 和 Unix 允許用任意的字符序列來作為文件名,所以,如果您正在使用一個來自攻擊者的目錄或者接受他的一個文件名,一定要有所准備。攻擊者可以創建以“-”開頭的文件名,或者含有“&”等特殊字符的文件名,等等。
不要信任可以被不可信用戶控制的文件內容。這包括那些被程序浏覽或編輯的可能是由攻擊者寄來的文件。例如,著名的文本編輯器 vim 版本 5.7,當要編輯一個文件時,將查找一個內置的 statusline 命令來在它的狀態行上設置信息,而那個命令又可以執行任意的 shell 程序。攻擊者可以用電子郵件給受害者發送特別處理過的文件,如果受害者用 vim 來閱讀或者編輯它,受害者就可能會去運行攻擊者想要運行的任何程序!!!
避免從當前目錄中獲得配置信息,因為用戶可能會浏覽一個由攻擊者控制的目錄,攻擊者在那裡創建了一個惡意的配置文件(例如,攻擊者可能已經發送了一個包括數據和惡意配置文件的壓縮目錄)。而應該從 /etc、用戶的主目錄和/或桌面環境的庫中獲得配置信息。通常,將配置信息以及其他信息存儲在“~/.program-name”文件中是很方便的;文件名最前的句點是為了讓它不影響正常的顯示。如果您真正必須要從當前目錄下得到配置信息,那麼要非常嚴格地檢查其中的所有數據。
不要讓攻擊者控制任何臨時文件。我建議,如果一個用戶是可信的,那麼將臨時目錄放在那個用戶的主目錄下。如果這不可接受,那麼要用安全的方法來創建和使用臨時文件(我將在後一期文章中討論如何安全地創建臨時文件)。
文件描述符 不懷好意的攻擊者可能啟動一個程序而只是對它的標准輸入、標准輸出或者標准錯誤做一些奇怪的事情。例如,攻擊者可能會關閉它們中的一個或多個,以使得您打開的下一個文件同時也是正常的輸出位置。這對 setuid/setgid 程序來說尤其是一個問題。當前的一些 Unix 類系統已經可以防范這一問題,但不是所有的系統都可以。
setuid/setgid 程序防范這一攻擊的一種方法是,使用 open() 反復打開 /dev/null 直到文件描述符的值大於 2(您必須在打開文件前做這件事情,最好是在程序初始化時)。然後,如果對 open() 的第一次調用返回的是 2 或者更小,那麼不輸出任何消息並退出。通過先反復打開 /dev/null,您自己保護了自己 —— 如果您偶爾試圖去打開文件並輸出錯誤消息時,不會再發生壞的事情。在這種情況下不需要輸出錯誤消息,因為文件描述符 0 到 2 只是在攻擊者試圖去攪亂您的程序的時候才會被關閉。
命令行 程序啟動時可以接受來自命令行的數據 —— 但是您可以相信這些數據嗎?setuid/setgid 程序尤其不能。如果您不能相信這些數據,那就要自己做好一切准備,包括大的參數、大量的參數、不可知的字符,等等。注意,程序的名字只是命令行值的第 0 個參數 —— 不要 相信程序名,因為攻擊者會改變它。
不但如此,還要盡力去設計您的命令行語法,以使它更容易安全地使用。例如,支持用標准的“--”(雙破折號)選項來表示“不再有選項”,這樣腳本就可以使用這個選項來防止攻擊者通過創建以破折號開頭的文件名 (如“-fr”)來攻擊。否則,攻擊者可以創建“-fr”文件,並讓用戶運行“yourcommand *”;這時您的程序可能會將文件名(“-fr”)曲解為一個選項。
圖形用戶界面(GUI) 這裡是應對災難的一個方案:一個進程擁有特別的特權(例如,如果它設置了 setuid/setgid),它使用操作系統的圖形用戶界面(GUI)庫,而且 GUI 用戶不是完全可信任的。問題是,GUI 庫(包括 Unix 的、Linux 的和 Windows 的)並不是設計這樣使用的。這樣做也不現實 —— GUI 庫很大,而且依賴於龐大的基礎結構,所以很難為了安全性而去分析所有的代碼。GTK+ GUI 庫當發現它是在 setuid 程序中運行時甚至會停止,因為它沒有假定會這樣使用(感謝 GTK+ 的開發者主動預防了這一安全問題)。
難道這意味著您只能使用命令行?不是。將您的程序分為小一些的部分,用沒有特權的部分去實現 GUI,用單獨的部分去實現需要特權的操作。下面是一些常見的做法:
通常,簡單的方法是將有特權的操作作為命令行程序來實現,由 GUI 調用 —— 那樣您可以“無償地”獲得 GUI 和命令行界面(CLI),從而簡化了腳本編寫和調試。典型地,CLI 特權程序是一個 setuid/setgid 程序。當然,有特權的程序必須保護自己不受攻擊,但是這個方法通常意味著這一部分必須是安全的程序,這些程序應更小並且更容易保護。
如果您需要高速通信,將這個程序作為有特權的程序啟動,把它分為可以安全通信的獨立的進程,然後將一個進程永久釋放特權並去運行 GUI。
另一種方法是實現有特權的
服務器來響應請求,並創建 GUI 作為客戶機。
使用 Web 界面;創建一個有特權的服務器,然後使用 Web 浏覽器作為客戶機。這實際上是先前方法的一個特例,但它很靈活,因此通常值得考慮。如任何其他為我們帶來網絡數據問題的 Web 應用程序一樣,您將需要使它安全。
網絡數據 如果數據來自於網絡,您應該認為它是高度不可信的。不要相信“源 IP”地址、HTTP“Referrer”頭的值或者類似的數據所告訴您的數據來自何方;那些來自發送者的值可以被偽造。當心來自域名系統(DNS)的值;DNS 實現的是一個分布式數據庫,那些值中有一些可能是攻擊者提供的。
如果您有一個客戶機/服務器系統,服務器應該永遠不要相信客戶機。客戶機數據在到達服務器前可以被操縱,客戶機程序可能已經被修改,或者攻擊者可能創建了他們自己的客戶機(很多這種情況!)。如果您正在從 Web 浏覽器獲得數據,不要忘記 Web cookie、HTML 表單數據、URL 等可以被用戶設置為任意的值。這是網絡購物車應用程序中常見的問題;許多這樣的應用程序使用隱藏的 HTML 表單域來存儲產品信息(比如價格)和相關信息(比如運費),當用戶發送這些值時就盲目地接收。用戶不僅能將產品價格設置為更低的值或者零,有時他們還可以設置負值的價格,以得到商品和附加的現金反款。記住,必須檢查 所有 的數據;有一些網絡購物車檢查了產品數據卻忘記去檢查運費。
如果您正在編寫一個 Web 應用程序,查詢數據時要限制使用 GET 請求。不要讓 GET 請求實際上去改變數據(比如傳輸的錢數)或者進行其他行為。用戶很容易被欺騙去點擊他們 Web 浏覽器中惡意的超鏈接,這樣就會發送 GET 請求。相反,如果您得到的 GET 請求有查詢以外的行為,那麼返回一個“you asked me to do X,is that okay? (Ok,Cancel)”格式的確認消息。注意,限制 GET 查詢不能幫您解決錯誤的客戶機數據的問題(如先前段落所討論的) —— 服務器還是需要檢查來自它們的客戶機的數據!
其他來源 程序有很多其他的輸入,比如當前目錄、信號、內存映射、System V IPC、umask、文件系統狀態。有了在這裡獲得的信息,重要的是不要忽略這些也可以作為輸入,即使它們有時看起來不像是輸入。
結束語 安全的程序必須檢查每一個不可信的輸入通道,這樣做可以避免很多問題。但是那也還不夠。有時,即使只是讀入數據也可以是安全漏洞 —— 甚至在數據被檢查之前!處理數據可以導致程序以可怕的方式失敗。我們將要討論的是當前第 1 號安全漏洞 —— 緩沖區溢出。我的下一期文章將論述這個漏洞是什麼樣的,如何進行防范,以及為什麼可以期望它未來將不再是問題。