逆向分析加固 apk 的完整过程

bhmd4047 4年前
   <p>此次逆向的 apk 为 <a href="/misc/goto?guid=4959734197288886285" rel="nofollow,noindex">tmri_10101_1461033981072_release.apk (pass: 3qun)</a> ,首先把 apk 安装在模拟器上,然后抓包分析网络流量,把 apk 拖进模拟器中之后安装并设置代理,虽然 app 走的 http 但是抓到的包全是乱码,所以在内部肯定对流量进行了加密操作。</p>    <h2>0x01 jeb 静态分析</h2>    <p>用 jeb 打开此 apk 之后查看代码,因为在 app 打开的时候会把系统信息传给服务器,而且访问的链接是 writeDeviceInfo ,经过我的一番查找,居然在 com.tmri.services 下面找到 writeDeviceInfo 这样的函数,右键反编译,打开这个函数发现确实是app发送设备信息的代码:</p>    <p><img src="https://simg.open-open.com/show/d58b274e952d615d036b2e432b2c1948.jpg"></p>    <p>然后对代码进行查看,发现类x可能是发送请求的代码,然后点击x后打开继续跟进:</p>    <p><img src="https://simg.open-open.com/show/e585999e6b6c0602d7eecb085bca5a1a.jpg"></p>    <p>看到 /m/deviceInfo/writeDeviceInfo (猜想正确),往下拉继续查看这个类:</p>    <p><img src="https://simg.open-open.com/show/f2177b72356396fbc376add84a779313.jpg"></p>    <p>发现这样的一个函数,其中 this.d().a(……) 这个函数似乎是在把数据包组装起来准备做发送,然后我注意到这个函数的参数有一个 ((HttpEntity)v2) ,这样的东西,我猜测这个可能是发送的 http body ,然后跟踪 v2 这个变量,发现在上面 v2 = new f(v0_3, ((Map)v1), null); 进行了初始化 现在跟进f类,在f类中找到这样的函数:</p>    <p><img src="https://simg.open-open.com/show/c9ccbd1ccb8b33dde53067b657585a27.jpg"></p>    <p>d.d(……) 这个函数是要发送的明文,那么下面的 this.a(v2, this.f); 就是加密函数了吧?跟进去之后发现确实是加密的核心函数:</p>    <p><img src="https://simg.open-open.com/show/f8c22a33354f0fa5de882401c944a858.jpg"></p>    <p>其中arg7传入的明文数据,而arg6是传出的密文数据,这个代码的最关键部分在:</p>    <pre>  <code class="language-java">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]);</code></pre>    <p>而v2就是加密之后的数据,继续跟进 com.tmri.app.services.packet.c.b :</p>    <p><img src="https://simg.open-open.com/show/5f4282110c61ec96db96c1b8bc6b03d0.jpg"></p>    <p>跟到这一步之后,静态分析就显得有点吃力了,首先:</p>    <pre>  <code class="language-java">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]);</code></pre>    <p>这个函数中传入的 j 和 k 的值不能确定,进而无法确定这个函数将流量进行了何种的加密方式,所以下面将继续 apk 的动态分析。</p>    <h2>0x02 apk 动态分析</h2>    <p>这里参考 <a href="/misc/goto?guid=4959734197383261585" rel="nofollow,noindex">http://www.52pojie.cn/thread-502219-1-1.html</a> ,首先用 apktool 解开 app,然后修改 AndroidManifest.xml 中:</p>    <pre>  <code class="language-java"><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"></code></pre>    <p>把android:debuggable=”true” 改成true,随后使用 apktool 打包,再用 signapk.jar 做个签名,最后 baksmali 反编译一下:baksmali tmri_sign.apk -o SmaliDebug/src ,剩下的步骤和之前链接提到的差不多,但这里我们使用的 Android Studio (步骤也是差不多的),下面开始正式开始动态调试。</p>    <p>首先运行 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 ,在终端运行:</p>    <pre>  <code class="language-java">adb shell am start -D -n com.tmri.app.main/com.tmri.app.ui.activity.TmriActivity</code></pre>    <p>模拟器如下图所示的话说明成功:</p>    <p><img src="https://simg.open-open.com/show/9c981477fe28baf6db5d21dcf440fdf3.jpg"></p>    <p>再次运行这个 app,在终端中输入 adb shell ps,找到这个app的pid,现在运行的pid是:2166 ,运行: adb forward tcp:8700 jdwp:2166,将调试进程附到 2166 这个进程中,然后在android studio中的src目录下找到刚才找到的加密函数,这个函数在 com.tmri.app.services.packet 的f类中:</p>    <p><img src="https://simg.open-open.com/show/0e271095ef4bb1136b24c596c6a5e77a.jpg"></p>    <p>阅读smali代码,找到刚才那个加密函数:</p>    <p><img src="https://simg.open-open.com/show/9cb1daf75ab61fef53d034178a2e86b3.jpg"></p>    <p>在 smali 代码中找到:</p>    <p><img src="https://simg.open-open.com/show/2e5580c0f784b26e5c1b4b05ff7f0ab7.jpg"></p>    <p>然后在这个函数的 iget-byte v4, p0, Lcom/tmri/app/services/packet/f;->k:B 下断点,然后在菜单上点 run-》debug”debug” ,成功后这样的:</p>    <p><img src="https://simg.open-open.com/show/ed575df52a6acbcb2dbd7bd2b42f945e.jpg"></p>    <p>现在随便输入一个用户名和密码点登陆,发现 android studio 成功的断了下来:</p>    <p><img src="https://simg.open-open.com/show/48217694adfce3e415a98b9c2340cd96.jpg"></p>    <p>在右下角的窗口我添加 v0,v1,v2,v3 查看各个寄存器的值,然后按下f8单步运行这个apk ,看到i=0,j=0,k=3 ,再用jeb找到具体的加密函数,看他到底是怎么加密流量的:</p>    <p><img src="https://simg.open-open.com/show/806a92d8881fd71ef91edfd491ff6057.jpg"></p>    <p>这个对应的 smali 在 com.tmri.app.services.packet 的c这个类里面:</p>    <p><img src="https://simg.open-open.com/show/115fa25ecfd1fabeef7a16d2104e1912.jpg"></p>    <p>我在每一个加密函数的入口加一个断点,看看它会用哪个函数进行加密流量,运行之后看到他运行了:</p>    <pre>  <code class="language-java">invoke-static {p0, v0},Lcom/tmri/app/services/packet/AesCipherJni;->native_t_set([BI][B</code></pre>    <p>这个函数,对应的java代码是:</p>    <pre>  <code class="language-java">arg3 = AesCipherJni.native_t_set(arg3, arg3.length);</code></pre>    <p>我们可以在在右下角的寄存器中看到进入函数的值是:</p>    <p><img src="https://simg.open-open.com/show/b05e8d0c3a75130ca4251b8b23e3e95a.jpg"></p>    <p>到这里就很明显了,它调用了一个动态链接库进行加密,然后把加密结果再传递出来,我们可以通过jeb的静态分析知道这个这个加密函数调用的是哪个动态链接库:</p>    <p><img src="https://simg.open-open.com/show/68e783b2ccd0062c029b33fdfad56473.jpg"></p>    <p>这个库在 lib 目录下,armeabi/armeabi-v7a/x86 这三个目录对应的是cpu的类型,每个 cpu 类型的目录下都有名字一样的 so 文件,因为我的模拟器跑着x86的模拟器里面,所以我要分析的动态链接库在 x86/ libAesCipher-Jni.so 中。</p>    <h2>0x03 静态分析 so 文件</h2>    <p>使用 IDA 进行分析,终于分析到倒数第二个函数(sub_DB0)的时候我看到了熟悉的东西:</p>    <p><img src="https://simg.open-open.com/show/9dcaa3ba366599f462c39ea192b4a27c.jpg"></p>    <p>openssl的aes加密函数,其中参数a3,a4就是我想要的aes加密密钥key和iv,但是点进去之后,却看不到任何东西,看来只能动态调试了,这里先把sub_DB0的起始虚拟地址和结束虚拟地址找到,分别为 0000DB0 和 0000EF0 :</p>    <p><img src="https://simg.open-open.com/show/dceabd6d97ad01125498218caa439794.jpg"></p>    <h2>0x04 动态分析 so 文件</h2>    <p>首先从网上下载 gdbserver 上传到手机里面,然后 adb shell ps 找到 app 的 pid 是 2166:</p>    <p><img src="https://simg.open-open.com/show/6127ada1f01c93cf2ad9e47a66b1a49f.jpg"></p>    <p>在 gdbserver 目录下运行 ./gdbserver :23946 –attach 2166 ,再开一窗口运行 adb forward tcp:23946 tcp:23946 ,运行这两个指令之后我就能用本地的 gdb 调试代码了,但是我想用ida pro(破解版,只能在win上跑)提供的方便的功能进行调试,怎么办?于是我就想到了端口转发工具,于是我在网上找的一份这样的 <a href="/misc/goto?guid=4959734197466097356" rel="nofollow,noindex">开源代码</a> :python rtcp.py l:3333 c:127.0.0.1:23946 ,我实现的调试拓扑:</p>    <p><img src="https://simg.open-open.com/show/37530ff838c2c23e8ee53c4d1f995861.jpg"></p>    <p>在远程的 win 打开 ida pro,选择菜单的 debugger->attach->Remote GDB debugger ,然后 houstname 填写地址,port 是监听端口,之后进入:</p>    <p><img src="https://simg.open-open.com/show/d86c3d5c85d17c4a9462e97dd0890ec8.jpg"></p>    <table>     <tbody>      <tr>       <td>下面我来寻找动态链接库的基地址,因为我调制的 pid 是 2166 ,所以查看 proc maps 文件来找动态链接库的基地址,在终端中运行 adb shell cat /proc/2166/maps</td>       <td>grep libAes, :</td>      </tr>     </tbody>    </table>    <p><img src="https://simg.open-open.com/show/aa1d4ffa306a2be9f3eeafb7d91a1111.jpg"></p>    <p>这个程序加载了三次这个动态链接库,但是每个动态链接库的执行权限不同,分别是是r-xp,r—p,rw-p,要调试的动态链接库应该是具有执行权限的,即r-xp,所以调试的基地址是:e2269000,刚才找到的函数的虚拟地址是0000DB0,所以函数在内存中的地址是基地址+虚拟地址:e2269000+0000DB0=e2269db0 ,然后跳到 e2269db0 下断点运行起来:</p>    <p><img src="https://simg.open-open.com/show/08d2cc0a90254e20a954ea435a41f54d.jpg"></p>    <p>在手机中输入用户名和密码之后调试器被断住,断点正好在我下断的e2269db0位置,然后先不着急按f8调试,先在里面右键创建函数,然后再右键编辑函数,找到函数结束的地址为刚才找到的函数结束虚拟地址+基地址:e2269000+0000EF0= e2269EF0 进行修改:</p>    <p><img src="https://simg.open-open.com/show/90a4cbd3a10a6aa23b8ed50b3a117b6b.jpg"></p>    <p>点确定后把光标移到函数中,按下 f5 找到加密的函数下断点然后运行起来可以看到程序被成功的断下:</p>    <p><img src="https://simg.open-open.com/show/163001b344ce300a4573257f448ef0a3.jpg"></p>    <p>将鼠标指向a3和a4的位置上,查看他们的地址:</p>    <p><img src="https://simg.open-open.com/show/7c21aa19cdedf3fc54c724efab947667.jpg"></p>    <p>然后找到对应的寄存器或者栈地址,将其显示出来:</p>    <p><img src="https://simg.open-open.com/show/3dc28aa98af45f3310b09bf98d572e09.jpg"></p>    <p>然后就找到aes加密的key是:</p>    <pre>  <code class="language-java">95 8A FA EB CA EF A4 96 EC 7B 7E 97 D0 75 EA 48</code></pre>    <p>iv是:</p>    <pre>  <code class="language-java">E0 A4 14 94 34 3A 26 1A 35 64 C6 3C 3A F0 43 57</code></pre>    <h2>0x05 END</h2>    <p>最后编写流量解密程序来验证拿到的key和iv是不是正确的,先在下面的两个函数上下断点来看加密之后的数据是什么样的:</p>    <p><img src="https://simg.open-open.com/show/7af4537257b737a365d76788e212493e.jpg"></p>    <p>发现是02 9d 79 2c开头的 23 8a d1 88 先设置burp的拦截,然后运行app,这样burp成功拦下app发出的数据:</p>    <p><img src="https://simg.open-open.com/show/5dab02f433ba4cafe887d0813a4e8b4d.jpg"></p>    <p>然后我们用 .net 写了一个 aes 解密程序来进行破解:</p>    <p><img src="https://simg.open-open.com/show/1a80330b3bacf118641efc42246ffe12.jpg"></p>    <p>解密程序:</p>    <pre>  <code class="language-java">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);          }          }  }</code></pre>    <p> </p>    <p>来自:http://www.n0tr00t.com/2017/01/10/Reverse-reinforcement-app-full-Course.html</p>    <p> </p>