安全文库

【技術分享】Android應用逆向工程


【技術分享】Android應用逆向工程

 


閱讀本文最好花費45Min~

你經常在用 Burp 攔截信息的時候很迷茫么?你經常在分析用加密的數據進行通信的App,對於需要理解它的數據而疑惑么?在本文,我將會分享很多方法來用於逆向分析APK。我們將會對目標APP採用動態和靜態的分析方法。

我創建了一個簡單的APP作為分析目標,它的功能只是單純地對我們輸入的數據進行驗證,如果用戶輸入正確的話,將會在屏幕上顯示「Congratulations「。

我們先看一下這個應用的源代碼以便於我們一會兒能夠將它與反編譯后的APK代碼進行比較。

package com.punsec.demo;  import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Base64; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView;  import javax.crypto.SecretKey;  public class MainActivity extends AppCompatActivity {      TextView result;     EditText input;     Button button;      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);          input = (EditText) findViewById(R.id.input);         result = (TextView) findViewById(R.id.result);         button = (Button) findViewById(R.id.ok);          button.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 String a = input.getText().toString();                 String b = getString(R.string.a);                 try {                     SecretKey secretKey = Util.a(new String(Base64.decode(getString(R.string.b), Base64.DEFAULT)));                     byte e[] = Util.a(a, secretKey);                     String er = Base64.encodeToString(e, Base64.DEFAULT).trim();                      if(er.equals(b)) {                         result.setText(getString(R.string.d));                     }else {                         result.setText(getString(R.string.e));                     }                  } catch (Exception e) { //                    Log.d("EXCEPTION:", e.getMessage());                 }             }         });     } }

這個應用使用下面的這個輔助類來執行一些重要的操作:

package com.punsec.demo;  import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidParameterSpecException;  import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec;   class Util {     static SecretKey a(String secret)             throws NoSuchAlgorithmException, InvalidKeySpecException     {         MessageDigest md = MessageDigest.getInstance("MD5");         md.update(secret.getBytes());         byte[] digest = md.digest();         StringBuilder sb = new StringBuilder();         for (byte b : digest) {             sb.append(String.format("%02x", (0xFF & b)));         }         return new SecretKeySpec(sb.toString().substring(0,16).getBytes(), "AES");     }      static byte[] a(String message, SecretKey secret)             throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidParameterSpecException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException     {         Cipher cipher = null;         cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");         cipher.init(Cipher.ENCRYPT_MODE, secret);         return cipher.doFinal(message.getBytes("UTF-8"));     }      static String a(byte[] cipherText, SecretKey secret)             throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidParameterSpecException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException     {         Cipher cipher = null;         cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");         cipher.init(Cipher.DECRYPT_MODE, secret);         return new String(cipher.doFinal(cipherText), "UTF-8");     } }

你可以下載已經編譯好的APK From -> CrackMe

在我們進行下一步的操作之前,先列舉分析所需的背景知識:

一個已經root的安卓設備或者虛擬機(雖然並不是所有的分析方法都需要root許可權,但是有一個root的設備是不錯的)。

Frida

Python

Inspeckage

Xposed Framework

APKTool

APKStudio

ByteCodeViewer

Dex2Jar

JarSigner(Java JDK)

JD-JUI

Brain

我們將會使用的三種分析方法:

動態分析和Hooking. 二進位文件Patch(byte code修改). 靜態分析和代碼複製.

動態/運行時環境 分析和函數Hooking:


我們需要使用的分析工具: Frida, dex2jar, JD-GUI.

用 Frida分析:

到底什麼是Frida ?

It's Greasemonkey for native apps, or, put in more technical terms, it』s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, Linux, iOS, Android, and QNX. Frida also provides you with some simple tools built on top of the Frida API. These can be used as-is, tweaked to your needs, or serve as examples of how to use the API.

用簡單的術語來說,它能夠被用來Hook函數調用,注入你自己的代碼未來能夠來修改應用本身的執行流程。我們將會使用它來通過檢測和來識別不同的變數。

為了能夠安裝Frida,我們可以將手機開啟USB調試之後用數據線連接電腦,並且在電腦端運行

# check adb devices whether connected or not adb devices # push/copy the latest frida server to phone adb push frida-server-10.4.0-android-arm /data/local/tmp/frida # set permissions for frida, grant SU permissions if prompted adb shell su -c "chmod 755 /data/local/tmp/frida" # start frida server on android device adb shell su -c "./data/local/tmp/frida &" # install frida-python on your Windows/Mac/Linux device pip install --user frida

運行了上面的命令之後,我們的Frida Server就已經運行在了我們的電腦上,讓我們來檢驗一下,打開終端,運行python:

Python 2.7.10 (default, Feb  7 2017, 00:08:15) Type "help", "copyright", "credits" or "license" for more information. >>> import frida >>> frida.get_usb_device() Device(id="802b7421", name="LG SCH-XXXX", type='tether')

為了方便以後的分析,現在讓我們創建一個python腳本:

import frida, sys, time  encrypted = None  def on_message(message, data):     global encrypted     try:         if not encrypted:           encrypted = message['payload']['encrypted']           print('[+] Received str : ' + encrypted)           return     except:         pass      if message['type'] == 'send':      print('[*] {0}'.format(message['payload']))   elif message['type'] == 'error':     if "'encrypted' undefined" in message['description']:           print('[!] Encrypted value not updated yet, try to rotate the device')     else:           print('[!] ' + message['description'])   else:     print message        jscode = open('punsec.js').read() print('[+] Running') process_name = 'com.punsec.demo' device = frida.get_usb_device() try:     pid = device.get_process(process_name).pid     print('[+] Process found') except frida.ProcessNotFoundError:     print('[+] Starting process')     pid = device.spawn([process_name])     device.resume(pid)     time.sleep(1) process = device.attach(pid) script = process.create_script(jscode) script.on('message', on_message) script.load() while True:     time.sleep(1)     if encrypted:       script.post({'type':'encrypted','value':encrypted})       break sys.stdin.read()

讓我們慢慢講一些這些代碼:

這個Encrypted變數最初是一個無類型的對象,它不久之後就會被腳本來將它更新為一個加密的值。這個 on_message 函數是一個回調函數能夠被 frida 的 javascript 代碼來利用,在javascript代碼之中,我們將注入到我們程序的進程中來回調我們的python代碼。這個回調函數能夠被通過在javascript代碼中的send() 函數來執行。下一個變數是jscode, 它能夠將我們的js代碼注入到程序的進程中。為了更方便我們閱讀,js代碼被寫到另一個文件中。Process_name變數是我們的進程名字。我們能夠通過在adb shell中運行 “ps” 命令 “pm list packages” 命令得到我們應用的進程名字。

這個 device 變數是來連接我們的USB設備(手機)的。Try except 用於處理異常(萬一目標程序還沒有在我們的設備上運行的話,就會產生異常)。在知道了運行程序的UID后,我們可以掛接到目標程序上,並且在目標進程上注入jscode。通過使用js的 send() 函數,腳本就會開始註冊我們的回調函數。下面是 while 循環,可以看到frida實際上是有多麼的強大,在這個循環中,我們檢測是否encrypted變數它的類型已經不是None了,如果它的類型發生了改變,腳本的post()函數將會發送一個信息將我們的js代碼注入到目標進程,並且信息將會被在js代碼中的recv() 函數所處理。

在開始下一步的操作之前,我們需要對目標apk進行靜態分析。我們首先要反編譯apk並且將java bytecode轉換為.java格式的代碼來閱讀。在這裡,我們使用的分析工具是dex2jar。

$ ./d2j-dex2jar.sh CrackMe.apk dex2jar CrackMe.apk -> ./CrackMe-dex2jar.jar

現在讓我們通過JD-GUI來分析剛才生成的CrackMe-dex2jar.jar文件

 【技術分享】Android應用逆向工程

可以看到反編譯后的代碼與原始的java代碼還是有很大的不同的。我們來分析一下不同的地方:首先可以很明顯的看到資源 id由原來的R.x.x變換稱為了數字格式的。

正如我們上面看到的,MainActivity只包含一個 onCreate() 函數。我們首先來看一下android應用的生命周期:  

【技術分享】Android應用逆向工程

可以看到: onCreate() 函數在app啟動之後就運行。為了保持應用的實際功能,我們現在就在hook這個函數,來執行對原始函數的調用,能夠獲取到目前activity的上下文來得到一些字元串的值,就像下面這一行一樣:

String str = MainActivity.this.getString(2131099669);

現在讓我們創建punsec.js文件,來得到這些值。

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({"encrypted":this.getString(2131099669)});     }; });

Java.perform() 是 frida 定義的,它的功能是:告訴frida server來運行已經包裝好的js代碼。 Java.use() 是一個包裝器為了能夠動態的載入packages到我們的目標進程中。為了下一步的需要,我們將會使用send() 回調函數來發送數據到我們的python程序中。現在運行著的python腳本給我們返回了這樣的信息:

$ python punsec.py [+] Running [+] Starting process [+] Received str : vXrMphqS3bWfIGT811/V2Q==

要記住,要想onCreate() 函數觸發,必須要執行回調函數,也就是在啟動進程之後,必須要讓它在後台運行后再打開程序,請參考上面的Activity生命周期。

我們也看到了代碼中有幾個調用來執行 Base64.decode() 和通過數字id來得到string, 我們可能也會需要這些值,所以讓我們來修改一下我們的代碼

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({'encrypted':this.getString(2131099669)});     };     var base64 = Java.use('android.util.Base64');     base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {         send('Base64 Encoded : ' + x);         var buf = new Buffer(base64.decode(x, y));         send('Base64 Decoded : ' + buf.toString());         return base64.decode(x, y);     }   });

再一次運行我們的python程序將會得到下面的輸出:

$ python punsec.py [+] Running [+] Process found [*] Base64 Encoded : TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA== [*] Base64 Decoded : MySuperSecretL33tPassw0rd

Hmm, 似乎我們已經成功了。不要著急,現在讓我們再來仔細看一下我們的反編譯代碼:

if (Base64.encodeToString( Util.a(paramAnonymousView,  Util.a(new String( Base64.decode(MainActivity.this.getString(2131099683), 0) ) ) ),  0).trim().equals(str))

在上面的代碼中,有兩次對 Util.a 函數的調用但是都採用的不同的參數類型,我們已經hook了 Base64.decode() 函數,所以現在讓我們用下面的代碼對 Util.a() 創建一個 hook :

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({'encrypted':this.getString(2131099669)});     };     var base64 = Java.use('android.util.Base64');     base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {         send('Base64 Encoded : ' + x);         var buf = new Buffer(base64.decode(x, y));         send('Base64 Decoded : ' + buf.toString());         return base64.decode(x, y);     }     var Util = Java.use('com.punsec.demo.Util');     Util.a.implementation;   });

運行我們的python代碼,然後可以得到以下的輸出:

$ python punsec.py [+] Running [+] Process found [!] Error: a(): has more than one overload, use .overload(<signature>) to choose from: .overload('java.lang.String') .overload('java.lang.String', 'javax.crypto.SecretKey') .overload('[B', 'javax.crypto.SecretKey')

這似乎出現了一點錯誤。看起來是我們的Util類中有函數重載(有相同的方法名稱但是擁有不同的參數)。為了克服這個問題, frida提供給我們額外的方法 overload(),通過這個方法,我們可以顯式地設置哪個方法來 override/hook。我們將會 hook Util.a(String, SecretKey)函數(因為它是一個負責加密的函數)來為了進行下一步分析:

 【技術分享】Android應用逆向工程

但是我們怎麼樣才能識別出這是一個加密函數的呢?首先可以看到這個函數的返回類型是byte,很顯然意味著並沒有返回一個string類型,同時,本地密碼初始化為1來作為第一個參數傳遞:

 【技術分享】Android應用逆向工程

現在,讓我們來修改我們的js代碼為了能夠合理地hook這個函數:

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({'encrypted':this.getString(2131099669)});     };     var base64 = Java.use('android.util.Base64');     base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {         send('Base64 Encoded : ' + x);         var buf = new Buffer(base64.decode(x, y));         send('Base64 Decoded : ' + buf.toString());         return base64.decode(x, y);     }     var Util = Java.use('com.punsec.demo.Util');     Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {         send('UserInput : ' + x);         return this.a(x,y);     }   });

再次運行我們的python程序,觀察輸出有哪些改變:

$ python punsec.py [+] Running [+] Process found [*] Base64 Encoded : TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA== [*] Base64 Decoded : MySuperSecretL33tPassw0rd [*] UserInput : wrongSecretTest

極好的,我們現在可以攔截我們的輸出了。現在我們可以發現 Util 類還有一個函數 Util.a(byte, SecretKey) 一直沒有在app中使用,通過分析可以看到這是一個解密函數。所以現在我們該如何做呢? 加密函數已經接收到了密鑰,所以我們可以在解密函數中利用,但是我們還需要第一個參數。第一個參數是一個 base64 解密的string 變數。所以讓我們來修改我們的代碼,為了能夠在我們的 js中收到這個參數,並且過掉這個解密函數,這樣的話,我們就能解密最終的Key來完成這次挑戰。現在最後一次修改我們的js代碼:

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({'encrypted':this.getString(2131099669)});     };     var base64 = Java.use('android.util.Base64');     base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {         // send('Base64 Encoded : ' + x);         // var buf = new Buffer(base64.decode(x, y));         // send('Base64 Decoded : ' + buf.toString());         return base64.decode(x, y);     }     var Util = Java.use('com.punsec.demo.Util');     Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {         recv('encrypted', function onMessage(payload) {              encrypted = payload['value'];          });         cipher = base64.decode(encrypted, 0); // call the above base64 method         secret = this.a(cipher, y); // call decrypt method         send('Decrypted : ' + secret)         return this.a(secret,y);     }   });

我們把一個 recv() 調用放在了函數中以便於可以收到我們寫的python程序中已經存儲的加密string。現在解密這個已經被加密過的base64密鑰並且和密鑰一起發送到解密函數中。現在讓我們再一次運行我們的python程序:

$ python punsec.py [+] Running [+] Process found [!] Encrypted value not updated yet, try to rotate the device [+] Received str : vXrMphqS3bWfIGT811/V2Q== [*] Decrypted : knb*AS234bnm*0

woah, 我們得到了key。這也會覆蓋掉任何的用戶輸入並將其替換為解密的string, 所以現在每一個用戶輸入都是起作用的:  

【技術分享】Android應用逆向工程

現在我們不僅用實際的secret覆蓋了用戶輸入,而且還覆蓋了實際的secret phrase為了通過這個挑戰。

假使我們的apk應用中沒有解密函數,我們該怎麼辦呢? 不必擔心,我們能巧妙的將js代碼插入到package中來執行解密操作並且用必要的參數覆蓋這個方法,或者我們還可以用下面的python代碼來解密:

import frida, sys, time, md5 from Crypto.Cipher import AES  encrypted = None secretKey = None   def decrypt(encrypted, key):   key = md5.new(key).hexdigest()[:16]   cipher = AES.new(key)   decrypted = cipher.decrypt(encrypted.decode('base64'))[:14]    for i in range(1,len(encrypted.decode('base64'))/16):     cipher = AES.new(key, AES, encodedEncrypted.decode('base64')[(i-1)*16:i*16])     decrypted += cipher.decrypt(encodedEncrypted.decode('base64')[i*16:])[:16]    return decrypted.strip()  def on_message(message, data):     global encrypted, secretKey     try:       if not encrypted:         encrypted = message['payload']['encrypted']       if not secretKey:         secretKey = message['payload']['secretKey']     except:       pass     if message['type'] == 'send':       print('[*] {0}'.format(message['payload']))     elif message['type'] == 'error':       if 'ReferenceError' in message['description']:         print('[!] Rotate the device')       else:         print('[!] ' + message['description'])     else:       print message        jscode = open('punsec.js').read()  print('[+] Running')  process_name = 'com.punsec.demo' device = frida.get_usb_device()  try:     pid = device.get_process(process_name).pid     print('[+] Process found') except frida.ProcessNotFoundError:     print('[+] Starting process')     pid = device.spawn([process_name])     device.resume(pid)     time.sleep(1)  process = device.attach(pid)  script = process.create_script(jscode) script.on('message', on_message) script.load() while True:     time.sleep(0.2)     if encrypted and secretKey:       script.post({'type':'encrypted','value':decrypt(encrypted, secretKey)})       break sys.stdin.read()

我們更新后的js代碼:

Java.perform(function () {     var MainActivity = Java.use('com.punsec.demo.MainActivity');     MainActivity.onCreate.implementation = function(a) {         this.onCreate(a);         send({'encrypted':this.getString(2131099669)});     };     var base64 = Java.use('android.util.Base64');     base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {         var buf = new Buffer(base64.decode(x, y));         send({'secretKey': buf.toString()});         return base64.decode(x, y);     }     var Util = Java.use('com.punsec.demo.Util');     Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {         recv('encrypted', function onMessage(payload) {              secret = payload['value'];          });         send('Decrypted : ' + secret)         return this.a(secret,y);     }   });

現在運行我們的python程序:

$ python punsec.py [+] Running [+] Process found [*] {u'secretKey': u'MySuperSecretL33tPassw0rd'} [!] Rotate the device [*] {u'encrypted': u'vXrMphqS3bWfIGT811/V2Q=='} [*] {u'secretKey': u'MySuperSecretL33tPassw0rd'} [*] Decrypted : knb*AS234bnm*0

 

用 Inspeckage 來分析


我們將會使用到Inspeckage, Xposed Framework 和 ApkStudio/ByteCodeViewer.

Inspeckage – Android Package Inspector

Inspeckage is a tool developed to offer dynamic analysis of Android applications. By applying hooks to functions of the Android API, Inspeckage will help you understand what an Android application is doing at runtime.

Inspeckage可以讓你來用簡單的web介面進行分析。Inspeckage需要你安裝Inspeckage Xposed module並且在 Xpose 框架中激活它。在你的android設備上啟動Inspeckage App並且選擇我們的目標應用並且在Inspeckage Webserver中瀏覽。

【技術分享】Android應用逆向工程 

 【技術分享】Android應用逆向工程

打開自動刷新開關,點擊在webserver上的設置按鈕並且關閉一些Actvity檢測就像下面這張圖一樣,最後點擊 start App 並且刷新頁面。

 【技術分享】Android應用逆向工程

一旦我們的App在手機上運行,就在App上輸入測試的數據並點擊ok按鈕,然後觀察Inspeckage webserver上的通知(注意要開啟自動刷新):

 【技術分享】Android應用逆向工程

 【技術分享】Android應用逆向工程

這兩張截圖都顯示出了我們使用了frida方法。用 Inspeckage分析是相當簡單的,你可以檢測app執行的文件系統Activities, SQL隊列操作,在這背後使用的是和我們使用frida方法相同的概念: 在加密,文件系統,hash等操作函數上進行hook,但是在這裡,我們可以執行函數hook嗎? 當然了,正如你在最後一個標籤上看到的,它提供了一個hook選項。但是隨之而來的問題是:它不像frida那樣,Inseckage沒有提供對重載的方法的覆蓋,現在點擊hook標籤並且創建一個hook來驗證我們的想 法:  

【技術分享】Android應用逆向工程

所以現在為了能夠創建一個有效的hook,我們將會使用 ByteCodeViewer 或者 APKStudio 來修改apk中的 bytecode(位元組碼)。下面這是我們對位元組碼的patch:

 【技術分享】Android應用逆向工程

(注意:當打開apk的時候,取消選擇”Decode Resource”,否則你將會遇到下面這些問題)

ERROR: 9-patch image C:/Users/labuser/Desktop/CrackMe/res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png malformed.        Must have one-pixel frame that is either transparent or white. ERROR: Failure processing PNG image C:/Users/labuser/Desktop/CrackMe/res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png ERROR: 9-patch image C:/Users/labuser/Desktop/CrackMe/res/drawable-hdpi-v4/abc_list_divider_mtrl_alpha.9.png malformed.        Must have one-pixel frame that is either transparent or white. ERROR: Failure processing PNG image C:/Users/labuser/Desktop/CrackMe/res/drawable-hdpi-v4/abc_list_divider_mtrl_alpha.9.png ERROR: 9-patch image C:/Users/labuser/Desktop/CrackMe/res/drawable-xhdpi-v4/abc_list_divider_mtrl_alpha.9.png malformed.        Must have one-pixel frame that is either transparent or white. ERROR: Failure processing PNG image C:/Users/labuser/Desktop/CrackMe/res/drawable-xhdpi-v4/abc_list_divider_mtrl_alpha.9.png ERROR: 9-patch image C:/Users/labuser/Desktop/CrackMe/res/drawable-xxhdpi-v4/abc_list_divider_mtrl_alpha.9.png malformed.        Must have one-pixel frame that is either transparent or white. ERROR: Failure processing PNG image C:/Users/labuser/Desktop/CrackMe/res/drawable-xxhdpi-v4/abc_list_divider_mtrl_alpha.9.png

在上面那副截圖中,可以看到第168行,我們通過識別第168行的參數類型和返回值,成功的識別出了這就是加密函數,在第197行,這個被賦值為1的變數也是我們之前看到的。我們已經把這個函數的名字改成了b ,並且解密函數名稱改為c。現在為了保證我們的app可以正常運行,我們需要在MainActivity的位元組碼上做出相同的更新:

 【技術分享】Android應用逆向工程

現在我們的任務已經完成了,可以創建一個keystore來對我們的apk進行簽名。

C:/Program Files/Java/jdk1.8.0_144/bin>keytool -genkey -v -keystore C:/users/labuser/Desktop/my.keystore -alias alias_na me -keyalg RSA -keysize 2048 -validity 10000 Enter keystore password: Re-enter new password: What is your first and last name?   [Unknown]: What is the name of your organizational unit?   [Unknown]: What is the name of your organization?   [Unknown]: What is the name of your City or Locality?   [Unknown]: What is the name of your State or Province?   [Unknown]: What is the two-letter country code for this unit?   [Unknown]: Is CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct?   [no]:  yes Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days         for: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown Enter key password for <alias_name>         (RETURN if same as keystore password): [Storing C:/users/labuser/Desktop/my.keystore] C:/Program Files/Java/jdk1.8.0_144/bin>jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore C:/users/labuser/Desktop/my.keystore C:/users/labuser/Desktop/CrackMe.apk alias_name

將已經簽名的apk安裝到設備上。重啟Inspeckage,開始hook來驗證是否我們的修改已經起作用了。

 【技術分享】Android應用逆向工程

極好地,我們的修改是完美的,現在我們可以對目標函數Util.b() 下hook。選擇這個函數並且點擊 Add hook 按鈕。現在讓我們點擊ok按鈕並且觀察Inspeckage Server的通知。

 【技術分享】Android應用逆向工程

我們可以看到Inspeckage已經成功地從已經hook的函數中截取到數據並且提供給我們了函數的參數和返回值。現在點擊 Replace 按鈕並且配置如下的選項。

 【技術分享】Android應用逆向工程

在這裡我們將第一個參數傳遞給了我們的加密函數,這個函數擁有我們已經用frida識別出來的秘密值。無論什麼時候進行輸入測試(大小寫敏感),Hook都會替換數據並且傳遞我們提供的值,然後將Congratulations再一次顯示在我們的屏幕上。

 【技術分享】Android應用逆向工程

 

二進位補丁(位元組碼修改)


在這個方法中,我們將會使用ApkStudioJarsigner。 我們將會通過修改反編譯的Apk,之後重新編譯它來修改程序的邏輯。啟動 ApkStudio並且再次載入文件( 記住要取消選擇”Decode Resources”複選框),之後在MainActivity$1.smali中定位到程序代碼中進行比較的位置

 【技術分享】Android應用逆向工程

我們可以在第113行看到程序會比較兩個不同的值來執行檢測,如果比較失敗了,會顯示”Umm, Try Again”。但是如果程序總是將兩個相同的值進行比較會怎麼樣呢?在這種情況下,程序將會跳過else條件直接返回true。所以現在讓我們將代碼修改後重新編譯並對我們的Apk進行簽名,然後做測試。

 【技術分享】Android應用逆向工程

再一次運行應用驗證是否程序是否通過了原來的程序邏輯。

 

靜態分析和代碼複製


在這個方法中,我們將會使用Android Studio/IntelliJ ByteCodeViewer來進行靜態代碼分析。

Static analysis Also called static code analysis, is a method of computer program debugging that is done by examining the code without executing the program. The process provides an understanding of the code structure, and can help to ensure that the code adheres to industry standards.

啟動 ByteCodeViewer(BCV) 並且等待它來安裝依賴項。一旦安裝好了之後,我們將可以直接在它裡面打開apk文件。在BCV中,點擊File->Add 並且選擇 CrackMe.apk,然後讓它完成載入這個文件。點擊View->Pane1->Procyon->javaView->Pane2->Smali/Dex->Samli/Dex 。你的界面將會看起來和下面的一樣

 【技術分享】Android應用逆向工程

在第9行,我們可以看到”final String string2 = this.this$0.getString(2131099669);”。在當前活動上下文的getString()方法,可以使用”this”,”MainActivity.this “或者”getApplicationContext() ” 通過一個整數值來得到資源值。這些數字id的索引在R類中被創建,所以我們將會在R$string.class 中尋找資源id,BCV可以將內容識別為xml 文件格式。

 【技術分享】Android應用逆向工程

我們可以看到這個整數值被分配給a,現在我們不得不對a在strings.xml中做一個查找,你可以在BCV中通過展開CrackMe.apk->Decoded Resources->res->values->strings.xml

 【技術分享】Android應用逆向工程

有時候BCV打開文件會呈現出二進位形式而不是xml格式,對於這種情況,我們可以點擊File->Save As Zip ,然後解壓zip並且在編輯器中打開strings.xml。

 【技術分享】Android應用逆向工程

極好的,我們已經找到了這個字元串。我們將會用這個方法恢復所有的字元串並且保存它們。

2131099669 -> a -> vXrMphqS3bWfIGT811/V2Q== 2131099683 -> b -> TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA== 2131099685 -> d -> Congratulations 2131099686 -> e -> Umm, Try again

我們將會使用IntelliJ來寫我們的代碼來試圖實現逆向原始函數的功能,通過從BCV反編譯后的文件中複製代碼。 當所有的代碼讓在一塊的時候,它將會看起來像下面的代碼

import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.util.Base64; class Decrypt {      public static void main(String args[]) {         String a = "vXrMphqS3bWfIGT811/V2Q==";         String b = "TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA==";         String new_b = new String(Base64.getDecoder().decode(b));          byte[] array = Base64.getDecoder().decode(a);         String decoded = decrypt(array, getKey(new_b));          System.out.println("Decoded : " + decoded);     }      private static String decrypt(byte[] array, SecretKey secretKey) {         String decoded = null;         try {              Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");             instance.init(2, secretKey);             decoded = new String(instance.doFinal(array), "UTF-8");         }catch (Exception e) {             // do something         }         return decoded;     }          private static SecretKey getKey(String s) {         SecretKeySpec secretKeySpec = null;         try {             MessageDigest instance = MessageDigest.getInstance("MD5");             instance.update(s.getBytes());             byte[] digest = instance.digest();             StringBuilder sb = new StringBuilder();             for (int length = digest.length, i = 0; i < length; ++i) {                 sb.append(String.format("%02x", digest[i] & 0xFF));             }             secretKeySpec = new SecretKeySpec(sb.toString().substring(0, 16).getBytes(), "AES");         } catch (Exception e) {             // do something         }         return secretKeySpec;     } }

將文件命名為Decrypt.java 並保存文件。我們需要編譯這個文件,然後運行它來檢測我們的代碼是否起作用了。

// create new file $ nano Decrypt.java // compile file $ javac Decrypt.java // run file $ java Decrypt Decoded : knb*AS234bnm*0

我們可以在python代碼中做同樣的事情,就像先前frida那樣,但是有時候複製代碼是更簡單的,因為只需要做很小的修改就可以使它運行。

我們已經描述了所提到的所有工具和方法,現在是時候喝杯咖啡了。