阅读 369

去哪儿 Android 客户端隐私安全处理方案

去哪儿 Android 客户端隐私安全处理方案

图片

作者:江保贵 去哪儿网前端架构师

2011年4月加入去哪儿网,目前在基础研发大前端团队,专注于移动端质量效率提升,监控体系设计搭建等工作。先后多次参与客户端框架改版设计、制定开发规范,设计开发移动端差分升级系统、移动端交互日志&性能采集系统、渠道快速打包自动发布系统等,喜欢积极向上的团队氛围、不断学习新技术、追求技术创新带来的效率及性能提升。

1 背景

2019年央视3.15晚会曝光的个人隐私通过手机 app 泄露的案例令人触目惊心,作为一个应用开发者为了快速实现功能的快速开发,需要使用如广告、推送、统计、定位/地图、支付、社交等功能时都会引用相关的第三方 SDK,这些 SDK 很少以源码方式提供,也就是说这些 SDK 内执行逻辑对开发者来说是未知的,这些 SDK 自身滥用或者有安全漏洞,非法收集应用和用户隐私信息、远程下发执行恶意代码等造成的后果将不堪设想。

这里我分享一下去哪儿网如何通过技术手段,管控自身和三方 SDK 获取隐私信息的,因篇幅较长,先讲一下我们实现的功能特性及优势,后边详细介绍一下技术实现细节。

图片

2 功能特性及优势

1、全局监控

应用自身和第三方 SDK 对敏感 API 调用,目前已经监控的如下:

  • 网络请求

  • 请求申请应用权限

  • 读取设备应用列表

  • 读取 Android SN(Serial)

  • 读写联系人、通话记录、日历、本机号码

  • 获取定位、基站信息

  • Mac 地址、IP 地址

  • 读取 IMEI(DeviceId)、MEID、IMSI、ADID(AndroidID)

2、全面高效

不需要升级更新原有 SDK 版本依赖和更改业务逻辑代码,不用更改原有开发模式,只需要一次配置,无论之前或者以后新增 SDK 都可以监控。

3、开发简单 

新增监控开发简单,一个自定义方法+一行注解,编译运行就能生效。

4、对工具库无版本依赖

工具库有哪些方法,就 hook 哪些 API,不需要复杂的版本依赖判断。

5、可扩展性强

工具方法自定义,可记录调用堆栈、返回空数据等可自主控制。

3 初步方案

我们首先考虑在 Android 项目编译器使用自定义的 Android Transform,全局 hook 相关 API 调用,替换为我们自定义的方法进行限制,这样无论是 javaClass 还是 jar 都可以控制。

图片

AOP 操作字节码可用的技术很多,如 ASM、AspectJ、javassit 等,这里我们用 ASM 做一个简单替换原生获取 IMEI 的例子:

原始方法调用:

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY\_SERVICE);  
telephonyManager.getDeviceId();
复制代码

要实现的效果:

 TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY\_SERVICE);  
 //调用我们的工具类QTelephonyManager
 QTelephonyManager.getDeviceId(telephonyManager);
复制代码

我们跳过 Android Transform 部分介绍(不了解的可以看一下 Transform 详解:www.jianshu.com/p/37a5e0588… ASMPlugin 中 ASMified 查看 class 字节码,对比前后发生变化,修改原来的字节码:忽略相同代码,原始方法 ASMCode 如下:

//... 
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "android/telephony/TelephonyManager", "getDeviceId", "()Ljava/lang/String;", false);
复制代码

**原始方法ASMCode解析:
**

  • methodVisitor 是方法解析器

  • visitMethodInsn 代表解析方法调用

  • INVOKEVIRTUAL 是方法修饰符,代表调用实体类的方法

  • android/telephony/TelephonyManager 代表的是调用该方法的 owner

  • getDeviceId 代表的是调用的方法名称

  • ()Ljava/lang/String;() 代表的是方法的参数,后边是返回值

需要替换成我们的方法对应的 ASMCode 如下:

//...
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/mqunar/goldcicada/lib/QTelephonyManager", "getDeviceId", "(Landroid/telephony/TelephonyManager;)Ljava/lang/String;", false);
复制代码

调用自定义方法ASMCode解析

  • INVOKESTATIC 替换实体类方法调用为静态方法

  • 方法所在的类替换为我们自定义的工具类 com/mqunar/goldcicada/lib/QTelephonyManager

  • 方法名称我们保持跟原生方法一致

  • 方法的参数和返回值,我们修改为接收 android/telephony/TelephonyManager,返回值保持不变

自定义工具类 QTelephonyManager 代码实现:

    public static String getDeviceId(TelephonyManager telephonyManager) {
        if (!GoldCicada.isCanRequestPrivacyInfo()) {
            // print or record StackTrace
            return "";
        }
        return telephonyManager.getDeviceId();
    }

复制代码

Transform中对字节码处理代码实现:

public static boolean shouldInject = false
    final static def QTelephonyManager = 'com/mqunar/goldcicada/lib/QTelephonyManager'
    final static def TelephonyManager = "android/telephony/TelephonyManager"
    static byte\[\] transform(byte\[\] bytes) {
        def classNode = new ClassNode()
        new ClassReader(bytes).accept(classNode, 0)//读取字节码,结果存放到classNode中
        classNode = transform(classNode)//操作classNode处理class字节码
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE\_MAXS)
        classNode.accept(cw)
        return cw.toByteArray()
    }
    static ClassNode transform(ClassNode klass) {
        if (!shouldInject) {//开关控制
            return klass
        }
        // 检测到是自己工具类时不注入
        if (klass.name.startsWith(QTelephonyManager)) {
            return klass
        }
        klass.methods?.each { MethodNode method ->
            method.instructions?.iterator()?.each { AbstractInsnNode insnNode ->
                if (insnNode.opcode == INVOKEVIRTUAL 
                        && insnNode.owner == TelephonyManager 
                        && insnNode.name == "getDeviceId" 
                        && insnNode.desc == "()Ljava/lang/String;") {
                    QBuildLogger.log "QHOOK ${insnNode.owner}.${insnNode.name}${insnNode.desc} @ ${klass.name}.${method.name}${method.desc}"
                    insnNode.owner = QTelephonyManager
                    insnNode.opcode = INVOKESTATIC
                    insnNode.desc = "(L${TelephonyManager};)Ljava/lang/String;"
                }
            }
        }
        return klass
    }
复制代码

这种方案实现了全局替换的功能,但是方案的缺点很明显:

  • Transform脚本代码硬编码判断;

  • 对工具类库有依赖,如果工具类库新增了 hook 类或者方法,对老版本打包的时候就需要做版本判断,要不然运行时就报 ClassNotFoundException;

  • 开发需要对 ASM 字节码编程有一定了解,每次新增 hook 都需要对比前后变化进行修改验证,工作量比较大。

有没有一种方法,同时解决以上三个问题?

4 进阶方案

要解决应上边遇到的问题,首先我们要考虑使用一个通用的配置,Transform 的字节码处理器不用关心具体要 hook 的类和方法,这里我们使用到自定义注解,在自定义方法上加上注解,告诉我要替换什么类和方法、该方法是静态、非静态,方法参数等。然后经过 Transform 先把加了自定义注解的所有配置读取出来,生成一份需要 hook 的方法配置列表。再次 Transform 根据配置进行 hook,就初步解决以上三个问题。流程图如下:

图片

大家可能有一个疑问:自定义注解都是和注解处理器一起使用的,你这里为什么使用一个 Transform 进行注解的读取和解析?

其实大家用过注解处理器就知道:注解处理器处理最大的优点是可以在生成 class 字节码之前执行,处理的是 java 源码,此时可以结合 javapoet 动态生成 java 源代码;缺点是每个使用注解的项目都需要配置注解处理器,这样首先配置就比较麻烦,再一个结合我们这个项目,注解的作用是给 Transform 用的,这时候就适合在 App 打包时用 Android Transform 先生成一份配置,接着再交给下一个 Transform 进行 Hook。

4.1 创建一个自定义注解库 apt-annotation

声明自定义注解 AsmField,声明三个参数

  • @Retention 表示的是注解生效的生命周期,RetentionPolicy.CLASS 表示注解被保留到 class 文件,但 jvm 加载 class 文件时候被遗弃

  • oriClass 代表需要 hook 的类

  • oriMehod 代表被 hook 的方法名称,默认与工具类方法名一致

  • oriAccess 方法类型,默认是 static 方法

  • 方法参数和返回值由原方法的类型和自定义方法决定,后边在注解处理器初详细描述

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface AsmField {
    Class oriClass();
    String oriMehod() default "";
    MethodAccess oriAccess() default MethodAccess.INVOKESTATIC;
    public enum MethodAccess {
        /\*\* 静态方法 static \*/
        INVOKESTATIC(Opcodes.INVOKESTATIC),
        /\*\* 接口方法,如调用 onClickListener.onClick(view) \*/
        INVOKEINTERFACE(Opcodes.INVOKEINTERFACE),
        /\*\* 实体类方法 \*/
        INVOKEVIRTUAL(Opcodes.INVOKEVIRTUAL),
        /\*\* 调用super方法 \*/
        INVOKESPECIAL(Opcodes.INVOKESPECIAL);
        int value;
        MethodAccess(int value) {
            this.value = value;
        }
        public int value() {
            return value;
        }
    }
}
复制代码

4.2 工具类库处理 toolsLibrary

  • 让工具类库 toolsLibrary 依赖我们的 apt-annotation

  • 在工具类 QTelephonyManager 的自定义方法上增加注解

 @AsmField(oriClass = TelephonyManager.class, oriAccess = MethodAccess.INVOKEVIRTUAL)
    public static String getDeviceId(TelephonyManager telephonyManager) {
        if (!GoldCicada.isCanRequestPrivacyInfo()) {
            // print or record StackTrace
            return "";
        }
        return telephonyManager.getDeviceId();
    }
复制代码

4.3 主项目依赖工具库

  • 在主项目中 build.gradle 的 dependencies 中配置上 toolsLibrary 项目
implementation project(":toolsLibrary")
复制代码

4.4 自定义 Android 插件 AnnotationParserTransform 读取注解,生成配置列表

  • 解析注解 AnnotationParserTransform 核心代码如下:
//
    static void parseAsmAnnotation(byte\[\] bytes) {
        def klass = new ClassNode()
        new ClassReader(bytes).accept(klass, 0)//读取字节码,结果存放到classNode中
        klass.methods.each { method ->
            method.invisibleAnnotations?.each { node ->
                if (node.desc == 'Lcom/mqunar/qannotation/AsmField;') {
                    asmConfigs << new AsmItem(klass.name, method, node)
                }
            }
        }
    }
    static class AsmItem {
        public String oriClass
        public String oriMethod
        public String oriDesc
        public int oriAccess = Opcodes.INVOKESTATIC

        public String targetClass
        public String targetMethod
        public String targetDesc
        public int targetAccess = Opcodes.INVOKESTATIC

        public AsmItem(String targetClass, MethodNode methodNode, AnnotationNode node) {
            this.targetClass = targetClass
            this.targetMethod = methodNode.name
            this.targetDesc = methodNode.desc
            String sourceName
            for (int i = 0; i < node.values.size() / 2; i++) {
                def key = node.values.get(i \* 2)
                def value = node.values.get(i \* 2 + 1)
                if (key == 'oriClass') {
                    sourceName = value.toString()
                    oriClass = sourceName.substring(1, sourceName.length() - 1)
                } else if (key == 'oriAccess') {
                    this.oriAccess = Opcodes."${value\[1\]}"
                } else if (key == "oriMethod") {
                    this.oriMethod = value
                }
            }
            if (this.oriMethod == null) {
                this.oriMethod = targetMethod
            }
            if (this.oriAccess == Opcodes.INVOKESTATIC) {//静态方法,参数和返回值一致
                this.oriDesc = targetDesc
            } else {
                String param = targetDesc.split("\\\\)")\[0\] + ")"
                String returnValue = targetDesc.split("\\\\)")\[1\]
                if (param.indexOf(sourceName) == 1) {
                    param = "(" + param.substring(param.indexOf(sourceName) + sourceName.length())
                }
                this.oriDesc = param + returnValue
            }
        }
    }
复制代码

4.5 自定义 Android 插件 ASMTransform 根据配置替换字节码

  • 根据配置列表,优雅地进行字节码替换
class AsmInjectProxy {
   static byte\[\] transform(byte\[\] bytes) {
        if (AsmAnnotationParser.asmConfigs.isEmpty()) {
            return bytes
        }
        def classNode = new ClassNode()
        new ClassReader(bytes).accept(classNode, 0)//读取字节码,结果存放到classNode中
        classNode = transform(classNode)
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE\_MAXS)
        classNode.accept(cw)
        return cw.toByteArray()
    }

    /\*\*
     \* 对ClassNode对象做处理
     \* @param klass
     \* @return
     \*/
    static ClassNode transform(ClassNode klass) {
        def asmItems = AsmAnnotationParser.asmConfigs
        for (def it : asmItems) {
            if (klass.name.startsWith(it.targetClass)) {
                return klass//目标类不注入
            }
        }
        klass.methods?.each { MethodNode method ->
            method.instructions?.iterator()?.each { AbstractInsnNode insnNode ->
                if (insnNode instanceof MethodInsnNode) {
                    asmItems.each { asmItem ->
                        if (asmItem.oriDesc == insnNode.desc && asmItem.oriMethod == insnNode.name) //
                        && insnNode.opcode == asmItem.oriAccess && insnNode.owner == asmItem.oriClass) {
                                insnNode.opcode = asmItem.targetAccess
                                insnNode.name = asmItem.targetMethod
                                insnNode.desc = asmItem.targetDesc
                                insnNode.owner = asmItem.targetClass
                                println "QHOOK ${insnNode.owner}.${insnNode.name}${insnNode.desc} @ ${klass.name}.${method.name}${method.desc}"
                            }
                        }
                    }
                }
            }

        return klass
    }
}
复制代码

至此我们使用通用配置、动态替换的目标已经实现,基本上能实现80%以上的需求,但是随着功能需求复杂度增加,如 hook 创建对象 new Instance,子类用 super 调用父类方法,子类用 this. 调用父类有的方法等,上边的功能无法实现,因此我们丰富完善支持更复杂的功能。

5 超级HOOK

5.1 子类使用 super 关键字调用父类的方法

如 Hook 自定义 MainActivity 中调用获取权限方法 super.requestPermissions:我们依然提供一个 public static 方法与 Activity 一样的 requestPermissions 方法,增加注解@AsmField(oriClass = Activity.class,oriAccess = MethodAccess.INVOKESPECIAL)。

 @AsmField(oriClass = Activity.class, oriAccess = MethodAccess.INVOKESPECIAL)
    public static void requestPermissions(Activity activity, String\[\] permissions, int requestCode) {
        if (!GoldCicada.isCanRequestPrivacyInfo()) {
            // print or record StackTrace
            return;
        }
        //避开自己方法调用,调用父类方法
        ReflectUtils.invokeSuperMethod(activity, "requestPermissions", new Class\[\]{String\[\].class, Integer.TYPE}, new Object\[\]{permissions, requestCode});
    }
复制代码

这样 super.requestPermissions(permissions, requestCode)编译后就被替换为QActivity.requestPermissions(permissions, requestCode)

大家注意,我们自定义方法中不能直接调用 activity.requestPermissions(permissions, requestCode)方法,因为这样调用是调用 MainActivity 自己的 requestPermissions 方法,如果 MainActivity 中重写了父类方法,并调用了 super.这样 hook 后就会死循环调用。我们这里的做法是,需要在工具类中反射调用父类的方法。

5.2 ReflectUtils 反射父类工具代码

 public static <T> T invokeSuperMethod(final Object obj, final String name, final Class\[\] types, final Object\[\] args) {
        try {
            final Method method = getMethod(obj.getClass().getSuperclass(), name, types);
            if (null != method) {
                method.setAccessible(true);
                return (T) method.invoke(obj, args);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null;
    }

    private static Method getMethod(final Class<?> klass, final String name, final Class<?>\[\] types) {
        try {
            return klass.getDeclaredMethod(name, types);
        } catch (final NoSuchMethodException e) {
            final Class<?> parent = klass.getSuperclass();
            if (null == parent) {
                return null;
            }
            return getMethod(parent, name, types);
        }
    }
复制代码

5.3 子类调用父类方法,非super.

如 Hook 自定义 Activity 中调用获取权限方法 this.requestPermissions/requestPermissions:我们依然提供一个 public static 方法与 Activity 一样的 requestPermissions 方法,增加注解 @AsmField(oriClass = java.lang.Object.class,oriAccess = MethodAccess.INVOKEVIRTUAL)

 @RequiresApi(api = Build.VERSION\_CODES.M)
    @AsmField(oriClass = Object.class, oriAccess = MethodAccess.INVOKEVIRTUAL)
    public static void requestPermissions(Object activity, String\[\] permissions, int requestCode) {
        if (!GoldCicada.isCanRequestPrivacyInfo()) {
            // print or record StackTrace
            return;
        }
        if (activity instanceof Activity) {
            ((Activity) activity).requestPermissions(permissions, requestCode);
        } else {//非Activity hook,反射调用
            ReflectUtils.invokeMethod(activity, "requestPermissions", new Class\[\]{String\[\].class, Integer.TYPE}, new Object\[\]{permissions, requestCode});
        }
    }
复制代码

这样 this.requestPermissions(permissions, requestCode)编译后就被替换为 QActivity.requestPermissions(permissions, requestCode)

大家注意,因为我们无法知道运行时的子类名称,因此我们也不知道被 hook 的类是谁,这里就用 Object 对象替代,并且参数也用 Object,在运行时判断传入的参数是否是 Activity 的子类,如果是就正常调用,不是的话就反射调用。

5.4 ReflectUtils 反射工具代码

    public static <T> T invokeMethod(final Object obj, final String name, final Class\[\] types, final Object\[\] args) {
        try {
            final Method method = getMethod(obj.getClass(), name, types);
            if (null != method) {
                method.setAccessible(true);
                return (T) method.invoke(obj, args);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }

        return null;
    }
复制代码

5.5 hook 创建对象方法

如我们想实现 okhttp 网络请求拦截,Hook 创建 new OkHttpClient.Builder()对象添加拦截器:

  • hook 之前,我们先查看 new 对象的 ASM 字节码和需要替换代码对比:

【变换前】

methodVisitor.visitTypeInsn(NEW, "okhttp3/OkHttpClient$Builder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "okhttp3/OkHttpClient$Builder", "<init>", "()V", false);
//...
methodVisitor.visitMaxs(2, 1);
复制代码

【变换后】

methodVisitor.visitMethodInsn(INVOKESTATIC, "com/mqunar/goldcicada/lib/QOkHttpClient", "getOkHttpClientBuilder", "()Lokhttp3/OkHttpClient/Builder;", false);
//...
methodVisitor.visitMaxs(1, 1);
复制代码

懵逼了,这里跟上边的所有的 hook 都不一样,这里三行代码 new 一个对象,后边还有一个数值变化了,这里我们就不仅仅替换,还需要移除修改代码。

我们先自定义一个 public static 方法,增加注解原始类和方法 @AsmField(oriClass = OkHttpClient.Builder.class, oriMehod = "", oriAccess = MethodAccess.INVOKESPECIAL),这里大家注意 oriMehod 需要声明为<init> ,oriAccess 需要声明为 MethodAccess.INVOKESPECIAL。

  @AsmField(oriClass = OkHttpClient.Builder.class, oriMehod = "<init>", oriAccess = MethodAccess.INVOKESPECIAL)
    public static OkHttpClient.Builder getOkHttpClientBuilder() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.addInterceptor(chain -> {
            if (!GoldCicada.isCanRequestPrivacyInfo()) {
                    // print or record StackTrace
                return new Response.Builder().code(404).protocol(Protocol.HTTP\_2)
                        .message("Can\`t use network by GoldCicada")
                        .body(ResponseBody.create(MediaType.get("text/html; charset=utf-8"), ""))
                        .request(chain.request()).build();
            }
            return chain.proceed(chain.request());
        });
        return builder;
    }
复制代码

首先,我们在 AnnotationParserTransform 注解解析中特殊处理方法的返回值为 "V"。

 if (oriAccess == Opcodes.INVOKESPECIAL && oriMethod == "<init>") {
        returnValue = "V"
    }
复制代码

其次,我们在实现 hook 的 AsmTransform 中特殊处理。

 klass.methods?.each { MethodNode method ->
            Map<AbstractInsnNode, Object> needReplaceInitNode = \[:\] as Map
            method.instructions?.iterator()?.each { AbstractInsnNode insnNode ->
                if (insnNode instanceof MethodInsnNode) {
                    asmItems.each { asmItem ->
                        if (asmItem.oriDesc == insnNode.desc && asmItem.oriMethod == insnNode.name) {//方法名称和参数返回值一样
                            if (insnNode.opcode == asmItem.oriAccess && (asmItem.oriClass == "java/lang/Object" || insnNode.owner == asmItem.oriClass)) {
                                //处理init方法
                                if (asmItem.oriMethod == "<init>" && asmItem.oriAccess == Opcodes.INVOKESPECIAL) {
                                    needReplaceInitNode.put(insnNode, asmItem)
                                } else {
                                    insnNode.opcode = asmItem.targetAccess
                                    insnNode.name = asmItem.targetMethod
                                    insnNode.desc = asmItem.targetDesc
                                    insnNode.owner = asmItem.targetClass
                                }
                                println "QHOOK ${insnNode.owner}.${insnNode.name}${insnNode.desc} @ ${klass.name}.${method.name}${method.desc}"
                            } else if (insnNode.opcode == Opcodes.INVOKESPECIAL && insnNode.owner != asmItem.oriClass && insnNode.name != "<init>") {
                                println "跳过相同方法名称和参数调用Hook,如需要hook请提供超级hook(oriClass = \\"java.lang.Object.class\\"),具体参考com.mqunar.goldcicada.lib.QActivity,\\n${insnNode.opcode} ${insnNode.owner}.${insnNode.name}${insnNode.desc} @ ${klass.name}.${method.name}${method.desc}"
                            }
                        }
                    }
                }
            }
            needReplaceInitNode.each { replaceNode, asmItem ->
                //替换
                replaceNode.opcode = asmItem.targetAccess
                replaceNode.name = asmItem.targetMethod
                replaceNode.desc = asmItem.targetDesc
                replaceNode.owner = asmItem.targetClass
                //向前查找需要remove的 NEW TypeInsnNode 和 DUP InsnNode
                //查找逻辑是,如果replaceNode之前有相同init 相同owner desc不一样的数,有就跳过
                def newNode = findTypeInsn(replaceNode, asmItem, 0)
                //一定要先移除后边的,要不然当前的移除之后就成无主node了
                method.instructions.remove(newNode.next)
                method.instructions.remove(newNode)
                //改变栈空间变化
                method.maxStack = method.maxStack - 1
            }
        }
        ...
        //向前查找newTypeInsn节点:参考 methodVisitor.visitTypeInsn(NEW, "okhttp3/OkHttpClient$Builder");
  private static AbstractInsnNode findTypeInsn(AbstractInsnNode initNode, def asmItem, int skipCount) {
        def preNode = initNode.previous
        if (preNode.opcode == Opcodes.NEW && preNode.desc == asmItem.oriClass) {
            if (skipCount > 0) {
                skipCount--
            } else {
                return preNode
            }
        }
        if (preNode.opcode == asmItem.oriAccess
                && preNode.owner == asmItem.oriClass
                && preNode.name == asmItem.oriMethod
                && preNode.desc != asmItem.oriDesc) {//如果找到一个不替换的,跳过一次
            skipCount++
        }
        return findTypeInsn(preNode, asmItem, skipCount)
    }
复制代码

这样 new OkHttpClient.Builder() 编译后就被替换为 QOkHttpClient. getOkHttpClientBuilder() ,且不会造成方法内栈空间错乱。

细心的读者可能会发现 findTypeInsn 最后有一个参数 skipCount,这个什么作用呢?😜,卖个关子,大家可以动手 hook 一个复杂的 new 嵌套试试,如:new File(new File("DirPath"),new File("childPath").getName());

6 写在最后

至此本项目已经实现对自身 app 及第三方 SDK 的调用监控,在需要 hook 一个方法调用时,用一个自定义方法、一行注解编译运行即可完成,而不需要修改源代码,升级第三方 SDK,脚本没有硬编码,也不需要做工具类版本判断。另外:如果想要 hook 第三方 SDK 反射调用,如运行时加载 dex 等功能,也可以用以上方法,hook 整个反射的调用过程,记录反射调用的类、方法、参数和返回值,从而保证整个客户端的安全。

END

文章分类
后端
文章标签