文 / Gwyn Fisher
對于所有類型環(huán)境中的開發(fā)人員來說,安全性正成為一個越來越重要的主題,即便過去一直認為安全性不成問題的嵌入式系統(tǒng)也是如此。本文將介紹幾種類型的編碼漏洞,指出漏洞是什么、如何降低代碼被攻擊的風險、如何更好地找出代碼中的此類缺陷。
注入攻擊
通過將信息注入正在運行的流程,攻擊者可以危害進程的運行狀態(tài),以反射到開發(fā)人員無法保護的某種最終目標。例如,攻擊者可能會通過堆棧溢出(stack
corruption)將代碼注入進程,從而執(zhí)行攻擊者選定的代碼。此外,攻擊者也可能嘗試將數(shù)據(jù)注入數(shù)據(jù)庫,供將來使用;或?qū)⑽词鼙Wo的字符串注入數(shù)據(jù)
庫查詢,獲取比開發(fā)人員更多的信息。無論出于怎樣的目的,注入總是一件壞事,總是需要謹慎對待的。
最惡劣的注入攻擊形式也許是代碼注入——將新代碼置入正在運行的進程的內(nèi)存空間,隨后指示正在運行的進程執(zhí)行這些代碼。此類攻擊如果成功,則幾乎可以進行任何操作,因為正在運行的進程完全被劫持,可執(zhí)行攻擊者希望執(zhí)行的任何代碼。
此類攻擊最著名的示例之一就是 Windows 動畫光標攻擊,這正是本文要討論的模式。攻擊者利用一個簡單的 Web
頁面將形式不當?shù)膭赢嫻鈽宋募螺d到查看者的 PC
中,導致瀏覽器調(diào)用此動畫光標,動畫光標調(diào)用時可能發(fā)生任意代碼的注入。實際上,這是一個完美的攻擊載體:因為它不要求對被攻擊機器的任何實際訪問、最終
用戶根本意識不到任何可能發(fā)生的麻煩;此外,如果攻擊效果的惡意也是適度的,則對最終用戶的外部影響幾乎是零。
考慮示例
1(a),當然,這改寫自 Windows
攻擊,它構成了此類攻擊載體的基礎。這里的開發(fā)人員對于傳入流的可靠性做出了基本的假設。信任流和并相信一切都沒問題。使用基于堆棧的將被非串形化
(deserialized)的類型調(diào)用函數(shù),未知數(shù)據(jù)流和代碼注入肯定會在某個時間點出現(xiàn)。
(a)
void LoadTypeFromStream(unsigned char* stream, SOMETYPE* typtr)
{
int len;
// Get the size of our type's serialized form
memcpy(&len, stream, sizeof(int));
// De-serialize the type
memcpy(typtr, stream + sizeof(int), len);
}
(b)
void foo(unsigned char* stream)
{
SOMETYPE ty;
LoadTypeFromStream(stream, &ty);
}
(c)
void LoadTypeFromStream
(unsigned char* stream, SOMETYPE* typtr)
{
int len;
// Get the size of our type's serialized form
memcpy(&len, stream, sizeof(int));
// GUARD
if( len < 0 || len > sizeof(SOMETYPE) )
throw TaintedDataException();
// De-serialize the type
memcpy(typtr, stream + sizeof(int), len);
}
示例1 注入攻擊。
這是怎樣發(fā)生的?假設您調(diào)用示例
1(b)中的函數(shù)。我們就得到了一個易于利用的攻擊載體。這里的問題在于 SOMETYPE 在編譯時的大小是固定的。假設此類型在內(nèi)存中使用 128
個字節(jié)表示。再假設您構建傳入流時,使前 4 個字節(jié)(要非串形化的內(nèi)容的長度)的讀數(shù)為 256。現(xiàn)在,您沒有檢查正在處理的內(nèi)容的有效性,而是將
256 個字節(jié)復制到了僅為 128 個字節(jié)的保留堆棧空間內(nèi)。
考慮到發(fā)布模式堆棧的典型布局,您顯然遇到了麻煩。查看堆棧,了解原因所在。每個被調(diào)用的函數(shù)都會將其本地數(shù)據(jù)布設到堆棧的一個幀內(nèi),通常是通過在輸入時
從堆棧指針減去本地數(shù)據(jù)的已知大小(加上處理調(diào)用鏈本身所需的任何管理數(shù)據(jù))實現(xiàn)的。編譯器發(fā)出的理想函數(shù) prolog(偽代碼)如下所示:
.foo
sub sp, 128 ; sizeof SOMETYPE
隨后,對可利用函數(shù)的調(diào)用應如下所示:
push sp ; push the SOMETYPE
local variable
push ap ; push the stream
pointer (comes from 1st argument)
call LoadTypeFromStream
ret
在調(diào)用 foo() 時,調(diào)用方將流地址以及返回地址(作為使用調(diào)用指令或平臺上可用的同等部分的隱式效果)壓入堆棧,使堆棧內(nèi)容中有 128 個字節(jié)是為我們的類型保留的,且緊鄰返回給 foo() 調(diào)用方的返回地址,參見圖 1。
現(xiàn)在,LoadTypeFromStream 執(zhí)行,并將 256
個字節(jié)寫入所提供的地址,也就是在我們調(diào)用函數(shù)之前堆棧指針(SP)的值。這會覆蓋應該使用的 128 個字節(jié)(本例中位于地址 0x1000
處),加上隨后的 128 個字節(jié),包括傳入的參數(shù)指針、返回地址以及堆棧中隨后 128 個字節(jié)內(nèi)存儲的其他任何信息。
那么攻擊者怎樣利用這樣的漏洞呢?并不簡單,需要經(jīng)過反復的試錯。實際上,攻擊者要安排攻擊,使覆蓋的返回地址將控制權移交給攻擊者,而非預期調(diào)用方函
數(shù)。因而,攻擊者需要準確了解要利用哪些數(shù)據(jù)結構,這樣的數(shù)據(jù)結構在要攻擊的任意版本的操作系統(tǒng)或應用程序上有多大、周邊有哪些內(nèi)容(以便正確設定偽造的
返回地址)、如何有意義地插入足夠的信息以使返回地址和其他效果能夠?qū)崿F(xiàn)某種惡意操作。
這一切做起來并不簡單,但多種多樣的攻擊表明,總是有人有太多的空閑時間。
應如何防范此類攻擊?這是一次攻擊還是多重攻擊?所寫入的代碼是否真的像這里所顯示的這樣笨拙?現(xiàn)代編譯器是否會對堆棧幀布局做一些特殊處理,以避免此類問題?
總而言之,模糊處理就等于沒有防御。我們都認識到,程序員將攻擊預想得越簡單,攻擊出現(xiàn)的可能性就越高。然而,即便是復雜的代碼,若未進行合理防御,也遲
早會受到攻擊。這種利用被污染的數(shù)據(jù)流和非常基本的緩沖溢出漏洞的攻擊,多年以來這一直是熱門的研究課題,但每年仍然會出現(xiàn)大量此類攻擊。
防范此類攻擊的效果甚微,因為攻擊形式復雜——注意您的數(shù)據(jù)假設。只要在示例1(a)中添加一行簡單的代碼,就會使其更加安全,參見示例1(c)。顯然,
隨著流交互變得更加復雜,保護的要求也隨之復雜化,但基本上說代碼注入是編碼中“不可饒恕”的過失,因為防范它的方法是那樣普及和簡單。
SQL 注入
此外還存在其他一些類型的 SQL
注入,可能會給以數(shù)據(jù)庫為中心的應用程序造成嚴重的問題。在某些情況下,攻擊者只是嘗試訪問更多的內(nèi)容。在另一些情況下,攻擊者關注的則是在數(shù)據(jù)庫中存儲
新信息,以便使應用程序此后在不知情的前提下使用此類信息,入侵最終用戶的會話。
基于查詢的攻擊關注的是一種普遍應用的反模式,使用字符串串聯(lián)構建查詢。這種類型的漏洞常常出現(xiàn)在面向 Web 的應用程序中,在所有常用頁面產(chǎn)品——包括 PHP、ASP、JSP 等及其后備控制器邏輯中同樣常見。
這種漏洞的核心是開發(fā)人員使用直接查詢執(zhí)行,而非利用查詢準備來運行數(shù)據(jù)庫交互。考慮以下登錄驗證查詢示例:
SELECT ID FROM USERS WHERE NAME= 'user' AND PWD='password'
用戶將看到一個簡單的 HTML 表單,該表單包含兩個輸入框并使用了這種反模式。從表單傳入的參數(shù)(無論所討論的頁面產(chǎn)品是怎樣接收到這些參數(shù)的)都將通過串聯(lián)直接代入查詢的字符串形式。
考慮攻擊者提供的一組參數(shù):
NAME: x
PWD: x' OR '1' = '1
運行串聯(lián),結果將得到被利用的查詢:
SELECT ID FROM USERS WHERE NAME=
'x' AND PWD='x' OR '1' = '1'
如果登錄僅檢查該語句執(zhí)行成功與否(而未考慮結果行),攻擊者即可迅速獲得該應用程序所處理的任意用戶記錄可提供的任意訪問權限。很多應用程序的用戶表的第一行都是為超級用戶保留的,攻擊此類應用程序輕而易舉。
利用未謹慎處理數(shù)據(jù)庫語句內(nèi)代入字符串的應用程序,攻擊者可實現(xiàn)多種其他形式的攻擊。這種反模式極為常見(參見最近的 Microsoft
公告和其他內(nèi)容了解其普遍性),緩解方法也同樣簡單,并可置于基本數(shù)據(jù)庫 API 之中:使用準備好的語句而非字符串串聯(lián)。
例如,考慮示例2 中的錯誤實現(xiàn)。此函數(shù)嚴格遵循反模式,還通過拋出包含傳入(未過濾)數(shù)據(jù)(即用戶名)的異常而執(zhí)行了另外一項重要的 no-no 操作。如果以響應的形式為用戶呈現(xiàn)此數(shù)據(jù),您就很可能遇到某些惡意利用,特別是可能遭遇跨站腳本攻擊。
public void validateUser(String user, String pwd, Connection db)
throws InvalidUserException
{
Statement stmt = null;
ResultSet rs = null;
try
{
// Create the statement
stmt = db.createStatement();
String sql = "select id from users where user='" + user +
"' and pwd='" + pwd + "'";
// Execute it, process the result
rs = stmt.executeQuery(sql);
if( rs == null || rs.next() == null )
throw new InvalidUserException(user);
}
catch( SQLException e )
{
throw new InvalidUserException(user);
}
finally
{
try { if( rs != null ) rs.close(); } catch( Exception e ) { }
try { if( stmt != null ) stmt.close(); } catch( Exception e ) { }
}
}
示例2 錯誤實現(xiàn)。
為了修正此代碼,不應動態(tài)構建 SQL 查詢,而是直接構建準備好的語句,并使用它來代替?zhèn)魅雲(yún)?shù)。
我們將準備的語句會為參數(shù)保留空間,并且不易受此類攻擊利用,原因就在于它的詞匯方面并不像字符串串聯(lián)那樣脆弱。
考慮以下語句(準備該語句的目的與前面提到的串聯(lián)字符串相同):
SELECT ID FROM USERS WHERE USER=?
AND PWD=?
我使用這個準備好的語句代入了 user 和 pwd 參數(shù)的傳入數(shù)據(jù)。如果我們將之前被利用的字符串作為輸入,結果將是查詢代入過程出錯,因為不能將包含單引號等特殊字符的參數(shù)提供給準備好的查詢。
其他可能出現(xiàn)的利用也能在不同階段捕捉到,但如示例3 所示,新實現(xiàn)的創(chuàng)建與原實現(xiàn)一樣簡單,但安全性要高得多(我們也從拋出的異常中刪除了用戶名,這樣可以避免在未經(jīng)過濾的情況下將其公開給調(diào)用方的危險)。
public void validateUser(String user, String pwd, Connection db)
throws InvalidUserException
{
PreparedStatement stmt = null;
ResultSet rs = null;
try
{
// Prepare the statement, rather than concatenating it
String sql = "select id from users where user=? and pwd=?");
stmt = db.prepareStatement(sql);
// Substitute our incoming parameters into the query
stmt.setString(1, user);
stmt.setString(2, pwd);
// Execute the query and process the results as before
rs = stmt.executeQuery();
if( rs == null || rs.next() == null )
throw new InvalidUserException();
}
catch( SQLException e )
{
throw new InvalidUserException();
}
finally
{
try { if( rs != null ) rs.close(); } catch( Exception e ) { }
try { if( stmt != null ) stmt.close(); } catch( Exception e ) { }
}
}
示例3 示例2 的較為安全的版本。
總體而言,無論是處理查詢還是DML,在處理來自最終用戶的數(shù)據(jù)時,始終應使用準備好的語句來利用數(shù)據(jù)庫本身內(nèi)置的過濾和解析功能。
跨站點腳本攻擊(XSS)
在早期的瀏覽器版本中,對于JavaScript
施加的第一項限制就是為頁面內(nèi)容建立一種邊界,使一個站點提供的一個框架內(nèi)執(zhí)行的腳本無法訪問其他站點提供的框架中的內(nèi)容。因而,跨站點腳本攻擊這種攻擊
模式關注的是使來自一個站點(攻擊者站點)的腳本能夠訪問其他站點的內(nèi)容(例如,用戶的銀行賬戶站點)。
為此,用戶通常必然要訪問一個惡意或不可信的網(wǎng)站,而社會工程的眾多試驗已經(jīng)顯示,用戶可能會被最古怪的站點吸引。
在此類漏洞中,最常見的形式就是簡單的反射漏洞,在一次服務器請求中將未經(jīng)過濾的 HTML
參數(shù)(通常是表單參數(shù))反射給用戶。這種攻擊載體的標準形式首先是通過搜索引擎結果頁面顯示出來的,通常會在頁面標題中反射用戶的查詢關鍵詞。如果未經(jīng)過
濾,這種反射回的查詢關鍵詞很可能包含一些編碼不當?shù)?HTML 標記,但可被接收方瀏覽器解釋為有效的 HTML。
實際上,未經(jīng)過濾的傳入數(shù)據(jù)的任何反射都會造成問題,因為 XSS數(shù)量和種類始終在增加,參見示例4。
public void doGet(HttpServletRequest req, HttpServletResponse res)
{
string title = req.getParameter("searchTerm");
res.getOutputStream().write(title.getBytes("UTF-8"));
}
示例4 未經(jīng)過濾的傳入數(shù)據(jù)本身就存在問題。
XSS反射的表現(xiàn)十分簡單,而解決此問題的方法也極為簡單——將從傳入請求中讀取的一切內(nèi)容編碼,之后再回發(fā)給瀏覽器即可。盡管我們在這里的示例中使用了
Java,但包括HTML編碼機制的所有常見頁面產(chǎn)品均可用以避免此類漏洞。例如,下面這條 ASP 語句就可能被利用:
Response.Write Request.Form("username""
反之,以下語句則不能被利用:
Response.Write Server.HTMLEncode( Request.Form("username"))
盡管仍然沒有內(nèi)置對象可用于執(zhí)行標準轉換,但也可在 Java 中進行類似轉換,以避免此類利用。也就是說,可輕松編寫一個類似的 String
轉換程序。對于尋找“現(xiàn)成”產(chǎn)品包的用戶,JTidy 項目(jtidy.sourceforge.net)是一個理想的起點。
其他更加復雜的 XSS 表現(xiàn)形式以未過濾用戶輸入的持久存儲為中心,此類輸入內(nèi)容會在隨后用于提供響應內(nèi)容。這是一類更難以診斷的 XSS,因為攻擊模式不僅依賴于所存儲的未經(jīng)過濾的用戶輸入,還依賴于此后對其他用戶可用的存儲數(shù)據(jù)。
在早期Web發(fā)展階段,不可信任的論壇提供的軟件包特別易受此類攻擊模式的影響。即便是現(xiàn)在,在數(shù)據(jù)庫(或文件)中存儲未經(jīng)過濾的傳入數(shù)據(jù)并隨后將所存儲的數(shù)據(jù)發(fā)送給用戶的應用程序也易于受到此類持久形式的 XSS 的攻擊。
同樣,解決方法非常簡單,只需通過編程,在存儲信息之前將信息編碼或在將信息從持久存儲發(fā)送給用戶之前編碼即可。總而言之,在存儲之前編碼數(shù)據(jù)總是更加安全,這種方式可以保證未來對此類數(shù)據(jù)的使用免遭XSS 攻擊。
查找漏洞
本文介紹的問題的規(guī)避方法易于實現(xiàn),但對于嘗試控制現(xiàn)有代碼庫或新建代碼庫的安全性的開發(fā)人員或開發(fā)組織而言,所面臨的最大挑戰(zhàn)就是找到漏洞所在。毫無疑
問,可以利用手動代碼檢查的方法,但我可以確定地說,圍坐在桌邊、查看大量代碼并嘗試找出可能成為漏洞的內(nèi)容絕非樂事。
靜態(tài)源代碼分析為此類問題提供了一種可行的解決方案,這種方法關注代碼中現(xiàn)有的潛在漏洞或弱點,而不是像傳統(tǒng)安全性應用程序或滲透測試工具那樣嘗試找到現(xiàn)有漏洞或攻擊載體 。利用 SCA 工具可顯著減少查找并緩解此類問題所需的時間和工作量。
目前有多種開源和商業(yè)工具可用,分別具有不同的功能。Klocwork(我目前效力的企業(yè))就提供了這樣一種商業(yè)靜態(tài)源代碼分析產(chǎn)品套件,主要關注
C、C++ 和 Java,為開發(fā)人員提供了快速、準確的運行缺陷和安全漏洞分析,并且能夠集成在您所選擇的 IDE 之中。■
原文鏈接:
http://www.ddj.com/cpp/210602504
作者簡介:
Gwyn是Klocwork的首席技術官,他有著20多年的全球技術經(jīng)驗。在Klocwork,Gwyn從事的工作仍然圍繞他最初的愛好——編譯器理論,他致力于將靜態(tài)源代碼分析推向更高級別。