前言

项目需求,需要对 Android 逆向有一定了解。本周对某加壳应用进行了简单逆向,现在在博客上记录一下逆向过程。

Java

Manifest

可以看到该应用使用了自定义的 Application 和 Activity 作为程序入口,继续跟到这两个类看相应源码:

App MAct

可以看到 initSASDK() 函数明显存在一定的问题,推测为加壳应用的壳入口,继续跟进相应代码:

start

可以看到最终调用的 start 函数里有如下的执行流程:

xzbs_ibq = WXMDGnzykrccwjZDVUmhfzZvEzBIzZfWBNKUhgaIOWSrSobBjR;
srq(yiqomGNhdxLIJidVfyApfmJTIHPyutBbUkJxcWjBQHBrvbIXsOy, AvHtcXKhnamHsyrioJsYtObXYmrNsqUJmGFVnRXJTojtxBcOqgIj, FrzFWtDbeEGzjMXTZsUhYhYauwqXwJmljQFVrazhYTpbTiUljnepX, UyWqeczsunPuNjiSzoJlbLiNwGcpywsOaFqtWYqnJLjSYuVPEaozta, peVYfmJPInUHjHaTbqicJtUHmRZtZdUBcwvWjFSbbWlllsIdzEOXioi, LrAaxenJlqbZtbwLTeVausucgeMQpsscBeYNQPGYxiwYFHTARqqJgYDR);
if (ezgyq()) {
    xtmltaqolx(2, 3, 3, 3);
    aozftlu();
    lfob();
    tevwztzyjqace();
}
umnyoz(324, 6, false, 6);

很明显,ezqyg() 函数试检测是否初始化完成的标志,阅读该函数代码:

private static boolean ezgyq() {
    String qyhhdfdclv = "aebchjtntz" + "xohpdsfuqm";
    String osrz = "zjydzywrik" + "yoxnmlxcskkcqfswphgt";
    int uzcjyygfg = (1471 + 1) * 2;
    return (new File(ozxzzjhyypbi()).exists() && new File(ozxzzjhyypbi()).length() == new File(dyozzxjqf()).length() / 2) ? false : true;
}

很明显除了最后一行外都是插入的 junk code,而最后一行的比较非常明显,是比较某一个文件的大小是否是另一个文件的两倍。继续阅读 ozxzzjhyypbi()dyozzxjqf() 函数的源码,我们知道这两个文件其实是 /data/user/0/aaa.bbb.ccc/files/libswbqgonYxEgdlSXzbootstrap.so/data/app/aaa.bbb.ccc-1/lib/arm/libswbqgonYxEgdlSXzbootstrap.so,很明显,在初始化的过程中,程序会将 libswbqgonYxEgdlSXzbootstrap.so 写入自身的 files 文件夹中,而且很有可能在写入过程中执行了某个解密函数,使得保存在本机的 so 文件只有之前的一半大小。

下面再看初始化函数会执行什么操作,首先是 xtmltaqolx 函数会将 assets 文件下的几个文件拷贝到 files 文件夹下,代码优化如下:

private static void xtmltaqolx(int asdEw, int ggplklssfq, int kzpjmjnwdg, int qziqcjievs) {
    File filespath = new File(xzbs_ibq.getFilesDir().getAbsolutePath() + "/" + nwm_tau);
    if (filespath.exists()) {
        filespath.delete();
        filespath.mkdirs();
    } else {
        filespath.mkdirs();
    }
    try {
        for (String sofile : xzbs_ibq.getAssets().list(nwm_tau)) {
            InputStream is = xzbs_ibq.getAssets().open("wrhlrci/" + sofile);
            FileOutputStream fos = new FileOutputStream(new File(filespath, sofile));
            byte[] data = new byte[1024];
            while (true) {
                int i = is.read(data);
                if (i == -1) {
                    break;
                }
                fos.write(data, 0, i);
            }
            fos.close();
            is.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

后面三个函数的逻辑较为简单,除去 junk code 发现其实三个函数执行的是一个功能,将 libswbqgonYxEgdlSXzbootstrap.so 解密并完成之前所说的写入 files 文件夹下的功能。

private static int hwfhcg_uyf = 0;
private static byte[] ytukd_pkq = new byte[40960];

private static void aozftlu() throws IOException {
    hwfhcg_uyf = new FileInputStream(dyozzxjqf()).read(ytukd_pkq) / 2;
}

private static void lfob() throws IOException {
    for (int i = 0; i < hwfhcg_uyf; i++) {
        byte[] bArr = ytukd_pkq;
        bArr[i] = (byte) (bArr[i] ^ ytukd_pkq[hwfhcg_uyf + i]);
    }
}

private static void tevwztzyjqace() throws IOException {
    new FileOutputStream(ozxzzjhyypbi()).write(ytukd_pkq, 0, hwfhcg_uyf);
}

可以看到 apk 包中存储的其实是加密过的 so 文件,真正的 libswbqgonYxEgdlSXzbootstrap.so 文件是由该文件的前半部分和后半部分异或而成的。

我们可以从 root 的 Android 手机上提取解密后的 so 文件,或者是编写 python 脚本来进行解密:

data = file('libswbqgonYxEgdlSXzbootstrap.so', 'rb').read()
mid = len(data) / 2
so = [chr(ord(i) ^ ord(j)) for i, j in zip(data[:mid:], data[mid::])]
with open('true.so', 'w') as f:
    f.write(so)

然后我们可以看到存在这样一行代码: umnyoz(324, 6, false, 6);,会动态加载相应 so 文件。至此,Java 端的加密/解密部分已经结束了,我们可以发现 Java 端的操作就是解密并释放 assets 和 lib 文件夹下的 so 文件,然后通过 System.load() 函数进行加载。

Native

但仅有这些操作是不够的,我们有足够的理由相信在 so 文件里还存在着加壳的部分逻辑,使用 ida 打开相应文件,找到 JNI_Onload 函数:

JNI_Onload

现在可以确认 JNI_Onload 函数里有加壳的逻辑操作,因此我们需要继续理解该函数。不过幸运的是,我们可以看到代码里加入了很多的 log 函数,我们可以使用这些输出的 log 加深我们对程序逻辑的理解。

这里截取部分 log:

03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: JNI_OnLoad
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: Got JNIEnv
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: 00
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: Got Context true
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: after setJavaObjectForDebug
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getNames:  1
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getNames:  1.1
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  1
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  2
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: adn/suflej/fvvhmduk
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  2.1
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getOutDexDir
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: ()Ljava/lang/String;
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  2.2
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  3
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: g_lib_dir /data/user/0/aaa.bbb.ccc/files/wrhlrci
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getNames:  2
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getNames:  4
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: join: libIQPFsmOUChmkwhbDdynamicloader.so
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: join: IQPFsmOUChmkwhbDdynamicloader.so
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: join: IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: so=libIQPFsmOUChmkwhbDdynamicloader.so, dex=IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: if (getNames()) is false
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: 2so=libIQPFsmOUChmkwhbDdynamicloader.so, dex=IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: decrypt
03-22 04:41:27.272 28000-28000/? E/sa_bootstrap: getLibDir:  1
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: g_lib_dir /data/user/0/aaa.bbb.ccc/files/wrhlrci
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: join: /libIQPFsmOUChmkwhbDdynamicloader.so
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: join: /data/user/0/aaa.bbb.ccc/files/wrhlrci/libIQPFsmOUChmkwhbDdynamicloader.so
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: join: /IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: join: /data/user/0/aaa.bbb.ccc/files/IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: src=/data/user/0/aaa.bbb.ccc/files/wrhlrci/libIQPFsmOUChmkwhbDdynamicloader.so, out=/data/user/0/aaa.bbb.ccc/files/IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: join: 53_81_64_77_28
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: key = 53_81_64_77_28
03-22 04:41:27.273 28000-28000/? E/sa_bootstrap: dexclassloader
03-22 04:41:27.274 28000-28000/? E/sa_bootstrap: join: /data/user/0/aaa.bbb.ccc/files/
03-22 04:41:27.274 28000-28000/? E/sa_bootstrap: join: /data/user/0/aaa.bbb.ccc/files/IQPFsmOUChmkwhbDdynamicloader.jar
03-22 04:41:27.430 28000-28000/? E/sa_bootstrap: load success
03-22 04:41:27.430 28000-28000/? E/sa_bootstrap: dexclassloader2
03-22 04:41:28.596 28000-28000/aaa.bbb.ccc E/sa_bootstrap: end JNI_OnLoad

根据 log 我们就能很明显地发现程序会将之前存在 /files/wrhlrci 下的几个 so 文件释放到 /files 文件夹下,并将后缀名改为 .jar。随意打开其中任意两个文件进行比较,可以发现存在着很明显的差异,这里释放的 jar 文件其实就是 apk 的变体,而 so 文件很明显是一段无意义的数据,显然二者间的转换过程也必然存在着一个解密的过程。

比较

使用 ida 继续逆向源码,很容易地就找到了相应的代码:

Decrpyt

继续查看相应源码,可以看到 decrypt 函数的具体实现,可以看到其会从 so 文件中加载解密的密钥,再通过该密钥解密 so 文件。由于 log 的存在,我们很容易就能发现 key = 53_81_64_77_28。然后 while 函数里主要就是利用 key 对密文进行解密。

Decrpyt 2

JNI_Onload 函数最后会调用 com.nativedroid.module.dynamicloader.Dynamicstart 函数,但令人奇怪的是 apk 文件中其实并无该库,所以这就是 apk 最后执行失败的原因?

可以发现该 app 和 Unmasking Android Malware: A Deep Dive into a New Rootnik Variant 一文描述的还是比较相像的,本质上还是用了一堆对抗技术的加壳恶意软件,但壳的难度和商业级别的壳比较还是远远不如。

总结

虽然头一天晚上看代码看的还是毫无头绪,不过在理解了其背后的逻辑之后,颇有种豁然开朗之感。这大概就是佛家所说的顿悟?

参考

  1. Unmasking Android Malware: A Deep Dive into a New Rootnik Variant, Part I
  2. Unmasking Android Malware: A Deep Dive into a New Rootnik Variant, Part II