移動安全--45--我設計的Java代碼混淆解決方案

特別聲明:本文是博主閱讀大量碩博論文和知網文獻後原創,非公司內部解決方案。

一 、Java代碼混淆方案圖

Java整體架構圖如下:
在這裏插入圖片描述

各模塊功能簡介:

程序預處理分析:對原應用程序進行程序分析預處理,爲後續混淆奠定結構基礎。

佈局混淆模塊:對代碼中有意義的標識符進行重命名。

控制流混淆模塊:對程序進行控制流混淆,包括插入多餘的分支路徑、壓扁控制流、強化不透明謂詞。

字符串混淆模塊:加密隱藏代碼中的常量字符串。

混淆算法庫:對程序的混淆處理主要依靠混淆算法庫支撐,算法庫中包含一系列的基本塊混淆算法。混淆算法庫爲可擴展。

本文先講理論,後附上demo代碼

二、混淆模塊整體設計

混淆模塊包括三部分:佈局混淆控制流混淆字符串混淆
在這裏插入圖片描述

三、佈局混淆子模塊

本模塊核心思想:對代碼中有意義的標識符進行重命名。

主要包括三步:構造包結構構造繼承樹標識符混淆

3.1、混淆標識符說明

可以混淆的標識符包括:

1、包名
2、類或者接口名稱
3、字段名
4、方法名
5、方法參數
6、局部變量

不可以混淆的標識符包括:

1、該實例方法實現父類抽象方法或接口方法
2、該實例方法覆蓋父類的實例方法
3、被外部調用的方法
4、該方法爲回調方法
5、系統不可混淆方法

3.2、佈局混淆方法

Java應用代碼主要有包結構和繼承結構。每個類必定繼承一個父類,默認爲java.lang.Object類。每個類可以實現0個或多個接口,接口也可以繼承接口。通過繼承結構,可以識別出上面描述的前兩類方法,並對其進行統一命名來保證多態性。

下圖顯示了一個Java程序的包結構圖:
在這裏插入圖片描述

其對應的繼承結構圖如下所示:

在這裏插入圖片描述

3.3、佈局混淆方法實行步驟

1、遍歷類信息結構,構建包結構和繼承結構,初始化包結構根節點爲ROOT,繼承結構根節點爲Object。

2、從根節點遍歷包結構,對於每個包節點的子節點,使用順序生成名稱代替原來的包結構名稱。如果生成的名稱序列爲a、b、c、……,那麼對於上圖包結構圖中所示,其混淆後的包結構將如下圖所示:

在這裏插入圖片描述

3、遍歷從根節點到葉子節點的繼承結構,對於每種類型,執行以下操作:

1)爲每個字段重新順序生成名稱。對於相同字段,使用相同的名稱進行混淆。

2)爲每個方法名重新順序生成名稱。需要注意的是,要保存其父節點已經遍歷過的方法名,並判斷該方法名是否可混淆,對於不可混淆的,不能混淆。

3)替換Java文件中所有的混淆名稱。

3.4、佈局混淆示例代碼

源碼:

public class Family{
	static String father;
	static String mother;
	static String son;
	static String daughter;
	
	public void FamilyA(){
		father = "wangjianlin";
		mother = "liujuan";
		son = "wangsicong";
		daughter = "wangli";
	}
	
	public void FamilyB(){
		father = "mayun";
		mother = "zhangying";
		son = "mahuateng";
		daughter = "madongmei";
   }
   
   public static void main(String[] args) {  
		Family Family = new Family();
		Family.FamilyA();
		Family.FamilyB();
}

佈局混淆後的代碼:

public class a{
	static String a;
	static String b;
	static String c;
	static String d;
	
	public void a(){
		a = "wangjianlin";
		b = "liujuan";
		c = "wangsicong";
		d = "wangli";
	}
	
	public void b(){
		a = "mayun";
		b = "zhangying";
		c = "mahuateng";
		d = "madongmei";
   }
   
   public static void main(String[] args) {  
		a a = new a();
		a.a();
		a.b();
}

四、控制流混淆子模塊

說明:通過對Android源程序源碼文件遍歷,得到符合混淆條件的代碼塊;對代碼塊進行混淆操作,主要分爲插入多餘的分支路徑、壓扁控制流、強化不透明謂詞三個步驟。

4.1、控制流混淆算法

在混淆方案中,爲了控制性能開銷,插入的分支路徑實際上並不執行,壓扁的結構中的語句包括實際路徑和不執行的分支路徑,而不透明謂詞采用建立訪問控制策略的形式的強化。

控制流混淆方案OBJ(P、Q、R、W、O)定義:

P:爲原始程序代碼
Q:爲混淆後程序代碼
B={b1,b2,b3,……,bn}:爲原始程序代碼中符合混淆條件的n個代碼塊的集合
BR:經過插入多餘分支路徑後的程序代碼塊集合
Bw:經過壓扁控制流後的程序代碼塊集合。
R={r1,r2,r3,……,rn}:爲n種不同類型的代碼塊對應可插入多餘分支路徑的集合
w={w1,w2,w3,……,wn}:爲n種不同類型的代碼塊對應壓扁控制流方法的集合
O:不透明謂詞強化方法
:插入多餘分支路徑
:進行壓扁控制流操作
:強化不透明謂詞操作

終極公式:Q=(B●Rⓧw)⊕O

語言描述:符合混淆條件的代碼塊集合先進行插入多餘分支路徑,再進行壓扁控制流,最後強化不透明謂詞。

控制流詳細混淆方案詳細過程如下:

1、對於P進行詞法語法分析,遍歷得到符合混淆條件的代碼塊集合B

2、對代碼塊集合B分別進行插入多餘的分支路徑操作。對每一個屬於B的代碼塊,在R中找到相應的插入分支代碼類型,進行插入操作:(BR=B●R)

3、對插入多餘路徑後的代碼塊進行壓扁控制流操作。對每一個屬於BR的代碼塊,在w中找到相應的壓扁控制流方法,進行壓扁操作:(Bw=BRⓧw)

4、對壓扁控制流後代碼塊進行不透明謂詞強化操作:
(Q=Bw⊕O=(B●Rⓧw)⊕O)

5、將混淆後的程序Q返回給用戶。

控制流混淆方案架構圖:

在這裏插入圖片描述

架構圖解讀:

1、在程序中插入實際並不執行的多餘控制流路徑。

首先,在分析完程序控制流結構的基礎上,選取程序中完整的結構塊,其控制結構可能包含多個判斷或循環條件基本塊,每個判斷或條件基本塊與其相關語句組成一個基本結構塊,基本結構塊中仍含有判斷或條件基本塊的結構塊定義爲嵌套結構塊。

其次,在結構塊中的嵌套結構前插入一個一定爲真的不透明謂詞,不透明謂詞爲假的一邊中插入與嵌套結構塊結構相同但數據按條件隨機生成的代碼,作爲不執行的冗餘結構塊,冗餘結構塊最後的有向邊指向代碼中的下一個結構塊。
最後,將它們封裝成一個結構塊。

2、對部分實際執行路徑與插入的不執行的分支路徑進行壓扁處理,再封裝。

3、構建訪問控制策略,強化不透明謂詞。

將整個程序不透明謂詞的判斷轉化爲圖遍歷問題,構建訪問控制策略。每個封裝好的結構塊作爲一個節點,節點之間的跳轉作爲一條邊,每個節點的訪問都需要該節點的key,以及通往下一個節點的password,程序通過這條邊後,也就運行到了下一個節點,同時得到了訪問下一個節點的key。

大致如下:

在這裏插入圖片描述

4.2、插入分支路徑

插入多餘的分支路徑圖:
在這裏插入圖片描述

1、結構分析:

在程序中插入多餘的分支路徑的第一步就是要判斷插入位置,這是在程序分析的基礎上進行的。程序分析從最外層的結構開始,一層一層的分析程序嵌套結構和並列結構,直至最簡單的基本塊結構。

具體步驟爲:

1)將最外層的結構視爲一個結構塊,分析其內部包含的判斷或循環結構塊,無論判斷或循環結構塊內部是否包含嵌套結構,都將其視爲結構塊;

2)重複上一步驟,對分析出的判斷或循環結構塊使用上一步的方法繼續分析,直到分析的結構塊爲基本塊。

說明:只選擇結構嵌套層數爲兩層及以上的結構塊進行控制流混淆。因爲一層沒有必要也沒有意義做控制流混淆。

2、插入位置:

因爲不透明謂詞是判斷條件,所以在嵌套結構中的第一個判斷條件或循環條件前插入,使得添加插入的不透明謂詞一定爲真,保證控制流只會執行實際需要執行的路徑。也可以插入一段不執行的嵌套結構,使得後續的壓扁控制流後的結構看起來更復雜。

3、插入冗餘代碼:

在不透明謂詞爲假的邊中插入不會執行的冗餘代碼,冗餘的控制流在複製原基本塊的基礎上對數據進行改變。具體方法:

1)增加多餘控制流中循環執行的次數以及將嵌套結構中的判斷條件置反,並且將嵌套結構中執行的語句改爲對變量的增減語句,如果嵌套結構中循環條件有上限,則執行語句中的變量增,反之,變量減,若爲判斷條件,則統一改爲對變量的增語句。

2)然後插入不透明謂詞爲假的路徑中,冗餘的控制流最終指向下一個結構塊。冗餘代碼中數據大小的改變必須與原語句不同,且在數據量級上保持同一水平,使得冗餘代碼看上去像原程序中實際執行的代碼,能極大地保證混淆代碼的隱蔽性。

4、將原結構塊與插入的不透明謂詞以及冗餘代碼封裝成一個結構塊,以進行壓扁控制流處理。

4.3、壓扁控制流

架構圖如下:
在這裏插入圖片描述

壓扁前需要通過分析程序嵌套結構和條件基本塊的類型確定壓扁執行的次數以及調用基本塊壓扁控制流算法的類型。壓扁控制流相關算法包括壓扁控制流算法條件基本塊壓扁控制流算法

4.3.1、壓扁控制流算法

功能:主要包括分析結構塊嵌套深度(只分析原結構塊),判斷條件基本塊類型以調用相關基本塊壓扁控制流算法,以及控制根據嵌套深度控制壓扁次數。

在這裏插入圖片描述

4.3.2、條件基本塊壓扁控制流算法

壓扁控制流實際上就是壓扁程序中的嵌套結構,使之扁平化,破壞其控制流結構,增加分析程序的難度。

在進行壓扁控制流處理時,需將嵌套結構視爲一個條件基本塊,逐層分析其嵌套類型,之後逐層的將嵌套結構中部分實際和分支控制流結構進行壓扁。

4.3.2.1、if語句基本塊壓扁控制流算法

功能:主要是對if語句基本塊進行抽取部分實際和分支路徑進行壓扁。

如果僅有一個if-else結構則直接退出,不進行壓扁處理。

如果有多個if-else結構,則抽取實際和分支路徑的前半部分作爲待壓扁結構,將其轉換爲switch結構,switch結構的輸出作爲餘下實際路徑的輸入,繼續執行if-else流程,剩下的分支路徑將進行刪除操作,以控制混淆帶來的文件大小增長。

構造switch結構前,需要添加一個next變量和一個for循環,以支撐switch結構的運行。

構造switch結構時,優先實際執行的路徑,每添加一條case語句,next增加1,按照控制流圖中從上至下,從左至右的順序依次添加至多餘路徑語句的case語句,但是最後一條case語句爲嵌套結構中實際執行路徑的最後一個子節點,插入的多餘的分支路徑按同樣的步驟構造case語句插入到最後一條case語句之前。

if語句基本塊壓扁控制流算法圖:

在這裏插入圖片描述

4.3.2.2、while語句基本塊壓扁控制流算法

功能:主要是對while語句基本塊進行抽取部分實際和分支路徑進行壓扁。

對while語句基本塊進行壓扁前需要將循環條件構造爲if語句,通過該if語句判斷while語句基本塊中的語句是否執行,以及執行的次數。

通過if條件分析出循環次數n,若n爲偶數,保留前n/2次循環爲原始結構,剩下後半部分n/2語句構造case語句。若n爲奇數,則原始結構保留前(n+1)/2次循環,分支路徑抽取的語句爲循環次數比實際循環次數多的部分,構造case語句。

例如:循環了10次,前5次循環保留,後5次循環用來構造case語句。循環了9次,前5次循環保留,後4次用來構造case語句。

構造switch結構前,需要添加一個next變量和一個for循環,以支撐switch結構的運行。構造switch結構時,優先實際執行的路徑,每添加一條case語句,next增加1,按照控制流圖中從上至下,從左至右的順序依次添加至多餘路徑語句的case語句,但是最後一條case語句爲嵌套結構中實際執行路徑的最後一個子節點。分支路徑case語句插入最後一條case語句之前。

while語句基本塊壓扁控制流算法圖:

在這裏插入圖片描述

4.3.2.3、for語句基本塊壓扁控制流算法

功能:主要是對for語句基本塊進行抽取部分實際和分支路徑進行壓扁。

對for語句基本塊進行壓扁的首要步驟是分析循環執行的次數n,類似while語句基本塊壓扁控制流算法,抽取前半部分作爲原始結構,僅對後半部分進行壓扁控制流操作。然後,將其循環判斷條件和語句中對循環判斷因子進行操作的語句提取出來,分別對其構造case語句,使得壓扁後的switch結構能實現for循環。

for語句基本塊壓扁控制流算法圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-88kSKDCt-1582971013725)(evernotecid://EF5030FF-5388-4117-93C8-E0BD5EFE04D4/appyinxiangcom/27072091/ENResource/p134)]

4.3.2.4、switch語句基本塊壓扁控制流算法

功能:主要是對switch語句基本塊進行抽取部分實際和分支路徑進行壓扁。

由於switch語句基本塊的語句已經是case語句不需要再重新構造,但是由於需要抽取部分實際路徑和分支路徑,需要對next變量進行操作。

與其他條件基本塊一樣,對其抽取前半部分的實際和分支路徑,然後分別對抽取的和未抽取的路徑進行switch結構重包裝。與壓扁其他基本塊不同,未抽取的switch結構中的多餘的分支路徑語句並不進行刪除操作。對switch語句基本塊的壓扁操作實際上並不能算是壓扁,僅僅是對其語句的結構進行重組。

switch語句基本塊壓扁控制流算法圖:

在這裏插入圖片描述

4.3.2.5、do-while語句基本塊壓扁控制流算法

功能:主要是對do-while語句基本塊進行抽取部分實際和分支路徑進行壓扁。

對do-while語句基本塊的壓扁控制流算法類似於while語句基本塊壓扁控制流算法,區別在於循環體的case語句要先執行,即其next的值相較於循環條件較小。在構造case語句時,應先對循環體中的語句進行case語句的構造,保證其next的值從0開始。

do-while語句基本塊壓扁控制流算法圖:

在這裏插入圖片描述

4.3.2.6、try-catch語句基本塊壓扁控制流算法

將程序中try-catch結構的基本塊分別放到相應的case語句塊中,再壓扁成一個switch結構。進行壓扁操作時,將try、catch、finally基本塊都看作一個整體,對這個整體進行case語句的構造,按照程序中異常處理的方式添加next變量,構造成switch結構。

需要注意的是,如果函數中有個循環會被頻繁的執行,那麼可以把這個循環歸結爲一個節點,然後再進行壓扁。這樣,循環中的各個基本塊仍能集中在同一個case語句中,保持原有結構和執行效率不變,而不會被打散到各個case語句塊中,引發較大的性能開銷。

4.4、構建訪問控制策略(強化不透明謂詞)

構造訪問控制策略:

將程序中各個封裝好的結構塊作爲結點,結構塊之間的執行順序即結點之間的跳轉作爲一條邊,每個結點的訪問都需要該結點的key以及通往下一個節點的邊的password,通過整合key、password以及訪問路徑可以構建程序訪問控制策略。

每個結點的key爲插入的不透明謂詞的判斷條件,即不透明謂詞的構建可以通過構造一個單點函數(該函數只有在某個特殊的點上纔會爲真,其他情況全部爲假),在計算判斷結果的過程中,只有當輸入正確的信息後,布爾值纔會爲真。如果判斷條件有多個輸入,可以把有限多個單點函數組合在一起,構成多點函數, 即只在一個給定的情況集合下爲真,其他情況下均爲假的函數。

不透明謂詞的判斷條件可以利用hash來進行保護,構建訪問控制策略時,可以將key值事先保存在程序中的其他位置,在判斷是否能訪問節點時,將不透明謂詞的條件的hash值與相應的key值進行匹配。

4.5、控制流混淆示例代碼

源碼:

public static void P(){
    int x = 3;
    int i = 0;
    while (i < 4){
        if (x <= 3){
            x = 3;
            i++;
        }else{
            x = 3;
        }
    }
}

插入冗餘結構:

public static void P(){
    int x = 3;
    int i = 0;
    if (x = 3){
        while (i < 4){
            if (x <= 3){
                x = 3;
                i++;
            }else{
                x = 3;
            }
        }
    }else{     //if條件永遠爲真,此處永不執行
        while (i < 6){
            if (x <= 3){
                x = 3;
                i++;
            }else{
                x = 3;
            }
        } 
    }
}

壓扁控制流:

public static void P(){
    int x = 3;
    int i = 0;
    int next = 0;
    if (x = 3){
        while (i < 2){     //總共循環4次,保留前2次不變
            if (x <= 3){
                x = 3;
                i++;
            }else{
                x = 3;
            }
        }
        for ( ; ; ){  //for(i; i<4; i++)
            switch(next){      //總共循環4次,後2次寫入switch結構
                //常規路徑的case語句
                case 0: if (1<i<4) next = 1; break;
                case 1: if (x <= 3) next = 2; else next = 7; break;
                case 2: x = 3; i++; next = 0; break;
                //分支路徑的case語句。分支路徑中多餘的while直接刪除
                case 3: if (4<i<6) next = 4; break;
                case 4: if (x > 3) next = 5; else next = 6; break;
                case 5: x++; i++; next = 3; break;
                case 6: x++; break;
                case 7: x = 3; break;
            }
        }
    }
}

強化不透明謂詞:

//對3進行SHA-256計算。該代碼保存於其它位置
hash_x = 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce

強化不透明謂詞之後的代碼

public static void P(){
    int x = 3;
    int i = 0;
    int next = 0;
    //強化不透明謂詞
    if (x = hash_x){      //hash_x是對變量x(也就是數值3)進行SHA-256後的值
        while (i < 2){     //總共循環4次,保留前2次不變
            if (x <= 3){
                x = 3;
                i++;
            }else{
                x = 3;
            }
        }
        for ( ; ; ){  //for(i; i<4; i++)
            switch(next){      //總共循環4次,後2次寫入switch結構
                //常規路徑
                case 0: if (1<i<4) next = 1; break;
                case 1: if (x <= 3) next = 2; else next = 7; break;
                case 2: x = 3; i++; next = 0; break;
                //分支路徑。分支路徑中多餘的while直接刪除(前3次)
                case 3: if (4<i<6) next = 4; break;
                case 4: if (x > 3) next = 5; else next = 6; break;
                case 5: x++; i++; next = 3; break;
                case 6: x++; break;
                case 7: x = 3; break;
            }
        }
    }
}

未做混淆的代碼執行分析:
結果:
x = 3
i = 4

第一遍
        i = 0
        x = 3
第二遍
        i = 1
        x = 3
第三遍
        i = 2
        x = 3
第四遍
        i = 3
        x = 3
第五遍
        i = 4
        while循環失敗,退出循環,程序執行結束。

做了混淆的代碼執行分析:
結果:
x = 3
i = 4

第一遍
        i = 0
        x = 3
        ne = 0
第二遍
        i = 1
        x = 3
        ne = 0
第三遍
        i = 2
        while循環失敗,退出while循環,進入switch
        case 0
        ne = 1
第四遍
        case 1
        i = 2
        x = 3
        ne = 2
第五遍
        case 2
        x = 3
        i = 3
        ne = 0
第六遍
        case 0
        i = 3
        x = 3
        ne = 1
第七遍
        case 1
        i = 3
        x = 3
        ne = 2
第八遍
        case 2
        x = 3
        i = 4
        ne = 0
第九遍
        for循環失敗,退出循環,程序執行結束。

五、字符串混淆子模塊

本模塊核心思想:隱藏代碼中的常量字符串

字符串混淆方法首先提取出定義的常量字符串,然後調用加密算法將字符串加密爲字節數組,最後構造Java代碼來存儲得到的加密字節數組。

字符串混淆轉換圖:
在這裏插入圖片描述

5.1、關於密鑰存儲

博主之前也設計過一個密鑰存儲解決方案,不過被公司商用了,既然商用了那就不能公開了。整個方案還是非常複雜的!

大致原理是:先對待加密數據做對稱加密處理,同時將密鑰打散成若干密鑰片段,將這些密鑰片段運用拉格朗日插值多項式得出另外一串無關的字符串,之後刪除原密鑰與原密鑰片段,並將該無關字符串再打散保存在圖片的各個像素點中。解密時,從各個像素點中找回打散的無關字符串,對其並進行拉格朗日逆運算,得出若干密鑰片段並組合成密鑰,再運用密鑰解密得出原文。

也可以使用其他安全的存儲方案。

5.2、字符串混淆示例代碼

我們繼續使用佈局混淆後的代碼作爲源代碼使用

源代碼:

public class a{
	static String a;
	static String b;
	static String c;
	static String d;
	
	public void a(){
		a = "wangjianlin";
		b = "liujuan";
		c = "wangsicong";
		d = "wangli";
	}
	
	public void b(){
		a = "mayun";
		b = "zhangying";
		c = "mahuateng";
		d = "madongmei";
   }
   
   public static void main(String[] args) {  
		a a = new a();
		a.a();
		a.b();
}

提取常量字符串並加密爲字節數組(此處使用了AES加密,密鑰爲123456):
這裏需要構造Java代碼來存儲得到的加密字節數組,就不演示了。

String[] AES_data = {
	"U2FsdGVkX1+MKsFUub3iw735uDNLcrqTf7IFoV+5SP0=",
	"U2FsdGVkX18t8qdG8edKuvxuY9Sc9RtwR5ixY0nPkrM=",
	"U2FsdGVkX1+j21T/0UxVBt+B2vj9bptp7GORCo8nO+E=",
	"U2FsdGVkX18X4ibKJpnPwB6x+aC+RUUKUbnu5+fnXnQ=",
	"U2FsdGVkX19AE6biwPt7TIWdMD4zbNtC5pjELU5/vEw=",
	"U2FsdGVkX1+64L50tNYrncjYtzyzQO6/vCJN/Ew9v4s=",
	"U2FsdGVkX18qy5BJf6Njw0tqeXObe0Nl6X1OvlGdVbY=",
	"U2FsdGVkX18bHmicwKHTSKNtSga+gvBoN+XnGOz4bhU=",
	};

字符串混淆後的代碼:

public class a{
	static String a;
	static String b;
	static String c;
	static String d;
	
	public void a(){
		a = AES_data[0];   //此處時爲了簡寫說明,實際中不能這樣寫,下同
		b = AES_data[1];
		c = AES_data[2];
		d = AES_data[3];
	}
	
	public void b(){
		a = AES_data[4];
		b = AES_data[5];
		c = AES_data[6];
		d = AES_data[7];
   }
   
   public static void main(String[] args) {  
		a a = new a();
		a.a();
		a.b();
}

六、總結

本解決方案從三方面入手,通過佈局混淆、控制流混淆、字符串常量混淆三管齊下對Java代碼進行混淆,混淆強度大,破譯難度高。

但性能也會受影響,影響程度未做測試。

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