Carlisle's Site

Back

前提#

这里假设你已经了解 Charles、Frida、FDex2 等工具的使用,另外我会顺带穿插说一些与 Xposed 方式之间的比较。

实践#

目标:逆向分析某咕机和某喵机

1. 抓包获取数据接口#

通过 Charles 进行抓包,拦截到数据之后可以看到数据都加密了,所以还是需要借助一下其他手段来解密。

其实如果只是解密数据而不需要使用它们的接口做其他事情的话,使用 frida 就可以做到了,具体可参看 Frida 后记——看我是怎么不用脱壳&逆向来解密 APP 的数据

主要是通过获取堆栈调用信息来找到关键信息,然后 hook Cipher、SecretKeySpec、IvParameterSpec 等关键类来获取 mode、Key、iv,有了这些你就可以直接通过在线加解密的方式来解析数据了。

如果是使用 Xposed 的方式的话可以直接使用 Inspeckage,它也是直接 Hook 了加密相关的类,而且通过它提供的 web 页面,可以方便的查看各种拦截的其他信息。

这里有个比较有意思的点,一般的 app 加密使用的 key 都是不变的,比如这里的某喵机,但是某咕机的 key 每次都是变化的,所以如果要继续分析下去就不得不反编译看代码了。

2. 反编译#

两款 app 都是做了加固,所以还需要脱壳,这里使用的还是 FDex2。

之前 Xposed 实践的时候忘了说原理,这里讲一下,其实就是通过 Hook ClassLoader 的 loadClass 方法,反射调用 getDex 方法取得 Dex(com.android.dex.Dex 类对象),在将里面的 dex 写出。

具体代码如下:

public class DumpDexHook extends XC_MethodHook {
    public static final String TAG = "Inspeckage_DumpDexHook:";
    private static XSharedPreferences sPrefs;

    public static void loadPrefs() {
        sPrefs = new XSharedPreferences(Module.class.getPackage().getName(), Module.PREFS);
        sPrefs.makeWorldReadable();
    }

    public static void initAllHooks(XC_LoadPackage.LoadPackageParam lpparam) {
        findAndHookMethod("java.lang.ClassLoader", lpparam.classLoader, "loadClass", String.class, Boolean.TYPE, new XC_MethodHook() {
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                loadPrefs();
                String packagename = sPrefs.getString("package", "");

                Method getBytes = Class.forName("com.android.dex.Dex").getDeclaredMethod("getBytes", new Class[0]);
                Method getDex = Class.forName("java.lang.Class").getDeclaredMethod("getDex", new Class[0]);

                Class cls = (Class) param.getResult();
                if (cls == null) {
                    return;
                }
                byte[] bArr = (byte[]) getBytes.invoke(getDex.invoke(cls, new Object[0]), new Object[0]);
                if (bArr == null) {
                    return;
                }
                String dex_path = "/data/data/" + packagename + "/" + packagename + "_" + bArr.length + ".dex";
                XposedBridge.log(TAG + dex_path);
                File file = new File(dex_path);
                if (file.exists()) return;
                writeByte(bArr, file.getAbsolutePath());
            }
        });
    }

    private static void writeByte(byte[] bArr, String str) {
        try {
            OutputStream outputStream = new FileOutputStream(str);
            outputStream.write(bArr);
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            XposedBridge.log("文件写出失败");
        }
    }
}
java

然后通过 jadx-gui 打开写出的 dex 就可以看到源码了

接下来就是分析加解密相关的代码了

3. 数据加解密#

先看某咕机,反正代码也没混淆,就直接看吧,没啥看不懂的

这里可以通过 adb shell dumpsys activity | grep mFocusedActivity 获取当前 Activity,然后直接找个这个类去看相关的代码

主要的加解密的类是 MsgDES.java,主要代码如下

public class MsgDES {
    public static String DecryptDoNet(String str, String str2) {}

    public static String EncryptAsDoNet(String str, String str2) {}

    public static String getCurrentDate() {}

    public static String getDESKey(String str) {}

    public static DecryptionData toDecryptDoNet(String str) {}

    public static String toEncryptAsDoNet(String str) {}

    public static String toEncryptAsDoNet(String str, String str2) {}
}
java

在这里你就可以看到加密的 key 是如何变化的

其实这部分的代码直接拷出来用完全没问题..

有一点需要说的是 frida 可以直接绕过 360 加固,因为 xposed 在应用启动前就做好了 hook 准备,而这些类被壳隐藏了导致 hook 失败,而 frida 是等应用启动了以后才注入到进程 hook 所以能 hook 成功。

接下来就试试 hook 加密前的数据

import frida, sys

reload(sys)
sys.setdefaultencoding('utf-8')


def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


jscode = """
Java.perform(function () {
    var msgDES = Java.use("cn.memobird.app.webservice.MsgDES");
    msgDES.EncryptAsDoNet.implementation = function(str1, str2) {
        send("Encrypt: " + str1);
        return this.EncryptAsDoNet(str1, str2);
    };

    msgDES.DecryptDoNet.implementation = function(str1, str2) {
        var result = this.DecryptDoNet(str1, str2);
        send("Decrypt: " + result);
        return result;
    };
});
"""

session = frida.get_usb_device().attach('cn.memobird.app')
script = session.create_script(jscode)
script.on("message", on_message)
script.load()
sys.stdin.read()
python

再来看某喵机,不仅混淆了代码,而且加密放到了 native 层,与某咕机相比做的挺不错的,然而并没有卵用..

需要注意的是因为 dex 分包,所以某一个 dex 里的代码并不是全部的,所以可能出现怎么都找不到某个类的问题,这个时候记得多打开几个 dex 看一下,那个类一定在别的 dex 文件里

首先我们也是打开 app,查到当前 Activity,然后分析相关代码,最后定位到加解密的类是 EncryptUtils.java,主要代码如下

public class EncryptUtils {
    static {
        System.loadLibrary("alf_h_sdkcore");
    }

    public static String a(String str) throws Exception {}

    public static String a(byte[] bArr) throws Exception {}

    public static String a(byte[] bArr, String str) throws Exception {}

    public static byte[] a(String str, String str2) throws Exception {}

    private static String b(String str, String str2) throws Exception {}

    private static String b(byte[] bArr, String str) throws Exception {}

    public static byte[] b(String str) throws Exception {}

    public static String c(String str) throws Exception {}

    private static byte[] c(String str, String str2) throws Exception {}

    private static String d(String str, String str2) throws Exception {}

    private static native String getEncryptKey();

    private static native String getEncryptMode();
}
java

接下来就试试 hook 加解密的数据

import frida, sys

reload(sys)
sys.setdefaultencoding('utf-8')


def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


jscode = """
Java.perform(function () {
    var EncryptUtils = Java.use("com.lib_utils.encrypt.EncryptUtils");
    EncryptUtils.c.overload("java.lang.String").implementation = function(str) {
        var result = this.c(str);
        send("Decrypt: " + result);
        return result;
    };

    EncryptUtils.a.overload("java.lang.String").implementation = function(str) {
        send("Encrypt: " + str);
        return this.a(str);
    };
});
"""

session = frida.get_usb_device().attach('cn.paperang.mm')
script = session.create_script(jscode)
script.on("message", on_message)
script.load()
sys.stdin.read()
python

这里还有个小插曲,因为我不仅仅需要加解密数据,我还可能用到它的接口,所以刚开始的时候还想着要不要试一把 so 分析,也想着学习一下 IDA 的使用啥的,后来一想有点想多了

我直接把 so 文件拷到我的项目里,然后把 EncryptUtils.java 也按照它的包路径放到了项目相应路径下,发现运行起来一点毛病没有..

某喵机虽然把加解密方式放到了 native 层,但是没有做包名校验,所以直接拷出来也是可以用的

总结#

总结下来发现 frida 的用处就是 hook 的时候比较方便,不过大部分的工作还是反编译、找 hook point。

其他的收获大概就是:

  1. 请求数据要加密
  2. 加密方式使用 native 的方式,并做包名校验等
  3. 混淆,高级混淆,硬编码混淆
  4. 加固(对性能有一些影响)
  5. SSL Pinning
基于 Frida 的 Android 逆向实践
https://carlislechan.github.io/blog/20181220-frida-practice
Author Carlisle
Published at December 20, 2018
Comment seems to stuck. Try to refresh?✨