Title from Hongshen Play Android SVIP free exchange group
Week 1: Try to modify Java bytecode
1. Select JavAssist or ASM to try to modify a Java class. 2. Try adding time to a method in a Java Class method. 3. Try adding time detection to Java Class methods during Android project compilation.
1. Javassist modify word Aurora JAR package
In the previous project development, SDK of various platforms (Xiaomi, Huawei, OPPO, Vivo, Aurora) was designed to realize message push. In the process of writing the SDK of multi-platform push, I found that when Mi mobile phone was started, the services of Mi push and Aurora push were started at the same time. As a result, the push initiated by the background has been received twice (the day after tomorrow is the whole platform push). Originally, as long as the mobile terminal only need to start a push service, the result should only receive a push. The Analyze APK (Build ->Analyze APK) in Android Studio is used as a provider to enable the aurora service. Some SDKS will be initialized in the provider, see if your Android library is still initialized in the Application? .
<provider
android:name="cn.jpush.android.service.DownloadProvider"
android:exported="true"
android:authorities="com.wantu.kouzidashen.DownloadProvider" />
Copy the code
public class DownloadProvider extends ContentProvider {...private void init(a) {
try {
if (a.d(this.getContext().getApplicationContext())) {
JCoreInterface.register(this.getContext()); }}catch (Throwable var1) {
}
}
}
public class JCoreInterface {...public static void register(Context var0) {
Bundle var1 = new Bundle();
i.a().b(var0, "intent.INIT", var1); }}public final class i {
public final void b(Context var1, String var2, Bundle var3) {
try {
var1 = cn.jiguang.d.a.a(var1);
if (this.a(var1)) {
JCoreInterface.execute("SDK_MAIN".new j(this, var1, var2, var3), new int[0]); }}catch (Throwable var4) {
cn.jiguang.e.c.c("JServiceCommandHelper"."onAction failed", var4); }}}Copy the code
Jcoreinterface.execute is also called when the aurora service is normally started via the jpushInterface.init () method. Therefore, it is necessary to set a flag to control the execution of the execute method in order to avoid aurora push being started when xiaomi or Huawei phones with push platforms have their own push platforms:
public class JCoreInterface{
public static void execute{
if(flag)return;// Modify the code. }}Copy the code
At first I wanted to modify the code through JD-GUI and compile it into a new JAR package. However, it was too difficult to find, the relevant Context was not available, and the Aurora JAR package was confused, the final effect of THE JD-GUI decompression was not always correct, and some files were not recognized. In fact, the effect we want is to modify individual files and then overwrite the corresponding directories, so that the changes are minimal. Finally, javassist(Java Programming Assistant) came into my sight through the query. Javassist is a Java bytecode editing tool that makes it easy to modify classes in a way that has the same advantages as reflection interface calls.
First prepare JD – GUI, idea, and then download the javassist, platforms in the Android SDK directory under/Android – 28 find Android. The jar, then download the jar package of the aurora, we use the idea to create a new Java project, Then create a new libs directory and add javassist.jar. Right-click Add as Library to Add the Library. Create a new Test class in SRC and add the JPUSH_IS_INIT static variable to JCoreInterface (in jpush-Android-3.2.0.jar).
public class Test {
public static void main(String[] args) {
ClassPool pool = ClassPool.getDefault();
try {
pool.insertClassPath("/ XXX/JavassistTest/libs/jcore - android - 1.2.7. Jar");
pool.insertClassPath("/ XXX/JavassistTest/libs/jpush - android - 3.2.0. Jar");
pool.insertClassPath("/xxx/JavassistTest/libs/android.jar");
CtClass c = pool.get("cn.jpush.android.api.JPushInterface");// Find JPushInterface
CtField bField = new CtField(CtClass.booleanType,"JPUSH_IS_INIT",c2);// Add the JPUSH_IS_INIT static variable
bField.setModifiers(Modifier.PUBLIC|Modifier.STATIC);
c.addField(bField);
CtMethod initMethod = c.getDeclaredMethod("init");// Insert JPUSH_IS_INIT = true at the beginning of the init method;
initMethod.insertBefore("JPUSH_IS_INIT = true;");
CtMethod stopMethod = c.getDeclaredMethod("stopPush");////stopPush JPUSH_IS_INIT = true;
stopMethod.insertBefore("JPUSH_IS_INIT = false;");
c.writeFile("jpush-android"); // Output directory jpush-android
} catch(Exception e) { e.printStackTrace(); }}}Copy the code
In the current engineering jpush – android directory, we find the cn/jpush/android/API/JPushInterface class, the idea to open the decompiled is as follows:
public class JPushInterface {...public static booleanJPUSH_IS_INIT; .public static void init(Context var0) {
JPUSH_IS_INIT = true; . }...public static void stopPush(Context var0) {
JPUSH_IS_INIT = false;
g.a("JPushInterface"."action:stopPush"); . }Copy the code
The JPUSH_IS_INIT flag has been added to the JPushInterface, and is changed in init and stopPush. Next, you need to modify the JCoreInterface. Execute method in jcore-Android-1.2.7.jar. To do this, you need to override the current jpushInterface. class into jpush-Android-3.2.0.jar. Copy a copy named jpush-android-3.2.0-fix.jar, open it with 360 compression software, and overwrite the modified jpushInterface. class file to the corresponding directory. Now you can modify the JCoreInterface as follows:
ClassPool pool = ClassPool.getDefault();
try {
pool.insertClassPath("/ XXX/JavassistTest/libs/jcore - android - 1.2.7. Jar");
pool.insertClassPath("/ XXX/JavassistTest/libs/jpush - android - 3.2.0 - fix. Jar");
pool.insertClassPath("/xxx/JavassistTest/libs/android.jar");
CtClass c = pool.get("cn.jiguang.api.JCoreInterface");
CtMethod method = c.getDeclaredMethod("execute");
method.insertBefore("if(! cn.jpush.android.api.JPushInterface.JPUSH_IS_INIT)return;");//JPUSH_IS_INIT is false and returns directly
c.writeFile("jpush-android");
} catch (Exception e) {
e.printStackTrace();
}
Copy the code
Finally, the modified JCoreInterface is decomcompiled as follows:
packagecn.jiguang.api; .public class JCoreInterface {
public static void execute(String var0, Runnable var1, int. var2) {
if(JPushInterface.JPUSH_IS_INIT) { cn.jiguang.d.h.i.a(var0, var1); }}}Copy the code
It is slightly different from the code when modified, but the overall logic is correct. Then, by overwriting the modifications with compression software, we achieved a controlled startup aurora push JAR package.
2. Check the time of adding ASM methods
ASM is an open source framework for bytecode manipulation and analysis that can modify existing classes in binary (memory) form or dynamically generate classes. It provides a number of apis for bytecode transformation construction and analysis. Compared to Javassist, ASM is more complex and has a higher threshold. ASM operations provide a variety of modification and analysis apis that are small, fast, and powerful based on the instruction level.
- For an official introductory tutorial, see asm4-guide.pdf
- Doc document
Because ASM operation bytecode is based on instructions, it is necessary to have a certain understanding of the JVM. It is recommended that you read “Understanding Java Virtual Machine in Depth” and “Write Java Virtual Machine by Yourself”, and “Write Java Virtual Machine by Yourself” is practical, you can learn Java virtual machine through go programming form. The ASMapi has the following key classes: ClassReader: parses a class file, accepts a ClassVisitor object to access specific fields, methods, and so on. Class visitor: Class visitor. ClassWriter: Inherited from ClassVisitor, used to modify or generate classes, usually in conjunction with ClassReader and ClassVisitor to modify classes. Here on page 63 of the ASM4 – Guide add time detection to the method via LocalVariablesSorter
public class MethodLogAdapter extends ClassVisitor {
public MethodLogAdapter(int api) {
super(api);
}
private String owner;
private boolean isInterface;
public boolean changed; // Whether it has been modified
public MethodLogAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); owner = name; isInterface = (access & ACC_INTERFACE) ! =0;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
if(! isInterface && mv ! =null && !name.equals("<init>")) {
mv = new MethodLogAdapter.LogMethodAdapter(access, name, desc, mv);
}
return mv;
}
class LogMethodAdapter extends LocalVariablesSorter {
private int time;
private String name;
private boolean hasMethodLog;// Whether there are MethodLog annotations
public LogMethodAdapter(int access, String name, String desc,
MethodVisitor mv) {
super(ASM4, access, desc, mv);
this.name = name;
}
@Override
public void visitCode(a) {
super.visitCode();
if (hasMethodLog) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."nanoTime"."()J");
time = newLocal(Type.LONG_TYPE);// Declare the temporary variable time
mv.visitVarInsn(LSTORE, time);// Save the returned timestamp to a temporary variable}}@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
hasMethodLog = "Lannotations/MethodLog;".equals(descriptor);
if(! changed && hasMethodLog) changed =true;
return super.visitAnnotation(descriptor, visible);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
if (hasMethodLog) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."nanoTime"."()J");
mv.visitVarInsn(LLOAD, time);// Load the time temporary variable
mv.visitInsn(LSUB);// Subtract from the current timestamp
mv.visitVarInsn(LSTORE, 3);
Label l3 = new Label();
mv.visitLabel(l3);
Log. I (" current class name "," method name :"+time)
mv.visitLdcInsn(owner);
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false);
mv.visitLdcInsn(name + ":");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
mv.visitVarInsn(LLOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(J)Ljava/lang/StringBuilder;".false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log"."i"."(Ljava/lang/String; Ljava/lang/String;) V".false); }}super.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals); }}}Copy the code
Then, in conjunction with ClassReader, ClassWriter modifies the class and adds a method time detection to TestActivity
public class AsmTest {
public static void main(String[] args) {
try {
changeTest();
} catch(Exception e) { e.printStackTrace(); }}//class file information read
private static void changeTest(a) throws Exception {
String classPath = "out/production/ClassEditTest/test/TestActivity.class";
ClassReader reader = new ClassReader(new FileInputStream(new File(classPath)));
ClassWriter cw = new ClassWriter(reader,ClassWriter.COMPUTE_MAXS);
MethodLogAdapter adapter = new MethodLogAdapter(cw);
reader.accept(adapter,ClassReader.EXPAND_FRAMES);
System.out.println(adapter.changed);
byte[] bytes = cw.toByteArray();
FileOutputStream fos = new FileOutputStream(new File("test.class")); fos.write(bytes); }}Copy the code
Compare the original and modified code as follows
public class TestActivity {/ / modify before
@MethodLog
public void test(a) {
try {
Thread.sleep(100);
} catch(InterruptedException e) { e.printStackTrace(); }}public void test2(a) {
Log.i("test"."test123"); }}Copy the code
public class TestActivity {/ / modified
public TestActivity(a) {}@MethodLog
public void test(a) {
long var1 = System.nanoTime();
try {
Thread.sleep(100L);
} catch (InterruptedException var5) {
var5.printStackTrace();
}
long var3 = System.nanoTime() - var1;
Log.i("test/TestActivity"."test:" + var3);
}
public void test2(a) {
this.test(); }}Copy the code
3. Use Transform to add method time during Android compilation
Transform implements a simplified version of the routing framework, which is implemented by Javassist. Although the implementation is simpler, it is not as fast as ASM operation, so this time IT is implemented by ASM. Similarly, create an Android Library named buildSrc (case-sensitive) so that our plugin can be used directly. For details on how to implement the plugin, see the configuration of a more efficient component-based routing framework based on Transform. Adding the MethodLogTransform processing method takes time
class MethodLogTransform extends Transform {
@Override
String getName() {
return "MethodLog"}...@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
for (TransformInput input : inputs) {
for (DirectoryInput dirInput : input.directoryInputs) {// Class files in the directory
readClassWithPath(dirInput.file)
File dest = outputProvider.getContentLocation(dirInput.name,
dirInput.contentTypes,
dirInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(dirInput.file, dest)
}
for (JarInput jarInput : input.jarInputs) {//jar (third-party library, module)
if (jarInput.scopes.contains(QualifiedContent.Scope.SUB_PROJECTS)) {//module library
// Todo adds time for jar packages
}
copyFile(jarInput, outputProvider)
}
}
}
//
void readClassWithPath(File dir) {// Find annotations from the compile class file directory
def root = dir.absolutePath
dir.eachFileRecurse { File file ->
def filePath = file.absolutePath
if(! filePath.endsWith(".class")) return
def className = getClassName(root, filePath)
if (isSystemClass(className)) return
hookClass(filePath, className)
}
}
void hookClass(String filePath, String className) {
ClassReader reader = new ClassReader(new FileInputStream(new File(filePath)))
ClassWriter cw = new ClassWriter(reader,ClassWriter.COMPUTE_MAXS)
MethodLogAdapter adapter = new MethodLogAdapter(cw)
reader.accept(adapter,ClassReader.EXPAND_FRAMES)
System.out.println(adapter.changed)
if(adapter.changed){
byte[] bytes = cw.toByteArray()
FileOutputStream fos = new FileOutputStream(new File(filePath))
fos.write(bytes)
}
}
...
}
Copy the code
The project address
Github.com/iamyours/AS…