APK加固方式

2020-04-26  本文已影响0人  Coder_Sven

一:代码混淆

Proguard是一个代码优化和混淆工具。能够提供对Java类文件的压缩、优化、混淆,和预校验。压缩的步骤是检测并移除未使用的类、字段、方法和属性。优化的步骤是分析和优化方法的字节码。混淆的步骤是使用短的毫无意义的名称重命名剩余的类、字段和方法。压缩、优化、混淆使得代码更小,更高效。该加密方式只是对工程提供了最小的保护,并不是说不能逆向破解;只是说难度增加,需要耐心。混淆规则很简单,就不在这里详细说明了。

二:Dex文件加密

1587636690781.png

一个打包好的APK文件源代码是放在dex文件中的,反编译dex文件可以得到源代码。所以我们需要做的是将dex文件进行加密然后生成一个壳dex给系统去加载,那么别人反编译出来也看不到真正的代码内容。

思路:将源APK文件解压得到里面的所有dex文件,然后将这些文件加密,然后生成一个壳类型的dex文件交给系统去加载,这个文件就算被反编译也暴露不了我们真正的代码,然后在这个壳dex文件的代码中先得到加密了的Apk文件,将APK解压到system/appName文件夹下面,这个文件夹需要有root权限才能访问。解密之前所有加密了的dex文件,将解密的文件存到一个数组里面,然后将这些解密后的文件加载到系统中去

1,将dex文件加密

1587634835058.png

2,生成一个壳dex文件

1587635056750.png

3,将它打包成一个新的APK,并将新的APK执行对齐,签名操作

1587732689763.png

开始编写代码

步骤一:

新建一个命名proxy_core的AndroidLibrary的module,主项目引用这个module,并且将主项目的Application设置为module的ProxyApplication类。我们在这个类里面做解密APK操作,并加载到系统。在后面详细分析

步骤二:

新建一个命名proxy_tools的JavaLibrary的module,这个module是一个工具库,我们在这个库里进行APK加密操作。

操作一:加密源apk文件中所有的dex文件
1587634835058.png
     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);
            FileOutputStream fos=new FileOutputStream(new File(apkTemp,
                    "secret-"+dexFile.getName()));
            fos.write(encrypt);
            fos.flush();
            fos.close();
            dexFile.delete();
        }
操作二:生成壳dex文件。

获取proxy_core壳工程的aar文件:选中proxy_core,点击Build->Make Module 'proxy_core',就会在proxy_core下面的build目录中生成aar文件

1587733412585.png

解压aar文件,得到里面的classes.jar文件。然后使用androidSDK工具中的dx命令将class或者jar打包成dex文件。

1587634962900.png
       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 classesJar=new File(aarTemp,"classes.jar");
        File classesDex=new File(aarTemp,"classes.dex");

        //dx --dex --output out.dex in.jar
        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");
        }

PS:dx命令是在androidsdk的buidtools下面任意一个版本里面,比如我的是(D:\android-sdk\build-tools\26.0.3),需要设置环境变量。

操作三:将操作二中生成的classes.dex文件放到第一步的temp文件夹下。
1587635056750.png
 classesDex.renameTo(new File(apkTemp,"classes.dex"));
操作四:将temp文件夹重新压缩成一个APK文件
  File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
  Zip.zip(apkTemp,unSignedApk);
1587732689763.png

对新生成的APK进行对齐,然后签名

      //通过命令执行对齐
      File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        process=Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                        +" "+alignedApk.getAbsolutePath());
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("zipalign error");
        }
  //通过命令执行签名
  File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        //自己创建签名文件
        File jks=new File("proxy_tools/proxy2.jks");
        process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                            +" --ks-key-alias jett --ks-pass pass:123456 --key-pass pass:123456 --out "
                                +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
        process.waitFor();
        if(process.exitValue()!=0){
            throw new RuntimeException("apksigner error");
        }

如果runtime执行命令一直等待,可以使用cmd命令行来执行。经过上面的操作我们已经完成了APK加固,下一步就是分析如何解密Apk并且可以正常加载到系统中去。

proxy_core(接上面步骤一)
//代理Applicaiton
public class ProxyApplication extends Application {

    //定义好解密后的文件的存放路径
    private String app_name;
    private String app_version;

     /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个代理APPlication中进行解密dex,
     * 然后再把解密后的dex交给原来的APPlication去加载
     * @param base
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取用户填入的metadata
        getMetaData();
        //得到当前加密了的APK文件
        File apkFile = new File(getApplicationInfo().sourceDir);
        //把apk解压   app_name+"_"+app_version目录中的内容需要boot权限才能用
        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();
                //壳dex文件不需要解密
                if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
                    try {
                        AES.init(AES.DEFAULT_PWD);
                        //读取文件内容
                        byte[] bytes = Utils.getBytes(file);
                        //解密
                        byte[] decrypt = AES.decrypt(bytes);
                        //写到指定的目录
                        FileOutputStream fos = new FileOutputStream(file);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                        dexFiles.add(file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } else {
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }
      
        try {
            //把解密后的文件加载到系统[见下面loadDex()]
            loadDex(dexFiles, versionDir);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    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 (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}
1587636757618.png

查看源码分析一下Dex文件的加载过程。

Android主要有两个ClassLoader,分别是PathClassLoader,DexClassLoader。PathClassLoader只会加载系统类和已经安装的APK的dex。DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。一般我们都是用这个DexClassLoader来作为动态加载的加载器。

package dalvik.system;

public class PathClassLoader extends BaseDexClassLoader {
  
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

   
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
package dalvik.system;

import java.io.File;


public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

他们两个本身没什么好分析的,现在我们来看他们的父类BaseDexClassLoader

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }
    
        @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //调用的是DexPathList的findClass方法
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

[--->DexPathList.java]

    public Class findClass(String name, List<Throwable> suppressed) {
        //dexElements  从这个数组里面拿出来所有需要加载的dex文件。然后去加载
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

我们来看看dexElements是在哪里初始化的

final class DexPathList { 
  private Element[] dexElements;
    
  public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
            
        ...
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

        ....    
       
    }
}

(loadDex)这样我们就知道只需要反射得到数组dexElements,并且将我们需要添加的dex文件加入到这个数组中去就完成了。代码如下

   private void loadDex(List<File> dexFiles, File versionDir) throws Exception {
        //getClassLoader()  获取的是PathClassLoader对象
        //pathListField 是指PathClassLoader的父类BaseDexClassLoader的pathList字段
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        //DexPathList类对象
        Object pathList = pathListField.get(getClassLoader());
        //获取到DexPathList类的dexElements字段
        Field dexElementsField = Utils.findField(pathList, "dexElements");
        Object[] dexElements = (Object[]) dexElementsField.get(pathList);
        //反射得到初始化dexElements的方法
        Object[] addElements;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){//7.0  makeDexElements
            Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class,ClassLoader.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            addElements = (Object[]) makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions,getClassLoader());
        }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//6.0  makePathElements
            Method makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
        }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){//5.0 makeDexElements
            Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
        }else{
            return;
        }

        //合并数组
        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);
    }

至此,我们就完成了APK的解密操作,并且也能够正常加载APK应用了。但是我们发现了一个新的问题,那就是我们只能在ProxyApplication这个应用入口做各种初始化操作,不能在主项目中定义我们自己的MyApplicaiton来进行各种初始化工作。所以我们需要想办法将我们主项目的MyApplicaiton替换成真正的applicaiton入口。我在通过源码分析Activity和Applicaiton的启动流程(7.0源码)中分析过了APPlication的加载过程,所以我们可以从源码入手,分析想要替换成真正的MyApplication需要做哪些操作。

源码分析(结合通过源码分析Activity和Applicaiton的启动流程(7.0源码)

[--->ActivityThread.java]

 private void handleBindApplication(AppBindData data) {
        ...
        Application app = data.info.makeApplication(data.restrictedBackupMode, null);
        [第六个要替换的]
        mInitialApplication = app;
        ...        
}

[--->ActivityThread.java]

    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {

        Application app = null;

        [第一个要替换的]
        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }
        ...
      
        java.lang.ClassLoader cl = getClassLoader();
        ...
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        [第二个要替换的]
        appContext.setOuterContext(app);
        ... 
        [第四个要替换的]
        mActivityThread.mAllApplications.add(app);
        [第五个要替换的]
        mApplication = app;

        ...

        return app;
    }

[--->ContextImpl.java ]

    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        return new ContextImpl(null, mainThread,
                packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
    }
    
    
    private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {
   
    //ContextImpl类对象
    mOuterContext = this;

    ...
    //ActivityThread类对象
    mMainThread = mainThread;
    mActivityToken = activityToken;
    mFlags = flags;

    if (user == null) {
        user = Process.myUserHandle();
    }
    mUser = user;

    //LoadedApk类对象
    mPackageInfo = packageInfo;
    ...


}
    

[--->Instrumentation.java ]

    static public Application newApplication(Class<?> clazz, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        Application app = (Application)clazz.newInstance();
        [第三个要替换的]
        app.attach(context);
        return app;
    }

[--->Application.java]

    /**
     * @hide
     */
    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }

第一个替换的位置:String appClass = mApplicationInfo.className;

//反射得到  LoadedApk.mApplicationInfo字段
Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
 
//LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;        

第二个需要替换的位置:appContext.setOuterContext(app)

Field mOuterContext = contextImplClass.getDeclaredField
("mOuterContext");
mOuterContext.setAccessible(true);
mOuterContext.set(baseContext, application);

第三个需要替换的位置:app.attach(context);

  //创建用户真实的application(MyApplication)
Class<?> delegateClass = Class.forName(app_name);
application = (Application) delegateClass.newInstance();
//调用Application.attach(context)方法
        Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
 //设置可用
declaredMethod.setAccessible(true);
//得到Application.attach(Context context)传入的context对象
Context baseContext = getBaseContext();
declaredMethod.invoke(application,baseContext);  

第四个需要替换的位置: mActivityThread.mAllApplications.add(app);

  // ActivityThread类对象通过反射ContextImpl.mMainThread得到
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        //得到的是ActivityThread类对象
        Object mMainThread = mMainThreadField.get(baseContext);
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);
//        mAllApplications.remove(this);
        mAllApplications.add(application);

第五个需要替换的位置:mApplication = app;

Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo, application);

第六个需要替换的位置:mInitialApplication = app;

Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread, application);

开始替换

   /**
     * 开始替换APPlication,加载真正应用的Application
     */
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            bindRealApplicatin();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
       /**
     * 这个方法主要作用是创建其他程序的Context,
     * 通过这个Context可以访问该软件包的资源,甚至可以执行其他软件包的代码。
     * 这个代码不写的话,主项目中ContentProvider使用的context就没办法换回来,还是用的ProxyApplication
     * @param packageName
     * @param flags
     * @return
     * @throws PackageManager.NameNotFoundException
     */
    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(app_name)) {
            return super.createPackageContext(packageName, flags);
        }
        try {
            bindRealApplicatin();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return application;
    }
    
    private void bindRealApplicatin() throws Exception {
        if (isBindReal) {
            return;
        }
        if (TextUtils.isEmpty(app_name)) {
            return;
        }

        //创建用户真实的application(MyApplication)
        Class<?> delegateClass = Class.forName(app_name);
        application = (Application) delegateClass.newInstance();
        //调用Application.attach(context)方法
        Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
        //设置可用
        declaredMethod.setAccessible(true);
        //得到Application.attach(Context context)传入的context对象
        Context baseContext = getBaseContext();
        declaredMethod.invoke(application, baseContext);

        //第一处: String appClass = mApplicationInfo.className;
        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        //LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(baseContext);
        ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className = app_name;

        //第二处:appContext.setOuterContext(app);
        Field mOuterContext = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContext.setAccessible(true);
        mOuterContext.set(baseContext, application);

        //第三处:mActivityThread.mAllApplications.add(app);
        // ActivityThread类对象通过反射ContextImpl.mMainThread得到
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        //得到的是ActivityThread类对象
        Object mMainThread = mMainThreadField.get(baseContext);
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
//        mAllApplications.remove(this);
        mAllApplications.add(application);

        //第四处:mApplication = app;
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo, application);

        //第五处:mInitialApplication = app;
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread, application);

        //调用Application的oncreat方法
        application.onCreate();
        isBindReal = true;
    }

附上项目代码

[https://github.com/games2sven/ReinForceApk​]

上一篇下一篇

猜你喜欢

热点阅读