Gradle+ASM实战——隐私方法问题彻底解决之理论篇

前言

  • 之前两篇文章我写了入门篇:Gradle 插件 + ASM 实战——入门篇和Gradle+ASM实战——进阶篇,对gradle+ASM不熟的大家可以去上篇文章查看
  • ASM API文档: javadoc
  • ASM使用手册: 英文版、 中文版
  • github地址:https://github.com/Peakmain/AsmActualCombat

需求背景

  • 第三方sdk会总是频繁调用某些隐私方法,比如MAC地址,AndroidId等
  • 现在想要的需求是,比如调用设备id的时候,会调用telephoneManger方法的getDeviceId,如果我们能找到调用getDeviceId的方法,然后将其替换成我们自己的方法或者将方法体清空,问题不就解决了嘛
  • 按程序员的本质,我本想去偷个懒,找个库,也看过几篇文章,但是都没有达到自己的想要的,当前有关隐私方法调用或者隐私政策整改的文章,有的也只是简单的用别人的第三方如Epic,AOP,而这些实际也达不到我们想要的效果,有的也只是说检查隐私方法被那些方法调用
  • 所以就有了这篇文章和实现的库,希望可以帮助到大家,彻底解决第三方sdk频繁调用隐私方法被通报或者下架的问题,也可供学习ASM哦。
  • 通过 Gradle+ASM实战——进阶篇这篇文章我们知道我们实际只需要关注自己继承的ClassVisitor即可

基础知识

ClassVisitor

image.png
方法执行的顺序

我们直接看ClassVisitor的注解

image.png
  • []: 表示最多调用一次,可以不调用,但最多调用一次
  • ()|: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序
  • *: 表示方法可以调用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-f92fa5-1649898740300)]
我们会发现调用的方法是在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)
        }
    }
}

我们发现代码很简单,就是在方法体visitMethodInsn方法里面去找当前的方法名字+owner+desc是否相等,如果是TelephoneManager.getDeviceId()我们就替换成自己的方法,直接将super.visitMethodInsn里面的参数换成我们要替换的就可以了

实战三:清空方法体

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 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 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()
    }
}
  • 当我们调用到visitMethodInsn直接return的时候,就可以可以清空方法体了
  • 但是我们如果有返回值的时候,还是需要返回默认值,不然会直接报错
  • 上面我们说过,方法的返回类型和大小都在Type中,所以我们首先需要定义一个Type类型(ams的Type)
  • 判断当前是否是静态方法,如果是则接下来的参数按照顺序从零开始放到局部变量表,localSize大小就是参数大小+1,如果不是则从1开始放到局部变量表localSize大小就是参数大小
  • stack的大小实际是返回值的大小就可

总结

  • 至此Gradle+ASM实战——隐私方法问题彻底解决之理论篇就结束了,整体来说其实还是比较简单的,难点就是市场上对ASM的文章非常少,还有就是需要大家对ASM+Gradle熟悉使用
  • 大家是不是非常心动了呢,那就可以动手搞起来了。
  • 这个项目呢,我还在完善,后期我会开源成依赖库并再写一篇文章,方便大家直接使用,希望大家可以多关注关注
  • 最后再填上我的github地址:https://github.com/Peakmain/AsmActualCombat

你可能感兴趣的:(Gradle+ASM实战——隐私方法问题彻底解决之理论篇)