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,避免不合规。
网友评论