1. 程式人生 > >利用靜態分析和Clang發現心臟出血Bug

利用靜態分析和Clang發現心臟出血Bug

背景

上週五晚上,我喝著15年的Macallan威士忌,決定寫一個能發現心臟出血Bug的靜態檢測器。它應該作為Clang分析器之外的一個外掛,然後利用一些有心臟出血Bug的函式來測試它,最後在脆弱的OpenSSL上測試。

在LLVM編譯器基礎上,Clang專案產生了一個分析框架,它通過scan-build命令來呼叫。Clang插入一個鉤子到存在的Make系統,你必須在構建過程中插入Clang分析器,分析器像編譯器一樣,會使用同樣的引數被呼叫。通過這種方式,分析器在Clang的編譯下,可以分析程式的每一個編譯單元。Clang分析器的一些侷限性我將在討論部分談到。

這個練習變成了我喝酒是唯一能做的事:當喝啤酒時,我有最好的邏輯思維能力,當喝威士忌時,我有最好的Clang分析器。

策略

Coverity最近提出了一種靜態確定心臟流血問題的方法,它是通過汙染返回值來作為輸入資料,這個返回值是呼叫ntohl和ntohs函式返回的。在對於一個像OpenSSL這種大的狀態機做靜態分析時,你可能會遇到兩個問題,一個問題是你必須知道狀態機能否追蹤一些值,這些值能夠被攻擊者利用,從而影響到整個程式。另外就是你可能需要在程式中有一些註解,告訴分析器那兒有一個可用的輸入資料。

我喜歡這種觀測方式,因為它確實可行。標記ntohl函式呼叫來生產汙染資料,它是一種啟發式的並且相當不錯的方式,因為程式設計師並不會用htonl函式處理自己的資料。

我們的Clang分析器外掛做的是在程式中使用ntohl函式的位置,汙染他們,然後當這些被汙染的值作為大小引數在memcpy函式中使用時給予提示。可是這不完全正確,但使用上是安全的。我們也將在呼叫的位置檢查被感染的值的約束:如果被感染的值通過程式邏輯在某些情況下沒有被約束,並使用它作為memcpy的引數,則提示是一個bug 。這種做法可能會錯過一些bug,但我是在喝著蘇格蘭威士忌並在一個天內完成,增加準確度可以以後再做。

分析器的細節

Clang分析器實現了一種符號執行來分析C/C++程式。作為一個分析器插入這個框架,那麼你頭腦中必須要有一個程式狀態的Clang分析檢視。這個是我花費時間最多的地方。

分析器,本質上來說,就是執行一個探索程式狀態的符號/抽象過程。這個探索過程是流,並且是路徑敏感的。因此它不同於傳統的編譯資料流分析。分析過程中,會對程式中的每一條路保持一個狀態物件。這個狀態物件能夠被分析器查詢,分析器也可以改變狀態物件來包含一些分析器產生的資訊。

在寫分析器時遇到的最大障礙之一是 – 一旦在一個特殊情況下獲得一個“符號變數”,怎樣查詢該符號變數的範圍?就如下面這樣的程式碼片段:

C
12345678 intdata=ntohl(pkt_data);if(data>=0&&data<sizeof(global_arr)){// CASE A...}else{// CASE B...}

從分析器角度看這個程式時,狀態被劃分為A和B兩種狀態。在狀態A時,資料有一個特定的邊界約束,在狀態B時,資料有一個沒在特定邊界的約束。從檢測器上如何才能獲取這種資訊呢?

如果檢測器在一個給定的狀態物件上呼叫dump方法,資料將會被打印出來,像下面這樣:

符號值的範圍:

Shell
12 conj_$2{int}:{[-2147483648,-2],[0,2147483647]}conj_$9{uint32_t}:{[0,6]}

在這個例子中,conj_$9{uint32_t}是我們在狀態A時data變數的值。它的範圍在0-6之間。作為一個檢測器,怎樣才能觀察到這個範圍和沒有約束的範圍 [-2147483648, 2147483648]之間的不同呢?

答案是我們創造一個公式,測試data變數的符號值是否滿足一些既定條件,然後當公式為真時和假時,檢視程式的狀態。如果新的公式與一個已經存在的公式矛盾時,狀態就不可行了,這時就沒有狀態產生。比如我們建立一個公式“data>500”,它的意思就是詢問是否有data比500更大。當我們想獲得一個新狀態是真是假時的狀態, 它僅僅會給我們一個當它為假時的狀態。

這種慣用法被Clang分析器用來回答關於狀態約束的問題。陣列邊界檢測器就用了這種技巧來確定陣列的大小,但它不能用來作為陣列索引的約束。

實現

分析器是作為C++類來實現的。你可以定義不同的”check”函式,當分析器正掃描程式狀態時,你能夠被通知到。例如 ,假如你的分析器想要知道一個函式被呼叫之前傳遞給這個函式的引數,你可以建立一個帶有如下引數的成員方法:

C
1 voidcheckPreCall(constCallEvent&Call,CheckerContext&C)const;

因此我們的實現包括以下三步:

1. 呼叫ntohl/ntoh函式
2. 汙染這些方法呼叫的返回值
3. 使用這些被汙染的值

我們用*checkPostCall*方法實現了第一條和第二條,程式碼如下:

C
12345678910111213141516 voidNetworkTaintChecker::checkPostCall(constCallEvent&Call,CheckerContext&C)const{constIdentifierInfo*ID=Call.getCalleeIdentifier();if(ID==NULL){return;}if(ID->getName()=="ntohl"||ID->getName()=="ntohs"){ProgramStateRef State=C.getState();SymbolRef Sym=Call.getReturnValue().getAsSymbol();if(Sym){ProgramStateRef newState=State->addTaint(Sym);C.addTransition(newState);}}}

很簡單,我們剛剛得到了返回值,現在,汙染它,通過新增一個具有髒值的state,返回值作為一個輸出,可以通過addTransition方法訪問到。

為了第三步,我們有一個checkPreCall方法,如下所示:

C++
123456789101112131415161718192021222324 voidNetworkTaintChecker::checkPreCall(constCallEvent&Call,CheckerContext&C)const{ProgramStateRef State=C.getState();constIdentifierInfo*ID=Call.getCalleeIdentifier();if(ID==NULL){return;}if(ID->getName()=="memcpy"){SVal SizeArg=Call.getArgSVal(2);ProgramStateRef state=C.getState();if(state->isTainted(SizeArg)){SValBuilder&svalBuilder =C.getSValBuilder();Optional<NonLoc>SizeArgNL=SizeArg.getAs<NonLoc>();if(this->isArgUnConstrained(SizeArgNL,svalBuilder,state)==true){ExplodedNode*loc=C.generateSink();if(loc){BugReport*bug=newBugReport(*this->BT,"Tainted,unconstrained value used in memcpy size",loc);C.emitReport(bug);}}}}

也很簡單,我們的邏輯是呼叫isArgUnConstrained方法來判斷是否一個沒有被約束的值被隱藏,如果在當前路徑有一個被汙染的符號化的值有不充足的約束,我們就報出Bug。

實現上面的一些缺陷

實踐證明OpenSSL並沒有使用ntohs/ntohl函式, 它們用了n2s / n2l 這個巨集重新實現了位元組交換的邏輯。如果是LLVM的中介碼,我們就可以寫一個位元組交換識別器, 它會使用一定數量的邏輯去證明一段程式碼近似於位元組轉換的語義。

也有一些行為,我沒有發現。clang為openssl建立AST時,呼叫的ntohs函式被 __builtin_pre(__x)取代, 它沒有標示資訊,因此沒有名字。為了解決這個問題, 我用了一個xyzzy的函式取代了n2s巨集, 導致連線失敗,並且為了適應我之前的功能檢查,需要檢查一個叫做xyzzy的函式。識別心臟出血Bug, 這已經很管用了。

用演示程式和OpenSSL來解決輸出

首先看一個小程式:

C
123456789101112131415161718192021222324252627282930313233 $cat demo2.c...intdata_array[]={0,18,21,95,43,32,51};intmain(intargc,char*argv[]){intfd;charbuf[512]={0};fd=open("dtin",O_RDONLY);if(fd!=-1){intsize;intres;res=read(fd,&size,sizeof(int));if(res==sizeof(int)){size=ntohl(size);if(size<sizeof(data_array)){memcpy(buf,data_array,size);}memcpy(buf,data_array,size);}close(fd);}return0;}
Shell
1234567 $../docheck.shscan-build:Using'/usr/bin/clang'forstatic analysis/usr/bin/ccc-analyzer-odemo2 demo2.cdemo2.c:30:7:warning:Tainted,unconstrained value used inmemcpy size memcpy(buf,data_array,size);^~~~~~~~~~~~~~~~~~~~~~~~~~~~~1warning generated.scan-build:1bugs found.scan-build:Run'scan-view /tmp/scan-build-2014-04-26-223755-8651-1'toexamine bug reports.

最後,它在OpenSSL中捕捉到了心臟流血的Bug,如下

討論

上面的方式還需要一些改進,我們假定一個汙染值是“適當的”約束或者不是一種非常粗粒度的方式。有時候這可能是你能夠做的最好的了 – 如果你的分析器並不知道多大的特定緩衝區,或許只足以展現給分析者:“哎,這個值可能會大於5000 ,並且它是用來作為引數傳遞給memcpy的,這樣可以嗎? “

我真的不喜歡在操作AST時,Clang分析器的限制。

我花了很多時間來處理ntohs函式表述的Clang的遍歷抽象語法樹(AST),但我一直不明白這個問題的根源。我只是想以一種非常簡單的語義來思考在虛擬機器中的程式語義,因此LLVM IR似乎很理想。這可能只是我的PL(不知道什麼意思)根源表現。

我真的很喜歡的Clang分析器的介面路徑約束。該介面是相當強大的,一旦你意識到請求狀態可以解決你的問題時,那麼如果一個新的狀態能夠滿足你的約束就變得可行了,寫新的分析器就變得很簡單了。

程式碼

我已經將程式碼放到了Github上,這裡可以下載
譯者注:AST:遍歷抽象語法樹
心臟出血Bug的具體解釋