Android Apk 加固之Dex文件 方案NDK 版本

https://blog.csdn.net/I123456789T/article/details/91562328

这篇文章介绍了 基本的加固流程,但有一个明显的问题就是 解密是 java 代码实现,存在的问题就是key可以被找到,很容易被破解;

一、写一下基础流程:

参考文档:https://developer.android.google.cn/studio/build/multidex.html#keep

 

看到这张图其实,还是很好理解的,就是我们把需要加固的apk,外部包装一层壳,而这个壳的作用是为了解密源apk的

二、加固仍存在一些问题:

1、解密壳不能加密,解密都是 java 代码实现,存在的问题就是key可以被找到,很容易被破解;
2、解密之后的apk源程序放在指定目录的话,还是存在被破解的风险,因为这种落地方式解密,是很容易获取解密之后的apk的
3、在解密得到源程序apk,然后再用DexClassLoader进行加载,这里相当于两次把apk加载到内存中,第一次是解密的时候,第二次是加载apk的时候,那么这效率就会大大降低了

三、解决方案:

第一个问题,使用NDK 解决;使用NDK 解密,本文使用的RC4 算法,处理较快,如果安全性高也可以使用AES,demo中都已加入;

第二个问题,加载后可以删除掉,但每个加载时间会长;

第三个问题暂时无解决;5.0前是可以解决见:Android中内存加载dex https://blog.csdn.net/zzx410527/article/details/51673908

四、主要使用的一个API DexClassLoader

DexClassLoader当然也是一种ClassLoader,但本身属于顾名思义是用来加载Dex文件的,是安卓系统独有的一种类加载器。
基础概念

在此之前可以稍微回顾下ClassLoader的相关基础:

    ClassLoader是用来加载class文件的,它负责将*.class加载为内在中的Class对象
    加载机制为“双亲委派”,即能交给父类加载器去加载的,绝不自行加载
 

使用方法

 只需要清楚其构造方法的参数意义就可以。

DexClassLoader (String dexPath, 
                String optimizedDirectory, 
                String librarySearchPath, 
                ClassLoader parent)
参数含义
dexPath包含dex文件的jar包或apk文件路径
optimizedDirectory释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空
librarySearchPathnative库的路径,可为空
parent父类加载器

 

五、 加壳的具体流程:

1.创建一个空的demo进行,

2.然后在项目中添加一个代理module(解密,和系统源码交互功能)和tools工具加密Java library 的module ;

1、代码中需要用到几个类,AES加解密类,Zip压缩解压类等工具类

首先我先proxy_core代理module下写一个代理application ,然后继承至Application,代码目录结构请看:

接着把我们这个代理的application加到我们最常写的配置文件中AndroidManifest.xml 中,我们是不是每个App都有一个application,然后把它配置到AndroidManifest.xml中,这里唯一不同的是,不是把我们项目中的那个application写到AndroidManifest.xml中,而是把我们在代理的写上。然后把我们app自己用到的application也加上,自己的application写在meta-data中,另一个meta-data按照下面的写就行,写法和位置如下

这个是我们自己项目用到的初始化application,上面的代理只是处理代理操作的。

我们自己的MyApplication里面目前啥也没写,这个使我们项目中用于初始化的,这里先不写东西。

这里开始写代理了,在ProxyApplication 中:
 

public class ProxyApplication extends Application {
    //定义好的加密后的文件的存放路径
    private String app_name;
    private String app_version;

    /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个方法中进行解密,同时把dex交给Android去加载
     * @param base
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取用户填入的metaData
        getMetaData();

        //得到当前apk文件
        File apkFile = new File(getApplicationInfo().sourceDir);

        //把apk解压  这个目录中的内容需要root权限才能使用
        File versionDir = getDir(app_name+"_" + app_version,MODE_PRIVATE);

        File appDir = new File(versionDir,"app");
        File dexDir = new File(appDir,"dexDir");

        //得到我们需要加载的dex文件
        List<File> dexFiles = new ArrayList<>();
        //进行解密 (最好做md5文件校验)
        if (!dexDir.exists() || dexDir.list().length == 0){
            //把apk解压到appDir
            Zip.unZip(apkFile,appDir);
            //获取目录下所有的文件
            File[] files = appDir.listFiles();
            for (File file:files){
                String name = file.getName();

                if (name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
                    try{
                        
// 使用 ndk rc4 解密**************************************
Utils.native_rc4_de(file.getAbsolutePath(),file.getAbsolutePath());
                        dexFiles.add(file);

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }else {
            for (File file:dexDir.listFiles()){
                dexFiles.add(file);
            }
        }

        try {
            loadDex(dexFiles,versionDir);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void loadDex(List<File> dexFiles,File versionDir) throws Exception{
        //1、获取pathList
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //2、获取数组dexElements
        Field dexElementsField = Utils.findField(pathList,"dexElements");
        Object[] dexElements = (Object[]) dexElementsField.get(pathList);
        //3、反射到初始化makePathElements的方法
        Method makeDexElements = Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);

        ArrayList<IOException> suppressedException = new ArrayList<>();
        Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedException);

        Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + addElements.length);
        System.arraycopy(dexElements,0,newElements,0,dexElements.length);
        System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);

        //替换classloader中的element数组
        dexElementsField.set(pathList,newElements);
    }


    private void getMetaData(){
        try {
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData = applicationInfo.metaData;
            if (null != metaData){
                if (metaData.containsKey("app_name")){
                    app_name = metaData.getString("app_name");
                }
                if (metaData.containsKey("app_version")){
                    app_version = metaData.getString("app_version");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 开始替换application
     */
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            bindRealApplication();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 让代码走入if的第三段中
     * @return
     */
    @Override
    public String getPackageName() {
        if (!TextUtils.isEmpty(app_name)){
            return "";
        }
        return super.getPackageName();
    }

    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(app_name)){
            return super.createPackageContext(packageName, flags);
        }
        try {
            bindRealApplication();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return delegate;

    }

    boolean isBindReal;
    Application delegate;
    //下面主要是通过反射系统源码的内容,然后进行处理,把我们的内容加进去处理
    private void bindRealApplication() throws Exception{
        if (isBindReal){
            return;
        }
        if (TextUtils.isEmpty(app_name)){
            return;
        }
        //得到attchBaseContext(context) 传入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //创建用户真实的application  (MyApplication)
        Class<?> delegateClass = null;
        delegateClass = Class.forName(app_name);

        delegate = (Application) delegateClass.newInstance();

        //得到attch()方法
        Method attach = Application.class.getDeclaredMethod("attach",Context.class);
        attach.setAccessible(true);
        attach.invoke(delegate,baseContext);

        //获取ContextImpl ----> ,mOuterContext(app);  通过Application的attachBaseContext回调参数获取
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //获取mOuterContext属性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        mOuterContextField.set(baseContext,delegate);

        //ActivityThread  ----> mAllApplication(ArrayList)  ContextImpl的mMainThread属性
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        Object mMainThread = mMainThreadField.get(baseContext);

        //ActivityThread  ----->  mInitialApplication       ContextImpl的mMainThread属性
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread,delegate);

        //ActivityThread ------>  mAllApplications(ArrayList)   ContextImpl的mMainThread属性
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        mApplications.remove(this);
        mApplications.add(delegate);

        //LoadedApk ----->  mApplicaion             ContextImpl的mPackageInfo属性
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(baseContext);


        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo,delegate);

        //修改ApplicationInfo  className  LoadedApk
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className = app_name;


        delegate.onCreate();
        isBindReal = true;
    }
}

 这里我换成了 rc4解密;这里也可以判断一下,如果目标文件不存在再做这些操作,可以提高性能,但风险增加。最安全的方式是加载完成,直接删除;

2、下面在proxy_tools中写一个Main类,和一个main方法,直接运行处理,代码如下:

public class Main {

    public static void main(String[] args) throws Exception{

        /**
         * 1、制作只包含解密代码的dex文件
         */
        File aarFile = new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
        File aarTemp = new File("proxy_tools/temp");
        Zip.unZip(aarFile,aarTemp);

        File classesDex = new File(aarTemp,"classes.dex");
        File classesJar = new File(aarTemp,"classes.jar");
        //dx --dex --output out.dex in.jar     E:\AndroidSdk\Sdk\build-tools\23.0.3
        Process process = Runtime.getRuntime().exec("cmd /c dx --dex --output " + classesDex.getAbsolutePath()
                + " " + classesJar.getAbsolutePath());
        process.waitFor();
        if (process.exitValue() != 0){
            throw new RuntimeException("dex error");
        }
        process.destroy();

        /**
         * 2、加密apk中所有的dex文件
         */
        File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
        File apkTemp = new File("app/build/outputs/apk/debug/temp");
        Zip.unZip(apkFile,apkTemp);
        //只要dex文件拿出来加密
        File[] dexFiles = apkTemp.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                return s.endsWith(".dex");
            }
        });
        //AES加密
       // AES.init(AES.DEFAULT_PWD);
        for (File dexFile:dexFiles) {
            byte[] bytes = Utils.getBytes(dexFile);
          //  byte[] encrypt = AES.encrypt(bytes);
            byte[] encrypt =RC4.RC4Base(bytes,RC4.ACCESSKEY);
            FileOutputStream fos = new FileOutputStream(new File(apkTemp,"secret-" + dexFile.getName()));
            fos.write(encrypt);
            fos.flush();
            fos.close();
            dexFile.delete();
        }
        /**
         * 3、把dex放入apk解压目录,重新压成apk文件
         */
        classesDex.renameTo(new File(apkTemp,"classes.dex"));
        File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
        Zip.zip(apkTemp,unSignedApk);
        /**
         * 4、对其和签名,最后生成签名apk
         */
        //        zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
        File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        Process processAlign = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                +" "+alignedApk.getAbsolutePath());
//        System.out.println("signedApkprocess : 11111" + "  :----->  " +unSignedApk.getAbsolutePath() + "\n" +  alignedApk.getAbsolutePath());

        processAlign.waitFor( 10,TimeUnit.SECONDS);
//        if(process.exitValue()!=0){
//            throw new RuntimeException("dex error");
//        }
        processAlign.destroy();

//        apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
//        apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out  out.apk in.apk
        File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks=new File("proxy_tools/proxy1.jks");
        Process processsign= Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                +" --ks-key-alias wwy --ks-pass pass:123456 --key-pass pass:123456 --out "
                +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
        processsign.waitFor();
        if(processsign.exitValue()!=0){
            throw new RuntimeException("dex error");
        }
        processsign.destroy();
        System.out.println("excute successful");
    }

}

这里加密 我换成了RC4;

我们在写好前面的之后,直接运行这个main方法,就可以在我们的app -> build->outputs->apk->debug下面看到生成的几个apk,分别为 app-debug.apk,  app-unsigned.apk,  app-unsigned-aligned.apk,  app-signed-aligned.apk,最终 app-signed-aligned.apk 才是我们最后安装使用的apk,

使用 java 工具类,需要配置一下环境:

 

1)、配置电脑的环境变量:

如你的 Android   compileSdkVersion 28  请将 D:\AndroidSDK\build-tools\28.0.2 这个路径加入然后我就把这个路劲配置到用户变量中 path 中;重新 启动 Android Studio;

 最后会生成签名后的 apk

 上面的代理ProxyApplication被我们配置到Mainfest的application 标签中,这个位置经常是我们配置项目使用的application的,其实不用担心,代码中已经处理过了,当代理application处理完之后,会自动把我们配置的app里面的项目用到的MyApplication 类替换过来,所以项目在第一次运行完之后,正式运行还是以我们自己的MyApplication为主;


 本文是在https://blog.csdn.net/I123456789T/article/details/91819275 基本上 改进了一些;

1.加密,2.process.waitFor(); 运行问题,见:https://blog.csdn.net/q610098308/article/details/105197814

2.签名问题见:https://blog.csdn.net/q610098308/article/details/105138228

参数 博客:https://blog.csdn.net/zzx410527/article/details/51673908 不过之个 Android 5.0后就不能再用了。

https://blog.csdn.net/I123456789T/article/details/91819275

 

Demo 下载

Android Apk 加固之Dex文件 完善篇 InMemoryDexClassLoader 之内存加载dex 见

https://blog.csdn.net/q610098308/article/details/105246355

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恋恋西风

up up up

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值