Hades開源白盒審計系統V1.0.0
- 2020 年 3 月 6 日
- 筆記
一、引言
為什麼要開發這麼一個白盒審計系統呢?其實,自己開發白盒審計系統的想法已經在我的腦海中存在很久了。一方面,之前大學畢業之後在白盒方面花了挺多時間的,不過由於各方面原因,一直沒有特別突出的進展,之後又轉做木馬病毒方面的攻防研究,白盒的方面的研究擱置了很久,心裡一直耿耿於懷。另一方面,最近在做一個Android相關的項目,項目中自己實現了一套針對smali位元組碼的虛擬解釋執行引擎,然後就突發一個想法,android也是使用java進行開發的,那麼能不能直接復用這套虛擬解釋執行引擎來對java源碼進行分析呢?
經過調研,發現java位元組碼可以通過Androidsdk中的dx.jar轉為smali位元組碼。這樣一來,實現java白盒的代價就小了很多。雖然現在並不是在做白盒相關的工作,但是還是希望憑藉自己對語言的理解,以及編碼能力,自己構造出一個像樣的白盒審計系統,所以便有了Hades這個白盒審計系統的項目。而之所以開源這套系統,也是希望能夠吸引更多對白盒引擎開發感興趣的人才加入到這個項目當中,一起維護這個項目。

二、整體架構設計
1、前端方面
前端採用easyUI編寫,本來打算使用vue的,不過vue方面還不是特別的熟悉,考慮到解決bug來可能會比較耗時,所以採用比較簡單的easyUI來編寫前端。
先展示下前端效果。(應該不是很違和)

報告展示頁面


2、後端方面
後端使用django實現文件接收介面,並對文件類型和合法性進行校驗,通過redis消息隊列將任務消息發布到消息隊列中,傳遞給後端引擎。
3、底層引擎方面
通過redis消息隊列接收到任務資訊,新建一個任務執行緒對上傳的文件進行分析。針對源碼的處理是,如果是maven管理的項目則使用mvn compile進行編譯。如果是普通的java項目的話則使用javac進行單文件編譯(考慮到整體編譯的話會造成編譯失敗,遺失位元組碼文件)。然後,將最終得到的java位元組碼文件轉為smali位元組碼文件(為什麼要轉為smali位元組碼文件呢?後面會進行說明),最後將最終的位元組碼文件作為輸入傳遞給主引擎程式進行執行分析。
4、軟體整體的執行流程示意圖如下

三、smali位元組碼簡介
因為本白盒程式主要基於smali文件進行分析的,所以這裡我先簡要介紹下smali位元組碼。而純粹講smali位元組碼,可能不太好理解。所以,我先講下程式的編譯過程。程式的編譯一般需要經過六個過程,即: 詞法分析、語法分析、語義分析、中間程式碼生成、程式碼優化、目標程式碼生成。下面簡要說明下這六個過程的各自的工作。詞法分析,主要是對輸入的源碼文件中的字元從左到右逐個進行分析,輸出與源程式碼等價的token流。語法分析,主要是基於輸入的token流,根據語言的語法規則,做一些上下文無關的語法檢查,語法分析結束之後,生成AST語法樹。語義分析,主要是將AST語法樹作為輸入,並基於AST語法樹做一些上下文相關的類型檢查。語義分析結束後,生成中間程式碼,而此時的中間程式碼,是一種易於轉為目標程式碼的一種中間表示形式。程式碼優化,則是針對中間程式碼進行進一步的優化處理,合併其中的一些冗餘程式碼,生成等價的新的中間表示形式,最後生成目標程式碼。這針對DVM而已,這裡的最終生成的程式碼即smali位元組碼,smali位元組碼也是DVM的解釋執行的底層彙編程式碼。

那麼,我為什麼要選擇smali作為分析java程式的目標程式碼呢?引言中已經提到了,一方面,最近在做一個android相關的項目,項目中已經實現了smali層面的污點跟蹤引擎,如果能夠直接復用之前寫的這套污點分析程式碼,那豈不是很爽,不用繼續造輪子了。另一方面,相較java位元組碼來說,smali位元組碼更加的簡單,因為smali位元組碼是一種基於暫存器的指令系統,它的指令是二地址和三地址混合的,指令中指明了操作數的地址。而JVM是基於棧的虛擬機,JVM將本地變數放到一個本地變數列表中,在進行指令解釋的時候,將變數push到操作數棧中,由操作碼對應的解釋函數來進行解釋執行。所以,java位元組碼操作無疑會比smali位元組碼更複雜一些,複雜主要體現在後續的堆棧設計以及程式碼解釋執行。
四、白盒引擎實現詳細說明
下面我分六個部分,詳細說明下我的白盒引擎實現思路。
0×1 指令控制流圖構造
要實現指令控制流圖的構造,首先我們需要對目標函數的位元組碼資訊進行切片處理,解析出一個個的指令程式碼塊,然後將各個指令程式碼塊進行依賴分析,並保存為圖的形式。但是總不至於針對每一個函數都構造指令控制流圖吧,這樣太費時,而且有些函數,程式在執行過程中不一定會調用到的,那麼我們構建它又有什麼意義呢?。我們知道,每一個程式都會有一個入口函數,比如java程式中的main函數,或者一些涉及網路請求映射處理的函數,比如servlet中的doPost,doGet等。只要從這些入口點進行指令控制流圖的構建,就可以儘可能的覆蓋程式執行過程中可能走過的路線。
Hades構造的指令控制流圖大致效果圖:

局部放大版本

0×2 通路計算
構建好了指令控制流圖之後,我們先別急著對程式碼進行解釋執行分析,到這一步還無法進污點分析。我們可以先設想一下,如果我們每一條路線,每個指令都解釋執行一遍,那肯定是十分耗時的,所以考慮到執行效率方面,Hades會先找到sink點和source點各自所處的程式碼塊節點,通過圖的查詢演算法,查詢兩個節點(程式碼塊節點)之間的通路(這個通路可能不止一條),然後將這條通路中的所有程式碼塊合併在一起,重新組成一段線性的程式碼塊,最後對這段程式碼塊記性解釋執行。其他更高級的減少分析成本的方案暫時還沒想出來,或許你們可以給我提些建議。
0×3 解釋執行
在講解釋執行之前,我先簡要說明下虛擬解釋器的原理。虛擬解釋器並不是真正的解釋器,但是它應該具備解釋器的主要功能,即對位元組碼指令進行解釋,對堆棧進行存取處理。而如果我們希望在解釋執行的時候能夠收集到污點的一些傳遞情況,那麼單純的進行解釋執行肯定是不夠的,我們還需要在指令解釋執行的基礎上,增加污點的傳播分析。那麼,如何實現污點的傳播分析呢?這是我們需要著重考慮的點。我們知道,傳統的解釋器,在解釋執行的程式的時候,需要根據用戶傳入的參數,進行一些值的處理的。而與傳統解釋器不同的是,我們的虛擬執行引擎並不是真正的執行起來,也不需要讓程式真正的執行起來,我們解釋執行的目的不是為了對傳入的值進行處理,而是分析傳入的值的走勢(即數據是怎麼流動的),我們關注的是它怎麼走的,這個點。所以,我們會把用戶傳入的參數認定為污點,而相應的獲取用戶輸入參數的函數的返回值所存儲的暫存器便會被我們標記為污點,這個函數是整個污點分析的起點,而這個污點暫存器則是這個污點網路主幹路線上的第一個污點。
我們在通過對函數引用進行解釋之後,獲得了污點網路中的第一個污點資訊,那麼現在我們來思考下接下來污點是如何分析的呢?其實我們知道,污點的傳播無非是幾個方式,一種是通過賦值的指令進行傳遞,第二種是通過數組操作指令將污點傳遞給了數組成員,第三種是通過函數調用,將污點傳遞給了函數執行結束的返回值(這裡是暫存器)。所以我們只需要注重對這些賦值操作進行解釋執行即可實現對污點的跟蹤分析。
Hades解釋執行的指令:
"invoke-virtual":self.handle_invoke, "invoke-virtual/range": self.handle_invoke, "invoke-static": self.handle_invoke, "invoke-direct": self.handle_invoke, "invoke-super": self.handle_invoke_super, "invoke-static/range": self.handle_invoke, "invoke-interface": self.handle_invoke, "invoke-interface-range": self.handle_invoke, "invoke-super/range": self.handle_invoke_super, "invoke-direct/range": self.handle_invoke, "move-result-object": self.handle_move_result, "move-result": self.handle_move_result, "move-result-wide": self.handle_move_result, "move-exception": self.handle_move_result, "move-object": self.handle_move, "move-object/from16":self.handle_move, "move-object/16":self.handle_move, "move-wide": self.handle_move, "move-wide/from16": self.handle_move, "move-wide/16": self.handle_move, "move": self.handle_move, "move/from16": self.handle_move, "move/16": self.handle_move, "const-string": self.handle_const, "const-string-jumbo": self.handle_const, "const": self.handle_const, "const/4":self.handle_const, "const/8":self.handle_const, "const/16":self.handle_const, "const/high16":self.handle_const, "const-wide/16":self.handle_const, "const-wide/32":self.handle_const, "const-wide":self.handle_const, "const-wide/high16":self.handle_const, "const-class": self.handle_const, "goto":self.handle_goto, "goto/16":self.handle_goto, "goto/32":self.handle_goto, ".registers":self.handle_registers, ".line":self.handle_line, "check-cast":self.default, "if-eqz":self.default, "default":self.default, "return":self.handle_return, "return-object":self.handle_return, "return-void":self.handle_return, "return-wide":self.handle_return, "new-array": self.handle_array_create, "aput-object": self.handle_aput, "aget-object": self.handle_aget, "aget-wide": self.handle_aget, "aput-wide": self.handle_aput, "aget-boolean": self.handle_aget, "aput-boolean": self.handle_aput, "aget-byte": self.handle_aget, "aput-byte": self.handle_aput, "aget-char": self.handle_aget, "aput-char": self.handle_aput, "aget-short": self.handle_aget, "aput-short": self.handle_aput, "iput-object": self.handle_aput, "iget-object": self.handle_aget, "iget-wide": self.handle_aget, "iput-wide": self.handle_aput, "iget-boolean": self.handle_aget, "iput-boolean": self.handle_aput, "iget-byte": self.handle_aget, "iput-byte": self.handle_aput, "iget-char": self.handle_aget, "iput-char": self.handle_aput, "iget-short": self.handle_aget, "iput-short": self.handle_aput, "sput-object": self.default, "sget-object": self.default, "sget-wide": self.default, "sput-wide": self.default, "sget-boolean": self.default, "sput-boolean": self.default, "sget-byte": self.default, "sput-byte": self.default, "sget-char": self.default, "sput-char": self.default, "sget-short": self.default, "sput-short": self.default, "new-instance": self.default, "packed-switch": self.default, "sparse-switch": self.default,
其中,default表示該指令目前Hades暫不解釋,但在之後的版本中,可能會考慮對其進行解釋。
0×4 記憶體模擬
1、暫存器
現在我們知道了污點追蹤分析的方法,但是僅僅對指令進行解釋執行是不夠的,因為指令在解釋執行的時候是需要操作暫存器和記憶體的,如果沒有記憶體的介入,那麼指令解釋毫無意義,所以,我們還需要設計一個用於污點分析的記憶體結構。Hades的記憶體結構主要包括三個部分,分別是,污點暫存器,污點棧及污點堆部分。污點暫存器,包含屬性如下:
Name,value,isTained,分別對應對應暫存器名,暫存器值,及污點屬性。之前說了,Hades的虛擬執行系統不需要值的介入,這裡為什麼還要有一個value呢?這裡,雖然Hades不care值的處理,但是,為了能夠準確的定位到數組索引,處理數組操作相關的污點傳播問題,還是需要對這部分的值進行保存的。
2、污點棧
為什麼程式沒有真正執行起來,還需要有棧結構呢?這裡,設計污點棧的目的是為了污點的保存。在函數間污點分析的過程中,我們在跟入分析一個被調用的函數的時候,也需要對當前函數的污點現場進行保存,以便在在分析完被調用函數回來之後,能夠恢復到原來的污點現場繼續向下進行污點分析,所以一個合理的棧結構的設計是十分必要的。下面是Hades污點棧結構示意圖:
+- - - - - - - - - –+ - out0 - +——————-+ <– stack pointer + … + +——————-+ <– frame pointer:for func1 + v0 == local0 + +—————–+ +——————-+ + out0 + + v1 == in0 + +—————–+ +——————-+ + out1 + + v2 == in1 + +—————–+ +——————-+ + … + +—————–+ <— frame pointer:for oncreate + v0 == local0+ +—————–+ + v1 == local1+ +—————–+ + v2 == in0 + +—————–+ + v3 == in1 + +—————–+ + v4 == in2 + +—————–+ - - - - - - +—————–+ <– 棧起始位置
Hades污點棧結構設計參考了DVM的的棧設計方案,有點類似』暫存器窗口』,這也是dvm解釋器棧的一個特色,當然這只是大概的示意圖,具體的棧結構和棧幀結構在上圖的結構的基礎上做了一些更加適配污點跟蹤分析的設計,在此就不給出了。下面解釋下棧幀頂部的out區域和in區域的作用,這裡out和int區域的設計主要是為了函數間參數的傳遞,雖然我們不需要真實參數值/對象的傳遞,但是我們需要獲取上一個函數傳入的參數的污點屬性。為了獲取上一個函數中的污點屬性,需要在上一個函數執行到函數調用的時候,將參數push到當前棧幀的頂部out區域,然後參數由out區域傳入到in區域,當執行到調用的函數的時候,程式從棧幀的底部in區域獲取到參數資訊(包含了污點屬性的參數資訊,以及一些必要的值資訊)。依據此污點棧設計,我們就可以對函數間的污點傳播進行跟蹤分析。
3、堆部分
Hades中的堆的設計並不是很多,也不是很完善,目前主要用來存放數組資訊,用於處理數組成員的污點追蹤操作。
0×5 污點追蹤
污點追蹤是整個系統最重要的環節,前面的設計也是為了該部分服務。Hades的污點傳播方式分為棧幀型污點傳播,和指令型污點傳播。指令型污點傳播,顧名思義,就是通過解釋執行位元組碼指令來進行污點傳播的,在上面我們也提過,那麼什麼是棧幀型污點傳播呢?在進行一個新函數的污點分析之前,Hades會為新函數分配一個污點棧幀,在這個污點棧幀中會為新函數分配好指令解釋執行過程中需要的所有虛擬暫存器,以滿足指令解釋執行的需要。(smali是一種基於暫存器操作的位元組碼,值的操作都是基於虛擬暫存器的,不像c及java等語言位元組碼,有大量值的存取操作)新棧幀中一部分暫存器來自函數的入參,入參來自上一個棧幀的輸出區域。在上一個函數發生函數調用的時候,Hades會將函數的傳入參數(包含暫存器中的污點資訊,值資訊)保存在上一個函數棧幀的輸出區域中,在解釋執行新函數的之前,舊棧幀會對新棧幀進行一個污點的傳遞,將污點資訊傳遞到新函數的入參中,這樣就完成了函數間的污點傳遞,即棧幀與棧幀之間的污點傳播。
0×6 污點凈化部分
Hades之初並不將凈化函數加入到污點分析過程中,主要的考慮到沒有一個第三方安全廠商能夠百分百保證他們的安全sdk中的凈化函數不存在被繞過的風險。但是實際攻擊過程中,必要的防護程式碼確實能夠防住絕大部分的攻擊測試。後續,Hades方面將會逐步增加凈化函數方面規則的支援。
五、規則配置部分
Hades的規則配置部分以下幾個部分:
1、 sink點規則配置。 2、 source點規則配置。 3、 clean點配置。 4、 程式入口點規則配置。
規則方面,由於都是位元組碼形式的規則,手動收集的話會比較麻煩,所以我額外開發了一個規則生成器,在utils包中,只要輸入函數名及所屬的jar包的路徑,即可生成相應的函數調用形式。
六、用到的第三方工具
以下是Hades用到的第三方工具:
1、dx.jar
來自androidSDK,用於將java位元組碼轉為smali位元組碼。
2、baksmali.jar
用於將dex文件反編譯為smali。
七、Demo測試
這裡我們以Benchmark的靶場作為測試的示常式序。
1、測試1 命令執行漏洞
本測試單元,對應benchmark中的第17個靶場。
benchmark靶場項目地址:https://github.com/OWASP/Benchmark
程式碼:
/** * OWASP Benchmark v1.2 * * This file is part of the Open Web Application Security Project (OWASP) package org.owasp.benchmark.testcode; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(value="/cmdi-00/BenchmarkTest00017") public class BenchmarkTest00017 extends HttpServlet { private static final long serialVersionUID = 1L; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // some code response.setContentType("text/html;charset=UTF-8"); String param = ""; java.util.Enumeration<String> headers = request.getHeaders("BenchmarkTest00017"); if (headers != null && headers.hasMoreElements()) { param = headers.nextElement(); // just grab first element } // URL Decode the header value since req.getHeaders() doesn't. Unlike req.getParameters(). param = java.net.URLDecoder.decode(param, "UTF-8"); String cmd = ""; String osName = System.getProperty("os.name"); if (osName.indexOf("Windows") != -1) { cmd = org.owasp.benchmark.helpers.Utils.getOSCommandString("echo"); } Runtime r = Runtime.getRuntime(); try { Process p = r.exec(cmd + param); org.owasp.benchmark.helpers.Utils.printOSCommandResults(p, response); } catch (IOException e) { System.out.println("Problem executing cmdi - TestCase"); response.getWriter().println( org.owasp.esapi.ESAPI.encoder().encodeForHTML(e.getMessage()) ); return; } }
這裡簡要的分析一下程式:
後端程式通過request.getheader函數獲取客戶端網路請求的頭部資訊,並從中讀取第一個element資訊作為參數進入到命令執行函數中,這就構成了一個命令執行漏洞。
下面是污點傳遞的示意圖

好,下面我們使用Hades來對該程式進行檢測。
以下是Hades生成的smali位元組碼
.classpublic Lorg/owasp/benchmark/testcode/BenchmarkTest00017; .superLjavax/servlet/http/HttpServlet; .source"BenchmarkTest00017.java" #annotations .annotationruntime Ljavax/servlet/annotation/WebServlet; value = { "/cmdi-00/BenchmarkTest00017" } .endannotation # staticfields .fieldprivate static final serialVersionUID:J = 0x1L # directmethods .methodpublic constructor <init>()V .registers 1 .prologue .line 30 invoke-direct {p0},Ljavax/servlet/http/HttpServlet;-><init>()V return-void .endmethod # virtualmethods .method public doGet(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .registers 3 .param p1, "request" # Ljavax/servlet/http/HttpServletRequest; .param p2, "response" # Ljavax/servlet/http/HttpServletResponse; .annotation systemLdalvik/annotation/Throws; value = { Ljavax/servlet/ServletException;, Ljava/io/IOException; } .end annotation .prologue .line 36 invoke-virtual {p0, p1, p2},Lorg/owasp/benchmark/testcode/BenchmarkTest00017;->doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .line 37 return-void .endmethod .method public doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .registers 13 .param p1, "request" # Ljavax/servlet/http/HttpServletRequest; .param p2, "response" # Ljavax/servlet/http/HttpServletResponse; .annotation system Ldalvik/annotation/Throws; value = { Ljavax/servlet/ServletException;, Ljava/io/IOException; } .end annotation .prologue .line 42 const-string v7,"text/html;charset=UTF-8" invoke-interface {p2, v7},Ljavax/servlet/http/HttpServletResponse;->setContentType(Ljava/lang/String;)V .line 45 const-string v5, "" .line 46 .local v5,"param":Ljava/lang/String; const-string v7,"BenchmarkTest00017" invoke-interface {p1, v7},Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration; move-result-object v2 .line 48 .local v2,"headers":Ljava/util/Enumeration;,"Ljava/util/Enumeration<Ljava/lang/String;>;" if-eqz v2, :cond_1b invoke-interface {v2},Ljava/util/Enumeration;->hasMoreElements()Z move-result v7 if-eqz v7, :cond_1b .line 49 invoke-interface {v2},Ljava/util/Enumeration;->nextElement()Ljava/lang/Object; move-result-object v5 .end local v5 # "param":Ljava/lang/String; check-cast v5, Ljava/lang/String; .line 53 .restart local v5 # "param":Ljava/lang/String; :cond_1b const-string v7, "UTF-8" invoke-static {v5, v7},Ljava/net/URLDecoder;->decode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; move-result-object v5 .line 56 const-string v0, "" .line 57 .local v0,"cmd":Ljava/lang/String; const-string v7, "os.name" invoke-static {v7},Ljava/lang/System;->getProperty(Ljava/lang/String;)Ljava/lang/String; move-result-object v3 .line 58 .local v3,"osName":Ljava/lang/String; const-string v7, "Windows" invoke-virtual {v3, v7}, Ljava/lang/String;->indexOf(Ljava/lang/String;)I move-result v7 const/4 v8, -0x1 if-eq v7, v8, :cond_38 .line 59 const-string v7, "echo" invoke-static {v7},Lorg/owasp/benchmark/helpers/Utils;->getOSCommandString(Ljava/lang/String;)Ljava/lang/String; move-result-object v0 .line 62 :cond_38 invoke-static {},Ljava/lang/Runtime;->getRuntime()Ljava/lang/Runtime; move-result-object v6 .line 65 .local v6,"r":Ljava/lang/Runtime; :try_start_3c new-instance v7, Ljava/lang/StringBuilder; invoke-direct {v7},Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v7, v0},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v7 invoke-virtual {v7, v5},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v7 invoke-virtual {v7},Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v7 invoke-virtual {v6, v7},Ljava/lang/Runtime;->exec(Ljava/lang/String;)Ljava/lang/Process; move-result-object v4 .line 66 .local v4,"p":Ljava/lang/Process; invoke-static {v4, p2},Lorg/owasp/benchmark/helpers/Utils;->printOSCommandResults(Ljava/lang/Process;Ljavax/servlet/http/HttpServletResponse;)V :try_end_54 .catch Ljava/io/IOException; {:try_start_3c.. :try_end_54} :catch_55 .line 74 .end local v4 # "p":Ljava/lang/Process; :goto_54 return-void .line 67 :catch_55 move-exception v1 .line 68 .local v1,"e":Ljava/io/IOException; sget-object v7,Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v8, "Problem executingcmdi - TestCase" invoke-virtual {v7, v8},Ljava/io/PrintStream;->println(Ljava/lang/String;)V .line 69 invoke-interface {p2},Ljavax/servlet/http/HttpServletResponse;->getWriter()Ljava/io/PrintWriter; move-result-object v7 .line 70 invoke-static {},Lorg/owasp/esapi/ESAPI;->encoder()Lorg/owasp/esapi/Encoder; move-result-object v8 invoke-virtual {v1},Ljava/io/IOException;->getMessage()Ljava/lang/String; move-result-object v9 invoke-interface {v8, v9},Lorg/owasp/esapi/Encoder;->encodeForHTML(Ljava/lang/String;)Ljava/lang/String; move-result-object v8 .line 69 invoke-virtual {v7, v8},Ljava/io/PrintWriter;->println(Ljava/lang/String;)V goto :goto_54 .end method
這裡我們將doPost作為我們的入口點進行指令控制流的構造,那麼如何生成一個入口點呢?這裡可以使用我項目中的一個入口點生成工具funcInvokeGenerate.py即可生成期望得到的入口點資訊,只需要輸入入口點函數名稱和相應的所屬的jar包路徑即可。
這裡我們將Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration;作為source點,
將Ljava/lang/Runtime;->exec(Ljava/lang/String;)Ljava/lang/Process;作為sink點進行分析。
以下是hades構造的指令控制流圖(由於生成的控制流圖過大,故我只截取了部分的控制流圖)
source點

sink點

以下是Hades的分析報告:
{ "source":"Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration;", "linearCode": [ "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V1", "1: .registers 13", "2: .param p1, \request\ #Ljavax/servlet/http/HttpServletRequest;", "3: .param p2, \response\ #Ljavax/servlet/http/HttpServletResponse;", "4: .annotation systemLdalvik/annotation/Throws;", "5: value = {", "6:Ljavax/servlet/ServletException;,", "7: Ljava/io/IOException;", "8: }", "9: .end annotation", "10: .prologue", "11: .line 42", "12: const-string v7,\text/html;charset=UTF-8\", "13: invoke-interface {p2, v7},Ljavax/servlet/http/HttpServletResponse;->setContentType(Ljava/lang/String;)V", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017; doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V14", "14: .line 45", "15: const-string v5, \\", "16: .line 46", "17: .local v5,\param\:Ljava/lang/String;", "18: const-string v7,\BenchmarkTest00017\", "19: invoke-interface {p1, v7},Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V20", "20: move-result-object v2", "21: .line 48", "22: .local v2,\headers\:Ljava/util/Enumeration;,\Ljava/util/Enumeration<Ljava/lang/String;>;\", "23: if-eqz v2, :cond_1b", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V24", "24: invoke-interface {v2},Ljava/util/Enumeration;->hasMoreElements()Z", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V25", "25: move-result v7", "26: if-eqz v7, :cond_1b", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V34", "34: :cond_1b", "35: const-string v7,\UTF-8\", "36: invoke-static {v5, v7},Ljava/net/URLDecoder;->decode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V37", "37: move-result-object v5", "38: .line 56", "39: const-string v0, \\", "40: .line 57", "41: .local v0,\cmd\:Ljava/lang/String;", "42: const-string v7,\os.name\", "43: invoke-static {v7},Ljava/lang/System;->getProperty(Ljava/lang/String;)Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V44", "44: move-result-object v3", "45: .line 58", "46: .local v3,\osName\:Ljava/lang/String;", "47: const-string v7,\Windows\", "48: invoke-virtual {v3, v7},Ljava/lang/String;->indexOf(Ljava/lang/String;)I", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V49", "49: move-result v7", "50: const/4 v8, -0x1", "51: if-eq v7, v8, :cond_38", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V52", "52: .line 59", "53: const-string v7,\echo\", "54: invoke-static {v7},Lorg/owasp/benchmark/helpers/Utils;->getOSCommandString(Ljava/lang/String;)Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V55", "55: move-result-object v0", "56: .line 62", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V57", "57: :cond_38", "58: invoke-static {},Ljava/lang/Runtime;->getRuntime()Ljava/lang/Runtime;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V59", "59: move-result-object v6", "60: .line 65", "61: .local v6, \r\:Ljava/lang/Runtime;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V62", "62: :try_start_3c", "63: new-instance v7, Ljava/lang/StringBuilder;", "64: invoke-direct {v7},Ljava/lang/StringBuilder;-><init>()V", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V65", "65: invoke-virtual {v7, v0},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V66", "66: move-result-object v7", "67: invoke-virtual {v7, v5},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V68", "68: move-result-object v7", "69: invoke-virtual {v7},Ljava/lang/StringBuilder;->toString()Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V70", "70: move-result-object v7", "71: invoke-virtual {v6, v7},Ljava/lang/Runtime;->exec(Ljava/lang/String;)Ljava/lang/Process;", "" ], "sinkBelongTo":"org/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V", "sink":"Ljava/lang/Runtime;->exec(Ljava/lang/String;)Ljava/lang/Process;", "sourceBelongTo":"org/owasp/benchmark/testcode/BenchmarkTest00017;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V" }
其中linearcode中記錄的是從source點到sink點這條污點路線走過的所有指令。這裡,我本來是想只解釋執行source點到sink點之間的程式碼的,不過,後來發現如果只解釋執行source點到sink點之間的指令程式碼的話,針對數組方面的污點分析能力就會變弱。為什麼呢?因為數組成員的一些賦值操作是在source點之前進行的,如果沒有對這些賦值操作進行解析,那麼後續針對數組成員的污點分析無法實現,只能將數組整體標記為污點,認定從該數組取出來的元素都為污點,這樣無疑會增加誤報的幾率。
這裡,只顯示位元組碼,和源碼的關聯性還沒有做,不過要做的話也容易,因為位元組碼里都有對該部分的位元組碼對應的java源碼的行號標記,即.line xx。我們根據位元組碼里的行號標記在源碼里的對應行找到相應源碼即可。
2、測試2 SQL注入漏洞
本測試單元對應的是benchmark的靶場18
/** * OWASPBenchmark v1.2 **/ packageorg.owasp.benchmark.testcode; importjava.io.IOException; importjavax.servlet.ServletException; importjavax.servlet.annotation.WebServlet; importjavax.servlet.http.HttpServlet; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; @WebServlet(value="/sqli-00/BenchmarkTest00018") publicclass BenchmarkTest00018 extends HttpServlet { private static final longserialVersionUID = 1L; @Override public void doGet(HttpServletRequestrequest, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } @Override public void doPost(HttpServletRequestrequest, HttpServletResponse response) throws ServletException, IOException { // some code response.setContentType("text/html;charset=UTF-8"); String param = ""; java.util.Enumeration<String>headers = request.getHeaders("BenchmarkTest00018"); if (headers != null &&headers.hasMoreElements()) { param =headers.nextElement(); // just grab first element } // URL Decode the header valuesince req.getHeaders() doesn't. Unlike req.getParameters(). param =java.net.URLDecoder.decode(param, "UTF-8"); String sql = "INSERT INTOusers (username, password) VALUES ('foo','"+ param + "')"; try { java.sql.Statementstatement = org.owasp.benchmark.helpers.DatabaseHelper.getSqlStatement(); intcount = statement.executeUpdate( sql ); org.owasp.benchmark.helpers.DatabaseHelper.outputUpdateComplete(sql,response); } catch (java.sql.SQLException e){ if(org.owasp.benchmark.helpers.DatabaseHelper.hideSQLErrors) { response.getWriter().println( "Errorprocessing request." ); return; } else throw newServletException(e); } } }
簡要的說明下該程式程式碼:
後端程式通過request.getheader函數獲取客戶端網路請求的頭部資訊,並從中讀取第一個element資訊作為參數進入到拼接形式的sql語句中,並最終進入到sql操作函數中。
下面是污點傳遞的示意圖

好,下面我們使用Hades來對該程式進行檢測。
以下是Hades生成的smali位元組碼
.classpublic Lorg/owasp/benchmark/testcode/BenchmarkTest00018; .superLjavax/servlet/http/HttpServlet; .source"BenchmarkTest00018.java" #annotations .annotationruntime Ljavax/servlet/annotation/WebServlet; value = { "/sqli-00/BenchmarkTest00018" } .endannotation # staticfields .fieldprivate static final serialVersionUID:J = 0x1L # directmethods .methodpublic constructor <init>()V .registers 1 .prologue .line 30 invoke-direct {p0},Ljavax/servlet/http/HttpServlet;-><init>()V return-void .endmethod #virtual methods .methodpublicdoGet(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .registers 3 .param p1, "request" # Ljavax/servlet/http/HttpServletRequest; .param p2, "response" # Ljavax/servlet/http/HttpServletResponse; .annotation systemLdalvik/annotation/Throws; value = { Ljavax/servlet/ServletException;, Ljava/io/IOException; } .end annotation .prologue .line 36 invoke-virtual {p0, p1, p2},Lorg/owasp/benchmark/testcode/BenchmarkTest00018;->doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .line 37 return-void .endmethod .methodpublicdoPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V .registers 11 .param p1, "request" # Ljavax/servlet/http/HttpServletRequest; .param p2, "response" # Ljavax/servlet/http/HttpServletResponse; .annotation systemLdalvik/annotation/Throws; value = { Ljavax/servlet/ServletException;, Ljava/io/IOException; } .end annotation .prologue .line 42 const-string v6,"text/html;charset=UTF-8" invoke-interface {p2, v6},Ljavax/servlet/http/HttpServletResponse;->setContentType(Ljava/lang/String;)V .line 45 const-string v3, "" .line 46 .local v3,"param":Ljava/lang/String; const-string v6,"BenchmarkTest00018" invoke-interface {p1, v6},Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration; move-result-object v2 .line 48 .local v2,"headers":Ljava/util/Enumeration;,"Ljava/util/Enumeration<Ljava/lang/String;>;" if-eqz v2, :cond_1b invoke-interface {v2},Ljava/util/Enumeration;->hasMoreElements()Z move-result v6 if-eqz v6, :cond_1b .line 49 invoke-interface {v2},Ljava/util/Enumeration;->nextElement()Ljava/lang/Object; move-result-object v3 .end local v3 # "param":Ljava/lang/String; check-cast v3, Ljava/lang/String; .line 53 .restart local v3 # "param":Ljava/lang/String; :cond_1b const-string v6, "UTF-8" invoke-static {v3, v6},Ljava/net/URLDecoder;->decode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; move-result-object v3 .line 56 new-instance v6, Ljava/lang/StringBuilder; invoke-direct {v6},Ljava/lang/StringBuilder;-><init>()V const-string v7, "INSERT INTO users(username, password) VALUES ('foo','" invoke-virtual {v6, v7},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v6 invoke-virtual {v6, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v6 const-string v7, "')" invoke-virtual {v6, v7},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v6 invoke-virtual {v6},Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v4 .line 59 .local v4,"sql":Ljava/lang/String; :try_start_3a invoke-static {},Lorg/owasp/benchmark/helpers/DatabaseHelper;->getSqlStatement()Ljava/sql/Statement; move-result-object v5 .line 60 .local v5,"statement":Ljava/sql/Statement; invoke-interface {v5, v4},Ljava/sql/Statement;->executeUpdate(Ljava/lang/String;)I move-result v0 .line 61 .local v0, "count":I invoke-static {v4, p2},Lorg/owasp/benchmark/helpers/DatabaseHelper;->outputUpdateComplete(Ljava/lang/String;Ljavax/servlet/http/HttpServletResponse;)V :try_end_45 .catch Ljava/sql/SQLException;{:try_start_3a .. :try_end_45} :catch_46 .line 71 return-void .line 62 .end local v0 # "count":I .end local v5 # "statement":Ljava/sql/Statement; :catch_46 move-exception v1 .line 69 .local v1,"e":Ljava/sql/SQLException; new-instance v6,Ljavax/servlet/ServletException; invoke-direct {v6, v1},Ljavax/servlet/ServletException;-><init>(Ljava/lang/Throwable;)V throw v6 .end method 以下是我們需要配置的source點: Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration; 以下是我們需要配置的sink點: Ljava/sql/Statement;->executeUpdate(Ljava/lang/String;)I
以下是Hades生成的指令控制流圖

由於指令控制流圖太大,無法清晰的展示中間的指令資訊,所以下面重點展示下source點和sink點相關的片段。
Source點:

sink點

下面是Hades的檢測報告:
{ "source":"Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration;", "linearCode": [ "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V1", "1: .registers 11", "2: .param p1, \request\ # Ljavax/servlet/http/HttpServletRequest;", "3: .param p2, \response\ #Ljavax/servlet/http/HttpServletResponse;", "4: .annotation systemLdalvik/annotation/Throws;", "5: value = {", "6:Ljavax/servlet/ServletException;,", "7: Ljava/io/IOException;", "8: }", "9: .end annotation", "10: .prologue", "11: .line 42", "12: const-string v6,\text/html;charset=UTF-8\", "13: invoke-interface {p2, v6},Ljavax/servlet/http/HttpServletResponse;->setContentType(Ljava/lang/String;)V", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V14", "14: .line 45", "15: const-string v3, \\", "16: .line 46", "17: .local v3,\param\:Ljava/lang/String;", "18: const-string v6,\BenchmarkTest00018\", "19: invoke-interface {p1, v6},Ljavax/servlet/http/HttpServletRequest;->getHeaders(Ljava/lang/String;)Ljava/util/Enumeration;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V20", "20: move-result-object v2", "21: .line 48", "22: .local v2,\headers\:Ljava/util/Enumeration;,\Ljava/util/Enumeration<Ljava/lang/String;>;\", "23: if-eqz v2, :cond_1b", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V24", "24: invoke-interface {v2},Ljava/util/Enumeration;->hasMoreElements()Z", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V25", "25: move-result v6", "26: if-eqz v6, :cond_1b", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V27", "27: .line 49", "28: invoke-interface {v2},Ljava/util/Enumeration;->nextElement()Ljava/lang/Object;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V29", "29: move-result-object v3", "30: .end local v3 # \param\:Ljava/lang/String;", "31: check-cast v3,Ljava/lang/String;", "32: .line 53", "33: .restart local v3 # \param\:Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V34", "34: :cond_1b", "35: const-string v6,\UTF-8\", "36: invoke-static {v3, v6},Ljava/net/URLDecoder;->decode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V37", "37: move-result-object v3", "38: .line 56", "39: new-instance v6, Ljava/lang/StringBuilder;", "40: invoke-direct {v6},Ljava/lang/StringBuilder;-><init>()V", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V41", "41: const-string v7, \INSERTINTO users (username, password) VALUES (\'foo\',\'\", "42: invoke-virtual {v6, v7},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V43", "43: move-result-object v6", "44: invoke-virtual {v6, v3},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V45", "45: move-result-object v6", "46: const-string v7,\\')\", "47: invoke-virtual {v6, v7},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V48", "48: move-result-object v6", "49: invoke-virtual {v6},Ljava/lang/StringBuilder;->toString()Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V50", "50: move-result-object v4", "51: .line 59", "52: .local v4,\sql\:Ljava/lang/String;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018;doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V53", "53: :try_start_3a", "54: invoke-static {},Lorg/owasp/benchmark/helpers/DatabaseHelper;->getSqlStatement()Ljava/sql/Statement;", "", "Lorg/owasp/benchmark/testcode/BenchmarkTest00018; doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V55", "55: move-result-object v5", "56: .line 60", "57: .local v5,\statement\:Ljava/sql/Statement;", "58: invoke-interface {v5, v4},Ljava/sql/Statement;->executeUpdate(Ljava/lang/String;)I", "" ], "sinkBelongTo":"org/owasp/benchmark/testcode/BenchmarkTest00018; doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V", "sink":"Ljava/sql/Statement;->executeUpdate(Ljava/lang/String;)I", "sourceBelongTo":"org/owasp/benchmark/testcode/BenchmarkTest00018; doPost(Ljavax/servlet/http/HttpServletRequest;Ljavax/servlet/http/HttpServletResponse;)V" }
八、項目地址
https://github.com/zsdlove/Hades
九、招募項目長期維護人員
目前該項目中只支援java源碼的審計,後續計劃支援php,c/c++,js等語言。如果你對白盒感興趣,並具有一定的編程能力(最好全棧),可先加我為好友,我將把你拉近Hades白盒審計系統開源項目的交流群中。