android 防止反编译 安全加固技术

先说下加固技术发展历史

基础加固技术——1.代码混淆(proguard);2.签名比对;3.NDK(.so)库动态使用

第一代加固技术——动态加载(包括第一代加壳技术:落地加载)

第二代加固技术——不落地加载;

第三代加固技术——指令抽离;

第四代加固技术——指令转换,即现在经常被应用的VMP加固技术

preview

一图流(2018.09)

代码混淆(ProGuard)技术

重新组织和处理Class文件,降低代码逆向编译后的可读性,即反编译后的代码很难理解。但该技术无法防止加壳技术进行加壳(加入吸费、广告、病毒等代码),而且只要是细心时间够多的人,依然可以对代码依然可以对代码进行逆向分析,所以该技术并没有从根本解决破解问题,只是增加了破解难度。 

但是从实际情况来看,由于混淆技术的多元化发展,混淆理论的成熟,混淆的Java代码可以很好地防止反编译。并且还可以起到压缩APK包的作用。

混淆前

混淆后

可以从这个简单的反编译效果看出,所谓混淆就是用简单的单词或字母代替类名,降低可读性,同时也节俭了空间。

确实这么一看,解读就变困难了点呢。

如何混淆代码
Eclipse的话可以通过Jocky插件、project.properties文件等。现在时代应该没人用了吧(小声),不详细说


Android Studio的话可以通过借助SDK中自带的Proguard工具,需要修改build.gradle中的一行配置即可。

release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}

我都是这么写的

android {
 ....



    buildTypes {
        release {
            //混淆开关
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            signingConfig signingConfigs.config
            //加快编译速度
            ext.alwaysUpdateBuildId = false
            //如果不需要运行 Crashlytics 报告,停用该插件以提高调试 Build 的构建速度:
            ext.enableCrashlytics = false
        }
    }


}

把混淆开关开启后最好多测试几遍全部功能。因为并不是所有代码都能混淆,例如接入了第三方的工具库,或者使用了sdk。如果混淆了会出现正式包某些功能找不到代码从而报各种异常和错误。

其中android studio 工程中的混淆保护文件为proguard-rules.pro

-keep class com.owncloud.android.** {*;}

其中例如这句表示 com.owncloud.android.前缀的文件一律不混淆。

具体还有keepattributes,keepclasseswithmembernames,dontwarn等参数可以设置。这个暂不具体说明,另外学习。其实一般来说都只是为了sdk而设置混淆过滤,而正规sdk都会在接入文档说明混淆要加什么。如果没有,那么上面那句或许也够你用了。

混淆sdk/Jar方法
        1.通过ClassLoader动态加载的方式进行加密。
        2.通过安卓sdk自带/tools目录下的proguard GUI进行配置,执行到最后一步即可。

签名比对技术

 签名比对技术 该技术主要防止加壳技术进行加壳,但代码逆向分析风险依然存在。而且该技术并不能根本解决被加壳问题,如果破解者将签名比对代码注释掉,再编译回来,该技术就被破解了。

Android APK的发布是需要签名的。签名机制在Android应用和框架中有着十分重要的作用。

例如,Android系统禁止更新安装签名不一致的APK;如果应用需要使用system权限,必须保证APK签名与Framework签名一致,等等。在《APK Crack》一文中,我们了解到,破解一个APK,必然需要重新对APK进行签名。而这个签名,一般情况无法再与APK原先的签名保持一致。(除非APK原作者的私钥泄漏,那已经是另一个层次的软件安全问题了。)

简单地说,签名机制标明了APK的发行机构。因此,站在软件安全的角度,我们就可以通过比对APK的签名情况,判断此APK是否由“官方”发行,而不是被破解篡改过重新签名打包的“盗版软件”。

这个“签名”到底是什么东西?
如果你手头方便的话,可以随便解压一个APK。打开解压目录,“META-INF”里面就是签名内容保存的地方(下面会提到)。

首先,签名程序将遍历APK里的所有文件,并用SHA1算法得出每个文件的“摘要信息” 。这些摘要信息就通通记录在“META-INF”目录的“MANIFEST.MF”文件中。(第170行)
所以得出解决方案一:可以比对MANIFEST.MF里各个摘要信息是否相同,来获知APK是否被修改。(比对样本是“官方”APK,下同)

真正的“签名”却还没开始,因为还没有用到公/私钥。MANIFEST.MF的生成,提供了用密钥进行签名的清单。
接下来,会对MANIFEST.MF里所有文件的“摘要信息”用私钥进行二次加密,采用的加密算法是“RSA”非对称加密算法。生成的信息记录在目录下".SF"结尾的文件里。(第475行第259行
和解决方案一相同,我们同样可以比对此文件,获知APK是否被篡改。姑且不占用“解决方案二”这个名头,继续往下分析。

最后,将公钥用 PKCS#7 算法加密保存在 “.RSA” 结尾的文件中(准确说是将包含了公钥等其它相关信息的数字证书加密保存。关于公/私钥、数字证书我搜了一篇文章)。这个文件如果直接用文本阅读器打开,会看到乱码。(第483行第305行
解决方案二:通过判断公钥是否一致,来获知软件是否被重新“打包”。

《APK Crack》一文中,我们了解到,要签名一个没有签名过的APK,可以使用一个叫作Auto-sign的工具。Auto-sign工具实际运行的是一个叫做Sign.bat的批处理命令。用文本编辑器打开这个批处理文件,我们可以发现,实现签名功能的命令主要是这一行命令:

java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

这条命令的意义是:通过signapk.jar这个可执行jar包,以“testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk”。

对于此处所使用的私钥和公钥的生成方式,这里就不做进一步介绍了。这方面的资料大家可以找到很多。我们这里要讲的是signapk.jar到底做了什么。

signapk.jar是Android源码包中的一个签名工具。由于Android是个开源项目,所以,很高兴地,我们可以直接找到signapk.jar的源码!路径为/build/tools/signapk/SignApk.java。

对比一个没有签名的APK和一个签名好的APK,我们会发现,签名好的APK包中多了一个叫做META-INF的文件夹。里面有三个文件,分别名为MANIFEST.MF、CERT.SF和CERT.RSA。signapk.jar就是生成了这几个文件(其他文件没有任何改变。因此我们可以很容易去掉原有签名信息)。

APK签名比对的应用场景

经过以上的论述,想必大家已经明白签名比对的原理和我的实现方式了。那么什么时候什么情况适合使用签名对比来保障Android APK的软件安全呢?

个人认为主要有以下三种场景:

1、 程序自检测。在程序运行时,自我进行签名比对。比对样本可以存放在APK包内,也可存放于云端。缺点是程序被破解时,自检测功能同样可能遭到破坏,使其失效。

2、 可信赖的第三方检测。由可信赖的第三方程序负责APK的软件安全问题。对比样本由第三方收集,放在云端。这种方式适用于杀毒安全软件或者APP Market之类的软件下载市场。缺点是需要联网检测,在无网络情况下无法实现功能。(不可能把大量的签名数据放在移动设备本地)。

3、 系统限定安装。这就涉及到改Android系统了。限定仅能安装某些证书的APK。软件发布商需要向系统发布上申请证书。如果发现问题,能追踪到是哪个软件发布商的责任。适用于系统提供商或者终端产品生产商。缺点是过于封闭,不利于系统的开放性。

以上三种场景,虽然各有缺点,但缺点并不是不能克服的。例如,我们可以考虑程序自检测的功能用native method的方法实现等等。软件安全是一个复杂的课题,往往需要多种技术联合使用,才能更好的保障软件不被恶意破坏。

NDK .so动态库技术

该技术实现是将重要核心代码全部放在C文件中,利用NDK技术,将核心代码编译成.so动态库,再用JNI进行调用。该技术虽然能将核心代码保护起来,但被加壳风险依然存在。

主要要学习使用ndk编译生成动态库。ndk技术是个大坑,但也算进阶的必经之路,有空写一个。

动态加载技术

第一代Android加固技术用于保护应用的逻辑不被逆向与分析,最早普遍在恶意软件中使用,可以有效的防止逆向分析、被破解、被加壳等问题。其主要基于Java虚拟机提供的动态加载技术。该技术在Java中是一个比较成熟的技术。

动态加载技术分为以下几步:

  • 将核心代码编译成dex文件的Jar包
  • 对jar包进行加密处理
  • 在程序主入口利用NDK进行解密
  • 再利用ClassLoader将jar包进行动态加载
  • 利用反射技术将ClassLoader 设置成系统的ClassLoader。

主要优点有:
       1.核心代码在被加密的jar中,所以破解者无法解压出class文件,如果加密秘钥被破解者拿到,那将是另外一层面的安全问题了。
      2.该技术也可以有效防止加壳技术,代码是动态加载上来的,破解者的壳程序无法加入到已加密的jar包中,及时破解者注入壳程序入口,壳程序因为不在ClassLoader 的jar包中,所以也无法被执行起来,除非破解者替换ClassLoader的jar包,关掉NDK解密代码.但这种安装到手机上,已经不在是我们的应用,用户一定会将其卸载掉。

其保护流程是:

开发阶段中将程序切分成加载(Loader)与关键逻辑(Payload)两部分,并分别打包;

运行时加载部分(Loader)会先运行,释放出关键逻辑(Payload),然后java的动态加载技术进行加载,并转交控制权。

(图:启动流程)

(图:核心代码)

  • 备注(multidex组件的加固原理):

Android的DEX文件在设计之初程序普遍较小,所以在DEX文件设计时,只允许包含65535个函数引用。而随着Android应用的发展,大量的应用的代码已经超过了65535的限制,为了解决这个问题,Android5.0之后原生支持加载多个dex,而为了对旧版本的兼容,Android提供了multidex组件。该组件的实现原理与上面介绍的是一致的。

  • 缺陷与对抗

第一代加固技术的缺陷是依赖Java的动态加载机制,而这个机制要求关键逻辑(Payload)部分必须解压,并且释放到文件系统,这就给了攻击机会去获取对应的文件。虽然可以通过关键逻辑(Payload)被加载后,被从文件系统删除,用于防止被复制,但是攻击者可以拦截对应的删除函数,阻止删除。

而关键逻辑(Payload)会被加密后保存,可用于对抗静态分析,但是攻击者可以通过自定义虚拟机,拦截动态加载机制所使用的关键函数,在这个函数内部,复制文件系统中的关键逻辑(Payload)文件。

//   1.Jar包加密加密解密文件//  
public static boolean enOrDecryptFile(byte[] paramArrayOfByte,  
        String sourceFilePath, String destFilePath,int mode){  
    File sourceFile = new File(sourceFilePath);  
    File destFile = new File(destFilePath);  
    CipherOutputStream cout = null;  
    FileInputStream in  = null;  
    FileOutputStream out = null;  
    if (sourceFile.exists() && sourceFile.isFile()) {  
        if (!destFile.getParentFile().exists()) {  
            destFile.getParentFile().mkdirs();  
        }  
        try {  
            destFile.createNewFile();  
            in = new FileInputStream(sourceFile);  
            out = new FileOutputStream(destFile);  
            // 获取密钥//  
            init();  
            SecretKeySpec secretKeySpec = new SecretKeySpec(defPassword, "AES");  
            Cipher cipher;  
            cipher = Cipher.getInstance("AES");  
            cipher.init(mode, secretKeySpec);  
            cout = new CipherOutputStream(out, cipher);  
            byte[] cache = new byte[CACHE_SIZE];  
            int nRead = 0;  
            while ((nRead = in.read(cache)) != -1) {  
                cout.write(cache, 0, nRead);  
                cout.flush();  
            }  
        }catch (IOException e) {  
            e.printStackTrace();  
            return false;  
        } catch (NoSuchAlgorithmException e) {  
            e.printStackTrace();  
            return false ;  
        } catch (NoSuchPaddingException e) {  
            e.printStackTrace();  
            return false ;  
        }catch (InvalidKeyException e) {  
            e.printStackTrace();  
            return false;  
        }finally{  
                if(cout != null){  
                    try {  
                        cout.close();  
                    } catch (IOException e) {  
                        e.printStackTrace();  
                    }  
                }  
                if(out != null){  
                    try {  
                        out.close();  
                    } catch (IOException e) {  
                        e.printStackTrace();  
                    }  
                }  
                if(in != null){  
                    try {  
                        in.close();  
                    } catch (IOException e) {  
                        e.printStackTrace();  
                    }  
                }  
        }  
        return true;  
    }  
    return false;  
}  

jar用SDK\platform-tools\下的dx命令进行dex格式转化

dx   --dex    --output=生成的目标文件的地址(绝对路径)     需要转化的jar文件(绝对路径)

然后再用加密工具将生成jar文件进行加密处理

最后通过代码动态加载:

File file = new File("/data/data/" + base.getPackageName() + "/.cache/");  
        if (!file.exists()) {  
            file.mkdirs();  
        }  
        try {  
            Runtime.getRuntime().exec("chmod 755 " + file.getAbsolutePath()).waitFor();  
        } catch (InterruptedException e1) {  
            // TODO Auto-generated catch block  
            e1.printStackTrace();  
        } catch (IOException e1) {  
            // TODO Auto-generated catch block  
            e1.printStackTrace();  
        }  
        Util.copyJarFile(this);  
        Object currentActivityThread = RefInvoke.invokeStaticMethod(  
                "android.app.ActivityThread", "currentActivityThread",  
                new Class[] {}, new Object[] {});  
        String packageName = getPackageName();  
        HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect(  
                "android.app.ActivityThread", currentActivityThread,  
                "mPackages");  
        WeakReference wr = (WeakReference) mPackages.get(packageName);  
        MyClassLoader dLoader = new MyClassLoader("/data/data/"  
                + base.getPackageName() + "/.cache/classdex.jar", "/data/data/"  
                + base.getPackageName() + "/.cache", "/data/data/"  
                + base.getPackageName() + "/.cache/", base.getClassLoader());  
        try {  
            Class<?>  class1 = dLoader.loadClass("com.example.test.TestActivity");  
            Log.i("b364","----------->class1: "+class1);  
        } catch (ClassNotFoundException e){  
            Log.i("b364","----------->class not found Exception!");  
            e.printStackTrace();  
        }  
        Log.i("b364","------>PackageInfo: "+wr.get());  
        // DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,  
        // libPath, (ClassLoader) RefInvoke.getFieldOjbect(  
        // "android.app.LoadedApk", wr.get(), "mClassLoader"));  
        RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",  
                wr.get(), dLoader);  

第二代加固技术——不落地加载

相对第一代加固技术,第二代加固技术在APK修改方面已经完善,能做到对开发的零干扰。开发过程中不需要对应用做特殊处理,只需要在最终发布前进行保护即可。而为了实现这个零干扰的流程,Loader需要处理好Android的组件的生命周期。

主要流程:

1)Loader被系统加载。

2)系统初始化Loader内的StubApplication。

3)StubApplication解密并且加载原始的DEX文件(Payload)。

4)StubApplication从原始的DEX文件(Payload)中找到原始的Application对象,创建并初始化。

5)将系统内所有对StubApplication对象的引用使用替换成原始Application,此步骤使用JAVA的反射机制实现。6)由Android系统进行其他组件的正常生命周期管理。

(图:对开发零干扰的加固后启动流程)

另一方面,不落地加载技术是在第一代加固技术的基础上改进,主要解决第一代技术中Payload必须释放到文件系统(俗称落地)的缺陷,其主要的技术方案有两种:

A. 拦截系统IO相关的函数(如read、write),在这些函数中提供透明加解密。具体的流程是:

1)关键逻辑(Payload)以加密的方式存储在APK中。

2)运行时加载部分(Loader)将关键逻辑释(Payload)放到文件系统,此时关键逻辑(Payload)还处于加密状态。

3)加载部分拦截对应的系统IO函数(read,write等)。

4)加载部分(Loader)正常调用Java动态加载机制。由于虚拟机的IO部分被拦截,所以虚拟机读取到已经解密的关键逻辑(Payload)。

(透明加解密方案流程)

B. 直接调用虚拟机提供的函数进行不落地的加载,具体流程是:

1)关键逻辑(Payload)以加密的方式存储在APK中。

2)运行时加载部分(Loader)将关键逻辑释(Payload)放到内存。

3)加载部分调用虚拟机内部接口进行加载。

(图:不落地加载流程)

关键的系统函数如下:

(图:关键的系统函数)

  • 兼容性

方案A透明加密方案由于其需要拦截系统的IO函数,这部分会使用inline hook或者got hook等技术,其会带来一定的兼容性问题

方案B的不落地加载方案由于其调需要调用系统内部的接口,而这个接口并不导出,各个厂商在实现时又有各自的自定义修改,导致该方案存在兼容性问题。

  • 缺陷与对抗

第二代加固技术在应用启动时要处理大量的加解密加载操作,会造成应用长时间假死(黑屏),用户体验差。

在加固技术实现上没有本质区别,虽然能防止第一代加固技术文件必须落地被复制的缺陷,但是也可以从以下方面进行对抗:

例如内存中的DEX文件头会被清除,用于防止在dump文件中被找到;DEX文件结构被破坏,例如增加了一些错误的数据,提高恢复的成本。

但是Payload被加载之后,在内存中是连续的,利用gdb等调试工具dump内存后可以直接找到Payload,进行简单的处理之后可以恢复出100%的Payload文件。

和第一代加固技术的对抗方法一样,不落地加载也无法对抗自定义虚拟机。只需对上述的关键函数进行拦截然后将对应的内存段写出去,即可恢复Payload。注意,由于IO相关的函数被拦截,所以无法直接调用read/write等函数进行直接的读写,需要使用syscall函数进行绕过。

虽然厂商会自己实现可能上述函数,从而绕过上述函数的拦截。但是Android的类加载器必须能找到对于的结构体才能正常执行,攻击者可以以类加载器做为起点,找到对应的Payload在内存中的位置。

第三代加固技术——指令抽离

由于第二代加固技术仅仅对文件级别进行加密,其带来的问题是内存中的Payload是连续的,可以被攻击者轻易获取。第三代加固技术对这部分进行了改进,将保护级别降到了函数级别。

主要的流程是:发布阶段将原始DEX内的函数内容(Code Item)清除,单独移除到一个文件中。

(图:发布阶段)

运行阶段将函数内容重新恢复到对应的函数体。恢复的时间点有几个方式:

A. 加载之后恢复函数内容到DEX壳所在的内存区域

(图:运行阶段)

B. 加载之后将函数内容恢复到虚拟机内部的结构体上:虚拟机读取DEX文件后内部对每一个函数有一个结构体,这个结构体上有一个指针指向函数内容(CodeItem),可以通过修改这个指针修改对应的函数内容。

C. 拦截虚拟机内与查找执行代码相关的函数,返回函数内容。

  • 兼容性

指令抽离技术使用了大量的虚拟内部结构与未被文档的特性,再加上Android复杂的厂商定制,带来大量的兼容性问题。

  • 缺陷与对抗

指令抽离技术的某些方案与虚拟机的JIT性能优化冲突,无法达到最佳的运行性能。依旧使用了java虚拟机进行函数内容的执行。攻击者可以通过自定义Android虚拟机,在解释器的代码上做记录一个函数的内容(CodeItem)。接下来遍历触发所有函数,从而获取到全部的函数内容。最终重新组装成一个完整的DEX文件。目前已经有自动化工具可以指令抽离技术中脱壳。

(图:第三代加固DEX文件脱壳流程)

第四代加固技术——指令转换/VMP

第三代加固技术在函数级别的保护,使用Android虚拟机内的解释器执行代码,带来可能被记录的缺陷,第四代加固技术使用自己的解释器来避免第三代的缺陷。而自定义的解释器无法对Android系统内的其他函数进行直接调用,必须使用JAVA的JNI接口进行调用。其主要实现由两种:

A. DEX文件内的函数被标记为native,内容被抽离并转换成一个符合JNI要求的动态库。 动态库内通过JNI和Android系统进行交互。

B. DEX文件内的函数被标记为native,内容被抽离并转换成自定义的指令格式,该格式使用自定义接收器执行,和A一样需要使用JNI和Android系统进行调用。

  • 兼容性

第四代VMP加固技术一般配合第三代加固技术使用,所以第三代的所有兼容性问题,指令转换/VMP加固也存在。

  • 缺陷与对抗

不论使用指令转换/VMP加固的A方案或者B方案,其必须通过虚拟机提供的JNI接口与虚拟机进行交互,攻击者可以直接将指令转换/VMP加固方案当作黑盒,通过自定义的JNI接口对象,对黑盒内部进行探测、记录和分析,进而得到完整DEX程序。

(图:第四代加固DEX文件恢复)

另外,第四代VMP加固技术只实现Java代码保护,没有做到使用VMP技术来保护C/C++等代码,安全保护能力有所欠缺。

下一代加固技术——虚机源码保护

跟第四代的VMP加固技术,虚机源码保护加固是用虚机技术保护所有的代码,包括Java,Kotlin,C/C++,Objective-C,Swift等多种代码,具备极高的兼容性;使App得到更高安全级别的保护,运行更加稳定。

虚机源码保护为用户提供一套完整的工具链,首先把用户待保护的核心代码编译成中间的二进制文件,随后生成独特的虚机源码保护执行环境和只能在该环境下执行的运行程序。

虚机源码保护会在App内部隔离出独立的执行环境,该核心代码的运行程序在此独立的执行环境里运行。即便App本身被破解,这部分核心代码仍然不可见。

(图:虚机源码保护加固流程)

生成的虚机源码保护拥有独特的可变指令集,极大的提高了指令跟踪、逆向分析的难度。同时,虚机源码保护还提供了反调试能力和监控能力。虚机源码保护可以通过自身的探针感知到环境的变化,实时探测到外界对本环境的调试、注入等非正常执行流程变化,将调试动作引入程序陷阱,并发出警报,进而进行实时更新,提高安全强度。

加固技术发展及其攻防对抗的更迭,伴随着互联网技术发展不断升级,我们深信邪不能胜正,而虚机源码保护加固作为当前领先的加固技术,在未来很长一段时间,能够为App提供足够强度的保护,为企业和开发者的业务发展保驾护航。

加壳技术

待补充

第三方平台加密以及检测漏洞

第三方平台使用, 或者在线加密。

1.https://twitter.com/apkprotect

参考内容:

https://blog.csdn.net/DJY1992/article/details/56016669

https://www.jianshu.com/p/8583f6a966e2

https://blog.csdn.net/earbao/article/details/82379117

https://zhuanlan.zhihu.com/p/28257081

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值