移动app安全测试 - 客户端篇(三)签名校验

前言:

二次打包作为移动app安全风险的一部分,通常由逆向破解者进行破解,然后插入广告、植入恶意代码、修改内购逻辑逃避支付等等。这些恶意行为严重危害移动产品和用户利益,同时也影响企业口碑。

签名校验:

防止二次打包最普遍的方式之一,便是进行签名校验。校验又分为很多层次,有针对package信息,有的针对文件hash,有的甚至针对代码段等等。这里只列举最简单的几种方式供参考。

1、普通校验

系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息。

  /**
    * 做普通的签名校验 - Java层
  */
private byte[] getCertificateSHA1Fingerprint(Context context) {
    PackageManager pm = context.getPackageManager();
    String packageName = context.getPackageName();
 
    try {
        PackageInfo packageInfo = pm.getPackageInfo(packageName, 
            PackageManager.GET_SIGNATURES);
        Signature[] signatures = packageInfo.signatures;
        byte[] cert = signatures[0].toByteArray();
        X509Certificate x509 = X509Certificate.getInstance(cert);
        MessageDigest md = MessageDigest.getInstance("SHA1");
        return md.digest(x509.getEncoded());
    } catch (PackageManager.NameNotFoundException | CertificateException |
            NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return null;
}

当然如果java层不够安全的话,可以放在native层去做。 native 代码调用 Java 函数的套路:先找到 jclassjmethodID,再 CallXXXMethod

#include <jni.h>
#include <stddef.h>
 
extern "C" {
 
JNIEXPORT jbyteArray JNICALL
Java_com_github_piasy_MainActivity_nativeGetSig(
        JNIEnv *env, jclass type, jobject context) {
    // context.getPackageManager()
    jclass context_clazz = env->GetObjectClass(context);
    jmethodID getPackageManager = env->GetMethodID(context_clazz, 
        "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject packageManager = env->CallObjectMethod(context, 
        getPackageManager);
 
    // context.getPackageName()
    jmethodID getPackageName = env->GetMethodID(context_clazz, 
        "getPackageName", "()Ljava/lang/String;");
    jstring packageName = (jstring) env->CallObjectMethod(context, 
        getPackageName);
 
    // packageManager->getPackageInfo(packageName, GET_SIGNATURES);
    jclass package_manager_clazz = env->GetObjectClass(packageManager);
    jmethodID getPackageInfo = env->GetMethodID(package_manager_clazz, 
        "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jint flags = 0x00000040;
    jobject packageInfo = env->CallObjectMethod(packageManager, 
        getPackageInfo, packageName, flags);
 
    jthrowable exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // packageInfo.signatures[0]
    jclass package_info_clazz = env->GetObjectClass(packageInfo);
    jfieldID fid = env->GetFieldID(package_info_clazz, "signatures",
        "[Landroid/content/pm/Signature;");
    jobjectArray signatures = (jobjectArray) env->GetObjectField(
        packageInfo, fid);
    jobject signature = env->GetObjectArrayElement(signatures, 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);
 
    // X509Certificate appCertificate = X509Certificate.getInstance(sig_bytes);
    jclass x509_clazz = env->FindClass("javax/security/cert/X509Certificate");
    jmethodID x509_getInstance = env->GetStaticMethodID(x509_clazz, 
        "getInstance", "([B)Ljavax/security/cert/X509Certificate;");
    jobject x509 = (jstring) env->CallStaticObjectMethod(x509_clazz, 
        x509_getInstance, sig_bytes);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // x509.getEncoded()
    jmethodID getEncoded = env->GetMethodID(x509_clazz, 
        "getEncoded", "()[B");
    jbyteArray public_key = (jbyteArray) env->CallObjectMethod(x509, getEncoded);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // 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;");
    jstring sha1_name = env->NewStringUTF("SHA1");
    jobject sha1 = env->CallStaticObjectMethod(message_digest_clazz, 
        message_digest_getInstance, sha1_name);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // sha1.digest(public_key)
    jmethodID digest = env->GetMethodID(message_digest_clazz, 
        "digest", "([B)[B");
    jbyteArray sha1_bytes = (jbyteArray) env->CallObjectMethod(
        sha1, digest, public_key);
 
    return sha1_bytes;
}
 
}

相应破解方式,动态代理IPackageManager。

2、动态代理检测

动态代理的原理是系统动态的为我们创建了一个代理类,所以检测 IPackageManager 的类名即可发现端倪

 /**
    * 检测 PM 代理
    */
@SuppressLint("PrivateApi")
private boolean checkPMProxy(){
    String truePMName = "android.content.pm.IPackageManager$Stub$Proxy";
    String nowPMName = "";
    try {
        // 被代理的对象是 PackageManager.mPM
        PackageManager packageManager = getPackageManager();
        Field mPMField = packageManager.getClass().getDeclaredField("mPM");
        mPMField.setAccessible(true);
        Object mPM = mPMField.get(packageManager);
        // 取得类名
        nowPMName = mPM.getClass().getName();
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 类名改变说明被代理了
    return truePMName.equals(nowPMName);
}

3、使用新的API

api28以上,可以使用新的api进行检测。

 /**
    * 使用较新的 API 检测
    */
@SuppressLint("PackageManagerGetSignatures")
private boolean useNewAPICheck(){
    String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
    String nowSignMD5 = "";
    Signature[] signs = null;
    try {
        // 得到签名 MD5
        if (VERSION.SDK_INT >= 28) {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    getPackageName(),
                    PackageManager.GET_SIGNING_CERTIFICATES);
            signs = packageInfo.signingInfo.getApkContentsSigners();
        } else {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    getPackageName(),
                    PackageManager.GET_SIGNATURES);
            signs = packageInfo.signatures;
        }
        String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
        nowSignMD5 = MD5Utils.MD5(signBase64);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return trueSignMD5.equals(nowSignMD5);
}

总结:

签名校验还有一些其他的骚操作,比如提早检测、校验application等。虽然使用校验不能防止应用被破解、二次打包,但是可以极大的提高破解者的破解成本。虽然目前Android项目已经采取了加固措施,但仍然无法防止应用被二次打包。

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