Asm hook隐私方法调用

1,593 阅读9分钟

背景

对于第三方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里面的一些包名信息改掉。