APP安全加固全过程(混淆/签名验证/反调试)

一、混淆

对于很多人而言是因为java才接触到“混淆”这个词,由于在前移动互联网时代在java程序中“混淆”也只是针对java代码,所以混淆基本就和对java源代码进行混淆等价。

但说到混淆的本质,不过就是将变量名、函数名由有助于开发维护人员理解其用途的名称(如my_name,get_key)改用a,b,c,d这种简短无意义的词去替换。从这个思路出发,资源其实也是可以混淆的。

在前移动互联网时代,对于B/S而言,前端页面资源则是放在服务器上且名称不适宜随便修改;对于C/S而言,由于java不擅长写界面所以java写的程序要么用到的资源很少要么就直接没有图形界面。

也就是说在前移动互联网时代,资源混淆确实是没多大用途和意义的。但在移动互联网时代,或者更直接一点对app而言,资源混淆还是有用武之地的。一是可以减小apk的大小,二是对比电脑客户端更多将信息直接放在变量中而言app将更多的信息存放于xml文件中,进行混淆有助于提高逆向者理解程序逻辑的难度。

1.1 资源混淆

1.1.1 资源混淆操作步骤

从这里要介绍的资源混淆操作方法看,因为是直接对apk进行操作所以放在最后讲才更合适。但顺从认识而言,资源混淆这种不是主角的东西就该放最前面讲。

资源混淆我们这里使用微信团队的AndResGuard

1)进入tool_output目录,下载AndResGuard jar文件和config.xml模板配置文件(注意不要右键直接保存那样下载的是html文件,jar文件点进去下载,config.xml点进去复制内容自己在本地新建个config.xml。或才直接下载整个项目再找出这两个文件)

2) 修改config.xml

config.xml各配置项具体说明见官方说明,我的大概理解是默认会对res目录下的各xml文件进行混淆,在config.xml可以配置不进行混淆的白名单(Whitelist项)及是否使用7z对图片进行压缩(Compress项)等。其中注意不是写在config.xml中的项就是启用的,各项自己的isactive值为"true"时才是启用的。

我这里只修改最后的sign项,配置签名信息其他都不做修改(其实出于安全考虑签名最好用-signature选项而不是配置在config.xml中,但出于教程的统一和简洁这里我就这么操作)

3)进行资源混淆

执行以下命令进行混淆,注意我这里是用到的文件都放在了当前目录下(C:\Users\ls\Desktop\app),如果不在要注意使用全路径或写好相对路径。

-jar指定----AndResGuard程序

SecTest.apk----是我做好的测试app,改成自己的

-config----指定使用的配置文件

-7zip----指定7z可执行文件的路径,改成自己的;其实如果不指定命令会报错,但只是不能生成经过压缩的apk而已,未压缩的apk还是成功生成了的。

-zipalign----指定zipalign可执行文件的路径,这个程序在android sdk中就有,到sdk目录下找就行了。

官方文档中说,若7zip或zipalign的路径已设置环境变量中,这两项不需要单独设置。一是这两个安装时都不会自己加到环境变量,二是官方文档7z用的是7za这个名字的可执行程序在我安装的7z版本中是没有的。也就是说推荐用直接指定命令位置而不是改环境变量的方法。

mkdir test_dir

java -jar AndResGuard-cli-1.2.12.jar SecTest.apk -config  config.xml -out test_dir -7zip D:\7-Zip\7z.exe -zipalign D:\Language\ASDK\build-tools\28.0.0\zipalign.exe

如果出现“java.io.IOException: the signature file do not exit, raw”等报错,那多半是文件名等信息写错了,重新检查一遍。

最后test_dir中得到的有以下文件,各文件官方有说明,就我这里想要的是混淆并进行了签名的SecTest_signed.apk

1.1.2 验证资源混淆成功【可选】

以activity_main.xml为例,项目中代码如下:

使用反编译工具查看layout,可以看到生成了一堆名为a,b,c,d的xml的文件。我找了半天才找到a2.xml是activity_main.xml,且可以看到其中的控件id和字符串名称等都已混淆

1.2 代码混淆

1.2.1 代码混淆操作步骤

代码混淆这里以Android Studio中使用ProGuard为例,Eclipse看了一下也都是指定一下规则文件而已就不多做介绍。至于其他混淆工具并没有研究。

将项目切换到Project视图,找到app文件夹下的build.gradle并打开,锁定到buildTypes节区,如图所示将minifyEnabled项由默认的false改为true即可。

minifyEnabled下方的proguardFiles用于指定混淆规则文件,其中的proguard-android.txt是Android Studio自带的基本的混淆规则(一般在$SDK_PATH\tools\proguard目录下)这个一般不要去做修改。另一个proguard-rules.pro是专门供写个性化混淆规则用的,如果有个性化混淆需求将自己的规则写入其中即可(在下图中也可看到改文件与build.gradle一样同处app目录下)。

我这里没有个性化需求,所以只将minifyEnabled项由默认的false改为true即可,其他都不做改动,这样编译时Android Studio就会自动调用ProGuard使用默认规则进行代码混淆。

1.2.2 验证代码成功混淆【可选】

以MainActivity.java为例,项目中OnCreate函数部分代码如下

使用反编译工具反编译代码,对应片段代码如下,可以看到变量名称已被i,j,k等代替

(不过注意到只有MainActivity.java实现了混淆其他java文件只有文件名改了一下并没有实现混淆,所以对真正生产项目而言自己写规则还是免不了的)

二、签名验证

签名验证,就是在APP中写入自己私钥的hash值,和一个获取当前签名中的私钥hash值的函数两个值相一致,那么就说明APP没有被改动允许APP运行。如果两值不一致那么说明APP是被二次打包的,APP就自我销毁进程。

签名验证又可以在两个地方做,一个是在MainActivity.java的OnCreate函数中做,一个是在原生代码文件的JNI_OnLoad函数中做。

在OnCreate函数中做,短处是反编译者只要找到在OnCreate中定位到验证函数,然后将其注释,重新打包APP就可以成功运行;好处就是代码简单。

在JNI_OnLoad中做,短处是比较复杂(需要创建支持C/C++原生代码的项目,获取hash需要绕道java代码获取等);好处就是反编译者需要进一步掌握ida等反汇编工具将验证函数删除才能绕过验证。

为了最大限度地提高安全性,可以考滤两种验证都使用。

最后为了避免争议,在此要做一下统一声明,以下代码基本我个人都不是原作者,个人在本节的作用是将几个方案整合成了一个比较合理的方案,并验证这些代码和整合出来的方案是可行的。

2.1 在MainActivity.java的OnCreate函数中进行签名验证

OnCreate函数内、setContentView后加入以下代码:

       // 获取当前上下文
        Context context = getApplicationContext();
        // 发布apk时用来签名的keystore中查看到的sha1值,改成自己的
        String cert_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";
        // 调用isOrgApp()获取比较结果
        boolean is_org_app = isOrgApp(context,cert_sha1);
        // 如果比较初始从证书里查看到的sha1,与代码获取到的当前证书中的sha1不一致,那么就自我销毁
        if(! is_org_app){
            android.os.Process.killProcess(android.os.Process.myPid());
        }

在MainActivity类内,OnCreate函数外加入以下代码:

    // 此函数用于返回比较结果
    public static boolean isOrgApp(Context context,String cert_sha1){
        String current_sha1 = getAppSha1(context,cert_sha1);
        // 返回的字符串带冒号形式,用replace去掉
        current_sha1 = current_sha1.replace(":","");
        return current_sha1.equals(current_sha1);
    }
    // 此函数用于获取当前APP证书中的sha1值
    public static String getAppSha1(Context context,String cert_sha1) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
            byte[] cert = info.signatures[0].toByteArray();
            MessageDigest md = MessageDigest.getInstance("SHA1");
            byte[] publicKey = md.digest(cert);
            StringBuffer hexString = new StringBuffer();
            for (int i = 0; i < publicKey.length; i++) {
                String appendString = Integer.toHexString(0xFF & publicKey[i]).toUpperCase(Locale.US);
                if (appendString.length() == 1)
                    hexString.append("0");
                hexString.append(appendString);
                hexString.append(":");
            }
            String result = hexString.toString();
            return result.substring(0, result.length()-1);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

我自己调试结果如下,确实可以成功获取sha1值(获取sha1函数代码原文链接

2.2 原生代码文件的JNI_OnLoad函数中进行签名验证

开始看到这位小哥哥的文章,就喜欢这种有图有真相的文章,说明其代码应该是真的可以获取到APP当前的sha1值的。

但后来理清他的做法是:从java中把context传过去,在c++中完成比较返回true或false;也就是说决定程序退不退出的if语句还是在java中的,这种做法和2.1中全在java中做除了显示技术比较强之外安全效果完全一样并没有提升啊。if应当在c++中实现,context也需要c++自己获取。

后来找到另一位小哥哥的文章,其指出判断需要在c++中做而且是在JNI_OnLoad函数中做并给出了方法,但是他获取context时实现的NoProGuard我没搞清楚在哪导入。

最后找到了又一位小哥哥的文章,其给出了JNI获取context的方案,验证也确实是可行的。

所以整合的方案就是:第二位小哥哥在JNI_OnLoad函数中做的思想+第三位小哥哥获取context的方法+第一位小哥哥获取sha1的方法。

(其实第二位小哥哥还有一个思想就是debug时不需要验证release才要验证,这也是可取的,我这里也采用了。但debug时要做验证也不是不可以的,只是要注意debug时运行在avd中的app使用的是Android Studio自己生成的keystore而不是我们发布apk时自己的keystore,所以此时填的sha1的值应当是Android Studio自己生成的keystore的sha1,

当然第二位小哥哥获取md5的方法改一下好像也是能获取正确的sha1值的)

2.2.1 C++中验证签名代码

最终C++中验证签名的代码如下,自己使用时要注意将其中的app_sha1赋值成自己keystore中的sha1值

#include <jni.h>
#include <string>

// const char *app_sha1="FAAB30C11EEF7333C81D48FECA25D21A18E2C789";
// 这里是keystore中的sha1值,改成自己的
const char *app_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";
const char hexcode[] = {‘0‘,‘1‘,‘2‘,‘3‘,‘4‘,‘5‘,‘6‘,‘7‘,‘8‘,‘9‘,‘A‘,‘B‘,‘C‘,‘D‘,‘E‘,‘F‘};

jobject getGlobalContext(JNIEnv *env)
{
    //获取Activity Thread的实例对象
    jclass activityThread = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");
    jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
    //获取Application,也就是全局的Context
    jmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");
    jobject context = env->CallObjectMethod(at, getApplication);
    return context;
}

char* getSha1(JNIEnv *env){
    // 调用getGlobalContext,获取上下文
    jobject context_object = getGlobalContext(env);
    if (context_object == NULL){
        printf("context is NULL");
        return NULL;
    }
    jclass context_class = env->GetObjectClass(context_object);

    //反射获取PackageManager
    jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject package_manager = env->CallObjectMethod(context_object, methodId);
    if (package_manager == NULL) {
        printf("package_manager is NULL!!!");
        return NULL;
    }

    //反射获取包名
    methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");
    jstring package_name = (jstring)env->CallObjectMethod(context_object, methodId);
    if (package_name == NULL) {
        printf("package_name is NULL!!!");
        return NULL;
    }
    env->DeleteLocalRef(context_class);

    //获取PackageInfo对象
    jclass pack_manager_class = env->GetObjectClass(package_manager);
    methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    env->DeleteLocalRef(pack_manager_class);
    jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);
    if (package_info == NULL) {
        printf("getPackageInfo() is NULL!!!");
        return NULL;
    }
    env->DeleteLocalRef(package_manager);

    //获取签名信息
    jclass package_info_class = env->GetObjectClass(package_info);
    jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");
    env->DeleteLocalRef(package_info_class);
    jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info, fieldId);
    if (signature_object_array == NULL) {
        printf("signature is NULL!!!");
        return NULL;
    }
    jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);
    env->DeleteLocalRef(package_info);

    //签名信息转换成sha1值
    jclass signature_class = env->GetObjectClass(signature_object);
    methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");
    env->DeleteLocalRef(signature_class);
    jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);
    jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream");
    methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V");
    jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte);
    jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory");
    methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");
    jstring x_509_jstring=env->NewStringUTF("X.509");
    jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring);
    methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));
    jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input);
    env->DeleteLocalRef(certificate_factory_class);
    jclass x509_cert_class=env->GetObjectClass(x509_cert);
    methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B");
    jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId);
    env->DeleteLocalRef(x509_cert_class);
    jclass message_digest_class=env->FindClass("java/security/MessageDigest");
    methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jstring sha1_jstring=env->NewStringUTF("SHA1");
    jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring);
    methodId=env->GetMethodID(message_digest_class,"digest","([B)[B");
    jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte);
    env->DeleteLocalRef(message_digest_class);

    //转换成char
    jsize array_size=env->GetArrayLength(sha1_byte);
    jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL);
    char *hex_sha=new char[array_size*2+1];
    for (int i = 0; i <array_size ; ++i) {
        hex_sha[2*i]=hexcode[((unsigned char)sha1[i])/16];
        hex_sha[2*i+1]=hexcode[((unsigned char)sha1[i])%16];
    }
    hex_sha[array_size*2]=‘\0‘;

    printf("hex_sha %s ",hex_sha);
    return hex_sha;
}

static jboolean checkSignature(JNIEnv *env) {
    // 调用getSha1获取app当前证书中的sha1
    char *sha1 = getSha1(env);
    // 调用checkValidity获取比较结果并直接返回
    // jboolean signatureValid = checkValidity(env,sha1);
    if (strcmp(sha1,app_sha1)==0)
    {
        return JNI_TRUE;
    }
    else{
        return JNI_FALSE;
    }
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // RELEASE_MODE这个宏是通过编译脚本设定的,如果是release模式,
    // 则RELEASE_MODE=1,否则为0或者未定义
    // 如果想不管release还是debug都进行签名验证,注释掉下方ifdef和endif两条预编译语句即可
#ifdef RELEASE_MODE
    // 如果是release版本,检查当前应用的签名是否一致;如果不签名不一致的话则返回-1,-1会引发app异常自动退出
    if (RELEASE_MODE == 1) {
        if (checkSignature(env) != JNI_TRUE) {
            return -1;
        }
    }
#endif

    return JNI_VERSION_1_6;
}

2.2.2 配置build_gradle

由于代码中使用了以下预编译语句,所以如果只是使用上边的代码,验证是没有生效的。说明如下:

build_gradle中未配置RELEASE_MODE=1----release/debug都不进行签名验证
build_gradle中配置RELEASE_MODE=1----release模式验证/debug模式不验证
注释掉ifdef和endif两条预编译语句----release/debug都进行签名验证

#ifdef RELEASE_MODE
    // 检查当前应用的签名是否一致,如果不签名不一致的话则返回-1,-1会引发app异常自动退出
    if (checkSignature(env) != JNI_TRUE) {
        return -1;
    }
#endif

所以为了启用验证,还需要打开app目录下的build_gradle文件,在如下图所示位置加入以下代码:

ndk {
    // release包定义RELEASE_MODE=1宏,供so库中的ifdef语句使用
    cFlags "-DRELEASE_MODE=1"
}

2.2.3 MainActivity.java中加载so文件

当然最还得要在java文件中,载入so文件才能起来作用。netive-test是我这里so的库名改成自己的

System.loadLibrary("native-test");

参考:

https://blog.csdn.net/Two_Water/article/details/70233983

https://blog.csdn.net/liyi0930/article/details/77413525

http://leehong2005.com/2016/08/08/android-so-signature-check/

https://blog.csdn.net/lb377463323/article/details/75315167

https://blog.csdn.net/leifengpeng/article/details/52681196

原文地址:https://www.cnblogs.com/lsdb/p/9340761.html

时间: 2024-11-02 18:57:00

APP安全加固全过程(混淆/签名验证/反调试)的相关文章

手工脱壳之 PESpin加密壳【SHE链硬件反调试】【IAT重定向】【混淆+花指令】

一.工具及壳介绍 使用工具:Ollydbg,PEID,ImportREC,LoadPE,IDA,Universal Import Fixer,OllySubScript 此篇是加密壳的第二篇,更详细的步骤和思考,请查看第一篇:手工脱壳之 未知加密壳 [IAT加密+混淆+花指令] PESpin壳: 二.脱壳之寻找OEP 1.硬件断点失效 尝试ESP定律,但硬件断点未断下. (原因其实是壳做了反调试,后面部分介绍) 2.采用API断点+单步跟踪 API下断: 壳导入了LoadLibrary 和 Ge

Android逆向之旅---应用的&quot;反调试&quot;方案解析(附加修改IDA调试端口和修改内核信息)

一.前言 在前一篇文章中详细介绍了Android现阶段可以采用的几种反调试方案策略,我们在破解逆向应用的时候,一般现在第一步都回去解决反调试,不然后续步骤无法进行,当然如果你是静态分析的话获取就没必要了.但是有时候必须要借助动态调试方可破解,就需要进行操作了.现阶段反调试策略主要包括以下几种方式: 第一.自己附加进程,先占坑,ptrace(PTRACE_TRACEME, 0, 0, 0)!第二.签名校验不可或缺的一个选择,本地校验和服务端校验双管齐下!第三.借助系统api判断应用调试状态和调试属

[转载]Android应用方法隐藏及反调试技术浅析

本文转载自: http://drops.wooyun.org/tips/9471 0x00 前言 Android应用的加固和对抗不断升级,单纯的静态加固效果已无法满足需求,所以出现了隐藏方法加固,运行时动态恢复和反调试等方法来对抗,本文通过实例来分析有哪些对抗和反调试手段. 0x01 对抗反编译 首先使用apktool进行反编译,发现该应用使用的加固方式会让apktool卡死,通过调试apktool源码(如何调试apktool可参见前文<Android应用资源文件格式解析与保护对抗研究>),发

2019年首批!网易易盾加固系统通过中国反网络病毒联盟认证

上周,网易易盾安卓加固系统通过中国反网络病毒联盟的<移动互联网应用程序安全加固系统认证>,并于今天正式通过公示期.中国反网络病毒联盟官方微信公众号上的公示通知 中国反网络病毒联盟(Anti Network-Virus Alliance of China),英文简称为ANVA,是根据工业和信息化部的统一部署,依托CNCERT/CC,联合基础互联网运营企业.网络安全厂商.增值服务提供商.搜索引擎.域名注册机构等单位于2009年7月7日共同发起成立. 对于为什么组织加固系统的安全验证,中国反网络病毒

修改Android手机内核,绕过反调试

0x1.手机设备环境 Model number: Nexus 5 OS Version: Android 4.4.4 KTU84P Kernel Version: 3.4.0-gd59db4e 0x2.Android内核提取 查找Android设备的boot分区文件.高通芯片的设备可以通过下面的命令进行查找. cd /home/androidcode/AndroidDevlop/modifyNexus5Boot adb shell ls -al /dev/block/platform/msm_s

安卓App应用加固、App安全保护!

据最新消息,Android 应用数量已成功超越苹果,并将率先突破100万大关.而在众多多的应用之中,却潜伏着太多"不速之客".由于利益驱使以及相关法律的缺失,不法分子将病毒.木马内置于手机App中进行传播,导致手机死机.个人信息泄露.收取短信彩信进行扣费等情况发生,严重影响手机用户的正常使用. 很多用户都认为手机病毒.木马离自己很远,其实则不然,这些携带病毒.木马或恶意广告的App就广泛存在于应用市场之中.据艾媒咨询最新发布的报告显示,在中国手机病毒和恶意软件主要传播途径方面,应用商店

Android Native一处反调试

之前转载了一篇文章介绍了两种反调试方式,分别是ptrace自身和查看TracerPid信息,文章地址: http://www.cnblogs.com/dacainiao/p/5124151.html 这一处反调试是在调试某加固时遇到的,原理是查看当前所有的tcp连接,如果有<00000000:5D8A>就退出,也就是本地连接的23946端口(IDA的默认监听端口). 当然,解决办法可以Path掉该处的反调试或者修改IDA的客户端把默认的监听端口改成其他的. 反调试的方法还有很多,只是分析的样本

Android代码混淆防反编译解决方案研究

做Android开发的都知道要做混淆去防apk被反编译.破解,通过proguard进行Java代码混淆.但是,Android代码混淆真的能起到实质性的作用吗?看下面分析 1.Android代码混淆 如上图,对Android 代码进行混淆后混淆器将代码中的所有变量.函数.类的名称加密为简短的英文字母代号,在APP被破解后增加破解者对代码的阅读难度. 但是混淆的功效只能运作在APP已经被破解后,而且只是增加破解者的难度时间,对其防止破解的作用意义不是很大. 那么,Android代码混淆不能从根本上防

App安全之代码混淆

参考: http://www.cnblogs.com/sunzn/archive/2013/03/06/2946952.html http://www.apkbus.com/android-240707-1-1.html 1.在项目根目录找到project.properties文件,将下图中圈出来的代码注释去除,即申明我们的混淆文件为proguard-project.txt: 2.在项目根目录找到proguard-project.txt文件,按需求添加修改以下代码: # 指定代码的压缩级别 -o