一、控制流整平的作用
學逆向的人都知道,if-else、while、for具有典型的跳轉等結構,即使通過多層嵌套、拓展條件等方法,依然可以通過“切片技術”來判斷。有了這些依據,就給程序分析帶來很多便利。
正是這個原因,爲了增加程序逆向的難度,我們得使得這些特徵結構變得模糊,並且能讓類似“切片技術”這樣基於具體語義分析的方法失效,迫使逆向分析人員進行完整的抽象語義分析,斬斷所謂的“捷徑”。
控制流整平的策略是這樣的,它把所有的典型控制流以及其衍生結構“統而爲一”,各種控制流的區別只是語義方面的,增加了理解控制流轉換關係的難度。
二、什麼是控制流整平
控制流整平迷惑,是通過打破程序原有的控制流之間的嵌套和順序關係,使得變換後的程序控制流扁平化的混淆方法,其基本思想是令程序中所有的基本塊擁有共同的前驅和後繼代碼塊。
如下圖(本文代碼思路皆使用C語言表示)
進行控制流整平後,使得面向過程的代碼片段,原來比較清晰的控制流向混雜在一起,同時這也比較好的並行圖形態,也有利於進一步的迷惑處理。
三、順序流整平
對於單純的順序流,一般不用控制流平整的方法,我們有更好的處理方法,但這裏爲了從基礎一步一步講控制流平整,所以從最簡單的順序流開始。
舉例:
int main()
{
int a,b,c;
Step1:
a=0;
Step2:
b=1;
Step3:
c=2
}
轉換爲:
int main()
{
int a,b,c;
int i=0
L1:
switch(i)
{
case 0:
a=0
i=1;
goto L1;
case 1:
b=1
i=2;
goto L1;
case 2
c=3;
i=3;
goto L1;
default:
NULL;
}
}
四、條件流平整
相信你看完順序流平整的代碼後,條件流平整應該也能“依葫蘆畫瓢”的寫出來,唯一的問題就是條件控制流經常會有嵌套的問題,還有不同條件如何套在switch裏。
例如:
int main()
{
int a,b;
bool b1=xxx,b2=xxx;
if(b1)
{
a=1;
if(b2)
b=1;
else
b=0;
}
轉換爲
int main()
{
int a,b;
bool b1=xxx,b2=xxx;
int i=b1
L1:
switch(i)
{
case True:
a=1;
i=b2+2;
goto L1;
case True+2:
b=1;
goto L1;
case Flase+2:
b=0;
goto L1
case Flase:
a=0;
goto L1;
}
}
五、循環控制流迷惑
循環控制流也可以被當做條件控制流一樣轉換,即循環和不循環的條件分支。但是爲了取得更好的迷惑效果,單次循環內的順序流也可以進行控制流整平,以增強迷惑效果。
六、分支變量保護
對於上面的方法面臨的一個問題是:“分支變量值的如何保護?”。如果分支變量的值能夠很容易地被分析出來,則程序可能被反迷惑成原程序,從而達不到代碼迷惑的效果,下面提出幾種可行的方案,供大家參考。
方法一:switch(i) 轉換爲 switch(f[i]),這樣控制流向需要“即時計算”來確定,其實就是Hash。
方法二:演示代碼中,各分支只進入一次,但可以使某些代碼塊多次重複進入,當然要考慮效率問題。
方法三:演示代碼只有一層,如果多層嵌套,並且把同一層的代碼塊放入各層的不同深度,但其付出的空間和時間成本是一個值得注意的問題,並且其最大的缺點是後續添加其他迷惑技巧時可能受到限制。
方法四:方法三可以認爲是縱向拓展,那麼橫向用多個switch之間的控制流連接,雖然它的複雜度比方法二小,但是繼續添加迷惑技巧時受限較小。因爲如果把方法一提到的“即時計算”部分挪到並行的switch裏,這樣前一個switch看起來都像正常的switch+break的形態。