逆向分析加固 apk 的完整過程
此次逆向的 apk 為 tmri_10101_1461033981072_release.apk (pass: 3qun) ,首先把 apk 安裝在模擬器上,然后抓包分析網絡流量,把 apk 拖進模擬器中之后安裝并設置代理,雖然 app 走的 http 但是抓到的包全是亂碼,所以在內部肯定對流量進行了加密操作。
0x01 jeb 靜態分析
用 jeb 打開此 apk 之后查看代碼,因為在 app 打開的時候會把系統信息傳給服務器,而且訪問的鏈接是 writeDeviceInfo ,經過我的一番查找,居然在 com.tmri.services 下面找到 writeDeviceInfo 這樣的函數,右鍵反編譯,打開這個函數發現確實是app發送設備信息的代碼:
然后對代碼進行查看,發現類x可能是發送請求的代碼,然后點擊x后打開繼續跟進:
看到 /m/deviceInfo/writeDeviceInfo (猜想正確),往下拉繼續查看這個類:
發現這樣的一個函數,其中 this.d().a(……) 這個函數似乎是在把數據包組裝起來準備做發送,然后我注意到這個函數的參數有一個 ((HttpEntity)v2) ,這樣的東西,我猜測這個可能是發送的 http body ,然后跟蹤 v2 這個變量,發現在上面 v2 = new f(v0_3, ((Map)v1), null); 進行了初始化 現在跟進f類,在f類中找到這樣的函數:
d.d(……) 這個函數是要發送的明文,那么下面的 this.a(v2, this.f); 就是加密函數了吧?跟進去之后發現確實是加密的核心函數:
其中arg7傳入的明文數據,而arg6是傳出的密文數據,這個代碼的最關鍵部分在:
byte[] v2 = com.tmri.app.services.packet.c.b(com.tmri.app.services.packet.b.b(arg7, com.tmri.app.services.packet.b$a.values()[this.j]), com.tmri.app.services.packet.c$a.values()[this.k]);
而v2就是加密之后的數據,繼續跟進 com.tmri.app.services.packet.c.b :
跟到這一步之后,靜態分析就顯得有點吃力了,首先:
com.tmri.app.services.packet.c.b(com.tmri.app.services.packet.b.b(arg7,
com.tmri.app.services.packet.b$a.values()[this.j]),
com.tmri.app.services.packet.c$a.values()[this.k]);
這個函數中傳入的 j 和 k 的值不能確定,進而無法確定這個函數將流量進行了何種的加密方式,所以下面將繼續 apk 的動態分析。
0x02 apk 動態分析
這里參考 http://www.52pojie.cn/thread-502219-1-1.html ,首先用 apktool 解開 app,然后修改 AndroidManifest.xml 中:
<application android:allowBackup="false" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.tmri.app.DemoApplication" android:theme="@style/AppTheme" android:debuggable="true">
把android:debuggable=”true” 改成true,隨后使用 apktool 打包,再用 signapk.jar 做個簽名,最后 baksmali 反編譯一下:baksmali tmri_sign.apk -o SmaliDebug/src ,剩下的步驟和之前鏈接提到的差不多,但這里我們使用的 Android Studio (步驟也是差不多的),下面開始正式開始動態調試。
首先運行 apk 然后在終端里面輸入:adb shell ps ,找到apk運行之后的名字,這個apk的名字是:com.tmri.app.main ,然后在 AndroidManifest.xml 找到 application 這個標簽,在這個標簽下找到 activity 標簽。在終端運行 android:name字段,這個字段就是 app 的主 activity ,這個 app 的主 activity 是:com.tmri.app.ui.activity.TmriActivity ,在終端運行:
adb shell am start -D -n com.tmri.app.main/com.tmri.app.ui.activity.TmriActivity
模擬器如下圖所示的話說明成功:
再次運行這個 app,在終端中輸入 adb shell ps,找到這個app的pid,現在運行的pid是:2166 ,運行: adb forward tcp:8700 jdwp:2166,將調試進程附到 2166 這個進程中,然后在android studio中的src目錄下找到剛才找到的加密函數,這個函數在 com.tmri.app.services.packet 的f類中:
閱讀smali代碼,找到剛才那個加密函數:
在 smali 代碼中找到:
然后在這個函數的 iget-byte v4, p0, Lcom/tmri/app/services/packet/f;->k:B 下斷點,然后在菜單上點 run-》debug”debug” ,成功后這樣的:
現在隨便輸入一個用戶名和密碼點登陸,發現 android studio 成功的斷了下來:
在右下角的窗口我添加 v0,v1,v2,v3 查看各個寄存器的值,然后按下f8單步運行這個apk ,看到i=0,j=0,k=3 ,再用jeb找到具體的加密函數,看他到底是怎么加密流量的:
這個對應的 smali 在 com.tmri.app.services.packet 的c這個類里面:
我在每一個加密函數的入口加一個斷點,看看它會用哪個函數進行加密流量,運行之后看到他運行了:
invoke-static {p0, v0},Lcom/tmri/app/services/packet/AesCipherJni;->native_t_set([BI][B
這個函數,對應的java代碼是:
arg3 = AesCipherJni.native_t_set(arg3, arg3.length);
我們可以在在右下角的寄存器中看到進入函數的值是:
到這里就很明顯了,它調用了一個動態鏈接庫進行加密,然后把加密結果再傳遞出來,我們可以通過jeb的靜態分析知道這個這個加密函數調用的是哪個動態鏈接庫:
這個庫在 lib 目錄下,armeabi/armeabi-v7a/x86 這三個目錄對應的是cpu的類型,每個 cpu 類型的目錄下都有名字一樣的 so 文件,因為我的模擬器跑著x86的模擬器里面,所以我要分析的動態鏈接庫在 x86/ libAesCipher-Jni.so 中。
0x03 靜態分析 so 文件
使用 IDA 進行分析,終于分析到倒數第二個函數(sub_DB0)的時候我看到了熟悉的東西:
openssl的aes加密函數,其中參數a3,a4就是我想要的aes加密密鑰key和iv,但是點進去之后,卻看不到任何東西,看來只能動態調試了,這里先把sub_DB0的起始虛擬地址和結束虛擬地址找到,分別為 0000DB0 和 0000EF0 :
0x04 動態分析 so 文件
首先從網上下載 gdbserver 上傳到手機里面,然后 adb shell ps 找到 app 的 pid 是 2166:
在 gdbserver 目錄下運行 ./gdbserver :23946 –attach 2166 ,再開一窗口運行 adb forward tcp:23946 tcp:23946 ,運行這兩個指令之后我就能用本地的 gdb 調試代碼了,但是我想用ida pro(破解版,只能在win上跑)提供的方便的功能進行調試,怎么辦?于是我就想到了端口轉發工具,于是我在網上找的一份這樣的 開源代碼 :python rtcp.py l:3333 c:127.0.0.1:23946 ,我實現的調試拓撲:
在遠程的 win 打開 ida pro,選擇菜單的 debugger->attach->Remote GDB debugger ,然后 houstname 填寫地址,port 是監聽端口,之后進入:
下面我來尋找動態鏈接庫的基地址,因為我調制的 pid 是 2166 ,所以查看 proc maps 文件來找動態鏈接庫的基地址,在終端中運行 adb shell cat /proc/2166/maps | grep libAes, : |
這個程序加載了三次這個動態鏈接庫,但是每個動態鏈接庫的執行權限不同,分別是是r-xp,r—p,rw-p,要調試的動態鏈接庫應該是具有執行權限的,即r-xp,所以調試的基地址是:e2269000,剛才找到的函數的虛擬地址是0000DB0,所以函數在內存中的地址是基地址+虛擬地址:e2269000+0000DB0=e2269db0 ,然后跳到 e2269db0 下斷點運行起來:
在手機中輸入用戶名和密碼之后調試器被斷住,斷點正好在我下斷的e2269db0位置,然后先不著急按f8調試,先在里面右鍵創建函數,然后再右鍵編輯函數,找到函數結束的地址為剛才找到的函數結束虛擬地址+基地址:e2269000+0000EF0= e2269EF0 進行修改:
點確定后把光標移到函數中,按下 f5 找到加密的函數下斷點然后運行起來可以看到程序被成功的斷下:
將鼠標指向a3和a4的位置上,查看他們的地址:
然后找到對應的寄存器或者棧地址,將其顯示出來:
然后就找到aes加密的key是:
95 8A FA EB CA EF A4 96 EC 7B 7E 97 D0 75 EA 48
iv是:
E0 A4 14 94 34 3A 26 1A 35 64 C6 3C 3A F0 43 57
0x05 END
最后編寫流量解密程序來驗證拿到的key和iv是不是正確的,先在下面的兩個函數上下斷點來看加密之后的數據是什么樣的:
發現是02 9d 79 2c開頭的 23 8a d1 88 先設置burp的攔截,然后運行app,這樣burp成功攔下app發出的數據:
然后我們用 .net 寫了一個 aes 解密程序來進行破解:
解密程序:
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.IO;
namespace aes
{
class AESHelper
{
/// <summary>
/// AES解密
/// </summary>
/// <param name="Data">被解密的密文</param>
/// <param name="Key">密鑰</param>
/// <param name="Vector">向量</param>
/// <returns>明文</returns>
public static String AESDecrypt(String Data, byte[] Key, byte[] Vector)
{
Byte[] encryptedBytes = Convert.FromBase64String(Data);
Byte[] bKey = Key;
Byte[] bVector = Vector;
Byte[] original = null; // 解密后的明文
Rijndael Aes = Rijndael.Create();
Aes.Mode = CipherMode.CBC;
Aes.Padding= PaddingMode.Zeros;
Aes.BlockSize = 128;
try
{
// 開辟一塊內存流,存儲密文
using (MemoryStream Memory = new MemoryStream(encryptedBytes))
{
// 把內存流對象包裝成加密流對象
using (CryptoStream Decryptor = new CryptoStream(Memory,
Aes.CreateDecryptor(bKey, bVector),
CryptoStreamMode.Read))
{
// 明文存儲區
using (MemoryStream originalMemory = new MemoryStream())
{
Byte[] Buffer = new Byte[1024];
Int32 readBytes = 0;
while ((readBytes = Decryptor.Read(Buffer, 0, Buffer.Length)) > 0)
{
originalMemory.Write(Buffer, 0, readBytes);
}
original = originalMemory.ToArray();
}
}
}
}
catch(Exception e)
{
Console.WriteLine("失敗"+e.ToString());
original = null;
return null;
}
return Encoding.UTF8.GetString(original);
}
// 把十六進制字符串轉換成字節型
public static byte[] StringToByte(string InString)
{
string[] ByteStrings;
ByteStrings = InString.Split(' ');
byte[] ByteOut;
ByteOut = new byte[ByteStrings.Length];
for (int i = 0; i < ByteStrings.Length; i++)
{
ByteOut[i] = Convert.ToByte("0x"+ByteStrings[i],16);
}
return ByteOut;
}
public static byte[] GetPictureData(string imagepath)
{
////根據圖片文件的路徑使用文件流打開,并保存為byte[]
FileStream fs = new FileStream(imagepath, FileMode.Open);//可以是其他重載方法
byte[] byData = new byte[fs.Length];
fs.Read(byData, 0, byData.Length);
fs.Close();
return byData;
}
static void Main(string[] args)
{
string strkey = "95 8A FA EB CA EF A4 96 EC 7B 7E 97 D0 75 EA 48";
string striv = "E0 A4 14 94 34 3A 26 1A 35 64 C6 3C 3A F0 43 57";
byte[] key = StringToByte(strkey);
byte[] iv = StringToByte(striv);
byte[] bfile = new byte[2048];
bfile=GetPictureData(@"C:\Users\hehe\apk\333");
string pic = Convert.ToBase64String(bfile);
string ok = AESDecrypt(pic, key, iv);
Console.WriteLine(ok);
}
}
}
來自:http://www.n0tr00t.com/2017/01/10/Reverse-reinforcement-app-full-Course.html