android 插桩分析涉及用户隐私敏感函数调用&快速找到点击的位置等功能

ASM 基本配置使用 见:https://www.jianshu.com/p/0a56e151e00b

android 隐私权限相关的api或者字段要求越来越严格,我们需要配合处理相关的函数调用,这就需要我们找到:

  • 在哪里调用的?
  • 调用的方法是什么?
  • 具体的调用堆栈是什么?
  • 改完了自后,怎么辅助自校验是否改好了?(很多第三方平台耗时较久,不适合快速测试)

比如我们想监控:getMacAddress()的调用:
我们想要观察的函数:

WifiManager wifi = (WifiManager)MainActivity.this.getSystemService("wifi");
WifiInfo info = wifi.getConnectionInfo();
if (info != null) {
    info.getMacAddress();//我们想要观察的函数
}

我们想要的结果:

WifiManager wifi = (WifiManager)MainActivity.this.getSystemService("wifi");
WifiInfo info = wifi.getConnectionInfo();
if (info != null) {
    MethodRecordSDK.recordMethodCall("com/canzhang/asmdemo/MainActivity$1_onClick_call:getMacAddress");//插桩代码
    info.getMacAddress();//我们想要观察的函数
}

简单来讲就是针对这种实例调用方法,我们可以在前面简单的插入一行我们自己的代码,并把当前所调用的函数名称传递出去。

具体实现:
通过插桩找到调用的函数,在其调用前方插入我们的函数,实现堆栈的调用分析。

           /**
             * 访问调用方法的指令(这里仅针对调用方法的指令,其他指令还有返回指令,异常抛出指令一类的)
             * @param opcode 指令
             * @param owner  指令所调用的方法归属的类
             * @param name   方法名
             * @param descriptor 方法描述(就是(参数列表)返回值类型拼接)
             * @param isInterface 是否接口
             */
            @Override
            public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
                //如果不知道下面的怎么写,可以在这里打个日志打印,重新build下工程就可以看到怎么写了
                if (opcode == Opcodes.INVOKEVIRTUAL) {//调用实例方法
                    //归属类、方法名、方法描述(返回值、入参类型)
                    String recordMethodName = null;
                    if ("android/net/wifi/WifiInfo".equals(owner) && name.equals("getMacAddress") && descriptor.equalsIgnoreCase("()Ljava/lang/String;")) {
                        recordMethodName = "getMacAddress";
                    }
                    if (recordMethodName != null) {
                        //加载一个常量
                        mv.visitLdcInsn(className + "_" + outName + "_call:" + recordMethodName);
                        //调用我们自定义的方法 (注意用/,不是.; 方法描述记得;也要)
                        mv.visitMethodInsn(INVOKESTATIC,  "com/canzhang/method_call_record_lib/MethodRecordSDK", "recordMethodCall", "(Ljava/lang/String;)V", false);
                    }
                }
                super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
            }

我们的方法(插桩调用的方法部分):

    /**
     * 记录敏感函数调用
     * 对于实例方法,可以简单通过插入我们的方法记录堆栈
     *
     * @param from
     */
    public synchronized static void recordMethodCall(String from) {
         Log.e("MethodRecordSDK", "调用的方法是:" + from);
         Log.e("MethodRecordSDK", String.format("\n\n----------------------%s调用堆栈开始------------------------\n\n", "敏感函数"));
         StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
         for (int i = 0; i < stackTraceElements.length; i++) {
             Log.d("MethodRecordSDK", stackTraceElements[i].toString());
         }
        Log.e("MethodRecordSDK", String.format("\n\n----------------------%s调用堆栈结束------------------------\n\n","敏感函数"));
    }

效果:

E: ----------------------敏感函数:调用堆栈开始------------------------
D: dalvik.system.VMStack.getThreadStackTrace(Native Method)
D: java.lang.Thread.getStackTrace(Thread.java:1720)
D: com.canzhang.method_call_record_lib.MethodRecordSDK.recordMethodCall(MethodRecordSDK.java:28)
D: com.canzhang.asmdemo.MainActivity$1.onClick(MainActivity.java:82)
D: android.view.View.performClick(View.java:7375)
D: android.view.View.performClickInternal(View.java:7336)
D: android.view.View.access$3900(View.java:822)
D: android.view.View$PerformClick.run(View.java:28214)
D: android.os.Handler.handleCallback(Handler.java:883)
D: android.os.Handler.dispatchMessage(Handler.java:100)
D: android.os.Looper.loop(Looper.java:238)
D: android.app.ActivityThread.main(ActivityThread.java:7829)
D: java.lang.reflect.Method.invoke(Native Method)
D: com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
D: com.android.internal.os.ZygoteInit.main(ZygoteInit.java:986)
E: ----------------------敏感函数:调用堆栈结束------------------------

具体代码参考:https://github.com/gudujiucheng/asm-plugin

  • sdk:method_call_record_lib
  • 插件:method_call_record_plugin
    说明:该项目还有其他功能测试module,可忽略。

目前已经支持外部配置需要监控的方法:(下方配置了常见的敏感函数,和常见的点击回调(用于观察哪里点击的))

apply plugin: 'com.canzhang.method_call_record_plugin'
methodCallRecordExtension {
    //日志打印测试,不知道方法描述怎么写可以在这里填写下方法名,build一下即可看到日志(模糊匹配)
    methodTest = ["loadLibrary"]
    //模糊匹配,只关注方法名、入参、返回参数(可传入空集合[],传入空集合的时候默认仅匹配方法名)
    fuzzyMethodMap = ["onClick"                 : ["(Landroid/view/View;)V", "(Landroid/content/DialogInterface;I)V", "(Landroid/content/DialogInterface;IZ)V"],
                      "onMenuItemClick"         : ["(Landroid/view/MenuItem;)Z"],
                      "onCheckedChanged"        : ["(Landroid/widget/RadioGroup;I)V", "(Landroid/widget/CompoundButton;Z)V"],
                      "onChildClick"            : ["(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z"],
                      "onItemSelected"          : ["(Landroid/widget/AdapterView;Landroid/view/View;IJ)V"],
                      "onListItemClick"         : ["(Landroid/widget/ListView;Landroid/view/View;IJ)V"],
                      "onStopTrackingTouch"     : ["(Landroid/widget/SeekBar;)V"],
                      "onRatingChanged"         : ["(Landroid/widget/RatingBar;FZ)V"],
                      "onTabChanged"            : ["(Ljava/lang/String;)V"],
                      "onNavigationItemSelected": ["(Landroid/view/MenuItem;)Z"],
                      "onTabSelected"           : ["(Landroid/support/design/widget/TabLayout\$Tab;)V", "(Lcom/google/android/material/tabs/TabLayout\$Tab;)V"],
                      "onGroupClick"            : ["(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z"],
                      "onItemClick"             : ["(Landroid/widget/AdapterView;Landroid/view/View;IJ)V"]
    ]
//    精准匹配,关注方法名、入参、返回参数、类名(仅适配非系统api调用的场景)
    accurateMethodMap = [
            "android/telephony/TelephonyManager": ["getLine1Number()Ljava/lang/String;",
                                                   "getDeviceId()Ljava/lang/String;",
                                                   "getSimSerialNumber()Ljava/lang/String;",
                                                   "getSubscriberId()Ljava/lang/String;"],
            "android/net/wifi/WifiInfo"         : ["getMacAddress()Ljava/lang/String;",
                                                   "getSSID()Ljava/lang/String;"],
            "java/net/NetworkInterface"         : ["getInetAddresses()Ljava/util/Enumeration;"],
            "java/net/InetAddress"              : ["getHostAddress()Ljava/lang/String;"],
            "android/provider/Settings\$System" : ["getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;"],
            "android/provider/Settings\$Secure" : ["getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;"]
    ]
}

静态方法:

说明:静态方法我们可以通过替换方法实现类实现更近一步的拦截处理,具体参见sdk

字段加载:

我们也可以监控某些字段的加载,可自行实现。

核心代码:

package com.canzhang.plugin;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

import static org.objectweb.asm.Opcodes.ASM6;

/**
 * ClassVisitor:主要负责遍历类的信息,包括类上的注解、构造方法、字段等等。
 */
public final class MethodCallRecordClassAdapter extends ClassVisitor {

    private String className;
    private String sdkClassPath = "com/canzhang/method_call_record_lib/MethodRecordSDK";


    MethodCallRecordClassAdapter(final ClassVisitor cv) {
        //注意这里的版本号要留意,不同版本可能会抛出异常,仔细观察异常
        super(ASM6, cv);
    }

    /**
     * 这里可以拿到关于.class的所有信息,比如当前类所实现的接口类表等
     *
     * @param version    表示jdk的版本
     * @param access     当前类的修饰符 (这个和ASM 和 java有些差异,比如public 在这里就是ACC_PUBLIC)
     * @param name       当前类名
     * @param signature  泛型信息
     * @param superName  当前类的父类
     * @param interfaces 当前类实现的接口列表
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }


    /**
     * 这里可以拿到关于method的所有信息,比如方法名,方法的参数描述等
     *
     * @param access     方法的修饰符
     * @param outName    方法名
     * @param desc       方法描述(就是(参数列表)返回值类型拼接)
     * @param signature  泛型相关信息
     * @param exceptions 方法抛出的异常信息
     * @return
     */
    @Override
    public MethodVisitor visitMethod(final int access, final String outName,
                                     final String desc, final String signature, final String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, outName, desc, signature, exceptions);
        mv = new AdviceAdapter(ASM6, mv, access, outName, desc) {

            @Override
            public void visitLdcInsn(Object cst) {//访问一些常量
              
                super.visitLdcInsn(cst);
            }

            @Override
            public void visitFieldInsn(int opcode, String owner, String name, String desc) {
//                if("com/canzhang/asmdemo/sdk/MyTest".equals(className)){
//                    LogUtils.log("--------------->>>>>\n\nopcode(操作码):" + opcode + "\n\nowner:" + owner + "\n\nname(:" + name + "\n\ndesc:" + desc + "\n\noutMethodName(上层类名_方法名):" +className+"_"+ outName);
//                }
//                if (opcode == Opcodes.GETSTATIC && "android/os/Build".equals(owner)) {
//                    //加载一个常量
//                    mv.visitLdcInsn(className + "_" + outName + "_load: fieldName:" + name + " fieldDesc:" + desc + " fieldOwner:" + owner);
//                    //调用我们自定义的方法 (注意用/,不是.; 方法描述记得;也要)
//                    mv.visitMethodInsn(INVOKESTATIC, sdkClassPath, "recordLoadFiled", "(Ljava/lang/String;)V", false);
//                }
                super.visitFieldInsn(opcode, owner, name, desc);


            }

            @Override
            protected void onMethodEnter() {
                super.onMethodEnter();
                //打印方法信息
                if (MethodCallRecordExtension.methodTest != null && MethodCallRecordExtension.methodTest.contains(outName)) {
                    LogUtils.log("----------测试打印数据---form 方法进入 -->>>>>"
                            + "\n\naccess(方法修饰符):" + access
                            + "\n\noutName(方法名):" + outName
                            + "\n\ndesc(方法描述(就是(参数列表)返回值类型拼接)):" + desc
                            + "\n\nsignature(方法泛型信息:):" + signature
                            + "\n\nclassName(当前扫描的类名):" + className);
                }
                //模糊匹配方法(忽略方法归属的类名)
                if (MethodCallRecordExtension.fuzzyMethodMap != null
                        && MethodCallRecordExtension.fuzzyMethodMap.containsKey(outName)
                        && MethodCallRecordExtension.fuzzyMethodMap.get(outName)!=null) {

                    if(MethodCallRecordExtension.fuzzyMethodMap.get(outName).size()>0){//有配置,就按照配置来匹配
                        for (String item: MethodCallRecordExtension.fuzzyMethodMap.get(outName)) {
                            if(item!=null&&item.equals(desc)){
                                //命中,则插桩
                                inputMethod(outName);
                                break;
                            }

                        }
                    }else{//没有配置就通配
                        //命中,则插桩
                        inputMethod(outName);
                    }

                }
            }

            /**
             * 访问调用方法的指令(这里仅针对调用方法的指令,其他指令还有返回指令,异常抛出指令一类的) 像接口回调这一类的是调用不到的(因为回调的点是系统api,这里捕获不到)
             * @param opcode 指令
             * @param owner  指令所调用的方法归属的类
             * @param name   方法名
             * @param descriptor 方法描述(就是(参数列表)返回值类型拼接)
             * @param isInterface 是否接口
             */
            @Override
            public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
                //打印方法信息
                if (MethodCallRecordExtension.methodTest != null && MethodCallRecordExtension.methodTest.contains(name)) {
                    LogUtils.log("----------测试打印数据---方法调用(与onMethodEnter 可能存在重复打印) -->>>>>"
                            + "\n\nopcode(方法调用指令):" + opcode
                            + "\n\nowner(方法归属类):" + owner
                            + "\n\naccess(方法修饰符):" + access
                            + "\n\nname(方法名):" + name
                            + "\n\nisInterface(是否接口方法):" + isInterface
                            + "\n\ndescriptor(方法描述(就是(参数列表)返回值类型拼接)):" + descriptor
                            + "\n\nsignature(方法泛型信息:):" + signature
                            + "\n\nclassName(当前扫描的类名):" + className);
                }

                if (MethodCallRecordExtension.accurateMethodMap != null
                        && MethodCallRecordExtension.accurateMethodMap.containsKey(owner)
                        && MethodCallRecordExtension.accurateMethodMap.get(owner) != null
                        && MethodCallRecordExtension.accurateMethodMap.get(owner).size() > 0) {
                    for (String item: MethodCallRecordExtension.accurateMethodMap.get(owner)) {
                        if(item!=null&&item.equals(name+descriptor)){
                            //命中,则插桩
                            inputMethod(name);
                            break;
                        }

                    }
                }

//                if (opcode == Opcodes.INVOKESTATIC) {//调用静态方法
//
//                    if (!isSdkPath() && ("android/provider/Settings$System".equals(owner) || "android/provider/Settings$Secure".equals(owner)) && name.equals("getString") && descriptor.equalsIgnoreCase("(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;")) {
//                        //变更父类
//                        super.visitMethodInsn(opcode, sdkClassPath, name, descriptor, isInterface);
//                        return;
//                    }
//                }
                super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
            }

            private void inputMethod(String recordMethodName) {
                if (!isSdkPath() && recordMethodName != null) {
//                    LogUtils.log("----------命中----->>>"+className + "_" + outName + "_call:" + recordMethodName);
                    //加载一个常量
                    mv.visitLdcInsn(className + "_" + outName + "_call:" + recordMethodName);
                    //调用我们自定义的方法 (注意用/,不是.; 方法描述记得;也要)
                    mv.visitMethodInsn(INVOKESTATIC, sdkClassPath, "recordMethodCall", "(Ljava/lang/String;)V", false);
                }
            }
        };
        return mv;

    }

    private boolean isSdkPath() {
        return sdkClassPath.equals(className);
    }


}

使用现有版本插件:

参考:https://github.com/gudujiucheng/asm-plugin/blob/master/README.md

现有能力:

  • 支持敏感函数调用筛查
  • 支持快速找到点击的位置(工程中已经配置了常用点击回调的hook,可以打印堆栈,快速找到点击的位置,提高开发效率)
  • 支持静态so加载位置筛查(方便查看静态so都加载了什么,编译期查看,我们同时可以配置运行时期的插桩查看,看到更精准的调用)
  • 可以尝试替换调用的方法(静态和实例方法均可,参考工程代码),使得有更多可能,比如可以在用户未同意协议前返回空值,使得不用调用到实际api,避免不合规。

具体代码参考:https://github.com/gudujiucheng/asm-plugin

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容