前言
- 之前两篇文章我写了入门篇: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
方法执行的顺序
我们直接看ClassVisitor的注解
-
[]
: 表示最多调用一次,可以不调用,但最多调用一次 -
()
和|
: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序 -
*
: 表示方法可以调用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方法,只是处理了构造函数(<init>())相关逻辑,如果直接使用visitCode()方法则可能导致<init>()方法出现错误
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(操作数栈)
方法刚开始的时候,操作数栈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类就是帮助我们进行两者之间的转换。
获取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<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()
}
}
- 当我们调用到visitMethodInsn直接return的时候,就可以可以清空方法体了
- 但是我们如果有返回值的时候,还是需要返回默认值,不然会直接报错
- 上面我们说过,方法的返回类型和大小都在Type中,所以我们首先需要定义一个Type类型(ams的Type)
- 判断当前是否是静态方法,如果是则接下来的参数按照顺序从零开始放到局部变量表,localSize大小就是参数大小+1,如果不是则从1开始放到局部变量表localSize大小就是参数大小
- stack的大小实际是返回值的大小就可
总结
- 至此Gradle+ASM实战——隐私方法问题彻底解决之理论篇就结束了,整体来说其实还是比较简单的,难点就是市场上对ASM的文章非常少,还有就是需要大家对ASM+Gradle熟悉使用
- 大家是不是非常心动了呢,那就可以动手搞起来了。
- 这个项目呢,我还在完善,后期我会开源成依赖库并再写一篇文章,方便大家直接使用,希望大家可以多关注关注
- 最后再填上我的github地址:https://github.com/Peakmain/AsmActualCombat