程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C、C++和Java安全編碼實踐提示與技巧

C、C++和Java安全編碼實踐提示與技巧

編輯:關於C++

對於所有類型環境中的開發人員來說,安全性正成為一個越來越重要的主題,即便過去一直認為安全性不成問題的嵌入式系統也是如此。本文將介紹幾種類型的編碼漏洞,指出漏洞是什麼、如何降低代碼被攻擊的風險、如何更好地找出代碼中的此類缺陷。

注入攻擊

通過將信息注入正在運行的流程,攻擊者可以危害進程的運行狀態,以反射到開發人員無法保護的某種最終目標。例如,攻擊者可能會通過堆棧溢出(stack corruption)將代碼注入進程,從而執行攻擊者選定的代碼。此外,攻擊者也可能嘗試將數據注入數據庫,供將來使用;或將未受保護的字符串注入數據庫查詢,獲取比開發人員更多的信息。無論出於怎樣的目的,注入總是一件壞事,總是需要謹慎對待的。

最惡劣的注入攻擊形式也許是代碼注入——將新代碼置入正在運行的進程的內存空間,隨後指示正在運行的進程執行這些代碼。此類攻擊如果成功,則幾乎可以進行任何操作,因為正在運行的進程完全被劫持,可執行攻擊者希望執行的任何代碼。

此類攻擊最著名的示例之一就是 Windows 動畫光標攻擊,這正是本文要討論的模式。攻擊者利用一個簡單的 Web 頁面將形式不當的動畫光標文件下載到查看者的 PC 中,導致浏覽器調用此動畫光標,動畫光標調用時可能發生任意代碼的注入。實際上,這是一個完美的攻擊載體:因為它不要求對被攻擊機器的任何實際訪問、最終用戶根本意識不到任何可能發生的麻煩;此外,如果攻擊效果的惡意也是適度的,則對最終用戶的外部影響幾乎是零。

考慮示例 1(a),當然,這改寫自 Windows 攻擊,它構成了此類攻擊載體的基礎。這裡的開發人員對於傳入流的可靠性做出了基本的假設。信任流和並相信一切都沒問題。使用基於堆棧的將被非串形化(deserialized)的類型調用函數,未知數據流和代碼注入肯定會在某個時間點出現。

(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 注入攻擊。

這是怎樣發生的?假設您調用示例 1(b)中的函數。我們就得到了一個易於利用的攻擊載體。這裡的問題在於 SOMETYPE 在編譯時的大小是固定的。假設此類型在內存中使用 128 個字節表示。再假設您構建傳入流時,使前 4 個字節(要非串形化的內容的長度)的讀數為 256。現在,您沒有檢查正在處理的內容的有效性,而是將 256 個字節復制到了僅為 128 個字節的保留堆棧空間內。

考慮到發布模式堆棧的典型布局,您顯然遇到了麻煩。查看堆棧,了解原因所在。每個被調用的函數都會將其本地數據布設到堆棧的一個幀內,通常是通過在輸入時從堆棧指針減去本地數據的已知大小(加上處理調用鏈本身所需的任何管理數據)實現的。編譯器發出的理想函數 prolog(偽代碼)如下所示:

.foo
sub sp, 128  ; sizeof SOMETYPE

隨後,對可利用函數的調用應如下所示:

push sp   ; push the SOMETYPE
local variable
push ap   ; push the stream
pointer (comes from 1st argument)
call LoadTypeFromStream
ret

在調用 foo() 時,調用方將流地址以及返回地址(作為使用調用指令或平台上可用的同等部分的隱式效果)壓入堆棧,使堆棧內容中有 128 個字節是為我們的類型保留的,且緊鄰返回給 foo() 調用方的返回地址,參見圖 1。

現在,LoadTypeFromStream 執行,並將 256 個字節寫入所提供的地址,也就是在我們調用函數之前堆棧指針(SP)的值。這會覆蓋應該使用的 128 個字節(本例中位於地址 0x1000 處),加上隨後的 128 個字節,包括傳入的參數指針、返回地址以及堆棧中隨後 128 個字節內存儲的其他任何信息。

那麼攻擊者怎樣利用這樣的漏洞呢?並不簡單,需要經過反復的試錯。實際上,攻擊者要安排攻擊,使覆蓋的返回地址將控制權移交給攻擊者,而非預期調用方函數。因而,攻擊者需要准確了解要利用哪些數據結構,這樣的數據結構在要攻擊的任意版本的操作系統或應用程序上有多大、周邊有哪些內容(以便正確設定偽造的返回地址)、如何有意義地插入足夠的信息以使返回地址和其他效果能夠實現某種惡意操作。

這一切做起來並不簡單,但多種多樣的攻擊表明,總是有人有太多的空閒時間。

應如何防范此類攻擊?這是一次攻擊還是多重攻擊?所寫入的代碼是否真的像這裡所顯示的這樣笨拙?現代編譯器是否會對堆棧幀布局做一些特殊處理,以避免此類問題?

總而言之,模糊處理就等於沒有防御。我們都認識到,程序員將攻擊預想得越簡單,攻擊出現的可能性就越高。然而,即便是復雜的代碼,若未進行合理防御,也遲早會受到攻擊。這種利用被污染的數據流和非常基本的緩沖溢出漏洞的攻擊,多年以來這一直是熱門的研究課題,但每年仍然會出現大量此類攻擊。

防范此類攻擊的效果甚微,因為攻擊形式復雜——注意您的數據假設。只要在示例1(a)中添加一行簡單的代碼,就會使其更加安全,參見示例1(c)。顯然,隨著流交互變得更加復雜,保護的要求也隨之復雜化,但基本上說代碼注入是編碼中“不可饒恕”的過失,因為防范它的方法是那樣普及和簡單。

SQL 注入

此外還存在其他一些類型的 SQL 注入,可能會給以數據庫為中心的應用程序造成嚴重的問題。在某些情況下,攻擊者只是嘗試訪問更多的內容。在另一些情況下,攻擊者關注的則是在數據庫中存儲新信息,以便使應用程序此後在不知情的前提下使用此類信息,入侵最終用戶的會話。

基於查詢的攻擊關注的是一種普遍應用的反模式,使用字符串串聯構建查詢。這種類型的漏洞常常出現在面向 Web 的應用程序中,在所有常用頁面產品——包括 PHP、ASP、JSP 等及其後備控制器邏輯中同樣常見。

這種漏洞的核心是開發人員使用直接查詢執行,而非利用查詢准備來運行數據庫交互。考慮以下登錄驗證查詢示例:

SELECT ID FROM USERS WHERE NAME= 'user' AND PWD='password'

用戶將看到一個簡單的 HTML 表單,該表單包含兩個輸入框並使用了這種反模式。從表單傳入的參數(無論所討論的頁面產品是怎樣接收到這些參數的)都將通過串聯直接代入查詢的字符串形式。

考慮攻擊者提供的一組參數:

NAME:   x

PWD:    x' OR '1' = '1

運行串聯,結果將得到被利用的查詢:

SELECT ID FROM USERS WHERE NAME=

'x' AND PWD='x' OR '1' = '1'

如果登錄僅檢查該語句執行成功與否(而未考慮結果行),攻擊者即可迅速獲得該應用程序所處理的任意用戶記錄可提供的任意訪問權限。很多應用程序的用戶表的第一行都是為超級用戶保留的,攻擊此類應用程序輕而易舉。

利用未謹慎處理數據庫語句內代入字符串的應用程序,攻擊者可實現多種其他形式的攻擊。這種反模式極為常見(參見最近的 Microsoft 公告和其他內容了解其普遍性),緩解方法也同樣簡單,並可置於基本數據庫 API 之中:使用准備好的語句而非字符串串聯。

例如,考慮示例2 中的錯誤實現。此函數嚴格遵循反模式,還通過拋出包含傳入(未過濾)數據(即用戶名)的異常而執行了另外一項重要的 no-no 操作。如果以響應的形式為用戶呈現此數據,您就很可能遇到某些惡意利用,特別是可能遭遇跨站腳本攻擊。

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 錯誤實現。

為了修正此代碼,不應動態構建 SQL 查詢,而是直接構建准備好的語句,並使用它來代替傳入參數。

我們將准備的語句會為參數保留空間,並且不易受此類攻擊利用,原因就在於它的詞匯方面並不像字符串串聯那樣脆弱。

考慮以下語句(准備該語句的目的與前面提到的串聯字符串相同):

SELECT ID FROM USERS WHERE USER=?

AND PWD=?

我使用這個准備好的語句代入了 user 和 pwd 參數的傳入數據。如果我們將之前被利用的字符串作為輸入,結果將是查詢代入過程出錯,因為不能將包含單引號等特殊字符的參數提供給准備好的查詢。

其他可能出現的利用也能在不同階段捕捉到,但如示例3 所示,新實現的創建與原實現一樣簡單,但安全性要高得多(我們也從拋出的異常中刪除了用戶名,這樣可以避免在未經過濾的情況下將其公開給調用方的危險)。

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,在處理來自最終用戶的數據時,始終應使用准備好的語句來利用數據庫本身內置的過濾和解析功能。

跨站點腳本攻擊(XSS)

在早期的浏覽器版本中,對於JavaScript 施加的第一項限制就是為頁面內容建立一種邊界,使一個站點提供的一個框架內執行的腳本無法訪問其他站點提供的框架中的內容。因而,跨站點腳本攻擊這種攻擊模式關注的是使來自一個站點(攻擊者站點)的腳本能夠訪問其他站點的內容(例如,用戶的銀行賬戶站點)。

為此,用戶通常必然要訪問一個惡意或不可信的網站,而社會工程的眾多試驗已經顯示,用戶可能會被最古怪的站點吸引。

在此類漏洞中,最常見的形式就是簡單的反射漏洞,在一次服務器請求中將未經過濾的 HTML 參數(通常是表單參數)反射給用戶。這種攻擊載體的標准形式首先是通過搜索引擎結果頁面顯示出來的,通常會在頁面標題中反射用戶的查詢關鍵詞。如果未經過濾,這種反射回的查詢關鍵詞很可能包含一些編碼不當的 HTML 標記,但可被接收方浏覽器解釋為有效的 HTML。

實際上,未經過濾的傳入數據的任何反射都會造成問題,因為 XSS數量和種類始終在增加,參見示例4。

public void doGet(HttpServletRequest req, HttpServletResponse res)
{
string title = req.getParameter("searchTerm");
res.getOutputStream().write(title.getBytes("UTF-8"));
}

示例4 未經過濾的傳入數據本身就存在問題。

XSS反射的表現十分簡單,而解決此問題的方法也極為簡單——將從傳入請求中讀取的一切內容編碼,之後再回發給浏覽器即可。盡管我們在這裡的示例中使用了Java,但包括HTML編碼機制的所有常見頁面產品均可用以避免此類漏洞。例如,下面這條 ASP 語句就可能被利用:

Response.Write Request.Form("username""

反之,以下語句則不能被利用:

Response.Write Server.HTMLEncode( Request.Form("username"))

盡管仍然沒有內置對象可用於執行標准轉換,但也可在 Java 中進行類似轉換,以避免此類利用。也就是說,可輕松編寫一個類似的 String 轉換程序。對於尋找“現成”產品包的用戶,JTidy 項目(jtidy.sourceforge.net)是一個理想的起點。

其他更加復雜的 XSS 表現形式以未過濾用戶輸入的持久存儲為中心,此類輸入內容會在隨後用於提供響應內容。這是一類更難以診斷的 XSS,因為攻擊模式不僅依賴於所存儲的未經過濾的用戶輸入,還依賴於此後對其他用戶可用的存儲數據。

在早期Web發展階段,不可信任的論壇提供的軟件包特別易受此類攻擊模式的影響。即便是現在,在數據庫(或文件)中存儲未經過濾的傳入數據並隨後將所存儲的數據發送給用戶的應用程序也易於受到此類持久形式的 XSS 的攻擊。

同樣,解決方法非常簡單,只需通過編程,在存儲信息之前將信息編碼或在將信息從持久存儲發送給用戶之前編碼即可。總而言之,在存儲之前編碼數據總是更加安全,這種方式可以保證未來對此類數據的使用免遭XSS 攻擊。

查找漏洞

本文介紹的問題的規避方法易於實現,但對於嘗試控制現有代碼庫或新建代碼庫的安全性的開發人員或開發組織而言,所面臨的最大挑戰就是找到漏洞所在。毫無疑問,可以利用手動代碼檢查的方法,但我可以確定地說,圍坐在桌邊、查看大量代碼並嘗試找出可能成為漏洞的內容絕非樂事。

靜態源代碼分析為此類問題提供了一種可行的解決方案,這種方法關注代碼中現有的潛在漏洞或弱點,而不是像傳統安全性應用程序或滲透測試工具那樣嘗試找到現有漏洞或攻擊載體 。利用 SCA 工具可顯著減少查找並緩解此類問題所需的時間和工作量。

目前有多種開源和商業工具可用,分別具有不同的功能。Klocwork(我目前效力的企業)就提供了這樣一種商業靜態源代碼分析產品套件,主要關注 C、C++ 和 Java,為開發人員提供了快速、准確的運行缺陷和安全漏洞分析,並且能夠集成在您所選擇的 IDE 之中。

文章來源:http://www.ddj.com/cpp/210602504

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved