基于 Frida 的 Android 逆向实践

2018年12月20日 · 1783 字 · 4 分钟 · Android Frida

前提

这里假设你已经了解 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("文件写出失败");
        }
    }
}

然后通过 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) {}
}

在这里你就可以看到加密的 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()

再来看某喵机,不仅混淆了代码,而且加密放到了 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();
}

接下来就试试 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()

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

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

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

总结

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

其他的收获大概就是:

  1. 请求数据要加密
  2. 加密方式使用 native 的方式,并做包名校验等
  3. 混淆,高级混淆,硬编码混淆
  4. 加固(对性能有一些影响)
  5. SSL Pinning