Gradle+ASM in practice - Theoretical articles on the complete solution of the privacy method problem

foreword

demand background

  • Third-party sdk will always frequently call certain privacy methods, such as MAC address, AndroidId, etc.
  • What I want now is that, for example, when calling the device id, the getDeviceId of the telephoneManger method will be called. If we can find a method to call getDeviceId, and then replace it with our own method or clear the method body, the problem will be solved. Well
  • According to the nature of the programmer, I wanted to be lazy, find a library, and read a few articles, but none of them achieved what I wanted. The current articles about privacy method calls or privacy policy rectification, some also It's just simple use of other third parties such as Epic and AOP, but these actually can't achieve the effect we want, and some just say that the check privacy method is called by those methods.
  • So I have this article and the implemented library. I hope it can help everyone and completely solve the problem that the third-party sdk frequently calls privacy methods to be notified or taken off the shelf. It can also be used to learn ASM.
  • Through Gradle+ASM actual combat - advanced article, we know that we actually only need to pay attention to the ClassVisitor we inherit.

Basic knowledge

ClassVisitor

image.png

order of method execution

Let's look directly at the annotations of ClassVisitor

image.png

  • []: Indicates that it is called at most once, it may not be called, but it is called at most once
  • ()|: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序
  • *: 表示方法可以调用0次或多次

我们主要关注以下几个方法

visit
(visitField |visitMethod)* 
visitEnd
复制代码
四个方法
1、visit方法,扫描类的时候会进入这里,最多被执行一次
 /**
    * @param version 类版本 ASM4~ASM9可选
    * @param access 修饰符 如public、static、final
    * @param name 类名 如:com/peakmain/asm/utils/Utils
    * @param signature 泛型信息 
    * @param superName 父类
    * @param interfaces 实现的接口
    */
   @Override
   void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {}
复制代码
2、visitField:访问属性的时候用到,用到不多,用到的时候细说
    @Override
    FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        return super.visitField(access, name, descriptor, signature, value)
    }
复制代码
3、visitMethod:扫描到方法的时候调用,这也是我们主要介绍的方法,细节下面介绍
    /**
     * 扫描类的方法进行调用
     * @param access 修饰符
     * @param name 方法名字
     * @param descriptor 方法签名
     * @param signature 泛型信息
     * @param exceptions 抛出的异常
     * @return
     */
    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        return  super.visitMethod(access, name, descriptor, signature, exceptions)
    }
复制代码
4、visitEnd:这是这些visitXxx()方法之后最后一个执行的方法,最多被调用一次
   @Override
    void visitEnd() {
        super.visitEnd()
    }
复制代码

MethodVisitor

通过调用ClassVisitor类的visitMethod()方法,会返回一个MethodVisitor类型的对象

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();
}
复制代码

假设我们有以下方法

public static String getMeid(Context context) {//方法体
    TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        return manager.getMeid();
    }
    return "getMeid";
}
复制代码

visitXxxInsn负责的就是这个方法的方法体内的内容,也就是指{}这个里面包含的属性,方法

方法调用的顺序
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
    visitCode
    (
        visitFrame |
        visitXxxInsn |
        visitLabel |
        visitInsnAnnotation |
        visitTryCatchBlock |
        visitTryCatchAnnotation |
        visitLocalVariable |
        visitLocalVariableAnnotation |
        visitLineNumber
    )*
    visitMaxs
]
visitEnd
复制代码
分组

我们可以分成三组

  • 第一组:visitCode方法之前的方法,主要负责parameter、annotation和attributes等内容。对于我们来说主要关注visitAnnotation即可
  • 第二组:visitCode和visitMaxs方法之间的方法,这些之间的方法,主要负责方法的“方法体”内的opcode内容。visitCode代表方法体的开始,visitMaxs代表方法体的结束
  • 第三组:visitEnd()方法,是最后一个进行调用的方法
注意点

我们需要注意的是:

  • visitAnnotation:会被调用多次
  • visitCode:只会被调用一次
  • visitXxxInsn:可以调用多次,这些方法的调用,就是在构建方法的方法体
  • visitMaxs:只会被调用一次
  • visitEnd:只会被调用一次

AdviceAdapter

我们在项目中用了AdviceAdapter,那么AdviceAdapter的是什么呢? AdviceAdapter实际是引入了两个方法onMethodEnter()方法和onMethodExit()方法。并且这个类属于MethodVisitor,也就是我们要讲的第三个方法

源码分析
onMethodEnter
  @Override
  public void visitCode() {
    super.visitCode();
    if (isConstructor) {//判断是否是构造函数
      stackFrame = new ArrayList<>();
      forwardJumpStackFrames = new HashMap<>();
    } else {
      onMethodEnter();
    }
  }
复制代码

实际还是调用了visitCode方法,只是处理了构造函数(())相关逻辑,如果直接使用visitCode()方法则可能导致()方法出现错误

onMethodExit

image.png 我们会发现调用的方法是在visitInsn方法中,那肯定有人问,为什么在visitInsn中而不是visitEnd里面呢?不是说它是最后一个方法调用。 假设我们有个方法是获取AndroidId的

public static String getAndroidId(Context context) {
    return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
复制代码

这个方法现在的正常ASM应该是

mv.visitCode()
mv.visitxxxInsn()
mv.visitInsn(AReturn)
mv.visitMaxs()
mv.visitEnd()
复制代码

这时候我们在visitEnd的时候添加或者visitMaxs添加,因为前面已经Return了,所以后面是不会执行的

方法初始化Frame

  • 在JVM Stack当中,是栈的结构,里面存储的是frames;
  • 每一个frame空间可以称之为Stack Frame。
  • 当调用一个新方法的时候,就会在JVM Stack上分配一个frame空间
  • 当方法退出时,相应的frame空间也会JVM Stack上进行清除掉(出栈操作)。
  • 在frame空间当中,有两个重要的结构,即local variables(局部变量表)和operand stack(操作数栈)

image.png 方法刚开始的时候,操作数栈operand stack为空,不需要存储任何数据,局部变量表需要考虑三个因素

  • 当前方法是否为static方法。如果当前方法是non-static方法,则需要在local variables索引为0的位置存在一个this变量;如果当前方法是static方法,则不需要存储this。
  • 当前方法是否接收参数。方法接收的参数,会按照参数的声明顺序放到local variables当中。
  • 方法的参数是否包含long或double,如果参数是long或者double类型,那么它在local variables占用两个位置

Type

在.java文件中,我们经常使用java.lang.Class类;而在.class文件中,需要经常用到internal name、type descriptor和method descriptor;而在ASM中,org.objectweb.asm.Type类就是帮助我们进行两者之间的转换。

image.png

获取Type的几个方式

Type类有一个private的构造方法,因此Type对象实例不能通过new关键字来创建。但是,Type类提供了static method和static field来获取对象。

  • 方式一:java.lang.class
Type type=Type.getType(String.class)
复制代码
  • 方式二:descriptor
Type type = Type.getType("Ljava/lang/String;");
复制代码
  • 方式三:internal name
Type type = Type.getObjectType("java/lang/String");
复制代码
  • 方式四:static field
 Type type = Type.INT_TYPE;
复制代码

常用的几个方法

  • getArgumentTypes()方法,用于获取“方法”接收的参数类型
  • getReturnType()方法,用于获取“方法”返回值的类型
  • getSize()方法,用于返回某一个类型所占用的slot空间的大小
  • getArgumentsAndReturnSizes()方法,用于返回方法所对应的slot空间的大小

实战

上面的基础知识大家学完了,那么就可以开始实战了。下面所有的实战都是继承AdviceAdapter

实战一:监控方法的耗时时间

  • 假设有以下代码:
public String getMethodTime(long var1) {

    try {
        Thread.sleep(1000L);
    } catch (InterruptedException var4) {
        var4.printStackTrace();
    }
    return "getMethod";
}
复制代码
目标

通过注解来监控获取该方法的耗时时间,代码的位置MonitorPrintParametersReturnValueAdapter

方案
- 每个方法动态添加一个long属性,名字是方法的前面+timer_,如上面的方法定义的属性是timer_getMethodTime
- 方法前后插入代码,实现效果如下
复制代码
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";
    }
}
复制代码
代码实现
  • 首先我们定义一个注解类com.peakmain.sdk.annotation.LogMessage
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMessage {
    /**
     * 是否打印方法耗时时间
     */
    boolean isLogTime() default false;

    /**
     *
     * 是否打印方法的参数和返回值
     */
    boolean isLogParametersReturnValue() default false;

}
复制代码
  • 需要判断方法是否有注解,毫无疑问我们用到的是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)
}
复制代码
  • 我们需要在方法体开始的时候插入属性,因为是方法开始位置,所以肯定是visitCode方法
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()
}
复制代码
//获取时间属性
static String getTimeFieldName(String methodName) {
    return "timer_" + methodName
}
复制代码

我们需要创建属性,那就需要用到classWriter属性,通过visitField去创建属性,需要注意的是,我们创建属性之后,一定要调用visitEnd

  • 接下来就是方法体开始的时候,添加timer_getMethodTime -= System.currentTimeMillis();,大家一定还记得AdviceAdapter的两个方法把,没错就是onMethodEnter和onMethodExit两个方法,因为是方法的开始,所以我们需要在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)
    }

}
复制代码

其实代码也很简单:首先我们获取自己在visitCode时定义的属性timer_getMethod,随后就是获取当前时间,获取当前时间是方法,所以用的是visitMethodInsn,随后进行相减,相减之后我们需要将结果给属性timer_getMethod,所以用到的还是visitFieldInsn

  • 方法结束的时候
@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)
}
复制代码

实战二:方法替换

目标

我们以TelephonyManager的getDeviceId方法为例 看需求的代码

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;
}
复制代码

我们定义一个静态类和方法com.peakmain.sdk.utils.ReplaceMethodUtils

public class ReplaceMethodUtils {

    public static String getDeviceId(TelephonyManager manager) {
        return "";
    }

    public static String getDeviceId(TelephonyManager manager, int slotIndex) {
        return "";
    }
}
复制代码
  • 实现的目标是将manager.getDeviceId()替换成我们ReplaceMethodUtils的getDeviceId()

这时候肯定有人问为什么将传入TelephonyManager实例,我们看TelephonyManager的getDeviceId方法,我们发现是个非静态方法,非静态方法会怎样?它会在局部变量表索引0的位置存在一个this变量,我们替换肯定是要把它给消费掉,那同理如果方法是静态方法就不需要添加this变量。注意我们这里说的this变量是TelephonyManager这个实例。

代码实现
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)
        }
    }
}
复制代码

We found that the code is very simple, that is to find out whether the current method name + owner + desc is equal in the method body visitMethodInsn method, if it is TelephoneManager.getDeviceId(), we will replace it with our own method, and directly replace the parameters in super.visitMethodInsn It's just what we want to replace

Actual combat three: clear 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()
    }
}
复制代码
  • When we call visitMethodInsn to return directly, we can clear the method body
  • But if we have a return value, we still need to return the default value, otherwise we will report an error directly
  • As we said above, the return type and size of the method are in Type, so we first need to define a Type type (Type of ams)
  • Determine whether it is currently a static method. If so, the next parameters are placed in the local variable table from zero in order. The size of localSize is the size of the parameter + 1. If not, it is placed in the local variable table from 1. The size of localSize is the size of the parameter.
  • The size of the stack is actually the size of the return value.

Summarize

  • At this point, the Gradle+ASM combat - the theoretical chapter on the complete solution of the privacy method problem is over. On the whole, it is actually relatively simple. The difficulty is that there are very few articles on ASM in the market, and everyone needs to be familiar with ASM+Gradle.
  • If everyone is very excited, then you can start it.
  • As for this project, I am still improving it. I will open source it as a dependency library and write another article in the future, so that you can use it directly. I hope you can pay more attention to it.
  • Finally, fill in my github address: github.com/Peakmain/As…

Guess you like

Origin juejin.im/post/7086262397644013575