斷斷續續的總算的把android開發和逆向的這兩本書看完了,雖然沒有java,和android開發的基礎,但總體感覺起來還是比較能接收的,畢竟都是觸類旁通的。當然要深入的話還需要對這門語言的細節特性和奇技淫巧進行挖掘。
這裡推薦2本書,個人覺得對android開發入門和android逆向入門比較好的教材:
《google android 開發入門與實戰》
《android 軟體安全與逆向分析》
1. 我對android逆向的認識
因為之前有一些windows逆向的基礎,在看android逆向的時候感覺很多東西都是能共通的。但因為android程式本身的特性,還是有很多不同的地方。
1.1 反編譯
android程式使用java語言編寫,從java到android虛擬機器(Dalvik)的dex代碼(可以看成是android虛擬機器的機器碼)需要一個中繼語言的轉換過程。類似.NET的IL中間虛擬指令。而我們知道,.NET的IL中間代碼之所以能很容易的"反編譯"回C#原始碼,是因為除了IL中繼語言,還包含了大量的META中繼資料,這些中繼資料使我們可以很容易的一一對應的反編譯回C#的原始碼。java的中繼語言.class檔案也是類似的道理,我們可以使用工具直接從dex機器碼反編譯回java原始碼。
1.2 逆向分析手段
windows的逆向分析中,我們可以使用OD或者C32ASM來分析彙編指令(當然OD還可以動態調試),或者使用IDA + F5(hex Ray反編譯外掛程式)來靜態分析原始碼(C/C++)
在android逆向分析過程中:
1) 我們可以使用ApkTool(本質上是BakSmali反組譯碼引擎)對apk檔案進行反組譯碼,得到各個類、方法、資源、布局檔案...的smali代碼,我們可以直接通過閱讀smali代碼來剖析器的代碼流,進行關鍵點的修改或者代碼注入。
2) 我們可以從apk中提取.dex檔案,使用dex2jar工具對dex進行反組譯碼,得到jar包(java虛擬指令),然後使用jd-gui等工具再次反編譯,得到java原始碼,從源碼級的高度來審計代碼,更快的找到關鍵點函數或者判斷,然後再回到smali層面,對代碼進行修改。這種方法更傾向於輔助性的,最終的步驟我們都要回到smali層面來修改代碼。
3) 使用IDA Pro直接分析APK包中的.dex檔案,找到關鍵點代碼的位置,記下檔案位移量,然後直接對.dex檔案進行修改。修改完之後把.dex檔案重新匯入apk中。這個時候要注意修改dex檔案頭中DexHeader中的checksum欄位。將這個值修複後,重新匯入apk中,並刪除apk中的META-INF檔案夾,重新簽名即可完成破解。
1.3 android與C的結合
在學習android逆向的時候感覺遇到的最難的問題就是分析原生代碼,即JNI代碼。開發人員使用android NDK編寫C/C++代碼供android的java代碼調用(通過java的代碼轉接層來完成介面的轉換)。
使用android NDK編寫的C/C++代碼最終會產生基於ARM的ARM ELF可執行檔,我們想要分析軟體的功能就必須掌握另一項技能,ARM彙編,ARM彙編個人感覺雖然和x86彙編類似,不過由於IDA Pro對ARM彙編沒有反編譯功能以及貌似沒有工具能動態調試ARM代碼(我網上沒找到),導致我們只能直接硬看ARM代碼,加上往往伴隨著複雜的密碼學演算法等等,導致對Native Code的逆向相對來說比較困難,對基本功的要求比較高。
1.4 關於分析android程式
1) 瞭解程式的AndroidManifest.xml。在程式中使用的所有activity(互動組件)都需要在AndroidManifest.xml檔案中手動聲明。包括程式啟動時預設啟動的主activity,通過研究這個AndroidManifest.xml檔案,我們可以知道該程式使用了多少的activity,主activity是誰,使用了哪些許可權,使用了哪些服務,做到心中有數。
2) 重點關注Application類
這本來和1) AndroidManifest.xml是一起的,但是分出來說是因為這個思路和windows下的逆向思路有相通之處。
在windows exe的資料目錄表中如果存在TLS項,那程式在載入後會首先執行這個TLS中的代碼,執行完之後才進行main主程式入口。
在android 中Application類比程式中其他的類啟動的都要早。
3) 定位關鍵代碼
3.1) 資訊反饋法(關鍵字尋找法)
通過運行程式,尋找程式UI中出現的提示訊息或標題等關鍵字,到String.xmlzhong中尋找指定字串的di,然後到程式中尋找指定的id即可。
3.2) 特徵函數法
這種做法的原理和資訊反饋法類似,因為不管你提示什麼訊息,就必然會調用相應的API函數來顯示這個字串,例如Toast.MakeText().show()
例如在程式中搜尋Toast就有可能很快地定位到調用代碼
3.3) 代碼注入法
代碼注入法屬於動態調試的方法,我們可以手動修改smali反組譯碼代碼,加入Log輸入,配合LogCat來查看程式執行到特定點時的狀態資料。
3.4) 棧跟蹤法
棧跟蹤法屬於動態調試方法,從原理上和我們用OD調試時查看call stack的思想類似。我們可以在smali代碼中注入輸出運行時的棧跟蹤資訊,然後查看棧上的函數調用序列來理解方法的執行流程(因為每個函數的執行都會在棧上留下記錄)
3.5) Method Profiling
Method Profiling,方法剖析(這是書上的叫法,我更願意叫BenchMark測試法),它屬於一種動態調試方法,它主要用於熱點分析和效能最佳化。在DDMS中有提供這個功能,它除了可記錄每個函數所佔用的CPU時間外,還能夠跟蹤所有的函數調用關係。
1.5 關於android的代碼混淆和加殼
java語言編寫的代碼本身就很容易被反編譯,google為此在android 2.3的SDK中正式加入了ProGuard代碼混淆工具,只要正確的配置好project.properties與proguard.cfg兩個檔案即可使用ProGuard混淆軟體。
java語言由於語言自身的特殊性,沒有外殼保護這個概念,只能通過混淆方式對其進行保護。對android NDK編寫的Native Code倒是可以進行加殼,但目前貌似只能進行ups的壓縮殼保護
2. CrackMe_1 分析學習
2.1 運行一下程式,收集一些基本資料
只有一個輸入框,那說明這個驗證碼的輸入來自別的地方,因為我們知道,不管你的密碼編譯演算法是啥,總是要有一個函數輸入源的,我們在UI介面上輸入的相當於是結果,而輸入源應該來自於別的地方,計算完之後和我們在UI上輸入的結果進行對比,大致是這個思路。
2.2 分析
使用apktool反編譯apk檔案。查看AndroidManifest.xml檔案。瞭解到主activity為:Main。
接著我們從apk中提取.dex檔案。用dex2jar->jd-gui來查看java原始碼。
看到裡面很多的a,b,c方法,基本上可以判定是配ProGuard混淆了,不過問題也不大,雖然顯示的是無意義的函數名但是不影響我們分析代碼流程。
2.2.1 類b的分析
從OnCreate()的代碼來看,我們首先從類b開始分析:
類 b 提供了一個公用的建構函式 public b(Context paramContext), 一個私人的成員函數private String b(), 以及一個公有成員函數 public final void a()。
b(): 通過TelephonyManager擷取裝置相關的一些資訊,然後通過PackageManager擷取到自身的簽名。然後把這些字串拼接起來返回給調用者。
TelephonyManager localTelephonyManager = (TelephonyManager)this.a.getSystemService("phone"); String str1 = localTelephonyManager.getDeviceId(); String str2 = localTelephonyManager.getLine1Number(); String str3 = localTelephonyManager.getDeviceSoftwareVersion(); String str4 = localTelephonyManager.getSimSerialNumber(); String str5 = localTelephonyManager.getSubscriberId(); Object localObject = ""; PackageManager localPackageManager = this.a.getPackageManager(); try { String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString(); localObject = str6; return str1 + str2 + str3 + str4 + str5 + (String)localObject; } a(): SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.a); SharedPreferences.Editor localEditor; if (!localSharedPreferences.contains("machine_id")) localEditor = localSharedPreferences.edit(); try { localEditor.putString("machine_id", b()); localEditor.commit(); return; }
a()調用方法b()擷取字串,然後通過SharedPreferences.Editor將這個字串值儲存到鍵machine_id,可以理解為機器碼。也就是說,這個加密函數的輸入是原生機器碼。
經過上面的分析,類b對外提供方法a,功能就是產生"機器碼"並儲存到系統中,對應的鍵為machine_id。
2.2.2 類c的分析
類c提供的方法較多,我們逐個分析。
1) 建構函式
Java代碼
public c(Context paramContext) { a = paramContext; b = "f0d412b5530e1f9841aab434d989cc77"; c = "4ec407446b872351e613111339daae9"; }
把參數環境內容Context本地化,並聲明了兩個字串。
2) public static boolean b()
Java代碼
MessageDigest localMessageDigest = MessageDigest.getInstance("MD5"); localMessageDigest.update(paramString.getBytes(), 0, paramString.length()); return new BigInteger(1, localMessageDigest.digest()).toString(16);
通過MessageDigest計算paramString 的MD5值。
3) public static boolean b()
Java代碼
PackageManager localPackageManager = a.getPackageManager(); try { String str = b(new String(localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toChars())); if (!str.equals(b)) { boolean bool = str.equals(c); if (!bool); } else { return false; } }
通過 getPackageManager 擷取自身的簽名,如果簽名與建構函式中的兩個字串b(f0d412b5530e1f9841aab434d989cc77)或者c(4ec407446b872351e613111339daae9)任意一個相等,那麼返回false,否則返回true。
4) public static int a(String paramString)
Java代碼
try { if (b()) return 0; SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a); if (b(localSharedPreferences.getString("machine_id", "")).equals(paramString)) { if (b()) return 0; SharedPreferences.Editor localEditor = localSharedPreferences.edit(); localEditor.putString("serial", paramString); localEditor.commit(); return 1; } }
可以看出這段代碼的功能為電腦器碼的 MD5,如果與傳入的參數paramString一致,那麼通過SharedPreferences存入到serial(機器碼的MD5值paramString)欄位中。 當然還有調用b方法進行一些判斷,自身的簽名不能是已知的兩個。
5) public static boolean a()
Java代碼
SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a); if (!localSharedPreferences.contains("serial")) return false; String str = localSharedPreferences.getString("serial", ""); if (str.equals("")) return false; return a(str) >= 0;
這個其實就是上面的 int a(String paramString)的封裝函數,通過SharedPreferences擷取serial欄位(機器碼的MD5值),並傳給這個方法,返回相應的傳回值(判斷結果)。
2.2.3 類a分析
可以看到,類a是一個CountDownTimer:
Schedule a countdown until a time in the future, with regular notifications on intervals along the way. Example of showing a 30 second countdown in a text field:(android Developer)
從onFinish函數我們看出這個類的功能是倒計時6秒,然後調用c.a(),也就是判斷我們輸入的serial是否等於"機器碼"的MD5值。如果不能通過,就設定TextView內容提示註冊。
2.2.4 類Main分析
1) 在onCreate(),先初始化b和c的類。然後調用b.a()產生並儲存"機器碼",然後調用c.a(),也就是判斷是否已經儲存了serial,並判斷是否能通過演算法校正。如果不能通過,則什麼都不做,這就是啟動時檢測註冊狀態的做法,即如果你之前已經註冊了,那在之後的登入後就會自動識別出來,但是我們如果是第一次啟動且沒有註冊,那這裡就什麼也不做。
如果能通過,則調用自身的方法a()。而自身的方法a()又調用了c.b()方法,即檢查我們輸入的serial和機器碼的MD5值是否相同,如果相同則什麼也不做,如果不同就把下面的按鈕和TextView等UI控制項給隱藏了。並啟動倒計時類a.start()。即二次驗證。
ps:
這裡要注意的是,由於程式使用了ProGuard來混淆代碼,所以用jd-gui翻譯出來的代碼全都是從a,b,c開始計數,而且經常是變數、類、方法的命名混合了起來。我們在看java代碼的時候遇到難懂的地方要結合smali代碼一起看,這樣才能擷取比較準確的對程式碼流的把握。
2) public void onClick(View paramView)
Java代碼
if (c.a(((EditText)findViewById(2131034114)).getText().toString()) == 0) { Toast.makeText(this, 2130968577, 0).show(); return; } Toast.makeText(this, 2130968578, 0).show();
判斷我們通過UI輸入的serial是否和"機器碼"的MD5值相同,如果不相同則彈出提示Invalid serial!(可以通過ID值反查出對應的字串),如果相同則彈出Thanks for purchasing!
通過以上分析,我們來綜合一下思路:
程式啟動時會做一些初始化的工作,然後產生本地對應的機器碼並儲存在SharedPreferences中。
檢查當前的SharedPreferences中是否已經儲存了serial索引值對,並檢查正確性,即檢查是否上一次已經註冊了。如果沒有這個索引值對,說明還沒註冊,如果存在這個索引值對且正確性也符合,代碼接下來會繼續檢查APK自身的簽名是否為代碼中定義的那兩個,如果相等則什麼都不做(即依然不通過檢查),如果不等則代碼繼續執行倒計時6秒的類a, 6秒後再次檢查一次serial索引值對。
對於那個按鈕點擊事件,onClick(),它擷取使用者通過UI輸入的serial,並檢測是否和"機器碼"的MD5值相等,如果相等則存進SharedPreferences中的索引值對中。
以上基本就是這個程式的代碼思路了。我們可以看到,作者這裡使用了雙重保護的思路,即不僅要你輸入的serial相同,而且對你的APK的簽名也有限制。
3. 破解思路
3.1 單純的破解,用代碼注入的方法得到註冊碼。
經過分析,我們知道應該在b.smali的155行:
move-result-object v2 這裡代碼注入,因為這個b()的作用就是擷取當前"機器碼"(注意,這裡擷取的是沒有MD5之前的"機器碼",因為程式中的MD5都是臨時算出來的)
我們在這裡加入:
const-string v3, "SN"
invoke-static {v3, v2}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
重新回編譯smalli代碼。
在命令列中執行 adb logcat -s SN:v ,然後再啟動程式
會在命令列中看到一大串字串,這些字串就是我們要的機器碼
將這些字串計算MD5值之後,就可以完成破解了。
3.2 讀取程式對應的檔案
我們知道,所謂的SharedPreferences本質上是儲存在當前程式空間下的/data/data/<package name>/shared_prefs/<package name>_preferences.xml檔案中的。
我們可以通過adb串連上去,直接讀取這個檔案的內容。
可以看到,和我們通過代碼注入的方式得到的機器碼是相同的。
3.3 編寫註冊機
這種方法是最好的,編寫註冊機要求我們對目標程式的代碼有全盤的認識,然後類比原本的演算法或者逆向原本的演算法寫出註冊機
我們用Eclipse重建一個新的工程 com.lohan.crackme。注意,工程的報名必須和目標程式的包名一致,這樣我們的註冊機運行後得到的APK簽名才會是一樣的。
核心演算法如下:
Java代碼
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setTitle("crackMe1_keyGen"); final Context context = getApplicationContext(); //擷取UI控制項 txt_machineCode = (TextView) findViewById(R.id.machineCode); txt_apkSig = (TextView) findViewById(R.id.apkSig); txt_serial = (TextView) findViewById(R.id.serial); btn_Go = (Button) findViewById(R.id.ok); //設定監聽事件 btn_Go.setOnClickListener(new OnClickListener(){ public void onClick(View v) { //電腦器碼 TelephonyManager localTelephonyManager = (TelephonyManager) context.getSystemService("phone"); String str1 = localTelephonyManager.getDeviceId(); String str2 = localTelephonyManager.getLine1Number(); String str3 = localTelephonyManager.getDeviceSoftwareVersion(); String str4 = localTelephonyManager.getSimSerialNumber(); String str5 = localTelephonyManager.getSubscriberId(); Object localObject = ""; PackageManager localPackageManager = context.getPackageManager(); try { String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString(); localObject = str6; String str_result = str1 + str2 + str3 + str4 + str5 + (String)localObject; //得出機器碼 txt_machineCode.setText(str_result); //計算當前APK的簽名 txt_apkSig.setText(str6); //計算註冊碼 MessageDigest localMessageDigest = null; try { localMessageDigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } localMessageDigest.update(str_result.getBytes(), 0, str_result.length()); String str_serial = new BigInteger(1, localMessageDigest.digest()).toString(16); txt_serial.setText(str_serial); } catch (PackageManager.NameNotFoundException localNameNotFoundException) { while (true) localNameNotFoundException.printStackTrace(); } } });
破解結果
APK:
http://pan.baidu.com/s/1qsygp
4. 總結
至此,這個android的CrackeMe_1就算破解完成了。這段時間的android學習也算暫時告一段落,移動無線安全是未來的新方向,在不遠的將來,基於android平台的各種應用和軟體不僅僅是手機甚至是各種的互聯終端都將進入人們的視野,無線安全的研究應該也會慢慢成為熱點。
我也希望下次再研究android安全的時候能有更深入的認識和體會。
有興趣的同學可以看下本文,謝謝大家對本站的支援!