10 Jan 2017 - niexinming
[+] Author: niexinming [+] Team: n0tr00t security team [+] From: http://www.n0tr00t.com [+] Create: 2017-01-10
此次逆向的 apk 为 tmri_10101_1461033981072_release.apk (pass: 3qun) ,首先把 apk 安装在模拟器上,然后抓包分析网络流量,把 apk 拖进模拟器中之后安装并设置代理,虽然 app 走的 http 但是抓到的包全是乱码,所以在内部肯定对流量进行了加密操作。
用 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 的动态分析。
这里参考 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 中。
使用 IDA 进行分析,终于分析到倒数第二个函数(sub_DB0)的时候我看到了熟悉的东西:
openssl的aes加密函数,其中参数a3,a4就是我想要的aes加密密钥key和iv,但是点进去之后,却看不到任何东西,看来只能动态调试了,这里先把sub_DB0的起始虚拟地址和结束虚拟地址找到,分别为 0000DB0 和 0000EF0 :
首先从网上下载 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
最后编写流量解密程序来验证拿到的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); } } }