加固了就安全了?几个措施让你的 Android 应用更安全

1、背景

最近新开发了一款工具类型的软件 移动工具箱,然而某天下午忽然群里来了一个不速之客说我的软件被破解了。虽然,该软件无需付费并且没有广告(很良心 :) ),也进行了安全加固,但是还是很轻易得被别人破解了。现象是,启动页换成了别人的页面,需要用户点击页面上的按钮分享几次破解者的信息才能进入应用。并且,每次打开应用都是如此。

看到这我的心情还是非常复杂的。一开始觉得自己做个应用都赚不到钱,还要被这些人利用。本是同根生,相煎何太急呀。然而,转念一想,这里有些东西还是可以学习一下,于是我去他们的链接下载了软件,本着学习的精神进行了一波分析并制定了几个方案来加强应用安全。

2、分析

2.1 检查破解应用签名

决定要进行分析之后,我首先想到的就是去检查下应用的签名,这里使用 keytool 即可,指令如下:

keytool -printcert -jarfile 你的apk地址

获取的结果如下,

image

很显然,应用的签名已经发生了变化,应用被别人二次打包了。实际上,我的应用是在 360 上面进行了软件加固(免费版),但是还是如此轻松地被别人完成了二次打包。

2.2 签名校验

如果仅仅是签名发生了变化,那么解决方式倒也简单。在应用内部增加一个签名校验就可以了。不过签名校验也有需要注意的地方。其一,签名校验可以放在 Java 层来完成,也可以放在 native 层通过 C++ 来完成。其二,在应用内部进行整个数字签名的校验还是部分校验。这是因为如果写入完整的数字签名很容易被别人发现,即便写入到 so 中,写入完整字符串比部分更容易被别人找到。

在 Java 中,你可以按照下面这种方式获取应用的数字签名。我已经把相关的方法写成了工具类,你可以在 Github 上面获取 AppUtils

    public static String getAppSignatureMD5(final String packageName) {
        return getAppSignatureHash(packageName, "MD5");
    }

    private static String getAppSignatureHash(final String packageName, final String algorithm) {
        if (StringUtils.isSpace(packageName)) return "";
        Signature[] signature = getAppSignature(packageName);
        if (signature == null || signature.length <= 0) return "";
        return StringUtils.bytes2HexString(EncryptUtils.hashTemplate(signature[0].toByteArray(), algorithm))
                .replaceAll("(?<=[0-9A-F]{2})[0-9A-F]{2}", ":$0");
    }

如果是使用 C++ 进行签名校验可以使用下面的方法,

jbyteArray getSignatureByteArray(JNIEnv *env, jobject context, jstring algorithm) {
    jclass context_clazz = env->GetObjectClass(context);
    // context.getPackageManager()
    jmethodID methodID_getPackageManager = env->GetMethodID(context_clazz,
            "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject packageManager = env->CallObjectMethod(context, methodID_getPackageManager);
    jclass packageManager_clazz = env->GetObjectClass(packageManager);
    jmethodID methodID_getPackageInfo = env->GetMethodID(packageManager_clazz,
            "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jmethodID methodID_getPackageName = env->GetMethodID(context_clazz,
            "getPackageName", "()Ljava/lang/String;");
    // context.getPackageName()
    jobject application_package_obj = env->CallObjectMethod(context, methodID_getPackageName);
    jstring application_package = static_cast<jstring>(application_package_obj);
    const char* package_name = env->GetStringUTFChars(application_package, JNI_FALSE);
    __android_log_print(ANDROID_LOG_DEBUG, "DiDiDaDa", "Package Name : %s", package_name);
    // packageManager->getPackageInfo(packageName, GET_SIGNATURES);
    jobject packageInfo = env->CallObjectMethod(packageManager, methodID_getPackageInfo, application_package_obj, /*GET_SIGNATURES*/ 64);
    jclass packageinfo_clazz = env->GetObjectClass(packageInfo);
    jfieldID fieldID_signatures = env->GetFieldID(packageinfo_clazz, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray signature_arr = (jobjectArray)env->GetObjectField(packageInfo, fieldID_signatures);
    // packageInfo.signatures[0]
    jobject signature = env->GetObjectArrayElement(signature_arr, 0);
    // signature.toByteArray()
    jclass signature_clazz = env->GetObjectClass(signature);
    jmethodID signature_toByteArray = env->GetMethodID(signature_clazz,"toByteArray", "()[B");
    jbyteArray sig_bytes = (jbyteArray) env->CallObjectMethod(signature, signature_toByteArray);
    // MessageDigest.getInstance("SHA1")
    jclass message_digest_clazz = env->FindClass("java/security/MessageDigest");
    jmethodID message_digest_getInstance = env->GetStaticMethodID(message_digest_clazz,
            "getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
    const char* algorithm_bytes = env->GetStringUTFChars(algorithm, JNI_FALSE);
    jstring algorithm_name = env->NewStringUTF(algorithm_bytes);
    jobject message_object = env->CallStaticObjectMethod(message_digest_clazz, message_digest_getInstance, algorithm_name);
    jthrowable exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) return NULL;
    // sha1.update()
    jmethodID message_digest_update = env->GetMethodID(message_digest_clazz,"update","([B)V");
    env->CallVoidMethod(message_object, message_digest_update, sig_bytes);
    // sha1.digest()
    jmethodID digest = env->GetMethodID(message_digest_clazz, "digest", "()[B");
    jbyteArray sha1_bytes = (jbyteArray) env->CallObjectMethod(message_object, digest);
    return sha1_bytes;
}

这里使用的是 CMake 进行编译,使用 Android Studio 进行开发。上面的逻辑很简单,就是把之前的 java 层获取签名的方法照搬到了 native 方法里。而且,借助于 Android Studio 进行开发,可以帮助我们减轻很多工作量,比如方法的映射关系就不需要你一个一个得进行对比,可以通过提示直接完成转换。当然,上面只是使用 java 的类完成了获取签名的字节数组的逻辑,进一步转换成为 hash 字符串还要进行其他操作。这里就不把代码贴出来了。

因为除了进行这些签名校验我还加了其他的安全措施,所以这部分代码不便开源。不过,我已经把一些基础功能的代码和环境配置打包成了压缩文件,你可以到我公众号里回复【安卓签名校验】获取完整的代码。

拿到了签名之后当然是进行签名校验了,基本的字符串比较即可。当然,你可以进行部分匹配,这样增加了破解的难度。

2.3 签名校验就安全了?

增加了上述的签名校验只不过是第一步,也是比较常规的安全操作。但是这样操作未必就能防止其他用户破解。进一步进行反编译,我又发现一些新的东西。破解包的目录结构挺有意思的,

image

相比于没有破解的包,在 lib 下面多了两个文件,这里的 libarm.so 的内容尚不清楚,这里的 libhook.apk 是一个 apk 文件,并且签名跟我的原始的包的签名一致,但是 apk 内容并不是我的完整包。所以,我猜测这里的 Apk 文件有其他用途,并且仅仅使用签名校验并不安全。因为别人可以读取你的应用的签名,然后通过某种方式进行伪造,让获取签名的方法返回的结果不是安装包真实的签名而是你的真实包的签名。这里的 apk 很有可能就是用来获取签名的。

于是,我进一步对应用进行资源反编译,这里使用 apktool 来完成,这里如下:

apktool d 破解应用.apk

于是我发现启动应用是被篡改掉了的,这里的启动应用已经被修改为 arm.SignerPro,并且这里的启动类已经被修改为 SplashActivity

image

然后,我们将 Apk 解压之后对 dex 文件进行反编译。这里使用的是 d2j-dex2jar 来完成。命令如下,

d2j-dex2jar classes.dex

然后,我们根据反编译的结果,查看启动 Application,

image

这里的 Application 继承了 InvocationHandler. InvocationHandler 相比大家都不陌生,Retrofit 就是通过在 InvocationHandler 实现的代理来解析请求参数,并根据注解和入参来完成 OkHttp 请求的。我们找到该接口的实现方法 invoke(). 从上图中很明显得可以看出,我们的 getPackageInfo() 方法有很大可能被别人 hook 了。这个方法就是上面说的用来获取应用签名的方法,这里会对 getPackageInfo() 方法进行处理,并返回 signs 作为签名的获取结果而不是应用真实的签名结果。

当然,上面都是猜着,因为他们也只破解了第一个版本的包,第二个版本的包我加了新的安全措施,并且悄悄地增加了几个隐藏的页面用来在应用内获取签名和其他的信息来验证我的想法。

不管怎么说,仅仅加上签名校验很可能是不安全的!

2.4 其他安全措施

其实即便他们 hook 了获取签名的 getPackageInfo() 方法我们仍然有许多措施来应对。他们这里的 hook 有几个地方可以入手,

  1. 首先,我们并没看到 signs 的赋值操作,我猜测是通过 native 方法来完成的赋值,他们如果每次都尝试返回 signs 的话并没有屡次对 signs 进行重新赋值。而从 PackageManager 中多次读取到的 Signature 对象应该是不同的。所以,如果我们多次读取签名的时候返回的同一对象,是不是可以认为该方法被别人 hook 了呢?

  2. 另外,这里 hook 了 getPackageInfo() 方法。只是我们无法判断他们 hook 的范围,我们可以尝试自定义一个 getPackageInfo() 方法,然后自定义返回的对象类型,如果返回的不是我们指定的类型,就可以得出结论,我们的 getPackageInfo() 方法被别人 hook 了。比如,

    public class Fake {
    
        public void getPackageInfo() { }
    
        public FakePackageInfo getPackageInfo(@NonNull String packageName, int flags) throws PackageManager.NameNotFoundException {
            return new FakePackageInfo();
        }
    
        public FakePackageInfo getPackageInfo(@NonNull VersionedPackage versionedPackage, int flags) throws PackageManager.NameNotFoundException {
            return new FakePackageInfo();
        }
    
        public static class FakePackageInfo { }
    }
    

    当然,如果他们只 hook 了 PackageManager 的方法,这个就不适用了。

  3. 除了签名校验,我们还可以检查其他项。比如检查 Application 是否是我们的启动 Application,可以使用如下代码获取应用的 Application 并进行比较,

    public static String getApplicationName(final String pkgName) {
        if (StringUtils.isSpace(pkgName)) return null;
        try {
            PackageManager pm = UtilsApp.getApp().getPackageManager();
            ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
            return ai == null ? null : ai.className;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
    
  4. 此外,我们还可以对应用的启动 Activity 进行校验。因为他们对我们的应用的启动 Activity 进行了修改,于是我们启动 Activity 进行了修改,所以这也可以作为一个应对措施:

    public static String getAppLauncher(final String pkgName) {
        if (StringUtils.isSpace(pkgName)) return null;
        try {
            PackageManager pm = UtilsApp.getApp().getPackageManager();
            PackageInfo pi = pm.getPackageInfo(pkgName, 0);
            if (pi == null) return null;
            Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
            resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
            resolveIntent.setPackage(pi.packageName);
            List<ResolveInfo> resolveInfoList = pm.queryIntentActivities(resolveIntent, 0);
            ResolveInfo resolveInfo = resolveInfoList.iterator().next();
            if (resolveInfo != null) {
                return resolveInfo.activityInfo.name;
            }
            return null;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
    
  5. 上面提到的也只是一部分方法,当然还有其他的可以操作的方法。我们也可以在对类似的破解事件进行多分析来总结,尝试从多个维度做好安全防护。

3、总结

上面是我在应对破解方面的一些简单的总结,除了进行常规的加固之外,还进行了其他的维度的校验。当然,我其实对逆向和破解的了解并不是那么深入,这里权当抛砖引玉了。

上文中提到的反编译工具,如果需要的话可以到我的工号里回复【安卓反编译工具】领取。这里提到的移动工具箱呢是我最近开发的一款工具软件,除了一些常用的工具之外,内部还包含了 18 种基于 OpenCV 的图像处理,部分代码我也丢到了 Github 上面。此外,还包含了两个 Github 爬虫工具用来帮助开发者更好地使用 Github,以及常见的字符串加密、文件加密和时间戳工具等,希望能够对你有帮助。

上述就是这次分享的内容,感谢阅读♥

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335