APK加固原理详解

article/2025/10/6 15:43:03
一、前言

之前使用的360加固,挺好用的,从2021年底的时候限制每天每个账号仅上传2次apk(免费的,不知道VIP的是不是这样)。通过这个事情,感觉技术还是掌握在自己手里稳妥点,不用受制于人,想怎么玩就怎么玩。

通过技术调研有两条路子可以走:

  • 方式一:直接对apk进行加密,启动应用时通过壳程序去加载apk运行;

  • 方式二:仅对原apk的dex文件进行加密,启动应用时对dex解密,通过DexClassLoader进行加载;

本文主要是参考了360免费加固的思路,所以主要研究的方式二。

二、原理

先看下流程,然后再来详细讲下具体的步骤

加固流程

根据上流程图可以总结如下七个步骤:
步骤一:将加固壳中的aar中的jar利用dx工具转成dex文件
步骤二:将需要加固的APK解压,并将所有dex文件打包成一个zip包,方便后续进行加密处理
步骤三:对步骤二的zip包进行加密,并与壳dex合成新dex文件
步骤四:修改AndroidManifest(替换Application的android:name属性和新增)
步骤五:将步骤三生成的新dex文件替换apk中的所有dex文件
步骤六:APK对齐处理
步骤七:对生成的APK进行签名

到这其实就把APK加固流程讲完了,下面就来结合项目对各步骤进行详解。

三、项目案例

从上步骤中可以看到,加固会涉及到三个工程为:demojiagu_shelljiaguLib,如下图:image.png

demo工程
普通的app工程,即我们平时开发的工程。主要用于生成待加固的apk。

jiaguLib工程
apk加固主工程,完成apk的加固工作。
1.将加固壳中的aar中的jar利用dx工具转成dex文件
生成Aar包:选中jiagu_shell工程,Build - Make Module ‘apkjiagu.jiagu_shell’,会在jiagu_shell - build - outputs - aar目录中生成jiagu_shell-debug.aar
aar包生成后,就可以利用dx工具生成dex了,如下核心代码:

/*** 步骤一:将加固壳中的aar中的jar转成dex文件* @throws Exception 异常*/private File shellAar2Dex() throws Exception{logTitle("步骤一:将加固壳中的aar中的jar转成dex文件");//步骤一:将加固壳中的aar中的jar转成dex文件File aarFile = new File(ROOT+"aar/jiagu_shell-release.aar");File aarTemp = new File(OUT_TMP+"shell");ZipUtil.unZip(aarFile, aarTemp);File classesJar = new File(aarTemp, "classes.jar");File classesDex = new File(aarTemp, "classes.dex");boolean ret = ProcessUtil.executeCommand(String.format(Locale.CHINESE,"dx --dex --output %s %s",classesDex.getAbsolutePath(),classesJar.getAbsolutePath()));if (ret){System.out.println("已生成======"+classesDex.getPath());}return classesDex;}

ZipUtil中unZip方法:

public static void unZip(File apkFile,File destDir) throws Exception{// 判断源文件是否存在if (!apkFile.exists()) {throw new Exception(apkFile.getPath() + "所指文件不存在");}//开始解压//构建解压输入流ZipInputStream zIn = new ZipInputStream(new FileInputStream(apkFile));ZipEntry entry = null;File file = null;while ((entry = zIn.getNextEntry()) != null) {if (!entry.isDirectory() && !entry.getName().equals("")) {file = new File(destDir, entry.getName());if (!file.exists()) {file.getParentFile().mkdirs();//创建此文件的上级目录}FileOutputStream fos = new FileOutputStream(file);int len = -1;byte[] buf = new byte[1024];while ((len = zIn.read(buf)) != -1) {fos.write(buf, 0, len);}// 关流顺序,先打开的后关闭fos.flush();fos.close();}else {file = new File(destDir, entry.getName());//是文件夹的时候创建目录if (!file.exists()){file.mkdirs();}}zIn.closeEntry();}zIn.close();}

ProcessUtil中executeCommand为执行命令方法,如下:

public static boolean executeCommand(String cmd) throws Exception{System.out.println("开始执行命令===>"+cmd);Process process = Runtime.getRuntime().exec("cmd /c "+cmd);ProcessUtil.consumeInputStream(process.getInputStream());ProcessUtil.consumeInputStream(process.getErrorStream());process.waitFor();if (process.exitValue() != 0) {throw new RuntimeException("执行命令错误===>"+cmd);}return true;}

jar转dex的命令

命令:dx --dex --output [输出dex] [输入的jar]

2.对待加固的APK解压,并将所有dex文件打包成一个zip包
直接对待加固的apk进行unzip,然后拿到解压目录中的所有dex文件,并打包成一个新的zip。代码如:

private File apkUnzipAndZipDexFiles(){logTitle("步骤二:将需要加固的APK解压,并将所有dex文件打包成一个zip包,方便后续进行加密处理");//下面加密码APK中所有的dex文件File apkFile = new File(ORIGIN_APK);File apkTemp = new File(OUT_TMP+"unzip/");try {//首先把apk解压出来ZipUtil.unZip(apkFile, apkTemp);//其次获取解压目录中的dex文件File dexFiles[] = apkTemp.listFiles(new FilenameFilter() {@Overridepublic boolean accept(File file, String s) {return s.endsWith(".dex");}});if (dexFiles == null) return null;//三:将所有的dex文件压缩为AppDex.zip文件File outTmpFile = new File(OUT_TMP);File outputFile = new File(outTmpFile,"AppDex.zip");//创建目录if (!outTmpFile.exists()){outTmpFile.mkdirs();}if (outputFile.exists()){outputFile.delete();}Zip4jUtil.zipFiles(dexFiles,outputFile);System.out.println("已生成======"+outputFile.getAbsolutePath());FileUtils.deleteFile(apkTemp.getAbsolutePath());return outputFile;}catch (Exception e){e.printStackTrace();}return null;}

这一步比较简单,仅涉及文件的解压和压缩操作。
值得注意:采用系统自带的ZipOutputSteam对dex压缩会存在Bad size问题,故这里采用zip4j包进行压缩。

3.对上述生成的zip进行加密,然后合并到壳dex中
这一步比较关键,涉及到dex文件格式,需要对dex格式进行一定了解。
可以参考Dex文件结构
我们只需要关注以下三个部分:

  • checksum,文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
  • signature,使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
  • file_size,Dex文件的总长度。

为什么说我们只需要关注这三个字段呢?

因为我们需要将一个文件(加密之后的源dex包)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。

不过这里还需要一个操作,就是标注一下我们加密的Zip的大小,当我们脱壳的时候,需要知道Zip的大小,才能正确的得到Zip。这个值直接放到文件的末尾就可以了。

所以总结一下我们需要做:修改Dex的三个文件头,将源Apk的dex包大小追加到壳dex的末尾就可以了。
我们修改之后得到新的Dex文件样式如下:
image.png
具体实现代码如下:

private File combine2NewDexFile(File shellDexFile,File originalDexZipFile){logTitle("步骤三:对步骤二的zip包进行加密,并与壳dex合成新dex文件");try {AESUtil aesUtil = new AESUtil();byte[] data = readFileBytes(originalDexZipFile);System.out.println("加密前数据大小为:"+data.length);byte[] payloadArray = aesUtil.encrypt(data);//以二进制形式读出zip,并进行加密处理//对源Apk进行加密操作byte[] unShellDexArray = readFileBytes(shellDexFile);//以二进制形式读出dexint 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 = OUT_TMP + "classes.dex";File file = new File(str);if (!file.exists()) {file.createNewFile();}//输出成新的dex文件FileOutputStream localFileOutputStream = new FileOutputStream(str);localFileOutputStream.write(newdex);localFileOutputStream.flush();localFileOutputStream.close();System.out.println("已生成新的Dex文件======"+str);//删除dex的zip包FileUtils.deleteFile(originalDexZipFile.getAbsolutePath());return file;} catch (Exception e) {e.printStackTrace();}return null;}

注意:为了提高破解难度,本文加解密代码采用C写的,并编译成dll文件被java工程引用。若不想那么麻烦可自行修改加密方式。
将C/C++编译成dll供Java工程使用

readFileBytes方法:

    private 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();}}}

修改文件大小方法,fixFileSizeHeader方法:

private void fixFileSizeHeader(byte[] dexBytes) {//新文件长度byte[] newfs = intToByte(dexBytes.length);System.out.println("fixFileSizeHeader ===== size : " + dexBytes.length);byte[] refs = new byte[4];//高位在前,低位在前掉个个for (int i = 0; i < 4; i++) {refs[i] = newfs[newfs.length - 1 - i];}System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)}

修改dex头中的sinature方法,fixSHA1Header:

    /*** 修改dex头 sha1值* @param dexBytes* @throws NoSuchAlgorithmException*/private void fixSHA1Header(byte[] dexBytes)throws NoSuchAlgorithmException {MessageDigest md = MessageDigest.getInstance("SHA-1");md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1byte[] 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);}}

修改CheckSum值

    /*** 修改dex头,CheckSum 校验码* @param dexBytes*/private void fixCheckSumHeader(byte[] dexBytes) {Adler32 adler = new Adler32();adler.update(dexBytes, 12, dexBytes.length - 12);//从12到文件末尾计算校验码long value = adler.getValue();int va = (int) value;byte[] newcs = intToByte(va);//高位在前,低位在前掉个个byte[] recs = new byte[4];for (int i = 0; i < 4; i++) {recs[i] = newcs[newcs.length - 1 - i];}System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)}

到这里,我们就生成了加密后的dex文件,这时在Android studio中查看,你会发现仅能看到脱壳的类信息。
4.修改原APK中的AndroidManifest.xml文件
为了保证能正常使用apktool命令对apk正常反编译和回编译,我们要先修改AndroidManifest.xml,再对dex进行替换。若先替换dex,在对apk进行回编译时,加密的数据回丢失,导致包错误。
在这一步,主要采用apktool对apk进行反编译,通过代码修改AndroidManifest.xml,然后在进行回编译重新生成新的Apk。
具体实现代码如下:

    private String modifyOriginApkManifest() throws Exception{String apkPath = ORIGIN_APK;String outputPath = OUT_TMP + "apk/";logTitle("步骤四:修改AndroidManifest(Application的android:name属性和新增<meta-data>)");String path = "";long start = System.currentTimeMillis();//1:执行命令进行反编译原apkSystem.out.println("开始反编译原apk ......");boolean ret = ProcessUtil.executeCommand("apktool d -o " + outputPath + " " + apkPath);if (ret){//2.修改AndroidManifest.xml,使用壳的Application替换原Application,并将原Application名称配置在meta-data中modifyAndroidManifest(new File(outputPath,"AndroidManifest.xml"));//3:重新编译成apk,仍以原来名称命名System.out.println("开始回编译apk ......");String apk = OUT_TMP + apkPath.substring(apkPath.lastIndexOf("/")+1);ret = ProcessUtil.executeCommand(String.format(Locale.CHINESE,"apktool b -o %s %s",apk,outputPath));if (ret){path = apk;}System.out.println("=== modifyOriginApkManifest ==== "+(System.currentTimeMillis()-start)+"ms");}return path;}

修改AndroidManifest.xml主要做的内容为:
1.替换标签中android:name值为com.zhh.jiagu.shell.StubApplication
2.添加记录原application配置的name值,
<meta-data android:name="APPLICATION_CLASS_NAME" android:value="原apk的Application name"/>
具体代码如下:

    private void modifyAndroidManifest(File xmlFile){if (xmlFile == null){System.out.println("请设置AndroidManifest.xml文件");return;}if (!xmlFile.exists()){System.out.println("指定的AndroidManifest.xml文件不存在");return;}System.out.println("开始修改AndroidManifest.xml......");String shellApplicationName = "com.zhh.jiagu.shell.StubApplication";String metaDataName = "APPLICATION_CLASS_NAME";String attrName = "android:name";//采用Dom读取AndroidManifest.xml文件try {//1.实例化Dom工厂DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();//2.构建一个builderDocumentBuilder builder = factory.newDocumentBuilder();//3.通过builder解析xml文件Document document = builder.parse(xmlFile);NodeList nl = document.getElementsByTagName("application");if (nl != null){Node app = nl.item(0);//获取原APK中applicationString applicationName = "android.app.Application";NamedNodeMap attrMap = app.getAttributes();//有属性时Node node = app.getAttributes().getNamedItem(attrName);//默认为系统的Applicationif (node != null){applicationName = node.getNodeValue();node.setNodeValue(shellApplicationName);}else {//不存在该属性时,则创建一个Attr attr = document.createAttribute(attrName);attr.setValue(shellApplicationName);attrMap.setNamedItem(attr);}//添加<meta-data>数据,记录原APK的applicationElement metaData = document.createElement("meta-data");metaData.setAttribute("android:name",metaDataName);metaData.setAttribute("android:value",applicationName);app.appendChild(metaData);//重新写入文件xml文件TransformerFactory outFactory = TransformerFactory.newInstance();Transformer transformer = outFactory.newTransformer();Source xmlSource = new DOMSource(document);Result outResult = new StreamResult(xmlFile);transformer.transform(xmlSource,outResult);System.out.println("已完成修改AndroidManifest文件======");}}catch (Exception e){e.printStackTrace();}}

这一步使用的命令为:

apktool d -o [输出目录] [apk]
apktool b -o [输出apk] [回编译目录]

5.将新编译的apk中的所有dex删除,并将上述生成的新dex文件添加进apk中
删除dex文件方法:

    public static void deleteDexFromZip(String zipFilePath) throws ZipException{ZipFile zipFile = new ZipFile(zipFilePath);List<FileHeader> files = zipFile.getFileHeaders();List<String> dexFiles = new ArrayList<>();for (FileHeader file : files) {if (file.getFileName().endsWith(".dex")) {dexFiles.add(file.getFileName());}}zipFile.removeFiles(dexFiles);}

添加dex到apk中的方法:

public static void addFile2Zip(String zip,String filepath,String rootFolder) throws ZipException{ZipFile zipFile = new ZipFile(zip);ZipParameters parameters = new ZipParameters();/** 压缩方式* COMP_STORE = 0;(仅打包,不压缩)* COMP_DEFLATE = 8;(默认)* COMP_AES_ENC = 99; 加密压缩*/parameters.setCompressionMethod(CompressionMethod.DEFLATE);/** 压缩级别* DEFLATE_LEVEL_FASTEST = 1; (速度最快,压缩比最小)* DEFLATE_LEVEL_FAST = 3; (速度快,压缩比小)* DEFLATE_LEVEL_NORMAL = 5; (一般)* DEFLATE_LEVEL_MAXIMUM = 7;* DEFLATE_LEVEL_ULTRA = 9;*/parameters.setCompressionLevel(CompressionLevel.NORMAL);// 目标路径if (rootFolder == null){rootFolder = "";}parameters.setRootFolderNameInZip(rootFolder);zipFile.addFile(filepath, parameters);}

如果将加密和解密通过JNI调用的,则记得要把so文件复制仅apk中(示例中就采用这种方式,有些可能仅采用Java加密,故复制so代码部分就不贴出来了,若感兴趣可以查看文章末尾的源码)。

6.apk对齐处理
到了这一步APK加固的主要工作其实已经完成了,只剩下对APK进行对齐处理和签名工作了。
apk对齐命令:zipalign -v -p 4 [输入的apk] [对齐后的apk]
具体实现代码如下:

private File zipalignApk(File unAlignedApk) throws Exception{logTitle("步骤六:重新对APK进行对齐处理.....");//步骤四:重新对APK进行对齐处理File alignedApk = new File(unAlignedApk.getParent(),unAlignedApk.getName().replace(".apk","_align.apk"));boolean ret = ProcessUtil.executeCommand("zipalign -v -p 4 " + unAlignedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());if (ret){System.out.println("已完成APK进行对齐处理======");}//删除未对齐的包FileUtils.deleteFile(unAlignedApk.getAbsolutePath());return alignedApk;}

7.签名
在Android系统中,未签名的Apk是无法正常安装运行的,因此我们要对上述对齐后的apk进行一次签名处理。

命令:apksigner sign --ks [签名文件] --ks-key-alias [alias名字] --min-sdk-version 21 --ks-pass pass:[keystore密码] --key-pass pass:[key密码] --out [输出apk] [输入apk]
具体实现代码为:

private File resignApk(File unSignedApk) throws Exception{logTitle("步骤七:对生成的APK进行签名");KeyStore store = KeyStoreUtil.readKeyStoreConfig((isRelease ? "":"jiaguLib/")+KEYSTORE_CFG);//步骤五:对APK进行签名File signedApk = new File(ROOT+"out",unSignedApk.getName().replace(".apk","_signed.apk"));//创建保存加固后apk目录if (!signedApk.getParentFile().exists()){signedApk.getParentFile().mkdirs();}String signerCmd = String.format("apksigner sign --ks %s --ks-key-alias %s --min-sdk-version 21 --ks-pass pass:%s --key-pass pass:%s --out %s %s",store.storeFile,store.alias,store.storePassword,store.keyPassword,signedApk.getAbsolutePath(),unSignedApk.getAbsolutePath());boolean ret = ProcessUtil.executeCommand(signerCmd);System.out.println("已完成签名======"+signedApk.getPath());//删除未对齐的包FileUtils.deleteFile(unSignedApk.getAbsolutePath());return signedApk;}

其实核心代码为:

String signerCmd = String.format("apksigner sign --ks %s --ks-key-alias %s --min-sdk-version 21 --ks-pass pass:%s --key-pass pass:%s --out %s %s",  store.storeFile,store.alias,store.storePassword,store.keyPassword,signedApk.getAbsolutePath(),unSignedApk.getAbsolutePath());
boolean ret = ProcessUtil.executeCommand(signerCmd);

由于笔者为了方便其他apk加固,采用读取签名配置的方式获取签名文件相关数据信息。

public static KeyStore readKeyStoreConfig(String configPath){File cf = new File(configPath);if (!cf.exists()){System.out.println("签名配置文件不存在");return null;}try {List<String> lines = Files.readAllLines(cf.toPath());if (lines == null || lines.size() <= 0){System.out.println("签名配置文件内容为空");return null;}KeyStore store = new KeyStore();for (String line : lines){if (line.trim().startsWith("storeFile")){store.storeFile = line.split("=")[1].trim();}else if (line.trim().startsWith("storePassword")){store.storePassword = line.split("=")[1].trim();}else if (line.trim().startsWith("alias")){store.alias = line.split("=")[1].trim();}else if (line.trim().startsWith("keyPassword")){store.keyPassword = line.split("=")[1].trim();}}return store;}catch (Exception e){e.printStackTrace();}return null;}

好了,到这里已经完成了APK的加固工作,可以正常安装apk了。
那么如何让我们加固后的APK进行脱壳呢?接下来就来看下jiagu_shell工程

jiagu_shell工程
该工程主要提供APK脱壳工作。
根据app启动流程不难发现脱壳工作必须要在壳Application中进行,先来看下脱壳的流程:
脱壳流程
attachBaseContext中的主要工作为:

  • 从apk中读取dex文件,获取加密的dex数据,并对其进行解密保存;
  • 通过DexClassLoader动态加载AppDex.zip;
  • 主动调用ActivityThread中的installContentProviders方法(后续问题中会提到这点);

onCreate主要工作:

  • 替换Application对象,并运行新的Application的create方法;

解析apk,读取dex文件数据进行解密,然后采用DexClassLoader动态加载:

public static boolean decodeDexAndReplace(Application context, int appVersionCode){try {//创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录File odex = context.getDir("payload_odex", Application.MODE_PRIVATE);
//            File libs = context.getDir("payload_lib", Application.MODE_PRIVATE);String odexPath = odex.getAbsolutePath();//按版本号来标记zipString dexFilePath = String.format(Locale.CHINESE,"%s/AppDex.zip",odexPath);LogUtil.info("decodeDexAndReplace =============================开始");File dexFile = new File(dexFilePath);LogUtil.info("apk size ===== "+dexFile.length());if (dexFile.exists()){dexFile.delete();}//第一次加载APPif (!dexFile.exists()) {//先清空odexPath目录中文件,防止数据越来越多File[] children = odex.listFiles();if (children != null && children.length > 0){for (File child : children){child.delete();}}LogUtil.info( " ===== App is first loading.");long start = System.currentTimeMillis();dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apkString apkPath = context.getApplicationInfo().sourceDir;// 读取程序classes.dex文件byte[] dexdata = Utils.readDexFileFromApk(apkPath);//从classes.dex中再取出AppDex.zip解密后存放到/AppDex.zip,及其so文件放到payload_lib下Utils.releaseAppDexFile(dexdata,dexFilePath);LogUtil.info("解压和解密耗时 ===== "+(System.currentTimeMillis() - start) + "  === " + dexFile.exists());}// 配置动态加载环境//获取主线程对象Object currentActivityThread = getCurrentActivityThread();String packageName = context.getPackageName();//当前apk的包名LogUtil.info("packageName ===== "+packageName);//下面两句不是太理解ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mPackages");LogUtil.info("反射得到的mPackages ===== "+mPackages);WeakReference wr = (WeakReference) mPackages.get(packageName);ClassLoader mClassLoader = (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader");//创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)DexClassLoader dLoader = new DexClassLoader(dexFilePath, odexPath, context.getApplicationInfo().nativeLibraryDir, mClassLoader);LogUtil.info("反射得到的dLoader ===== "+dLoader);//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);LogUtil.info("decodeDexAndReplace ============================= 结束");return true;} catch (Exception e) {LogUtil.error( "error ===== "+Log.getStackTraceString(e));e.printStackTrace();}return false;}

获取classes.dex数据的方法,其实就是解压的方式,代码如下:

public static byte[] readDexFileFromApk(String apkPath) throws IOException {LogUtil.info("从classes.dex解析出加密的原包的dex数据");ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();//获取当前zip进行解压ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(apkPath)));while (true) {ZipEntry entry = zipInputStream.getNextEntry();if (entry == null) {zipInputStream.close();break;}if (entry.getName().equals("classes.dex")) {byte[] arrayOfByte = new byte[1024];while (true) {int i = zipInputStream.read(arrayOfByte);if (i == -1)break;dexByteArrayOutputStream.write(arrayOfByte, 0, i);}}zipInputStream.closeEntry();}zipInputStream.close();return dexByteArrayOutputStream.toByteArray();}

接着从classes.dex中获取加密的数据并解密和输出到AppDex.zip文件:

public static void releaseAppDexFile(byte[] apkdata,String apkFileName) throws Exception {int length = apkdata.length;//取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化byte[] dexlen = new byte[4];System.arraycopy(apkdata, length - 4, dexlen, 0, 4);ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);DataInputStream in = new DataInputStream(bais);int readInt = in.readInt();LogUtil.info("============ 读取原Dex压缩文件大小 ======"+readInt);byte[] newdex = new byte[readInt];//把被加壳apk内容拷贝到newdex中System.arraycopy(apkdata, length - 4 - readInt, newdex, 0, readInt);LogUtil.info("============ 开始对加密dex进行解密======" + newdex.length);//对zip包进行解密newdex = AESUtil.decrypt(newdex);LogUtil.info("============ 解密后的大小为======" + newdex.length);//写入AppDex.zip文件File file = new File(apkFileName);try {FileOutputStream localFileOutputStream = new FileOutputStream(file);localFileOutputStream.write(newdex);localFileOutputStream.close();} catch (IOException localIOException) {throw new RuntimeException(localIOException);}}

最后通过反射获取原Application对象,在通过反射调用ActivityThread中的installContentProviders方法。

public static Application makeApplication(String srcApplicationClassName){LogUtil.info( "makeApplication ============== " + srcApplicationClassName);if (TextUtils.isEmpty(srcApplicationClassName)){LogUtil.error("请配置原APK的Application ===== ");return null;}//调用静态方法android.app.ActivityThread.currentActivityThread获取当前activity所在的线程对象Object currentActivityThread = getCurrentActivityThread();LogUtil.info("currentActivityThread ============ "+currentActivityThread);//获取当前currentActivityThread的mBoundApplication属性对象,//该对象是一个AppBindData类对象,该类是ActivityThread的一个内部类Object mBoundApplication = getBoundApplication(currentActivityThread);LogUtil.info("mBoundApplication ============ "+mBoundApplication);//读取mBoundApplication中的info信息,info是LoadedApk对象Object loadedApkInfo = getLoadApkInfoObj(mBoundApplication);LogUtil.info("loadedApkInfo ============ "+loadedApkInfo);//先从LoadedApk中反射出mApplicationInfo变量,并设置其className为原Application的className//todo:注意:这里一定要设置,否则makeApplication还是壳Application对象,造成一直在attach中死循环ApplicationInfo mApplicationInfo = (ApplicationInfo) RefInvoke.getFieldOjbect("android.app.LoadedApk", loadedApkInfo, "mApplicationInfo");mApplicationInfo.className = srcApplicationClassName;//执行 makeApplication(false,null)Application app = (Application) RefInvoke.invokeMethod("android.app.LoadedApk", "makeApplication", loadedApkInfo, new Class[] { boolean.class, Instrumentation.class }, new Object[] { false, null });LogUtil.info("makeApplication ============ app : "+app);//由于源码ActivityThread中handleBindApplication方法绑定Application后会调用installContentProviders,//此时传入的context仍为壳Application,故此处进手动安装ContentProviders,调用完成后,清空原providersinstallContentProviders(app,currentActivityThread,mBoundApplication);return app;}

反射调用ActivityThread中的installContentProviders方法:

private static void installContentProviders(Application app,Object currentActivityThread,Object boundApplication){if (app == null) return;LogUtil.info("执行installContentProviders =================");List providers = (List) RefInvoke.getFieldOjbect("android.app.ActivityThread$AppBindData",boundApplication, "providers");LogUtil.info( "反射拿到providers = " + providers);if (providers != null) {RefInvoke.invokeMethod("android.app.ActivityThread","installContentProviders",currentActivityThread,new Class[]{Context.class,List.class},new Object[]{app,providers});providers.clear();}}

最后新旧Application对象的替换工作。如下代码:

public static void replaceAndRunMainApplication(Application app){if (app == null){return;}LogUtil.info( "onCreate ===== 开始替换=====");// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。final String appClassName = app.getClass().getName();//调用静态方法android.app.ActivityThread.currentActivityThread获取当前activity所在的线程对象Object currentActivityThread = getCurrentActivityThread();//获取当前currentActivityThread的mBoundApplication属性对象,//该对象是一个AppBindData类对象,该类是ActivityThread的一个内部类Object mBoundApplication = getBoundApplication(currentActivityThread);//读取mBoundApplication中的info信息,info是LoadedApk对象Object loadedApkInfo = getLoadApkInfoObj(mBoundApplication);//检测loadApkInfo是否为空if (loadedApkInfo == null){LogUtil.error( "loadedApkInfo ===== is null !!!!");}else {LogUtil.info( "loadedApkInfo ===== "+loadedApkInfo);}//把当前进程的mApplication 设置成了原application,RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", loadedApkInfo, app);Object oldApplication = RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mInitialApplication");LogUtil.info( "oldApplication ===== "+oldApplication);ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldOjbect("android.app.ActivityThread", currentActivityThread, "mAllApplications");//将壳oldApplication从ActivityThread#mAllApplications列表中移除mAllApplications.remove(oldApplication);//将原Application赋值给mInitialApplicationRefInvoke.setFieldOjbect("android.app.ActivityThread", "mInitialApplication", currentActivityThread, app);
//        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;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);}LogUtil.info( "app ===== "+app + "=====开始执行原Application");app.onCreate();}

至此脱壳工作完成,运行APP了。

中间涉及JNI层代码这里就不多说明,可以看后续的源码。

四、加固工具及命令

1.jar转dex

命令:dx --dex --output [输出dex] [输入的jar]

2.apktool反编译与回编译

反编译:apktool d -o [输出目录] [apk]
回编译:apktool b -o [输出apk] [回编译目录]

3.apk对齐命令

命令:zipalign -v -p 4 [输入的apk] [对齐后的apk]

4.签名命令

命令:apksigner sign --ks [签名文件] --ks-key-alias [alias名字] --min-sdk-version 21 --ks-pass pass:[keystore密码] --key-pass pass:[key密码] --out [输出apk] [输入apk]

5.AndroidManifest二进制文件修改器(备用)
AXMLEditor强大的AndroidManifest.xml二进制修改器,无需对APK进行反编译和回编译,节约时间。

注:由于apktool反编译和回编译apk太过耗时,想采用该工具直接修改AndroidManifest.xml,提升打包效率,不知道为什么打包后,始终无法运行,不执行壳Application,最后不得不放弃,待后面有时间在好好研究吧。如果该方案可行,打包效率会提高几十倍。

五、遇到问题

问题1:解密后加载dex,提示文件大小问题(Bad size …)?

解决:采用ZipOutputStream进行压缩,导致的问题。所以改用了Zip4j进行压缩,解压时仍可使用系统提供的解压方式。

问题2:提示找不到androidx.core.content.FileProviders类问题?

原因:通过查阅源码(ActivityThread - handleBindApplication())发现,makeApplication后会若providers不为空,则会执行初始化ContentProvider的操作(installContentProviders()),而在makeApplication中会执行Application的attachBaseContext方法,若在此将providers清空,后面就不会初始化ContentProvider的操作了,但是又不能不执行初始化。

解决:获取原Application对象 -> 通过反射手动调用installContentProviders() -> 清空providers列表

问题3:通过反射调用LoadedApk类中的makeApplication方法后,运行出现一直重复执行Application的attachBaseContext方法?

原因:执行makeApplication时反射的类是mApplicationInfo.className,而该值仍为壳Application类,因此反射后获取的Application与壳Application类一样,导致重复执行。

解决:在反射makeApplication前,先获取LoadedApk中的mApplicationInfo对象,并设置其className属性的值为原Application的类名,这样在调用makeApplication实例化Application对象就可以了。

问题4:应用启动后,原Application也替换成功了,初始化操作的时候提示找不到so文件?

解决:在实例化DexClassLoader对象时,传入的librarySearchPath不正确,应当使用app的nativeLibraryDir目录,即:context.getApplicationInfo().nativeLibraryDir

问题5:加固时如何采用JNI对数据进行加密?

采用vs studio将C/C++代码编译成dll文件,在java工程中引用。
System.load()可以加载绝对路径的dll库,
System.loadLibrary()加载jre/bin中的dll文件。

欢迎留言,一起学习,共同进步!

github - 示例源码
gitee - 示例源码


http://chatgpt.dhexx.cn/article/b9mLLhyM.shtml

相关文章

Android中的Apk的加固(加壳)原理解析和实现

本文转载自&#xff1a;Android中的Apk的加固(加壳)原理解析和实现 - roccheung - 博客园 一、前言 今天又到周末了&#xff0c;憋了好久又要出博客了&#xff0c;今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟…

浅谈安卓apk加固原理和实现

转载本文需注明出处&#xff1a;微信公众号EAWorld&#xff0c;违者必究。 引言&#xff1a; 在安卓开发中&#xff0c;打包发布是开发的最后一个环节&#xff0c;apk是整个项目的源码和资源的结合体&#xff1b;对于懂点反编译原理的人可以轻松编译出apk的源码资源&#xff0c…

安卓逆向笔记--apk加固

安卓逆向笔记–apk加固 资料来源: 浅谈安卓apk加固原理和实现 Android中的Apk的加固(加壳)原理解析和实现 前两个太老了所以具体代码借鉴下面的 Android Apk加壳技术实战详解 一、apk常见加固方法 (1)代码层级加密–代码混淆 代码混淆是一种常见的加密方式。本质是把工程中原…

imx6ull uboot移植

以下内容来自&#xff1a;正点原子Linux驱动文档 一、简介 uboot移植主要是根据原厂的uboot移植&#xff1a;芯片厂商通常会做一块自己的评估板并发布BSP&#xff0c;当我们需要定制自己的开发板时可以根据自己的需求&#xff08;硬件上的不同&#xff09;&#xff0c;对原厂…

全志V3S嵌入式驱动开发(uboot移植)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 安装了ubuntu操作系统,有了开发板,下面就可以进行我们的开发工作了。第一步,我们要面临的问题就是uboot移植。一般来说,嵌入式的基础环境就是uboot、linux和rootfs。uboot完成一…

2021-09-14 uboot移植开发

引言&#xff1a;最近要改动uboot&#xff0c;实现像微软PC上&#xff0c;u盘一键刷机或手机上安全模式下刷机的操作 专门去好好研究了点uboot的启动过程&#xff1b;以下为总结&#xff1a; 嵌入式系统 微软-PC ——…

iTOP4412 uboot移植教程

好多刚开始学习uboot移植的同学总是觉得uboot好难&#xff0c;但是再难的问题如果把它一步步拆开&#xff0c;一个个解决&#xff0c;问题也就将迎刃而解。做uboot移植&#xff0c;我们首先就得了解uboot的编译流程&#xff0c;这里以在iTOP4412精英版2G内存的板子上移植u-boot…

<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)

&#xff1c;Linux开发&#xff1e; -之-系统移植 uboot移植过程详细记录&#xff08;第二部分&#xff09; 第一部分链接&#xff1a;系统移植-之-uboot移植第一部分 第一部分主要讲解了&#xff0c;uboot移植过程中使用的一些工具的安装&#xff0c;以及测试nxp远程uboot&a…

X210开发板(S5PV210芯片)uboot移植DM9000驱动移植

前言 本文是介绍在uboot中如何移植DM9000的驱动&#xff0c;并不深入去讲解DM9000芯片的操作时序和内部寄存器&#xff0c;想要读懂驱动代码要仔细阅读DM9000芯片的数据手册。移植的基础是手里有DM9000芯片可以用的驱动代码&#xff0c;只需要根据开发板中DM9000芯片的接线方式…

嵌入式linux UBoot移植篇

如何在U-boot添加自己的linux板卡并启动呢&#xff1f; uboot 的移植并不是说我们完完全全的从零开始将 uboot 移植到我们现在所使用的开发板或者开发平台上。这个对于我们来说基本是不可能的&#xff0c;这个工作一般是半导体厂商做的&#xff0c; 半导体厂商负责将 uboot 移…

Uboot移植流程

linux-Bootloader&#xff08;Uboot&#xff09;移植流程 前言 最近在做ZigBee的温室大棚项目&#xff0c;将自己学习的过程和经验分享给大家。本文基于linux3.4.39内核版本&#xff0c;s5p6818开发板实现。 1、uboot启动简介 uboot启动的过程比较复杂&#xff0c;这里就只…

IMX6ULL Uboot 移植

使用的开发板&#xff1a;正点原子ALPHA V2.2 Uboot简介 在学习STM32的过程中使用过IAP在线升级就会知道&#xff0c;有引导程序APP程序&#xff0c;即bootloader程序APP。在学习嵌入式Linux的时候也一样&#xff0c;这个引导程序就是Uboot. uboot移植主要是根据原厂的uboot移…

二、uboot移植

二、uboot移植 版本作者时间备注V 1.0bug设计工程师2021/11/10创建文件软件网盘链接0交叉编译工具链接:https://pan.baidu.com/s/1yFO2NDMet9_b1E1q1rMwEA提取码:42kluboot源码同上linux源码同上文件系统工具同上tftp工具同上2.1 简单说明 uboot制作结束会生成 u-boot-etc44…

linux-uboot 移植四 uboot的移植

概述 前边的章节中介绍到如果要移植uboot的话&#xff0c;最好的参考就是由官方提供的demo。 1、移植 1.1 添加board对应的板级文件夹 uboot 中每个板子都有一个对应的文件夹来存放板级文件&#xff0c;比如开发板上外设驱动文件等等。 NXP 的 I.MX 系列芯片的所有板级文件…

[uboot 移植]uboot 移植过程

文章目录 uboot 移植1 修改顶层 Makefile2 在 board 文件夹下添加开发板对应的板级文件2.1 imximage_lpddr2.cfg 和 imximage.cfg 文件2.2 plugin.S 文件2.3 Kconfig 文件2.4 igkboard.c 文件2.5 MAINTAINERS 文件2.6 Makefile 文件 3 添加 igkboard_defconfig 配置文件4 添加开…

UBoot 移植

1 NXP官方开发板uboot编译测试 1 查找 NXP 官方的开发板默认配置文件 因为我们的开发板是参考 NXP 官方的 I.MX6ULL EVK 开发板做的硬件&#xff0c;因此我们在移植 uboot 的时候就可以以 NXP 官方的 I.MX6ULL EVK 开发板为蓝本。 在 NXP 官方 I.MX6UL/6ULL 默认配置文件中找…

大话uboot 移植

结合作者多年的移植经验&#xff0c;尽量简单的为大家描述一个uboot 的移植过程。希望通过描述&#xff0c;给初入移植行道的你带来美好的希望。接下来&#xff0c;我们通过以下几个方面来描述。 1. arm soc 的启动方式 在描述soc 前&#xff0c;我们先看下一个简单的arm soc:…

uboot移植步骤

Uboot移植具体步骤(本例子为Samsung origen板) 第一步:准备源码 网上下载现成的uboot开源代码:https://ftp.denx.de/pub/u-boot/ 在该网站中选择与板子兼容的uboot源文件(公司中一般咨询硬件工程师) 在Linux系统下解压 tar xf 压缩包名 第二步:修改源码 1.抄板:将和你板子兼容…

(二)uboot移植--从零开始自制linux掌上电脑(F1C200S)<嵌入式项目>

目录 一、前言 二、F1C200s上电启动顺序 三、前期准备 四、新建用户 五、交叉编译环境配置 六、uboot简介 七、uboot移植 &#x1f34f; uboot下载 &#x1f34f; uboot默认配置 &#x1f34f; uboot图形界面配置 &#x1f34f; uboot编译 &#x1f34f; 烧录bin…

U-Boot 移植初探

1. NXP官方开发板uboot编译测试 uboot移植不需要从零开始将uboot移植到使用的开发板上。因为半导体厂商通常都会自己做一个开发板&#xff0c; 将uboot移植到他们自己的原厂开发板上&#xff0c;再将这个uboot&#xff08;原厂BSP 包&#xff09;发布出去。因此使用自已的开发…