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

努力尽今夕,少年犹可夸。这篇文章主要讲述APP安全加固全过程(混淆/签名验证/反调试)相关的知识,希望能为你提供帮助。
本教程所用android Studio测试项目已上传:https://github.com/PrettyUp/SecTest
  一、混淆对于很多人而言是因为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。或才直接下载整个项目再找出这两个文件)

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

文章图片

2) 修改config.xml
config.xml各配置项具体说明见官方说明,我的大概理解是默认会对res目录下的各xml文件进行混淆,在config.xml可以配置不进行混淆的白名单(Whitelist项)及是否使用7z对图片进行压缩(Compress项)等。其中注意不是写在config.xml中的项就是启用的,各项自己的isactive值为"true"时才是启用的。
我这里只修改最后的sign项,配置签名信息其他都不做修改(其实出于安全考虑签名最好用-signature选项而不是配置在config.xml中,但出于教程的统一和简洁这里我就这么操作)
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

 
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_dirjava -jar AndResGuard-cli-1.2.12.jar SecTest.apk -configconfig.xml -out test_dir -7zip D:\\7-Zip\\7z.exe -zipalign D:\\Language\\ASDK\\build-tools\\28.0.0\\zipalign.exe

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

文章图片

  如果出现“java.io.IOException: the signature file do not exit, raw”等报错,那多半是文件名等信息写错了,重新检查一遍。
最后test_dir中得到的有以下文件,各文件官方有说明,就我这里想要的是混淆并进行了签名的SecTest_signed.apk
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

1.1.2 验证资源混淆成功【可选】  以activity_main.xml为例,项目中代码如下:
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

使用反编译工具查看layout,可以看到生成了一堆名为a,b,c,d的xml的文件。我找了半天才找到a2.xml是activity_main.xml,且可以看到其中的控件id和字符串名称等都已混淆
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

 
1.2 代码混淆
1.2.1 代码混淆操作步骤代码混淆这里以Android Studio中使用ProGuard为例,Eclipse看了一下也都是指定一下规则文件而已就不多做介绍。至于其他混淆工具并没有研究。
将项目切换到Project视图,找到app文件夹下的build.gradle并打开,锁定到buildTypes节区,如图所示其中有minifyEnabled项该项控制编译时是否启用混淆,默认为false表示不使用。
minifyEnabled下方的proguardFiles用于指定混淆规则文件,其中的proguard-android.txt是Android Studio自带的基本的混淆规则(一般在$SDK_PATH\\tools\\proguard目录下)这个一般不要去做修改。另一个proguard-rules.pro是专门供写个性化混淆规则用的,如果有个性化混淆需求将自己的规则写入其中即可(在下图中也可看到改文件与build.gradle一样同处app目录下)。
proguard-android.txt中已排除了android关键组件然后对除此之外的java代码都进行混淆,已符合我当前需要,所以我这里只将minifyEnabled项由默认的false改为true,其他都不做改动。
如果要写个性化规则可参考:https://blog.csdn.net/Two_Water/article/details/70233983
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

1.2.2 验证代码成功混淆【可选】以MainActivity.java为例,项目中OnCreate函数部分代码如下
 
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

使用反编译工具反编译代码,对应片段代码如下,可以看到变量名称已被i,j,k等代替
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

另外再查看AESCoder.java,也已被成功混淆
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

 
二、签名验证签名验证,就是在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函数代码原文链接)
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

 
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值
APP安全加固全过程(混淆/签名验证/反调试)

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

文章图片
#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; } } #endifreturn JNI_VERSION_1_6; }

View Code 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" }

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

文章图片
 
2.2.3 MainActivity.java中加载so文件当然最还得要在java文件中,载入so文件才能起来作用。netive-test是我这里so的库名改成自己的
// Used to load the \'native-test\' library on application startup. static { System.loadLibrary("native-test"); }

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

文章图片

 
三、反调试反调试,这位小哥哥说可以有两个思路。
第一个是一个进程同时最多只能被一个进程所调试,所以可以自己使用ptrace()函数假装自己在调试自己,占住调试的位置以此来拒绝别的进程的调试请求。
第二个是查看/proc/{pid}/status文件如果发现TracerPid的值不等于0(TracerPid是调试进程的pid,如果不为0则表示有进程在调试),则kill掉自己。
第一个思路由于我在复现时没有起到反调试效果,未排查到原因暂且就先不管了。
第二个思路中作者给的具体做法是只是在JNI_Onload中只检测一次。在姜维的《Android 应用安全防护和逆向分析》中也提到了第二种思路,但他给出的具体做法是去启动一个线程不断地检测/proc/{pid}/status的TracerPid值,如果检测到不为0则使用exit退出。
不是很清楚有没有可能/proc/{pid}/status的TracerPid值开始为0,后来不为0的情况。但其实我是先看到姜维写的,所以这里就采用姜维的做法。
3.1 加入检测函数
在要防护的c++文件中加入以下两个函数
#include < pthread.h> #include < unistd.h> #include < stdio.h> void* thread_function(void *arg){ int pid = getpid(); char file_name[20] = {\'\\0\'}; sprintf(file_name,"proc/%d/status",pid); char line_str[256]; int i = 0,traceid; FILE *fp; while(1){ i = 0; fp = fopen(file_name,"r"); if(fp == NULL){ break; } while(!feof(fp)){ fgets(line_str,256,fp); if(i == 5){ // traceid = getnumberfor_str(line_str); traceid = atoi(& line_str[10]); if(traceid > 0){ exit(0); } break; } i++; } fclose(fp); sleep(5); } }void create_thread_check_traceid(){ pthread_t thread_id; int err = pthread_create(& thread_id,NULL,thread_function,NULL); if(err != 0){} }

3.2 在JNI_OnLoad函数开头调用检测函数
要强调两点,一个是这里是反调试只有ida进行动态分析时才能起到防护效果,ida静态打开还是不能阻止的。第二个是这里是反调试,自己开发过程中使用IDE debug也是调试,如果加了以下代码那IDE debug时进程也会自我销毁的(实际发现IDE中 run也是不行的)。
也就是说,在开发时要注意先注释该函数调用,在打包生成apk时才去掉注释。
APP安全加固全过程(混淆/签名验证/反调试)

文章图片

 
 
参考:
《Android 应用安全防护和逆向分析》
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://blog.csdn.net/feibabeibei_beibei/article/details/60956307
【APP安全加固全过程(混淆/签名验证/反调试)】https://www.cnblogs.com/biggerman/p/6940888.html

    推荐阅读