簡單理解符號執行技術

0X00 前言

因爲最近看的很多靜態檢測的論文中涉及到了符號執行的概念,而在我第一次聽到符號執行實際上是在我的一些搞二進制學長口中,自然認爲是和 web 沒啥關係,但是現在看來只是因爲我我太菜了,很多知識在更高的層次看起來都是交融的,而不是我現在看到的全部都是互不相關的板塊,或許這也就是爲什麼要讀研吧,不讀研那就瘋狂努力吧。好了,廢話不多講了,由於我對符號執行的理解沒有達到很高的層次,不能進行更詳盡的總結分析,故我只能在網上找了一些我個人認爲總結的比較好,並且通俗易懂的文章進行一些摘錄,在此之前先對這些優秀的作者表示感謝,文章之後我會附上我引用的文章或者論文的鏈接。

0X01 通俗地解釋符號執行

Wiki中的定義是:在計算機科學中,符號執行技術指的是通過程序分析的方法,確定哪些輸入向量會對應導致程序的執行結果爲某個向量的方法(繞)。通俗的說,如果把一個程序比作DOTA英雄,英雄的最終屬性值爲程序的輸出(包括攻擊力、防禦力、血槽、藍槽),英雄的武器出裝爲程序的輸入(出A杖還是BKB)。那麼符號執行技術的任務就是,給定了一個英雄的最終屬性值,分析出該英雄可以通過哪些出裝方式達到這種最終屬性值效果。

可以發現,符號執行技術是一種白盒的靜態分析技術。即,分析程序可能的輸入需要能夠獲取到目標源代碼的支持。同時,它是靜態的,因爲並沒有實際的執行程序本身,而是分析程序的執行路徑。如果把上述英雄的最終屬性值替換成程序形成的bug狀態,比如,存在數組越界複製的狀態,那麼,我們就能夠利用此技術挖掘漏洞的輸入向量了。

這裏再舉一個簡單的例子,讓大家有深刻的理解。

以下面的源代碼爲例子:

int m=M, n=N, q=Q; 
int x1=0,x2=0,x3=0;
if(m!=0)
{
    x1=-2;
}
if(n<12)
{
    if(!m && q)
    {
        x2=1;
    }
    x3=2;
}
assert(x1+x2+x3!=3)

上述代碼是一個簡單的c語言分支結構代碼,它的輸入是M,N,Q三個變量;輸出是x1,x2,x3的三個變量的和。我們這裏設置的條件是想看看什麼樣的輸入向量<M,N,Q>的情況下,得到的三個輸出變量的和等於3.

那麼我們通過下面的樹形結構來看看所有的情況:

此處輸入圖片的描述此處輸入圖片的描述

上面的分析圖把所有可能的情況都列舉出來了,其中,葉子節點顯示的數值表示當前輸入情況下,可以得到的數值。(比如,如果英雄出裝是M^(N<12),那麼最終的屬性值R=0)。其中M^(N<12)表達的是,M是非零值且N要小於12,Q爲任意值的情況下,得到R=0。可以發現,當條件爲~M^(N<5)^Q時,得到了最終結果等於3.即,我們通過這種方式逆向發現了輸入向量。如果把結果條件更改爲漏洞條件,理論上也是能夠進行漏洞挖掘了。

對於如何根據最終得到的結果求解輸入向量,已經有很多現成的數學工具可以使用。上述問題其實可以規約成約束規劃的求解問題(更詳細的介紹看這裏:Constraint_programming )。比較著名的工具比如SMT(Satisfiability Modulo Theory,可滿足性模理論)和SAT。

但是在實際的漏洞分析過程中,目標程序可能更加複雜,沒有我們上面的例子這麼簡單。實際的程序中,可能包含了與外設交互的系統函數,而這些系統函數的輸入輸出並不會直接賦值到符號中,從而阻斷了此類問題的求解

比如下面的這個包含了文件讀寫的例子:

int main(int argc, char* argv[])
{
    FILE *fop = fopen("test.txt");
    ...
    if(argc > 3)
    {
        fputs("Too many parameters, exit.", fop);
    }
    else
    {
        fputs("Ok, we will run normally.", fop);
    }
    ...
    output = fgets(..., fop);
    assert(!strcmp(output, "Ok, we will run normally."));
    return 0;
}

上述示例代碼中,想要發現什麼情況下會得到輸出”Ok, we will run normally.”這個字符串。通過一系列的執行到if語句,此時,根據輸入的參數個數將會產生兩個分支。分支語句中將執行系統的文件寫操作。在傳統的符號執行過程中,此類函數如果繼續沿着系統函數的調用傳遞下去的話,符號數值的傳遞將會丟失。而在之後的output = fgets(…, fop);這行代碼中,符號從外部獲得的數值也將無法正常的賦值到output中。因此,符號執行無法求解上述問題,因爲在調用系統函數與外設交互的時候,符號數值的賦值過程被截斷了。

此處輸入圖片的描述此處輸入圖片的描述

爲了解決這個問題,最經典的項目就是基於LLVM的KLEE(klee)它把一系列的與外設有關的系統函數給重新寫了一下,使得符號數值的傳遞能夠繼續下去。從比較簡化的角度來說,就是把上面的fputs函數修改成,字符串賦值到某個變量中,比如可以是上面的fop裏面。再把fgets函數修改成從某個變量獲取內容,比如可以是把fop的地址給output。這樣,就能夠把符號數值的傳遞給續上。當然,這裏舉的例子是比較簡單的例子,實際在重寫函數的時候,會要處理更復雜的情況。在KLEE中,它重新對40個系統調用進行了建模,比如open, read, write, stat, lseek, ftruncate, ioctl。感興趣的讀者可以進一步閱讀他們發表在OSDI2008年的論文(KLEE-OSDI08)他們的文章深入淺出,非常適合學習。

0X02 從公式原理上理解符號執行

符號執行的關鍵思想就是,把輸入變爲符號值,那麼程序計算的輸出值就是一個符號輸入值的函數。這個符號化的過程在上一篇AEG文章中已有簡要闡述,簡而言之,就是一個程序執行的路徑通常是true和false條件的序列,這些條件是在分支語句處產生的。在序列的i^{th} 位置如果值是true,那麼意味着i^{th} 條件語句走的是then這個分支;反之如果是false就意味着程序執行走的是else分支。

那麼,如何形式化地表示符號執行的過程呢?程序的所有執行路徑可以表示爲樹,叫做執行樹。接下來我們就以一個例子來闡述通過符號執行遍歷程序執行樹的過程。

此處輸入圖片的描述此處輸入圖片的描述

左邊的代碼中,testme()函數有3條執行路徑,組成了右圖中的執行樹。直觀上來看,我們只要給出三個輸入就可以遍歷這三個路徑,即圖中綠色的x和y取值。符號執行的目標就是能夠生成這樣的輸入集合,在給定的時間內探索所有的路徑。

爲了形式化地完成這個任務,符號執行會在全局維護兩個變量。其一是符號狀態 $\sigma$ ,它表示的是一個從變量到符號表達式的映射。其二是符號化路徑約束PC,這是一個無量詞的一階公式,用來表示路徑條件。在符號執行的開始,符號狀態$\sigma$ 會先初始化爲一個空的映射,而符號化路徑約束PC初始化爲true。$\sigma$ 和PC在符號執行的過程中會不斷更新。

在符號執行結束時,PC就會用約束求解器進行求解,以生成實際的輸入值。這個實際的輸入值如果用程序執行,就會走符號執行過程中探索的那條路徑,即此時PC的公式所表示的路徑

我們以左圖的例子來闡述這個過程。當符號執行開始時,符號狀態$\sigma$ 爲空,符號路徑約束PC爲true。當我們遇到一個讀語句,形式爲var=sym_input(),即接收程序輸入,符號執行就會在符號狀態$\sigma$ 中加入一個映射$var\rightarrow s$,這裏s就是一個新的未約束的符號值。左圖中代碼,main()函數的前兩行會得到結果$\sigma =\left{ x\rightarrow x_{0}, y\rightarrow y_{0}\right}$,其中$x_{0}$ 和$y_{0}$ 是兩個初始的未約束的符號化值。

當我們遇到一個賦值語句,形式爲 v=e,符號執行就會將符號狀態$\sigma$ 更新,加入一個v到$\sigma \left( e \right)$ 的映射,其中$\sigma \left( e \right)$ 就是在當前符號化狀態計算e得到的表達式。例如,在左圖中代碼執行完第6行時,$\sigma =\left{ x \rightarrow x_{0}, y \rightarrow y_{0}, z \rightarrow 2y_{0}\right}$ 。

當我們遇到條件語句if(e) S1 else S2,PC會有兩個不同更新。首先是PC更新爲PC$\wedge \sigma \left( e \right)$,這就表示then分支;然後是建立一個路徑約束PC’,初始化爲PC$\wedge \neg \sigma \left( e \right)$ ,這就表示else分支。如果PC是可滿足的,給一些實際值,那麼程序執行就會走then分支,此時的狀態爲:符號狀態$\sigma$ 和符號路徑約束PC。反之如果PC’是可滿足的,那麼會建立另一個符號實例,其符號狀態爲$\sigma$ ,符號路徑約束爲PC’,走else分支。如果PC和PC’都不能滿足,那麼執行就會在對應路徑終止。例如,第7行建立了兩個不同的符號執行實例,路徑約束分別是$x_{0} =2y_{0}$和$x_{0} \ne 2y_{0}$。在第8行,又建立了兩個符號執行實例,路徑約束分別是$\left( x_{0} =2y_{0} \right) \wedge \left( x_{0}>y_{0}+10 \right)$ ,以及$\left( x_{0} =2y_{0} \right) \wedge \left( x_{0} \leq y _{0}+10 \right)$ 。

如果符號執行遇到了exit語句或者錯誤(指的是程序崩潰、違反斷言等),符號執行的當前實例會終止,利用約束求解器對當前符號路徑約束賦一個可滿足的值,而可滿足的賦值就構成了測試輸入:如果程序執行這些實際輸入值,就會在同樣的路徑結束。例如,在左圖例子中,經過符號執行的計算會得到三個測試輸入:{x=0, y=1}, {x=2, y=1}, {x=30, y=15}。

此處輸入圖片的描述此處輸入圖片的描述

當我們遇到了循環和遞歸應該怎麼辦呢?如果循環或遞歸的終止條件是符號化的,包含循環和遞歸的符號執行會導致無限數量的路徑。比如上圖中的這個例子,這段代碼就有無數條執行路徑,每條路徑的可能性有兩種:要麼是任意數量的true加上一個false結尾,要麼是無窮多數量的true。我們形式化地表示包含n個true條件和1個false條件的路徑,其符號化約束如下:

$$(\wedge_ {i\in [1,n]}N_{i} >0) \wedge (N_{n+1}\leq 10)$$

其中每個$N_{i}$ 都是一個新的符號化值,執行結尾的符號化狀態是$\left{ N\rightarrow N_{n+1} ,sum\rightarrow \sum_{i\in [1,n]}^{}{N_{i}} \right}$ 。其實這就是符號執行面臨的問題之一,即如何處理循環中的無限多路徑。在實際中,有一些方法可以應對,比如對搜索加入限制,要麼是限制搜索時間的長短,要麼是限制路徑數量、循環迭代次數、探索深度等等。

還需要考慮到的一個問題就是,如果符號路徑約束包含不能由求解器高效求解的公式怎麼辦?比如說,如果原本的代碼發生變化,把twice函數替換爲下圖中的語句,那麼符號執行就會產生路徑約束$x_{0}\ne (y_{0} y_{0}) mod 50$以及$x_{0}= (y_{0} y_{0}) mod 50$。

此處輸入圖片的描述此處輸入圖片的描述

我們做另外一個假設,如果twice是一個我們得不到源碼的函數,也就是我們不知道這個函數有什麼功能,那麼符號執行會產生路徑約束$x_{0}\ne twice(y_{0})$ 和 $x_{0}= twice(y_{0})$ ,其中twice是一個未解釋的函數。這兩種情況下,約束求解器都是不能求解這樣的約束的,所以符號執行不能產生輸入。

解決方法是一種叫做動態符號執行的技術,我們會在後面的小節中介紹。

0X03 符號執行的具體流程

符號執行分爲過程內分析過程間分析(又稱全局分析)。過程內分析是指只對單個過程的代碼進行分析,全局分析指對整個軟件代碼進行上下文敏感的分析。所謂上下文敏感分析是指在當前函數入口點要考慮當前的函數間調用信息和環境信息等。程序的全局分析是在過程內分析的基礎上進行的,如果過程內分析中包含了函數調用,就引入了過程間分析,因此兩者之間是相對獨立又相互依賴的關係。

(1)過程內分析流程如下圖所示

此處輸入圖片的描述此處輸入圖片的描述

首先,對待分析的單個過程代碼對象構建控制流圖(Control Flow Graph,CFG)。控制流圖(CFG)是編譯器內部用有向圖表示一個程序過程的一種抽象數據結構,圖中的節點表示一個程序基本塊,基本塊是沒有任何跳轉的順序語句代碼塊,圖中的邊表示代碼中的跳轉,它是有向邊,起點和終點都是基本塊。在CFG上從入口節點開始模擬執行,在遇到分支節點時,使用約束求解器判定哪條分支可行,並根據預先設計的路徑調度策略實現對該過程所有路徑的遍歷分析,最後輸出每條可執行路徑的分析結果。其中約束求解是數學上的判定過程,形象地說是對一系列的約束方程進行求解。

如果要進行源代碼的安全性檢測,則需要在過程內分析時,根據具體的安全知識庫來添加安全約束。例如,如果要添加緩衝區溢出的安全約束,則在執行時遇到對內存進行操作的語句時,就要對該語句所操作的內存對象的邊界添加安全約束。以上面的方式來進行安全約束的添加,並且每次在添加之後就使用約束求解器對所有的安全約束進行求解,以判定當前是否可能潛在一個安全問題。

(2)程序全局分析流程如下圖所示:

此處輸入圖片的描述此處輸入圖片的描述

首先,爲整個程序代碼構建函數調用圖(Call Graph,CG),在函數調用圖中,節點表示函數,邊表示函數間的調用關係。根據預設的全局分析調度策略,對CG中的每個節點(對應一個函數)進行過程內分析,最終給出CG每種可行的調用序列的分析結果。

0X04 動態符號執行

符號執行在發展過程中出現了一種叫做動態符號執行的方法(concrete and symbolic, concolic)。動態符號執行是以具體數值作爲輸入來模擬執行程序代碼,與傳統靜態符號執行相比,其輸入值的表示形式不同。動態符號執行使用具體值作爲輸入,同時啓動代碼模擬執行器,並從當前路徑的分支語句的謂詞中搜集所有符號約束。然後修改該符號約束內容構造出一條新的可行的路徑約束,並用約束求解器求解出一個可行的新的具體輸入,接着符號執行引擎對新輸入值進行一輪新的分析。通過使用這種輸入迭代產生變種輸入的方法,理論上所有可行的路徑都可以被計算並分析一遍。

動態符號執行相對於靜態符號執行的優點是每次都是具體輸入的執行,在模擬執行這個過程中,符號化的模擬執行比具體化的模擬執行的開銷大很多;並且模擬執行過程中所有的變量都爲具體值,而不必使用複雜的數據結構來表達符號值,使得模擬執行的花銷進一步減少。但是動態符號執行的結果是對程序的所有路徑的一個下逼近,即其最後產生路徑的集合應該比所有路徑集合小,(但這種情況在軟件測試中是允許的)

此處輸入圖片的描述此處輸入圖片的描述

我們依舊以上圖的這個程序的例子來說明。Concolic執行會先產生一些隨機輸入,例如{x=22, y=7},然後同時實際地和符號化地執行程序。這個實際執行會走到第7行的else分支,符號化執行會在實際執行路徑生成路徑約束$x_{0} \ne 2 y_{0}$。然後concolic執行會將路徑約束的連接詞取反,求解$x_{0} = 2 y_{0}$得到一個測試輸入{x=2, y=1},這個新輸入就會讓執行走向一條不同的路徑。之後,concolic執行會在這個新的測試輸入上再同時進行實際的和符號化的執行,執行會取與此前路徑不同的分支,即第7行的then分支和第8行的else分支,這時產生的約束就是$(x_{0}=2y_{0})\wedge (x_{0}\leq y_{0}+10)$,生成新的測試輸入讓程序執行沒有被執行過的路徑。再探索新的路徑,就需要將上述的條件取反,也就是$(x_{0}=2y_{0})\wedge (x_{0}> y_{0}+10)$,通過求解約束得到測試輸入{x=30, y=15},程序會在這個輸入上遇到ERROR語句。如此一來,我們就完成了所有3條路徑的探索。

這個過程中,我們從一個實際輸入{x=22, y=7}出發,得到第一個約束條件$x_{0} \ne 2 y_{0}$,第一次取反得到$x_{0} = 2 y_{0}$,從而得到測試輸入{x=2, y=1}和新約束$(x_{0}=2y_{0})\wedge (x_{0}\leq y_{0}+10)$;$(x_{0}=2y_{0})\wedge (x_{0}> y_{0}+10)$,從而求解出測試輸入{x=30, y=15}。

注意在這個搜索過程中,其實concolic執行使用了深度優先的搜索策略。

Cristian Cadar在2006年發表EXE,以及2008年發表EXE的改進版本KLEE,對上述concolic執行的方法做了進一步優化。其創新點主要是在實際狀態和符號狀態之間進行區分,稱之爲執行生成的測試(Execution-Generated Testing),簡稱EGT。這個方法在每次運算前動態檢查值是不是都是實際的,如果都是實際的值,那麼運算就原樣執行,否則,如果至少有一個值是符號化的,運算就會通過更新當前路徑的條件符號化地進行。例如,對於我們的例子程序,第17行把y=sym_input()改變成y=10,那麼第6行就會用實際參數20去調用函數twice,並實際執行。然後第7行變成if(20==x),符號執行會走then路徑,加入約束x=20;對條件進行取反就可以走else路徑,約束是x≠20。在then路徑,第8行變成if(x>20),那麼then路徑就不能走了,因爲此時有約束x=20。簡言之,EGT本質上還是將實際執行與符號執行相結合,通過路徑取反探索所有可能路徑。

正是因爲concolic執行的出現,讓傳統靜態符號執行遇到的很多問題能夠得到解決——那些符號執行不好處理的部分、求解器無法求解的部分,用實際值替換就好了。使用實際值,可以讓因外部代碼交互和約束求解超時造成的不精確大大降低,但付出的代價就是,會有丟失路徑的缺陷,犧牲了路徑探索的完全性

我們舉一個例子來說明這一點。假設我們原始例子程序做了改動,即把twice函數的定義改爲返回(v*v)%50。假設執行從隨機輸入{x=22, y=7}開始,生成路徑約束$x_{0}\ne (y_{0} y_{0}) mod 50$。因爲約束求解器無法求解非線性約束,所以concolic執行的應對方法是,把符號值用實際值替換,此處會把$y_{0}$的值替換爲7,這就將程序約束簡化爲$x_{0}\ne49$。通過求解這個約束,可以得到輸入${x=49, y=7}$,走到一個此前沒有走到的路徑。傳統靜態符號執行是無法做到這一步的。但是,在這個例子中,我們無法生成路徑true, false的輸入,即約束$x_{0}= (y_{0} y_{0}) mod 50\wedge (x_{0}\leq y_{0}+10)$,因爲$y_{0}$的值已經實際化了,這就造成了丟失路徑的問題,造成不完全性。

然而總的來說,concolic執行的方法是非常實用的,有效解決了遇到不支持的運算以及應用與外界交互的問題。比如調用庫函數和OS系統調用的情況下,因爲庫和系統調用無法插樁,所以這些函數相關的返回值會被實際化。

0X05 挑戰&解決方案

符號執行曾經遇到過很多問題,使其難以應用在真實的程序分析中。經過研究者的不懈努力,這些問題多多少少得到了解決,由此也產生了一大批優秀的學術論文。這一部分將簡單介紹其中的一些關鍵挑戰以及對應的解決方案。

1.路徑選擇

由於在每一個條件分支都會產生兩個不同約束,符號執行要探索的執行路徑依分支數指數增長。在時間和資源有限的情況下,應該對最相關的路徑進行探索,這就涉及到了路徑選擇的問題。通過路徑選擇的方法緩解指數爆炸問題,主要有兩種方法:

1)使用啓發式函數對路徑進行搜索,目的是先探索最值得探索的路徑;
2)使用一些可靠的程序分析技術減少路徑探索的複雜性。

啓發式搜索是一種路徑搜索策略,比深度優先或者廣度優先要更先進一些。大多數啓發式的主要目標在於獲得較高的語句和分支的覆蓋率,不過也有可能用於其他優化目的。最簡單的啓發式大概是隨機探索的啓發式,即在兩邊都可行的符號化分支隨機選擇走哪一邊。還有一個方法是,使用靜態控制流圖(CFG)來指導路徑選擇,儘量選擇與未覆蓋指令最接近的路徑另一個方法是符號執行與進化搜索相結合,其fitness function用來指導輸入空間的搜索,其關鍵就在於fitness function的定義,例如利用從動態或靜態分析中得到的實際狀態信息或者符號信息來提升fitness function。

用程序分析和軟件驗證的思路去減少路徑探索的複雜性,也是一種緩解路徑爆炸問題的方式。

(1)通過靜態融合減少需要探索的路徑:具體說來就是使用select表達式直接傳遞給約束求解器,但實際上是將路徑選擇的複雜性傳遞給了求解器,對求解器提出了更高的要求。

(2)重用:即通過緩存等方式存儲函數摘要,可以將底層函數的計算結果重用到高級函數中,不需要重複計算,減小分析的複雜性。

(3)去除冗餘路徑: RWset技術的關鍵思路就是,如果程序路徑與此前探索過的路徑在同樣符號約束的情況下到達相同的程序點,那麼這條路徑就會從該點繼續執行,所以可以被丟棄。

2.約束求解

符號執行在2005年之後的突然重新流行,一大部分原因是因爲求解器能力的提升,能夠求解複雜的路徑約束。但是約束求解在某種程度上依然是符號執行的關鍵瓶頸之一,也就是說符號執行所需求的約束求解能力超出了當前約束求解器的能力。所以,實現約束求解優化就變得十分重要

這裏主要介紹兩種優化方法:不相關約束消除增量求解

1.不相關約束消除

在符號執行的約束生成過程中,尤其是在concolic執行過程中,通常會通過條件取反的方式增加約束,一個已知路徑約束的分支謂詞會取反,然後結果的約束集會檢查可滿足性以識別另一條路徑是否可行。一個很重要的現象是,一個程序分支通常只依賴一小部分程序變量,所以我們可以嘗試從當前路徑條件中移除與識別當前分支結果不相關的約束

例如,當前的路徑條件是$(x+y>10)\wedge (z>0)\wedge (y<12) \wedge (z-x=0)$,我們想對某個條件取反以探索新的路徑,即求解$(x+y>10)\wedge (z>0)\wedge \neg (y<12)$ 產生新輸入,其中$\neg (y<12)$ 是取反的條件分支,那麼我們就可以去掉對z的約束,因爲對$\neg (y<12)$ 的分支是不會有影響的。減小的約束集會給出x和y的新值,我們用此前執行的z值就可以生成新輸入了。如果更形式化地說,算法會計算在取反條件所依賴的所有約束的傳遞閉包。

2.增量求解

另一種方法本質上也是利用重用的思想。符號執行中生成的約束集有一個重要特性,就是表示爲程序源代碼中的靜態分支的固定集合。所以,很多路徑有相似的約束集,可以有相似的解決方案通過重用以前相似請求的結果,可以利用這種特性來提升約束求解的速度,這種方法在CUTE和KLEE中都有實現。

舉個例子來說明,在KLEE中,所有的請求結果都保存在緩存中,該緩存將約束集映射到實際變量賦值。例如,緩存中的一個映射可能是$(x+y<10)\wedge (x>5)\Rightarrow\left{ x=6, y=3 \right}$使用這些映射,KLEE可以迅速解答一些相似的請求類型,包括已經緩存的約束集的子集和超集。比如對於請求$(x+y<10)\wedge (x>5)\wedge(y\geq 0)$,KLEE可以迅速檢查{x=6, y=3}是一個可行的答案。這樣就可以讓求解過程加快很多。

3.內存建模

程序語句如何精確地翻譯爲符號化約束對符號執行得到的覆蓋率有很大影響。內存建模就是一個很大的問題,在訪問內存的時候,內存地址用來引用一個內存單元,當這個地址的引用來自於用戶輸入時,內存地址就成爲了一個表達式。當符號化執行時,我們必須決定什麼時候將這個內存的引用進行實際化。一個可靠的策略是,考慮從任何可能滿足的賦值加載,但這個可能值的空間很大,如果實際化不夠精確,會造成代碼分析的不精確。還有一個是別名問題,即地址別名導致兩個內存運算引用同一個地址,比較好的方法是進行別名分析,事先推理兩個引用是否指向相同的地址,但這個步驟要靜態分析完成。KLEE使用了別名分析和讓SMT考慮別名問題的混合方法。而DART和CUTE壓根沒解決這個問題,只處理線性約束的公式,不能處理一般的符號化引用。

符號化跳轉也是一個問題,主要是switch這樣的語句,常用跳轉表實現,跳轉的目標是一個表達式而不是實際值。

以往的工作用三種處理方法。

1)使用concolic執行中的實際化策略,一旦跳轉目標在實際執行中被執行,就可以將符號執行轉向這個實際路徑,但缺陷是實際化導致很難探索完全的狀態空間,只能探索已知的跳轉目標。

2)使用SMT求解器。當我們到達符號跳轉時,假設路徑謂詞爲$\Pi$,跳轉到e,我們可以讓SMT求解器找到符合$\Pi \wedge e$的答案。但是這種方案相比其他方案效率會低很多。

3)使用靜態分析,推理整個程序,定位可能的跳轉目標。實際中,源代碼的間接跳轉分析主要是指針分析。二進制的跳轉靜態分析推理在跳轉目標表達式中哪些值可能被引用。例如,函數指針表通常實現爲可能的跳轉目標表。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章