Asm hook隐私方法调用
Posted QIANDXX
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Asm hook隐私方法调用相关的知识,希望对你有一定的参考价值。
背景
对于第三方SDK隐私方法调用,所以字节码插桩(Gradle Plugin+Transform+ASM)来hook隐私方法,字节码修改工具有直接用ASM、Javassist、饿了么的lancet、滴滴开源的dokit等等,后两者本质都是对ASM的封装,本篇主要也是抱着学习ASM的目的。
分析
效果
先来看下hook效果
//替换前
Log.d("chlog", "androidID=" + Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID));
//替换后
StringBuilder append = new StringBuilder().append("androidID=");
ContentResolver contentResolver = getContentResolver();
PrivacyProxy.privacyLog(false, "android.provider.Settings$Secure.getString");
Log.d("chlog", append.append((String) PrivacyProxy.privacyRejectMethod("android.provider.Settings$Secure", "getString", null, Desc.getParams("(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;"), new Object[]contentResolver, "android_id")).toString());
可以看到getString返回值替换成PrivacyProxy.privacyRejectMethod返回值了,并且增加了PrivacyProxy.privacyLog方法是日志打印。
原理
首先Gradle Plugin+Transform的使用就不说了,可以看文章源码,重点看ASM的使用。 核心思路就是扫描每行代码,遍历每个类的每个方法(可以过滤掉一些类),对每个方法中调用到的每个方法进行筛选,如果是隐私方法,就使用asm hook。
1.首先将class转化为自定义的ClassVisitor
private static byte[] asmTransformCode(byte[] b1) throws IOException
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = new ClassReader(b1);
PrivacyClassVisitor insertMethodBodyAdapter = new PrivacyClassVisitor(cw);
cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
public class PrivacyClassVisitor extends ClassVisitor
public PrivacyClassVisitor(ClassVisitor classVisitor)
super(Opcodes.ASM7, classVisitor);
private String className;
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
super.visit(version, access, name, signature, superName, interfaces);
className = name.replace("/", ".");
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new PrivacyMethodVisitor(access, descriptor, methodVisitor, className, name);
2、自定义了个MethodVisitor,PrivacyMethodVisitor是针对方法级别的判断,比如可以在上面visitMethod中判断调用了某个类中的某个声明方法1,但我们需要对方法1中的每行代码做扫描,所以是在PrivacyMethodVisitor的重写方法visitMethodInsn中操作,visitMethodInsn是执行方法1时中每执行一个方法就回调该visitMethodInsn。而visitCode()和visitEnd()分别只能在方法1的前面和末尾插入指令,显然不能满足。
这里再说下局部变量表和操作数栈。局部变量表存储方法中的局部变量,ASM的指令操作核心是理解操作数栈的特点,就把他理解为一个栈,ASM的指令都是在操作数栈中进行,比如调用一个实例的方法,先是将自身实例加到操作数栈,然后再将全部参数加到操作数栈,然后调用方法时会根据参数个数从操作数栈取最近的几个变量,而且调用方法后会把所有参数和该实例出栈了,然后再把方法返回值入操作数栈,这意味着方法返回值就相当于参数入操作数栈。 如果对ASM指令不熟悉,可以在IDEA上安装ASM ByteCode Outline插件来获取代码对应的ASM指令。 下面代码的核心就是在visitMethodInsn中调用PrivacyProxy.privacyRejectMethod静态函数。
public class PrivacyMethodVisitor extends LocalVariablesSorter
private final boolean isAllow = PrivacyConfig.isAllow;
private String currentClass = "";
private String currentMethod = "";
public PrivacyMethodVisitor(final int access, final String descriptor, final MethodVisitor methodVisitor, String className, String methodName)
//注意三个参数的父类构造器会抛异常
super(Opcodes.ASM7, access, descriptor, methodVisitor);
currentMethod = methodName;
currentClass = className;
/**
* hook方法
*
* @param opcode
* @param owner 所在class全限定名,比如com/example/testgradle/TestActivity
* @param name 调用的方法名
* @param descriptor 调用的方法的参数和返回值,比如findViewById是(I)Landroid/view/View
* @param isInterface
*/
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface)
String mLongName = (owner + "/" + name).replace("/", ".");
String pageName = owner.replace("/", ".");
// if (mLongName.equals("com.example.testgradle.MainActivity.test"))
if (PrivacyConfig.methodHookValueSet.contains(mLongName))
//这样就不是通过代码动态控制,只能一开始通过配置修改,但避免了静态代码检测会出问题
if (isAllow)
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
else
//本来想调用Desc的getParams来获取参数个数,可能是现在加载class会报错
Type methodType = Type.getMethodType(descriptor);
//下面调用方法需要原来方法的实参数组,如何获取参数的实参?
//首先明白原来方法的参数现在都已进入操作数栈了,但这里无法直接获取
// 这里有两种方案
//1、既然参数入操作数栈了,调用参数个数相同的自定义方法,让操作数栈中的变量填入方法中
//继承LocalVariablesSorter,通过newLocal设置一个新的局部变量,那这个局部变量当前的最大的下标,
//这个局部变量类型应该是Object数组,但类型找不到,所以这里随便设置了一个普通类型,那数组存的不是这个下标,存的是加1的下标,类型是我们存进去再去决定。
//等调用方法时再根据下标加载变量。但也存在不确定参数个数,虽然可以通过下标和参数个数可以加载全部变量再加到数组中,但参数类型要和存取指令一一匹配,比较麻烦
//所以设置了几个方法去匹配参数个数,但基本类型需装箱,所以不行
//之前的,基本类型需装箱,所以不行
// int tempLocalIndex = newLocal(Type.LONG_TYPE) + 1;
// Type[] paramsTypes = methodType.getArgumentTypes();
// String[] paramMethod = AsmUtils.getPrivacyMethodParamsDes(paramsTypes.length);
// //这个必须放最前面,因为要获取原来方法参数
// mv.visitMethodInsn(Opcodes.INVOKESTATIC, PrivacyConfig.ProxyClass, paramMethod[0], paramMethod[1], false);
// mv.visitVarInsn(Opcodes.ASTORE, tempLocalIndex);
//2,倒序取出所有参数保存在局部变量表中,然后保存在数组中
List<String> parameterTypeList = AsmUtils.getParams(descriptor);
int parameterCount = parameterTypeList.size();
int tempLocalIndex = newLocal(Type.LONG_TYPE) + 1;
int arrayIndex = tempLocalIndex + parameterCount;
// 操作数栈的变量现在是倒着来的,第一个碰到,其实是最后一个参数,所以倒着遍历参数类型
for (int i = parameterCount - 1; i >= 0; i--)
int index = tempLocalIndex + i;
AsmUtils.typeCastBox(parameterTypeList.get(i), mv);
//保存到局部变量
mv.visitVarInsn(ASTORE, index);
AsmUtils.initArray("java/lang/Object", parameterCount, mv);
//将上面保存到局部变量的方法的参数存到数组
for (int i = 0; i < parameterCount; i++)
//new完通过dup复制操作数栈的栈顶变量,所以多了个变量,所以最后ASTORE是保存new完的,
//不能先ASTORE,会出栈变量
mv.visitInsn(DUP);
mv.visitIntInsn(SIPUSH, i);
mv.visitVarInsn(ALOAD, tempLocalIndex + i); //获取对应的参数
mv.visitInsn(AASTORE);
mv.visitVarInsn(ASTORE, arrayIndex);
// 或者这样写
// mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
// mv.visitVarInsn(ASTORE, arrayIndex);
// for (int i = 0; i < parameterCount; i++)
// mv.visitVarInsn(ALOAD, arrayIndex);
// mv.visitIntInsn(SIPUSH, i);
// mv.visitVarInsn(ALOAD, tempLocalIndex + i); //获取对应的参数
// mv.visitInsn(AASTORE);
//
//出栈调用该方法的实例
if (opcode == Opcodes.INVOKEVIRTUAL)
mv.visitInsn(Opcodes.POP);
//privacyRejectMethod的五个参数入操作数栈
mv.visitLdcInsn(pageName);
mv.visitLdcInsn(name);
//这个参数没用到
mv.visitInsn(Opcodes.ACONST_NULL);
//调用Desc的getParams,将参数从descriptor转化成Class[]
mv.visitLdcInsn(descriptor);
//注意一定要用/分割包,这个类是抄javasist的
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/ngmm365/privacychecker/Desc", "getParams", "(Ljava/lang/String;)[Ljava/lang/Class;", false);
//原来方法的参数数组
// mv.visitVarInsn(Opcodes.ALOAD, tempLocalIndex);
mv.visitVarInsn(Opcodes.ALOAD, arrayIndex);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, PrivacyConfig.ProxyClass, PrivacyConfig.Statement_Reject_SIMPLE_Method, "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Class;[Ljava/lang/Object;)Ljava/lang/Object;", false);
//privacyRejectMethod返回的是Object,得转成原方法返回类型
Type returnType = methodType.getReturnType();
//需要的是分号隔开的,而且没有L,比如java/lang/String,returnType.toString返回的是有L,returnType.getClassName是用点分割
AsmUtils.typeCastUnBox(mv, returnType.getInternalName());
mv.visitInsn(isAllow ? Opcodes.ICONST_1 : Opcodes.ICONST_0);
mv.visitLdcInsn(mLongName);
//调用PrivacyProxy的privacyLog
mv.visitMethodInsn(Opcodes.INVOKESTATIC, PrivacyConfig.ProxyClass, PrivacyConfig.Statement_Log_SIMPLE_Method, "(ZLjava/lang/String;)V", false);
//这里不需要pop,返回是void
//mv.visitInsn(Opcodes.POP);
systemOutPrintln(mLongName, -1, currentMethod, currentClass);
else
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
private void systemOutPrintln(String mLongName, int lineNumber, String currentMethod, String currentClass)
String sb = "\\n========" +
"\\ncall: " + mLongName +
"\\n at: " + currentMethod + "(" + currentClass + ".java:" + lineNumber + ")";
System.out.println(sb);
/**
* hook访问字段
*
* @param opcode
* @param owner
* @param name
* @param descriptor
*/
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor)
String mLongName = (owner + "/" + name).replace("/", ".");
if (PrivacyConfig.fieldHookValueSet.contains(mLongName))
System.out.println("Asm-visitFieldInsn" + " opcode=" + opcode + " owner=" + owner + " name=" + name + " descriptor=" + descriptor);
// if (mLongName.equals("com.example.testgradle.MainActivity.aa"))
if (opcode == Opcodes.GETFIELD)
mv.visitInsn(Opcodes.POP);
mv.visitLdcInsn(mLongName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, PrivacyConfig.IgnoreClass_PrivacyProxy.replace(".", "/"), PrivacyConfig.Statement_Reject_SIMPLE_Field, "(Ljava/lang/String;)Ljava/lang/Object;", false);
AsmUtils.typeCastUnBox(mv, Type.getType(descriptor).getInternalName());
//调用PrivacyProxy的privacyLog
mv.visitInsn(isAllow ? Opcodes.ICONST_1 : Opcodes.ICONST_0);
mv.visitLdcInsn(mLongName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, PrivacyConfig.ProxyClass, PrivacyConfig.Statement_Log_SIMPLE_Method, "(ZLjava/lang/String;)V", false);
else
super.visitFieldInsn(opcode, owner, name, descriptor);
上面代码注释基本都有的,难点就在于获取原来方法的参数值,首先为什么要获取呢?因为 Settings.Secure.getString()方法只有在第二个参数是android_id时才为隐私方法,所以privacyRejectMethod的处理是参数满足才hook,否则反射调用原来方法。
这个例子中这里最难的点在于怎么获取原来方法的参数值。就拿这个getString()为例,首先需要知道的是调用到visitMethodInsn时,getString的两个参数已经入操作数栈了,所以最开始做法是直接调用
public static Object[] privacyMethodParams2(Object object1,Object object2)
return new Object[]object1,object2;
将Object数组保存在局部变量表中,等下入栈参数时再从局部变量表中取。但后来发现不行,因为基本类型参数是无法直接转化为Object的,需要装箱后才可以。而且也不确定原来方法参数有多少个,需要写多个类似方法。
所以最终写法是将每个参数先取出来,该装箱装箱,并保存在局部变量表中,然后填充到实例化后的数组中。
注意的点: 1、要把值存到局部变量表中,需要当前局部变量表的最大下标,所以继承了LocalVariablesSorter,通过newLocal构建一个新的局部变量获取其下标。
2、比如调用方法1回调visitMethodInsn时,此时方法1的参数已入操作数栈,super.visitMethodInsn就是调用方法1了。注意如果不需要这些参数必须从操作数栈中移除,因为调用方法时,参数是取操作数栈里最近的几个变量, 比如方法2(参数1,方法1返回值作为参数2),如果对方法1进行hook,不移除方法1的参数,那方法1的参数还在操作数栈中,并在最顶端,那调用方法2时并不是取参数1,而是取方法1的参数了。移除是同构Pop指令,移除几次就调用几次pop。
3、注意hook方法要区分原方法是否是静态函数,如果不是静态函数,需要将实例pop掉,为啥需要pop的理由和上面一样,这个实例可能会被当成其他方法的参数,导致参数错乱。
4、装箱和拆箱带来的问题
4.1拆箱:
被hook方法有多个,我们是对方法统一处理,统一处理返回值是Object,肯定需要转化为之前的类型,所以会这样写mv.visitTypeInsn(Opcodes.CHECKCAST, type);
但是如果返回值是基本类型比如int,这样就有问题了,此时type是I,是无法转成I的,需要先转化成java/lang/Integer,然后再调用intValue变成基本类型,其他基本类型也是类似的。
当然如果原来返回就是java/lang/Integer,那就不需要额外拆箱了。
4.2装箱:
这里需要获取原来方法参数,并放到Object数组中,如果参数是基本类型,需要先装箱为对象类型,再放入到Object数组中,上面也说过了
源码在 github.com/chenhaomr/A… 使用注意事项,因为不是通过buildSrc,所以需要本地打包插件./gradlew uploadArchives,一开始如果找不到插件,先注释apply plugin: 'PrivacyCheckPlugin’再打包。如果移植,需要将PrivacyConfig里面的一些包名信息改掉。
以上是关于Asm hook隐私方法调用的主要内容,如果未能解决你的问题,请参考以下文章
Android检测获取MAC权限--基于Xposed的方法检测