前言本次測試對比是為了呈現incinerator與PNFSofteware出品的JEB以及國內出品GDA在Android逆向工程能力的對比,從而讓大家更好更直觀的了解相關的詳細信息
本次測試對比的產品信息如下:
Incinerator: 1.0.0
JEB:3.19.1.202005071620
GDA:3.9.0
注:文章編寫日期與發佈時間有一定時間間隔,所以以下內容並不代表各產品的後續性能指標。
反編譯器對循環結構的還原能力測試Test1第一個測試中,設計了循環頭和鎖節點都為二路條件循環結構,為了測試循環結構化分析能力,多嵌套了幾個if語句(代碼標號為基本塊號)。程序簡單如下:
publicvoidtest1(inty,inta){
while(y0){
if(a10){
if(a100){
a=a*5;
break;
}else{
y=y/a;
}
}
}
}
}
編寫testdemo將代碼段生成apk後,並分別使用JEB、GDA、Incinerator來進行反編譯操作,從而進行代碼可讀性和語義準確性上的對比,如下圖所示:
test1
通過上述對比可以看出在語義準確性上,JEB發生了語義錯誤,在a 100時,丟失了a *=5的代碼塊,Incinerator與GDA保持了語義的準確性。
在代碼可讀性上,三者相差不大可讀性都很好。 Incinerator在if-else上做了相應的優化,可讀性略有提升。
在代碼還原度上,Incinerator做了對應優化,GDA重複聲明了a、y變量,其他方面最為接近源碼。而JEB存在代碼塊丟失。
反編譯器語義準確性代碼可讀性代碼還原度Incinerator高高JEB×高中GDA高高Test2接下來看看他們對雙層循環的結構化分析的能力,設計一個雙層循環,在內層循環break出外層循環,實際上基本塊5即if(a 10),不僅會是內存循環的鎖節點,也會是外層循環的鎖節點。並且該鎖節點為二路條件節點,其一個分支路徑回到內層循環,另外一個分支結構回到外層循環。一般對循環結構算法都是循環頭-鎖節點一一對應,因此處理過程中可能會復雜化該類結構。代碼實現非常簡單如下:
publicvoidtest2(inty,inta){
while(y0){
while(a0){
if(a10){
break;
}
}
}
}
attachBaseContext(this);
}重新編譯apk後,再進行反編譯後,如下圖所示:
test2
通過上述對比可以看出在語義準確性上,Incinerator、JEB保持了語義的準確性,都識別除了雙重循環,GDA僅有函數聲明,丟失了整個函數的代碼塊。
在代碼可讀性上,Incinerator優化了if-else組合,JEB在if中加入continue省略else語句,兩者可讀性都很好。
在代碼還原度上,Incinerator、JEB除了各自在if-else上的優化,還原度都很高。
反編譯器語義準確性代碼可讀性代碼還原度Incinerator高高JEB高高GDA×低低Test3這一段代碼在退出循環的”if(a10)”語句中內嵌了另外一個if語句,這會導致內層循環的鎖節點發生變化,並且給內層循環添加了一個跟隨節點,另外代碼做了稍稍的改動。如下:
publicvoidtest3(inty,inta){
while(y0){
while(a50){
a=a+1;
y=y+1;
if(a10){
if(a100){
a=a*5;
break;
}else{
y=y/a;
}
}
}
this.attachBaseContext(this);
}
}繼續編譯成apk,再進行反編譯操作,如下圖所示:
test3
通過對比可以看出在語義準確性上,Incinerator、JEB仍然保持了語義的準確性,GDA重複聲明了a、y變量,並且繼續丟失函數內部的代碼塊。
在代碼可讀性上,Incinerator、JEB保持很好的代碼可讀性,JEB使用了continue來分割嵌套的if。
在代碼還原度上,Incinerator最為接近源碼,JEB改為使用continue來分割嵌套的if。
反編譯器語義準確性代碼可讀性代碼還原度Incinerator高高JEB高高GDA×低低Test4在內層循環的第一個if-else結構上添加一個後隨節點,並且最後break出內層循環到外層循環。並且將a=a*5語句後的break改成continue。代碼如下:
publicvoidtest4(inty,inta){
while(y0){
y++;
while(a50){
a=a+1;
y=y+1;
if(a10){
if(a100){
a=a*5;
continue;
}else{
y=y/a;
}
}
y=a*y;
break;
}
this.attachBaseContext(this);
}
}同樣編譯成apk後再反編譯,如下圖所示:
test4
通過上述對比可以看出
在語義準確性上,Incinerator在a *=5; 後面丟失了continue,在y *=a; 後面丟失了退出循環的break;JEB保持了語義的正確性;GDA重複聲明變量,也丟失了函數內的代碼塊。
在代碼可讀性上,Incinerator、JEB可讀性都很好。
在代碼還原度上,Incinerator與源碼最為相似,但是丟失了continue、break;JEB使用continue分開了if-else,將else後面的y /=a,與y *=a合併為新的y=y/a * a,並加入break,還原度上有了一定的改變。
反編譯器語義準確性代碼可讀性代碼還原度Incinerator×高中JEB高中GDA×低低Test5這次在“if(a10)”內部加入switch,在a為11、12時,執行“a=a * 5”,並continue返回內循環while,a為13時,執行“a=a * 6”,繼續往下執行,並不退出,a為14時,執行“a=a * 7” 退出switch, 與default中加入if-else,代碼如下:
publicvoidtest5(inty,inta){
while(y0){
y++;
while(a50){
a=a+1;
y=y+1;
if(a10){
switch(a){
case11:
case12:
a=a*5;
continue;
case13:
a=a*6;
case14:
a=a*7;
break;
default:
if(a100){
a=a*5;
continue;
}else{
y=y/a;
}
}
}
y=a*y;
break;
}
this.attachBaseContext(this);
}
}最後編譯成apk後反編譯,如下圖所示:
test5
通過上述對比可以看出在語義準確性上,Incinerator在switch將a為11、12、default中的continue錯誤表達為break,丟失了y *=a後面退出內循環的break;JEB保持了語義的正確性在,但在label_18的break之後,多了兩句無用的代碼a *=8;continue;GDA沒有識別出內循環,使用if與goto做處理,switch中a為11、12時多了break,沒有識別出a=14,且在default中,執行完y=y/a後繼續執行'a=a * 7'。
在代碼可讀性上,Incinerator識別出雙循環、switch-case可讀性上最好,JEB、GDA多次出現goto,在代碼可讀性上存在一定的影響。
在代碼還原度上,Incinerator與源碼最為相似,但在節點的退出上存在一定的問題,JEB、GDA在代碼的還原度上膨脹比較大。
反編譯器語義準確性代碼可讀性代碼還原度Incinerator×高中JEB中中GDA×中低調試能力測試逆向工程工具針對Apk可調試,對於研究人員來說有著極大的幫助,而對於已經發布後的應用再進行調試的話,可調試的前提條件會比較苛刻,如:設備是否root、調試屬性是否開啟、能否重打包等,這些因素都會影響著是否能夠調試,而影響調試功能的好壞、支持與否,取決於:能否stepover、stepinto、breakpoint,能否獲取/修改變量值等,這些因素都體現著調試器是否好用。所以我們從上述多個維度,對Incinerator、JEB的調試做下簡單對比,但因GDA不支持調試,所以下面的內容無法針對GDA進行測試對比。
這裡直接使用Android Studio自帶的example:Login Activity進行對比,如圖1-2所示:
圖1
圖2
在登錄驗證的位置做細微修改,讓它基本不可能登錄成功,然後再分別使用JEB、Incinerator進行分析登錄過程,並繞過登錄限制。修改的代碼與登錄失敗,如圖3-4所示:
圖3
圖4
調試設備:Nexus 5X
系統版本:7.1.2
root狀態:已root
主要測試功能:
下斷點
步進
步過
跑至光標
顯示與修改變量值
免debugger屬性調試
smali調試
偽代碼調試
JEB調試首先手動安裝編譯好的apk,然後使用JEB反編譯對應apk,點擊JEB上的start. 如圖5所示:
圖5
出來Attach界面,因為應用還沒啟動,所以並沒有看到Processes中有進程列表,如圖6所示:
圖6
通過命令(adb shell am start -D -n com.testdemo3/.ui.login.LoginActivity)啟動進程,JEB點擊(Refresh Machines List)刷新列表,看到已經跑起的測試案例,如圖7所示:
圖7
點擊Attach後,發現無法debug,按提示指app沒有開啟debuggable屬性或者設備沒有root,建議使用模擬器、root設備或者重打包app。 (實際上設備已root),如圖8所示:
圖8
我們在AndroidManifest中加入android:debuggable='true'並重新編譯(如果是第三方的app只能嘗試用apktool等工具進行重打包,有簽名、完整性等校驗的話再想辦法將其繞過)
現在可以成功Attach上去。
我們使用JEB反編譯登錄界面的activity:LoginActivity,在按鈕的點擊觸發代碼中加入斷點,如圖9所示:
圖9
因不支持在偽代碼中加入斷點,我們切換回smali(快捷鍵Q),在onClick的第一行按Ctrl+B加入斷點,如圖10所示:
圖10
操作app,輸入帳號密碼,點擊:SIGN IN OR REGISTER,在JEB中成功觸發斷點,如圖11所示:
JEB此處有個優勢,它的佈局可以根據個人喜好隨意拖拉
圖11
當前顯示的變量似乎有些異常,我們此時忽略他,鼠標放到00000040 處,點擊JEB的'Run to line',成功跳到指定行,如圖12所示:
圖12
JEB一開始將所有變量當作int類型處理,我們分析代碼,可以知道此處的V1、V2是輸入的帳號與密碼,類型是String,因此,我們點擊V1、V2中的Type將int改為String,如圖13所示:
圖13
v1、v2成功修正類型,對應的值也成功顯示出我們測試輸入的帳號密碼。
在JEB中點擊步進(Step Into)到LoginViewModel的login方法,如圖14所示:
圖14
成功步進LoginViewModel的login方法,但是在這裡可以看到,剛才修改的類型(此處對應的是p1、p2)又重新變回了int。
根據smali可知道,接下來會調用LoginRepository的login方法,隨後返回Result
我們再繼續點擊兩次步進(Step Into)進入LoginRepository的login方法,如圖15所示:
圖15
在此方法中,它會繼續將帳號密碼傳給LoginDataSource的login方法,返回Result
繼續步進(Step Into)兩次,進入LoginDataSource的log