安卓逆向笔记--apk加固

安卓逆向笔记–apk加固

资料来源:
浅谈安卓apk加固原理和实现
Android中的Apk的加固(加壳)原理解析和实现
前两个太老了所以具体代码借鉴下面的
Android Apk加壳技术实战详解

一、apk常见加固方法

(1)代码层级加密–代码混淆

        代码混淆是一种常见的加密方式。本质是把工程中原来具有含义的类名、变量名、方法名,修改成让人看不懂的名字。常见的代码混淆工具proguard。该加密方式只是对工程提供了最小的保护,并不是说不能逆向破解;只是说难度增加,需要耐心。

(2)Dex文件加密

        dex是Android工程中的代码资源文件,通过dex可以反编译出java代码。dex的加壳是常见的加密方式。通过对dex文件加密拼接加壳,可以有效的对工程代码进行保护。apk工程在安装成功后,app启动时会有dex解密的过程,然后重新加载解密后的dex文件。基本原理是在jni层, 使用DexClassLoader动态加载技术完成对加密classex.dex的动态加载,dex文件可以附属在assert或raw目录。

二、apk文件结构

(1)什么是dex文件

        dex是应用安装时生成的虚拟机可执行二进制文件,如果应用还存在,删除了下次手机开机时还会再次生成,卸载软件时会同时删除dex文件。
  对于Android DEX文件进行优化,需要注意的一点是DEX文件的结构是紧凑的,但是我们还是要想方设法的进行提高程序的运行速度,我们就仍然需要对DEX文件进行进一步优化。
  调整所有字段的字节序(LITTLE_ENDIAN)和对齐结构中的每一个域 验证DEX文件中的所有类 对一些特定的类进行优化,对方法里的操作码进行优化 。优化后的文件大小会有所增加,应该是原Android DEX文件的1-4倍。 优化发生的时机有两个:对于预置应用,可以在系统编译后,生成优化文件,以ODEX结尾。
  这样在发布时除APK文件(不包含DEX)以外,还有一个相应的Android DEX文件;对于非预置应用,包含在APK文件里的DEX文件会在运行时被优化,优化后的文件将被保存在缓存中。
  每一个Android应用都运行在一个Dalvik虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间。虚拟机的线程机制,内存分配和管理,Mutex等等都是依赖底层操作系统而实现的。

(2)dex文件格式

安卓逆向笔记--apk加固_第1张图片
dex文件头
安卓逆向笔记--apk加固_第2张图片
(1) checksum 文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
(2) signature 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
(3) file_size Dex 文件的总长度 。

加固原理

加固过程中的三个对象
(1)需要加固的java工程
(2)壳程序APK(负责解密apk工作)
(3)加密工具(将源apk进行加密和壳dex合并成新的dex)
主要流程
用加密算法加固源apk与壳apk进行合并得到新的dex文件,最后替换壳程序中的dex文件即可,得到新的apk(脱壳程序apk)。这个apk已经不是完整意义上的apk,他只负责解密源apk。加载apk,让其正常的运行起来。
安卓逆向笔记--apk加固_第3张图片
安卓逆向笔记--apk加固_第4张图片
加固后工程的加载原理
APP启动——>自定义Application中attachBaseContext()方法——>自定义Application工程onCreate()方法——>源Application
1、自定义Application来自于壳程序的dex,加密合成的新dex前半部分就是壳程序的dex,这部分是没任何问题,可以正常加载。该Application中attachBaseContext方法会做解密操作,解密出源dex并放置在固定目录下,添加dex的加载映射;映射到源dex目录。
2、自定义Application工程onCreate()方法添加源dex加载的入口;即源dex的application和mainActivity。
3、程序正常启动;源dex被正确加载。

加固实现

一、编写需要加壳的APK

首先需要一个项目,自己可以编写一个,借鉴大佬们的apk写了一个。
apk链接:https://pan.baidu.com/s/1d7ZS-_QWbSVqar-7ehjMTw 提取码:pzn9
没法用百度网盘了,要我充值超级会员才能上传,果断github
代码链接: https://github.com/iaa-mr/android_protect
demo为需要加壳的程序
在编写的的时候需要注意一下AndroidManifest.xml这个文件的配置,以及MyApplication.java的内容.
一定一定要注意AndroidManifest.xml文件内容和签名
一定一定要注意AndroidManifest.xml文件内容和签名
一定一定要注意AndroidManifest.xml文件内容和签名
重要的事情说三遍
AndroidManifest.xml文件内容




    
    

    
        
            
                

                
            
        
    


MyApplication.java内容

package com.example.demo;

import android.app.Application;
import android.util.Log;

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("demo", "apk onCreate:" + this);
    }
}
二、编写解壳的APK

代码链接: https://github.com/iaa-mr/android_protect
这个加壳主要原理是动态加载程序,具体原理:
http://blog.csdn.net/jiangwei0910410003/article/details/48104455
在编写的的时候需要注意一下AndroidManifest.xml这个文件的配置,以及MyApplication.java的内容.
一定一定要注意AndroidManifest.xml文件内容和签名
一定一定要注意AndroidManifest.xml文件内容和签名
一定一定要注意AndroidManifest.xml文件内容和签名
重要的事情说三遍
AndroidManifest.xml文件内容



因为这个壳程序使用来动态加载之前的demo程序,所以包名是demo的。
并且需要将demo的res文件整体以到这个解壳程序。
一定要注意这两段语句,否则加完壳后程序将无法运行。




    
    

    
        

        
            
                

                
            
        
    


ProxyApplication.java内容

package com.example.myunshell;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;

import com.example.myunshell.RefInvoke;

import dalvik.system.DexClassLoader;

public class ProxyApplication extends Application{
    private static final String appkey = "APPLICATION_CLASS_NAME";
    private String apkFileName;
    private String odexPath;
    private String libPath;

    //这是context 赋值
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
            File odex = this.getDir("demo_odex", MODE_PRIVATE);
            File libs = this.getDir("demo_lib", MODE_PRIVATE);
            odexPath = odex.getAbsolutePath();
            libPath = libs.getAbsolutePath();
            apkFileName = odex.getAbsolutePath() + "/shelldemo.apk";
            File dexFile = new File(apkFileName);
            Log.i("demo", "apk size:"+dexFile.length());
            if (!dexFile.exists())
            {
                dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
                // 读取程序classes.dex文件
                byte[] dexdata = this.readDexFileFromApk();

                // 分离出解壳后的apk文件已用于动态加载
                this.splitPayLoadFromDex(dexdata);
            }
            // 配置动态加载环境
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
            String packageName = this.getPackageName();//当前apk的包名
            //下面两句不是太理解
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);
            //创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
            DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
                    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));
            //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
            //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                    wr.get(), dLoader);

            Log.i("demo","classloader:"+dLoader);


        } catch (Exception e) {
            Log.i("demo", "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        {
            //loadResources(apkFileName);

            Log.i("demo", "onCreate");
            // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
            String appClassName = null;
            try {
                ApplicationInfo ai = this.getPackageManager()
                        .getApplicationInfo(this.getPackageName(),
                                PackageManager.GET_META_DATA);
                Bundle bundle = ai.metaData;
                if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
                    appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
                } else {
                    Log.i("demo", "have no application class name");
                    return;
                }
            } catch (NameNotFoundException e) {
                Log.i("demo", "error:" + Log.getStackTraceString(e));
                e.printStackTrace();
            }
            //有值的话调用该Applicaiton
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[]{}, new Object[]{});
            Object mBoundApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mBoundApplication");
            Object loadedApkInfo = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread$AppBindData",
                    mBoundApplication, "info");
            //把当前进程的mApplication 设置成了null
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
                    loadedApkInfo, null);
            Object oldApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mInitialApplication");
            //http://www.codeceo.com/article/android-context.html
            ArrayList mAllApplications = (ArrayList) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread",
                            currentActivityThread, "mAllApplications");
            mAllApplications.remove(oldApplication);//删除oldApplication

            ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
                            "mApplicationInfo");
            ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread$AppBindData",
                            mBoundApplication, "appInfo");
            appinfo_In_LoadedApk.className = appClassName;
            appinfo_In_AppBindData.className = appClassName;
            Application app = (Application) RefInvoke.invokeMethod(
                    "android.app.LoadedApk", "makeApplication", loadedApkInfo,
                    new Class[]{boolean.class, Instrumentation.class},
                    new Object[]{false, null});//执行 makeApplication(false,null)
            RefInvoke.setFieldOjbect("android.app.ActivityThread",
                    "mInitialApplication", currentActivityThread, app);


            ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mProviderMap");
            Iterator it = mProviderMap.values().iterator();
            while (it.hasNext()) {
                Object providerClientRecord = it.next();
                Object localProvider = RefInvoke.getFieldOjbect(
                        "android.app.ActivityThread$ProviderClientRecord",
                        providerClientRecord, "mLocalProvider");
                RefInvoke.setFieldOjbect("android.content.ContentProvider",
                        "mContext", localProvider, app);
            }

            Log.i("demo", "app:" + app);

            app.onCreate();
        }
    }

    /**
     * 释放被加壳的apk文件,so文件
     * @param
     * @throws IOException
     */
    private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
        int ablen = apkdata.length;
        //取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化
        byte[] dexlen = new byte[4];
        System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
        DataInputStream in = new DataInputStream(bais);
        int readInt = in.readInt();
        System.out.println(Integer.toHexString(readInt));
        byte[] newdex = new byte[readInt];
        //把被加壳apk内容拷贝到newdex中
        System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
        //这里应该加上对于apk的解密操作,若加壳是加密处理的话
        //?

        //对源程序Apk进行解密
        newdex = decrypt(newdex);

        //写入apk文件
        File file = new File(apkFileName);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(newdex);
            localFileOutputStream.close();
        } catch (IOException localIOException) {
            throw new RuntimeException(localIOException);
        }

        //分析被加壳的apk文件
        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(file)));
        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
            String name = localZipEntry.getName();
            if (name.startsWith("lib/") && name.endsWith(".so")) {
                File storeFile = new File(libPath + "/"
                        + name.substring(name.lastIndexOf('/')));
                storeFile.createNewFile();
                FileOutputStream fos = new FileOutputStream(storeFile);
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    fos.write(arrayOfByte, 0, i);
                }
                fos.flush();
                fos.close();
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();


    }

    /**
     * 从apk包里面获取dex文件内容(byte)
     * @return
     * @throws IOException
     */
    private byte[] readDexFileFromApk() throws IOException {
        ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
        ZipInputStream localZipInputStream = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(
                        this.getApplicationInfo().sourceDir)));
        while (true) {
            ZipEntry localZipEntry = localZipInputStream.getNextEntry();
            if (localZipEntry == null) {
                localZipInputStream.close();
                break;
            }
            if (localZipEntry.getName().equals("classes.dex")) {
                byte[] arrayOfByte = new byte[1024];
                while (true) {
                    int i = localZipInputStream.read(arrayOfByte);
                    if (i == -1)
                        break;
                    dexByteArrayOutputStream.write(arrayOfByte, 0, i);
                }
            }
            localZipInputStream.closeEntry();
        }
        localZipInputStream.close();
        return dexByteArrayOutputStream.toByteArray();
    }


    // //直接返回数据,读者可以添加自己解密方法
    private byte[] decrypt(byte[] srcdata) {
        for(int i=0;i

具体代码在github里面有

三、进行合并dex文件

代码链接: https://github.com/iaa-mr/android_protect
DexShellTool文件夹里就是代码
这个可以直接做一个单独的java程序以后改改就能用,并不需要新建一个单独的apk项目。
具体代码

File payloadSrcFile = new File("C:/Users/myh18/Desktop/DexShellTool/force/demo.apk");   //需要加壳的程序
File unShellDexFile = new File("C:/Users/myh18/Desktop/DexShellTool/force/shell.dex");    //解壳dex
String str = "C:\\Users/myh18/Desktop/DexShellTool/force/classes.dex";

这个里面的路径要自己写。
最核心的代码
用来合并两个dex文件,并修改合并后的dex文件的长度、SHA1和CheckSum的文件头

  int payloadLen = payloadArray.length;
            int unShellDexLen = unShellDexArray.length;
            int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
            byte[] newdex = new byte[totalLen]; // 申请了新的长度
            //添加解壳代码
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
            //添加加密后的解壳数据
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
            //添加解壳数据长度
            System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
            //修改DEX file size文件头
            fixFileSizeHeader(newdex);
            //修改DEX SHA1 文件头
            fixSHA1Header(newdex);
            //修改DEX CheckSum文件头
            fixCheckSumHeader(newdex);

加密语句

byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
 //直接返回数据,读者可以添加自己加密方法
    private static byte[] encrpt(byte[] srcdata){
        for(int i = 0;i

所有加壳代码

import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;

/**
 * Created by DeMon on 2017/10/17.
 */
public class DexShellTool {
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            File payloadSrcFile = new File("C:/Users/myh18/Desktop/DexShellTool/force/demo.apk");   //需要加壳的程序
            System.out.println("apk size:"+payloadSrcFile.length());
            File unShellDexFile = new File("C:/Users/myh18/Desktop/DexShellTool/force/shell.dex");    //解壳dex
            byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
            byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex
            int payloadLen = payloadArray.length;
            int unShellDexLen = unShellDexArray.length;
            int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
            byte[] newdex = new byte[totalLen]; // 申请了新的长度
            //添加解壳代码
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
            //添加加密后的解壳数据
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
            //添加解壳数据长度
            System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
            //修改DEX file size文件头
            fixFileSizeHeader(newdex);
            //修改DEX SHA1 文件头
            fixSHA1Header(newdex);
            //修改DEX CheckSum文件头
            fixCheckSumHeader(newdex);

            String str = "C:\\Users/myh18/Desktop/DexShellTool/force/classes.dex";
            File file = new File(str);
            if (!file.exists()) {
                file.createNewFile();
            }

            FileOutputStream localFileOutputStream = new FileOutputStream(str);
            localFileOutputStream.write(newdex);
            localFileOutputStream.flush();
            localFileOutputStream.close();


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //直接返回数据,读者可以添加自己加密方法
    private static byte[] encrpt(byte[] srcdata){
        for(int i = 0;i= 0; i--) {
            b[i] = (byte) (number % 256);
            number >>= 8;
        }
        return b;
    }

    /**
     * 修改dex头 sha1值
     * @param dexBytes
     * @throws NoSuchAlgorithmException
     */
    private static void fixSHA1Header(byte[] dexBytes)
            throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
        byte[] newdt = md.digest();
        System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
        //输出sha-1值,可有可无
        String hexstr = "";
        for (int i = 0; i < newdt.length; i++) {
            hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
                    .substring(1);
        }
        System.out.println(hexstr);
    }

    /**
     * 修改dex头 file_size值
     * @param dexBytes
     */
    private static void fixFileSizeHeader(byte[] dexBytes) {
        //新文件长度
        byte[] newfs = intToByte(dexBytes.length);
        System.out.println(Integer.toHexString(dexBytes.length));
        byte[] refs = new byte[4];
        //高位在前,低位在前掉个个
        for (int i = 0; i < 4; i++) {
            refs[i] = newfs[newfs.length - 1 - i];
            System.out.println(Integer.toHexString(newfs[i]));
        }
        System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
    }


    /**
     * 以二进制读出文件内容
     * @param file
     * @return
     * @throws IOException
     */
    private static byte[] readFileBytes(File file) throws IOException {
        byte[] arrayOfByte = new byte[1024];
        ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream(file);
        while (true) {
            int i = fis.read(arrayOfByte);
            if (i != -1) {
                localByteArrayOutputStream.write(arrayOfByte, 0, i);
            } else {
                return localByteArrayOutputStream.toByteArray();
            }
        }
    }
}
四、实现加壳的步骤

编写完程序后,确保所有程序的签名一致。
1、解壳程序MyUnShell的apk用压缩软件打开得到classes.dex,重命名为shell.dex
2、将demo.apk、shell.dex复制到force文件夹中
3、运行DexShellTool.java
运行方法,首先cmd到DexShellTool.java所在的位置,然后输入javac DexShellTool,java
然后,会生成一个class文件,不用管,继续输入java DexShellTool就可以了
生成的dex文件的大小近似于两个文件大小的和
4、将MyUnShell生成的apk命名为shell.apk,将生成的classes.dex替换里面的classes.dex文件
并将shell.apk的签名文件删除。
5、将shell.apk放在tools文件夹,运行sign.bat,生成shelldemo.apk就是已经加完壳的apk。
sign.bat内容

jarsigner -verbose -keystore DeMon.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar shelldemo.apk shell.apk key

DeMon.jks 可以换成你自己的签名文件,123456为签名密码,shelldemo.apk 为生成的文件,shell.apk 为需要签名的文件。

五、查看加壳是否成功

首先运行demo程序,和shelldemo程序。
demo程序
安卓逆向笔记--apk加固_第5张图片
shelldemo程序
安卓逆向笔记--apk加固_第6张图片
此时两个文件的包名就已经不一样了,但不影响程序正常运行。
拖到android killer
demo程序
安卓逆向笔记--apk加固_第7张图片
demo逆向后,发现可以看到源代码
shelldemo程序
安卓逆向笔记--apk加固_第8张图片
发现代码变成了解壳程序的代码,android killer中按照包名找不到所需要的文件。加壳成功
ps:附上查apk签名cmd命令
keytool -list -printcert -jarfile 文件路径
直接使用本文中的签名可能会看到。不是已签名的 jar 文件,从网上搜不到个所以然,我认为是因为重新签名。。。
然后apk的壳就写完了,但是360加固,腾讯乐固,都是在.so文件中写解壳程序,并加密。

你可能感兴趣的:(android,安全)