preface
- Gradle+ASM is a plugin for Gradle+ASM. Gradle+ASM is a plugin for Gradle+ASM
- ASM API documentation: Javadoc
- ASM User Manual: English and Chinese
- Github address: github.com/Peakmain/As…
Demand background
- Third party SDKS frequently invoke privacy methods such as MAC addresses, AndroidId, etc
- Now what we want is, for example, to call the telephoneManger method getDeviceId when the device ID is called. If we can find a method to call getDeviceId and replace it with our own method or empty the body of the method, we’ll solve the problem
- According to the nature of the programmer, a lazy I wanted to go to a library, read a few articles, but did not achieve their want, currently about privacy method calls or privacy policy changes, some others also simply use a third party such as Epic, AOP, and the actual effect of is not what we want, Some just say check that private methods are called by those methods
- So there is this article and the implementation of the library, I hope to help you, completely solve the third party SDK frequently call privacy methods were notified or removed from the shelf, but also for learning ASM oh.
- We learned from this article that we really only need to focus on our inherited ClassVisitor
Basic knowledge of
ClassVisitor
The order in which methods are executed
Let’s go straight to the annotation of the ClassVisitor
[]
: indicates that the call can be invoked at most once(a)
and|
: indicates that you can select any method in any order*
: indicates that the method can be called 0 or more times
We focus on the following approaches
visit
(visitField |visitMethod)*
visitEnd
Copy the code
The four methods
1. The visit method, which is entered when scanning classes, is executed at most once
/** * @param version Class version ASM4~ASM9 Optional * @param access modifier such as public, static, and final * @param name Class name such as: Com/peakmain/asm/utils/utils * @ param signature generics information * @ param superName parent * @ param interfaces implemented interface * / @ Override void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {}Copy the code
2, visitField: when accessing properties, use not much, when using details
@Override
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return super.visitField(access, name, descriptor, signature, value)
}
Copy the code
VisitMethod: call when the method is scanned. This is also the method we mainly introduce. Details will be introduced below
/** * scan class methods to make calls * @param access modifier * @param name method name * @param descriptor method signature * @param signature generic information * @param @override MethodVisitor visitMethod(int Access, String name, String descriptor, String signature, String[] exceptions) { return super.visitMethod(access, name, descriptor, signature, exceptions) }Copy the code
4. VisitEnd: This is the last method to execute after these visitXxx() methods and will be called at most once
@Override
void visitEnd() {
super.visitEnd()
}
Copy the code
MethodVisitor
An object of type MethodVisitor is returned by calling the visitMethod() method of the ClassVisitor class
Method
public abstract class MethodVisitor {
public void visitCode();
public void visitInsn(final int opcode);
public void visitIntInsn(final int opcode, final int operand);
public void visitVarInsn(final int opcode, final int var);
public void visitTypeInsn(final int opcode, final String type);
public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor);
public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor,
final boolean isInterface);
public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle,
final Object... bootstrapMethodArguments);
public void visitJumpInsn(final int opcode, final Label label);
public void visitLabel(final Label label);
public void visitLdcInsn(final Object value);
public void visitIincInsn(final int var, final int increment);
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels);
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels);
public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions);
public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type);
public void visitMaxs(final int maxStack, final int maxLocals);
public void visitEnd();
}
Copy the code
Suppose we have the following method
Public static String getMeid(Context Context) {TelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return manager.getMeid(); } return "getMeid"; }Copy the code
VisitXxxInsn is responsible for the contents of the method body, that is, the {} property, the method
The order in which methods are called
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitInsnAnnotation |
visitTryCatchBlock |
visitTryCatchAnnotation |
visitLocalVariable |
visitLocalVariableAnnotation |
visitLineNumber
)*
visitMaxs
]
visitEnd
Copy the code
grouping
We can divide into three groups
- The first group: the methods before the visitCode method, which are responsible for parameters, annotations, and attributes. For us, just focus on visitAnnotation
- The second group: the methods between visitCode and visitMaxs methods. The methods in between are responsible for the opCode content in the “method body” of the method. VisitCode represents the beginning of the method body, and visitMaxs represents the end of the method body
- The third group, the visitEnd() methods, is the last method to call
Pay attention to the point
We need to pay attention to:
- VisitAnnotation: Will be called multiple times
- VisitCode: Will only be called once
- VisitXxxInsn: Can call many times, these method calls, is in the construction of the method body
- VisitMaxs: Will only be called once
- VisitEnd: Will only be called once
AdviceAdapter
We used AdviceAdapter in the project, so what is AdviceAdapter? AdviceAdapter actually introduces two methods: the onMethodEnter() method and the onMethodExit() method. And this class belongs to MethodVisitor, which is the third method we’re going to talk about
Source code analysis
onMethodEnter
@Override public void visitCode() { super.visitCode(); StackFrame = new ArrayList<>(); forwardJumpStackFrames = new HashMap<>(); } else { onMethodEnter(); }}Copy the code
The visitCode method is actually called, but the constructor (()) logic is handled. Using the visitCode() method directly can cause an error in the () method
onMethodExit
If you call a method from visitInsn, why isit in visitInsn and not in visitEnd? Not that this is the last method call. Suppose we have a way to get AndroidId
public static String getAndroidId(Context context) {
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
Copy the code
This method should now be normal ASM
mv.visitCode()
mv.visitxxxInsn()
mv.visitInsn(AReturn)
mv.visitMaxs()
mv.visitEnd()
Copy the code
VisitMaxs = visitMaxs = visitEnd = visitMaxs = visitEnd = visitMaxs = visitEnd = visitMaxs = visitEnd = visitMaxs
The Frame () method initializes the Frame
- In the JVM Stack, there is the Stack structure, which stores frames;
- Each frame space can be called a Stack frame.
- When a new method is called, a frame space is allocated on the JVM Stack
- When the method exits, the corresponding frame space is also cleared on the JVM Stack (out of the Stack).
- Within the frame space, there are two important structures: local variables and operand stack.
When the method starts, the operand stack is empty, no data needs to be stored, and three factors need to be considered in the local variable table
- Whether the current method is static. If the current method is non-static, there needs to be a this variable in the local variables index 0. If the current method is static, there is no need to store this.
- Whether the current method accepts parameters. The parameters received by the method are placed in the local Variables in the order in which they are declared.
- Whether the argument to the method contains long or double. If the argument is of type long or double, it takes two places in the Local variables
Type
In.java files, we often use the java.lang.Class Class; In.class files, you’re going to use internal name, Type Descriptor, method descriptor all the time; In ASM, the org.objectWeb.asm. Type class helps convert between the two.
Several ways to get Type
The Type class has a private constructor, so instances of Type objects cannot be created using the new keyword. However, the Type class provides static Method and static Field to retrieve objects.
- Method 1: java.lang.class
Type type=Type.getType(String.class)
Copy the code
- Method two: Descriptor
Type type = Type.getType("Ljava/lang/String;" );Copy the code
- Method 3: Internal name
Type type = Type.getObjectType("java/lang/String");
Copy the code
- Method 4: Static field
Type type = Type.INT_TYPE;
Copy the code
Several commonly used methods
- The getArgumentTypes() method, which gets the parameter types received by the method
- The getReturnType() method, used to get the type of the return value of the method
- The getSize() method returns the amount of slot space occupied by a type
- GetArgumentsAndReturnSizes () method, is used to return method corresponds to the size of the slot space
In actual combat
The above basic knowledge we learned, so you can start actual combat. All of the following scenarios inherit AdviceAdapter
Actual combat 1: Monitoring method time consuming
- Suppose you have the following code:
public String getMethodTime(long var1) {
try {
Thread.sleep(1000L);
} catch (InterruptedException var4) {
var4.printStackTrace();
}
return "getMethod";
}
Copy the code
The target
Through annotations to monitor access to the method, take time, the location of the code MonitorPrintParametersReturnValueAdapter
plan
- Each method dynamically adds a long property named before the method +timer_, and the property defined above is timer_getMethodTimeCopy the code
public class TestActivity extends AppCompatActivity { public static long timer_getMethodTime; public String getMethodTime(long var1) { timer_getMethodTime -= System.currentTimeMillis(); try { Thread.sleep(1000L); } catch (InterruptedException var4) { var4.printStackTrace(); } timer_getMethodTime += System.currentTimeMillis(); LogManager.println(timer_getMethodTime); return "getMethod"; }}Copy the code
Code implementation
- First, we define an annotation class com. Peakmain. SDK. The annotation. LogMessage
@target ({elementtype.method}) @Retention(retentionPolicy.runtime) public @interface LogMessage {/** * Whether the METHOD takes time to print */ boolean isLogTime() default false; / * * * * whether print method parameters and return values * / Boolean isLogParametersReturnValue () default false; }Copy the code
- To determine if a method has an annotation, we’re definitely using visitAnnotation
AnnotationVisitor visitAnnotation(String descriptor, boolean b) { if (descriptor == "Lcom/peakmain/sdk/annotation/LogMessage;" ) { return new AnnotationVisitor(OpcodesUtils.ASM_VERSION) { @Override void visit(String name, Object value) { super.visit(name, value) if (name == "isLogTime") { isLogMessageTime = (Boolean) value } else if (name == "isLogParametersReturnValue") { isLogParametersReturnValue = (Boolean) value } } } } return super.visitAnnotation(descriptor, b) }Copy the code
- We need to insert the property at the beginning of the method body, which must be the visitCode method since this is where the method starts
private String mFieldDescriptor = "J" @Override void visitCode() { if (isLogMessageTime && ! OpcodesUtils.isNative(mMethodAccess) && ! OpcodesUtils.isAbstract(mMethodAccess) && ! OpcodesUtils.isInitMethod(mMethodName)) { FieldVisitor fv = mClassWriter.visitField(ACC_PUBLIC | ACC_STATIC, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor, null, null) if (fv ! = null) { fv.visitEnd() } } super.visitCode() }Copy the code
Static String getTimeFieldName(String methodName) {return "timer_" + methodName}Copy the code
We need to create a property, so we need to use the classWriter property, to create the property from visitField, it is important to note that after we create the property, we must call visitEnd
- Next, at the beginning of the method body, add timer_getMethodTime -= System.currentTimemillis (); , you must remember AdviceAdapter two methods, yes is onMethodEnter and onMethodExit two methods, because it is the beginning of the method, so we need to insert code in onMethodEnter
@Override protected void onMethodEnter() { super.onMethodEnter() if (isLogMessageTime && ! OpcodesUtils.isNative(mMethodAccess) && ! OpcodesUtils.isAbstract(mMethodAccess) && ! OpcodesUtils.isInitMethod(mMethodName)) { mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor) mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) mv.visitInsn(LSUB) mv.visitFieldInsn(PUTSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor) } }Copy the code
The code is quite simple: First we obtain their defined properties when visitCode timer_getMethod, then is to get the current time, gets the current time is a method, so use is visitMethodInsn, subsequent subtraction, After subtracting, we need to give the result to the timer_getMethod property, so visitFieldInsn is still used
- At the end of the method
@Override protected void onMethodExit(int opcode) { if (isLogMessageTime && ! OpcodesUtils.isNative(mMethodAccess) && ! OpcodesUtils.isAbstract(mMethodAccess) && ! OpcodesUtils.isInitMethod(mMethodName)) { mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor) mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false) mv.visitInsn(LADD) mv.visitFieldInsn(PUTSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor) mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor) mv.visitMethodInsn(INVOKESTATIC,LOG_MANAGER,"println","(J)V",false) } super.onMethodExit(opcode) }Copy the code
Practice 2: Method replacement
The target
Let’s take the getDeviceId method of TelephonyManager as an example to see the required code
public static String getDeviceId(Context context) {
String tac = "";
TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (manager.getDeviceId() == null || manager.getDeviceId().equals("")) {
if (Build.VERSION.SDK_INT >= 23) {
tac = manager.getDeviceId(0);
}
} else {
tac = manager.getDeviceId();
}
return tac;
}
Copy the code
We define a static class and method of com. Peakmain. SDK. Utils. ReplaceMethodUtils
public class ReplaceMethodUtils { public static String getDeviceId(TelephonyManager manager) { return ""; } public static String getDeviceId(TelephonyManager manager, int slotIndex) { return ""; }}Copy the code
- The goal is to replace Manager.getDeviceid () with getDeviceId() for our ReplaceMethodUtils
At this point, one of the questions is why are we passing in a TelephonyManager instance, so if we look at the getDeviceId method of TelephonyManager, we see that it’s a non-static method, what happens to a non-static method? It will have a this variable in the local variable table at index 0, which we must replace to consume, similarly if the method is static there is no need to add this variable. Notice that the this variable we’re talking about here is the TelephonyManager instance.
Code implementation
class MonitorMethodCalledReplaceAdapter extends MonitorDefalutMethodAdapter { private String mMethodOwner = "android/telephony/TelephonyManager" private String mMethodName = "getDeviceId" private String mMethodDesc = "()Ljava/lang/String;" private String mMethodDesc1 = "(I)Ljava/lang/String;" private final int newOpcode = INVOKESTATIC private final String newOwner = "com/peakmain/sdk/utils/ReplaceMethodUtils" private final String newMethodName = "getDeviceId" private int mAccess private ClassVisitor classVisitor private String newMethodDesc = "(Landroid/telephony/TelephonyManager;) Ljava/lang/String;" private String newMethodDesc1 = "(Landroid/telephony/TelephonyManager; I)Ljava/lang/String;" /** * Constructs a new {@link AdviceAdapter}. * * @param mv @param access the method's access flags (see {@link Opcodes}). * @param name the method's name. * @param desc */ MonitorMethodCalledReplaceAdapter(MethodVisitor mv, int access, String name, String desc, ClassVisitor classVisitor) { super(mv, access, name, desc) mAccess = access this.classVisitor = classVisitor } @Override void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { if (mMethodOwner == owner && name == mMethodName) { if(descriptor == mMethodDesc){ super.visitMethodInsn(newOpcode,newOwner,newMethodName,newMethodDesc,false) }else if(mMethodDesc1 == descriptor){ super.visitMethodInsn(newOpcode,newOwner,newMethodName,newMethodDesc1,false) } } else { super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface) } } }Copy the code
We found that the code is very simple, it is inside the method body visitMethodInsn method to find the method name + owner + desc are equal, if is TelephoneManager getDeviceId () we’ll replace your method, Simply replace the parameters in super.visitMethodinsn with the ones we want to replace
Practice 3: Empty the method body
class MonitorMethodCalledClearAdapter extends MonitorDefalutMethodAdapter { private String mMethodOwner = "android/telephony/TelephonyManager" private String mMethodName = "getDeviceId" private String mMethodDesc = "()Ljava/lang/String;" private String mMethodDesc1 = "(I)Ljava/lang/String;" private String mClassName private int mAccess ConcurrentHashMap<String, MethodCalledBean> methodCalledBeans = new ConcurrentHashMap<>() /** * Constructs a new {@link MonitorMethodCalledClearAdapter}. * * @param mv * @param access the method's access flags (see {@link Opcodes}). * @param name the method's name. * @param desc */ MonitorMethodCalledClearAdapter(MethodVisitor mv, int access, String name, String desc, String className, ConcurrentHashMap<String, MethodCalledBean> methodCalledBeans) { super(mv, access, name, desc) mClassName = className mAccess = access this.methodCalledBeans=methodCalledBeans } @Override void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { if (mMethodOwner == owner && name == mMethodName && (descriptor == mMethodDesc || mMethodDesc1 == descriptor)) { methodCalledBeans.put(mClassName + mMethodName + descriptor, new MethodCalledBean(mClassName, mAccess, name, descriptor)) clearMethodBody(mv,mClassName,access,name,descriptor) return } super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface); } static void clearMethodBody(MethodVisitor mv, String className, int access, String name, String descriptor) { Type type = Type.getType(descriptor) Type[] argumentsType = type.getArgumentTypes() Type returnType = type.getReturnType() int stackSize = returnType.getSize() int localSize = OpcodesUtils.isStatic(access) ? 0 : 1 for (Type argType : argumentsType) { localSize += argType.size } mv.visitCode() if (returnType.getSort() == Type.VOID) { mv.visitInsn(RETURN) } else if (returnType.getSort() >= Type.BOOLEAN && returnType.getSort() <= Type.DOUBLE) { mv.visitInsn(returnType.getOpcode(ICONST_1)) mv.visitInsn(returnType.getOpcode(IRETURN)) } else { mv.visitInsn(ACONST_NULL) mv.visitInsn(ARETURN) } mv.visitMaxs(stackSize, localSize) mv.visitEnd() } }Copy the code
- When we call visitMethodInsn directly return, we can clear the method body
- However, if we have a return value, we still need to return the default value, otherwise it will directly report an error
- As mentioned above, the method return Type and size are both in Type, so we first need to define a Type (Type of AMS).
- Check whether the current method is static. If it is, place the following parameters in the local variable table from zero in order, and the localSize size is the parameter size +1. If not, place the parameters in the local variable table from 1
- The size of the stack is actually the size of the return value
conclusion
- Gradle+ASM ASM ASM ASM ASM ASM ASM ASM ASM ASM ASM ASM
- Are you very impressed? Then you can do it.
- This project, I am still improving, later I will open source into a dependency library and write another article, convenient for you to use directly, I hope you can pay more attention to
- Finally fill in my Github address: github.com/Peakmain/As…