Android hot patch dynamic Repair Technology (3)-inject bytecode using Javassist to complete the prototype of the hot patch framework (available)

Source: Internet
Author: User

Android hot patch dynamic Repair Technology (3)-inject bytecode using Javassist to complete the prototype of the hot patch framework (available)
I. Limitations on the CSDN mardown Editor

Android hot patch dynamic Repair Technology (3) this blog post was actually released on the evening of April 8, followed by the fourth article, but when I saved (4) IT TO THE DRAFT box, the published article (3) disappears and is replaced by the fourth blog.

I have asked the moderator in the Forum, probably because of my misoperations. The third blog cannot be recovered.
It's really cool! I have written things for several days, but I prefer to think that this is a csdn bug ......
The markdown editor is absolutely pitfall! I think caching is a very serious Bug when writing new articles!

Ii. Preface

Due to the disappearance of the third blog post, the Demo project along with the demonstration has also modified a lot of content, and I am not so energetic to write another article, merge with the fourth blog as the third one, which may lead to a large content span and won't be as detailed as the previous blog. I hope you can have more support and understanding.

In the previous blog, we successfully injected patch_dex.jar into ClassLoader in Application.
However, with the CLASS_ISPREVERIFIED problem, the solution is to add a line of code to the constructor of all classes.System.out.println(AntilazyLoad.class);

Iii. Gradle, Transfrom, Task, and Plugin

Let's analyze how to addSystem.out.println(AntilazyLoad.class);

Add it directly in the source code. This is not acceptable. The class "AntilazyLoad. class" cannot be found. compilation does not bypass compilation. The byte code is operated using the ssist command to directly inject the code.

The second is feasible, but the AndroidStudio project is built using Gradle, and compilation, packaging, and signature are all automated.
When should we inject code?

After reading the articles recommended in my previous blog post, I know that Gradle completes the entire process by executing one Task, and all classes must be packaged into dex tasks.
(Gradle plugin 1.5 and later versions are somewhat different)

Under 1.5, the task preDex will package the compiled class of the dependent module into a jar, and then the task dex will package all the classes into dex1.5 and above, the preDex and Dex tasks have been replaced by TransfromClassesWithDexForDebug. 3.1 Transfrom

Transfrom is a new api developed by Gradle 1.5 and later. It is actually a Task, but the definition method is a little different from that of a Task.
For hot Patching, Transfrom is more useful than the original Task.

Before the Transfrom api is released, to operate the class before the project is packaged as dex, you must customize a Task and insert it to predex or dex, you can use javassist or asm to operate classes in custom tasks.

Transform is more convenient. Transfrom has its own execution time, so we do not need to insert it to a Task. Once registered, Tranfrom is automatically added to the Task execution sequence, just before the project is packaged as dex.

This article uses Gradle1.5 and later versions. The following is Google's description of Transfrom.
Http://tools.android.com/tech-docs/new-build-system/transform-api
Sometimes you cannot access it. You may need a ladder ......

3.2 inputs and outputs of a Task

Gradle can be seen as a script that contains a series of tasks. After executing these tasks in sequence, the project is packaged successfully.
Task has an important concept: inputs and outputs.
The Task obtains some items through inputs and outputs after processing is completed, while the inputs of the next Task is the outputs of the previous Task.

For example, a Task is used to compile java into a class. The inputs of this Task are the storage directory of the java file, and outputs are the output directory of the compiled class, the inputs of its next Task will be the directory for saving the compiled class.

3.3 Plugin

In Gradle, apart from the important api of Task, there is also a plug-in.
What is the role of Plugin? These two statements are hard to explain.

Gralde can only be regarded as a construction framework. How do so many tasks come from it? Who defined it?
Is Plugin, careful netizens will find that the first line in the build. gradle file under the module usually hasapply plugin : 'com.android.application'Orapply plugin : 'com.android.library'.

com.android.application: This is the Build. gradle of app module.
com.android.library: This is the Builde. gradle of the module on which the app depends.

These plugins provide tasks for project building. Different plugins provide different module functions.
It can be simply understood that Gradle is just a framework, and what actually works is plugin. The main function of plugin is to add tasks to the Gradle script.
Of course, these are actually very complex things, And plugin has other functions that cannot be used here.

4. How to register a Transfrom

We can customize a plugin and use plugin to register a Transfrom.

4.1 apply plugin

Before that, let's teach you how to customize a plug-in.
1. Create a module and select library module. The module name must be BuildSrc.
2. Delete all files under the module, except build. gradle. Clear the content in build. gradle.
3. Create the following directory src-main-groovy
4. Modify build. gradle as follows and synchronize

```apply plugin: 'groovy'repositories {    jcenter()}dependencies {    compile gradleApi()    compile 'com.android.tools.build:gradle:1.5.0'    compile 'org.javassist:javassist:3.20.0-GA'}```


5. In this case, you can create a package and a class like a common module. However, the class here ends with groovy. When creating a class, select file and use. groovy as the suffix.

Register is my custom Plugin (ignoring the Black Block, the Demo has been modified too much, and I despise csdn again)
The Code is as follows:

Package com. aitsuki. pluginimport org. gradle. api. Plugin; import org. gradle. api. Project/*** Created by hp on 2016/4/8. */public class Register implements Plugin
  
   
{@ Override public void apply (Project project) {project. logger. error "===================== the custom plug-in is successfully created! ============ "}}
  

Add the apply plug-in to buiil. gradle under app module

Note: If the module name of the plugin is not BuildSrc, you cannot apply the package name here and will be prompted that the package name cannot be found. So we also mentioned that the name must be buildsrc.

After running the project, you can see "=================== the custom plug-in is successfully created! =========="
The output related to gradle is displayed in the gradle console window.

4.2 custom Transfrom

Create a New groovy to inherit the Transfrom. Note that this Transfrom is requiredcom.android.build.api.transform.TransformThis package's
To import this package, add the dependency first, as shown below:

dependencies {    compile gradleApi()    compile 'com.android.tools.build:gradle:1.5.0'    compile 'org.javassist:javassist:3.20.0-GA'}

Javassist will be used later. It is added by the way.

We define a PreDexTransform. The Code is as follows:

Package com. aitsuki. pluginimport com. android. build. api. transform. * import com. android. build. gradle. internal. pipeline. transformManagerimport org. gradle. api. projectpublic class PreDexTransform extends Transform {Project project // Add the constructor. To get the Project object from plugin, public PreDexTransform (project Project project) {this. project = project} // Transfrom name in the Task list // TransfromClassesWithPreDexForXXXX @ Override String getName () {return "preDex"} // specifies the input type @ Override Set
  
   
GetInputTypes () {return TransformManager. CONTENT_CLASS} // specify the Transfrom scope @ Override Set
   
    
GetScopes () {return TransformManager. SCOPE_FULL_PROJECT} @ Override boolean isIncremental () {return false} @ Override void transform (Context context, Collection
    
     
Inputs, Collection
     
      
ReferencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {// inputs is the set of input files // outputProvider can get outputs path }}
     
    
   
  

Then add the code in the apply method of the plug-in to Register Transfrom.

def android = project.extensions.findByType(AppExtension)        android.registerTransform(new PreDexTransform(project))

Run the project again (clean the project first, otherwise apply plugin will not be re-compiled)

First, we can see that the custom PreDexTransfrom is running, but the following DexTransform reports an error.

That's because our custom Transfrom's transfrom method is empty and inputs are not output to outputs. DexTransfrom is under PreDexTransfrom and the obtained inputs are empty, so an error is reported.
You only need to copy the inputs file to the ouputs directory in Tranfrom. The Code is as follows.

// There are two types of Transfrom inputs: Directory and jar package. each {TransformInput input-> input. directoryInputs. each {DirectoryInput directoryInput-> // TODO can be used to process the input file, such as code injection! // Obtain the output directory def dest = outputProvider. getContentLocation (directoryInput. name, directoryInput. contentTypes, directoryInput. scopes, Format. DIRECTORY) // copy the input DIRECTORY to the specified output DIRECTORY FileUtils. copyDirectory (directoryInput. file, dest)} input. jarInputs. each {JarInput jarInput-> // TODO can process the input file, such as code injection! // Rename the output file (conflicts with the copyFile directory) def jarName = jarInput. name def md5Name = DigestUtils. md5Hex (jarInput. file. getAbsolutePath () if (jarName. endsWith (". jar ") {jarName = jarName. substring (0, jarName. length ()-4)} def dest = outputProvider. getContentLocation (jarName + md5Name, jarInput. contentTypes, jarInput. scopes, Format. JAR) FileUtils. copyFile (jarInput. file, dest )}}

If you add this code to the transform method and run it again, it will be okay!

There are two TODO annotations on the top. We can inject code to the class here before getting inputs to the outpus directory!

4.3 view inputs and ouputs

Let's take a look at the inputs and outputs of Transfrom. Here is a method:
Add the following code to build. gradle under app module.

applicationVariants.all { variant->        def dexTask = project.tasks.findByName("transformClassesWithDexForDebug")        def preDexTask = project.tasks.findByName("transformClassesWithPreDexForDebug")        if(preDexTask) {            project.logger.error "======preDexTask======"            preDexTask.inputs.files.files.each {file ->                project.logger.error "inputs =$file.absolutePath"            }            preDexTask.outputs.files.files.each {file ->                project.logger.error "outputs =$file.absolutePath"            }        }        if(dexTask) {            project.logger.error "======dexTask======"            dexTask.inputs.files.files.each {file ->                project.logger.error "inputs =$file.absolutePath"            }            dexTask.outputs.files.files.each {file ->                project.logger.error "outputs =$file.absolutePath"            }        }    }

The output is as follows:

Glide and xutils are jar packages that the app depends on.
Hotpatch is the result of the app dependent on this module after I extract the dex code loaded in the application into an independent module.
The remaining jar packages are the Default dependencies of the project.

It is concluded that the module on which the app depends will be packaged into classes. jar before dex and put together with other dependent jar packages.exploded-arrThis directory.
The dependent module will be placed inExploded-arr \ Project name \ module nameUnder this directory

Attaches hotPatch, a module that packs the code in the application.

Then this isinputs =D:\aitsuki\HotPatchDemo\app\build\intermediates\exploded-aar\HotPatchDemo\hotpatch\unspecified\jars\classes.jarExtracted results

5. Use javassist to inject code

It is recommended that you first understand the most basic usage of SIT, otherwise you may not understand what I am talking about.

5.1 create an Hack Module

InjectionSystem.out.println(AntilazyLoad.class);In this line of code, if the javasssit finds the AntilazyLoad. class, an exception is thrown.

Create AntilazyLoad. class and append the path of AntilazyLoad. class to classpath of ClassPool.

First, create an hack module, as shown below:

5.2 create hack. jar

Production methods are available in the previous blog.
Copy AntilazyLoad. class to a folder with the same package name and run the packaging command.

Put hack. jar in the assets folder of the app module,

Then we need to load this hack into classLoader before loading patch_dex. The method and steps for loading the hack are the same as those for loading the patch. We will not go into details. For details, please refer to the Demo, the download link is provided at the end.

5.3 use javassist to inject code

A little more code, so I will not explain it in detail. Here are two basic points.

After the app module is compiled, the class file is saved in the debug directory. You can directly traverse this directory and use javassist to inject the code to the module on which the app module depends. After compilation, it will be packaged into a jar file, in the exploded-aar directory, decompress the jar package, traverse the injection code, and package it into a jar package.

First, we will write an inject class specifically used to operate the javassist injection code.

Package com. aitsuki. pluginimport ipvsist. classPoolimport policsist. ctClassimport org. apache. commons. io. fileUtils/*** Created by AItsuki on 2016/4/7. * The injection code can be divided into two situations: one is the directory, and the class in the directory needs to be traversed for injection * the other is the jar package, and the jar package needs to be decompressed first, inject the code and then package it into jar */public class Inject {private static ClassPool pool = ClassPool. getDefault ()/*** add classPath to ClassPool * @ param libPath */public static void appendClassPath (String libPath) {Pool. appendClassPath (libPath)}/*** traverses all classes in the directory and injects code into all classes. * The following classes do not require code injection: * --- 1. R file related * --- 2. configuration file (BuildConfig) * --- 3. application * @ param path */public static void injectDir (String path) {pool. appendClassPath (path) File dir = new File (path) if (dir. isDirectory () {dir. eachFileRecurse {File file-> String filePath = file. absolutePath if (filePath. endsWith (". class ")&&! FilePath. contains ('R $ ')&&! FilePath. contains ('R. class ')&&! FilePath. contains ("BuildConfig. class") // here is the application name, which can be obtained by parsing the list file. It is first written to an end &&! FilePath. contains ("HotPatchApplication. class ") {// here is the application package name, which can also be obtained from the list file. First, write the int index = filePath. indexOf ("com \ aitsuki \ hotpatchdemo") if (index! =-1) {int end = filePath. length ()-6 //. class = 6 String className = filePath. substring (index, end ). replace ('\\','. '). replace ('/','. ') injectClass (className, path) }}}/ *** the jar package must be decompressed first, after the code is injected, re-generate the jar package * @ path absolute path of the jar package */public static void injectJar (String path) {if (path. endsWith (". jar ") {File jarFile = new File (path) // save path String jarZipDir = jarFile after the jar package is decompressed. getParent () + "/" + jarF Ile. getName (). replace ('. jar ', '') // decompress the jar package and return the complete set of class names (. class suffix) List classNameList = JarZipUtil. unzipJar (path, jarZipDir) // Delete the original jar package jarFile. delete () // inject code pool. appendClassPath (jarZipDir) for (String className: classNameList) {if (className. endsWith (". class ")&&! ClassName. contains ('R $ ')&&! ClassName. contains ('R. class ')&&! ClassName. contains ("BuildConfig. class ") {className = className. substring (0, className. length ()-6) injectClass (className, jarZipDir) }}// package jar JarZipUtil.zip Jar (jarZipDir, path) // Delete the FileUtils directory. deleteDirectory (new File (jarZipDir)} private static void injectClass (String className, String path) {CtClass c = pool. getCtClass (className) if (c. isFrozen () {c. defrost ()} def constructor = c. getConstructors () [0]; // enter the complete class name here; otherwise, constructor will be reported in javassist. insertAfter ("System. out. println (com. aitsuki. hack. antilazyLoad. class); ") c. writeFile (path )}}

Below is the class for extracting the jar package

Package com. aitsuki. pluginimport java. util. jar. jarEntryimport java. util. jar. jarFileimport java. util. jar. jarOutputStreamimport java.util.zip. zipEntry/*** Created by hp on 2016/4/13. */public class JarZipUtil {/*** decompress the jar package to the specified directory * @ param jarPath absolute path of the jar package * @ param destDirPath jar package decompress the Save path *@ return returns the complete set of class names of all classes contained in the jar package, one of the data items is com. aitski. hotpatch. xxxx. class */public static List unzipJar (String jarPath, String destDirPath) {List list = new ArrayList () if (jarPath. endsWith ('. jar') {JarFile jarFile = new JarFile (jarPath) Enumeration
  
   
JarEntrys = jarFile. entries () while (jarEntrys. hasMoreElements () {JarEntry jarEntry = jarEntrys. nextElement () if (jarEntry. directory) {continue} String entryName = jarEntry. getName () if (entryName. endsWith ('. class ') {String className = entryName. replace ('\\','. '). replace ('/','. ') list. add (className)} String outFileName = destDirPath + "/" + entryName File outFile = new File (outFileName) ou TFile. getParentFile (). mkdirs () InputStream inputStream = jarFile. getInputStream (jarEntry) FileOutputStream fileOutputStream = new FileOutputStream (outFile) fileOutputStream <inputStream fileOutputStream. close () inputStream. close ()} jarFile. close ()} return list}/*** re-package jar * @ param packagePath package all the files in this directory into jar * @ param destPath the absolute path of the packaged jar package */public static void zipJar (String packagePath, String destPath) {File file = new File (packagePath) JarOutputStream outputStream = new JarOutputStream (new FileOutputStream (destPath) file. eachFileRecurse {File f-> String entryName = f. getAbsolutePath (). substring (packagePath. length () + 1) outputStream. putNextEntry (new ZipEntry (entryName) if (! F. directory) {InputStream inputStream = new FileInputStream (f) outputStream <inputStream. close () }}outputstream. close ()}}
  

And then paste the entire class in Transfrom.

Package com. aitsuki. pluginimport com. android. build. api. transform. * import com. android. build. gradle. internal. pipeline. transformManagerimport org. apache. commons. codec. digest. digestUtilsimport org. apache. commons. io. fileUtilsimport org. gradle. api. projectpublic class PreDexTransform extends Transform {Project project public PreDexTransform (Project project) {this. project = project }@override String getName () {return "preDex"} @ Override Set
  
   
GetInputTypes () {return TransformManager. CONTENT_CLASS} @ Override Set
   
    
GetScopes () {return TransformManager. SCOPE_FULL_PROJECT} @ Override boolean isIncremental () {return false} @ Override void transform (Context context, Collection
    
     
Inputs, Collection
     
      
ReferencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {// obtain the debug directory of the hack module, that is, the directory where Antilazy. class is located. Def libPath = project. project (': hack '). buildDir. absolutePath. concat ("\ intermediates \ classes \ debug") // Add the path to classPath Inject of Classpool. appendClassPath (libPath) // traverses the inputs of transfrom. There are two types of inputs: Directory and jar. Inputs. each {TransformInput input-> input. directoryInputs. each {DirectoryInput directoryInput-> // obtain the output directory def dest = outputProvider. getContentLocation (directoryInput. name, directoryInput. contentTypes, directoryInput. scopes, Format. DIRECTORY) // TODO can process the input file, such as code injection! Inject. injectDir (directoryInput. file. absolutePath) // copy the input directory to the specified output directory FileUtils. copyDirectory (directoryInput. file, dest)} input. jarInputs. each {JarInput jarInput-> // TODO can process the input file, such as code injection! String jarPath = jarInput. file. absolutePath; String projectName = project. rootProject. name; if (jarPath. endsWith ("classes. jar ") & jarPath. contains ("exploded-aar" + "\" + projectName) {Inject. injectJar (jarPath)} // rename the output file (conflicts with the copyFile directory) def jarName = jarInput. name def md5Name = DigestUtils. md5Hex (jarInput. file. getAbsolutePath () if (jarName. endsWith (". jar ") {jarName = jarName. substring (0, jarName. length ()-4)} def dest = outputProvider. getContentLocation (jarName + md5Name, jarInput. contentTypes, jarInput. scopes, Format. JAR) FileUtils. copyFile (jarInput. file, dest )}}}}
     
    
   
  

Run the project (Repeat the last time: Remember to clean the project first !), Patch injected successfully! No error reported

Vi. Demo GIF demonstration


About SDCard: If the mobile phone supports a TF card, copy the patch to the internal storage.

Also, the patch is already in the root directory.
Http://download.csdn.net/detail/u010386612/9490542

7. Post

Note: In the above Code, we inject code into the compiled jar of all modules.
In fact, in hotpatch, this module does not need to be injected with code, because this module is used to load dex. When executing this module, AntilazyLoad. the class is definitely not loaded in, so the injection code is useless. We should exclude this module.

This blog article solved the class_ispreverified problem and successfully injected bytecode with javassist, completing the prototype of the hot Patching framework.
But there are several problems to be solved.
1. the patch is vulnerable to malicious code injection because it does not have signature verification.
2. When obfuscation is enabled, the class name may be replaced and the patch package may fail.
The next blog may be about obfuscation or patch signature.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.