[Android][ASM]指令注入入门(三)——实际需求实现(前)

6 篇文章 0 订阅

[Android][ASM]指令注入入门(三)——实际需求实现(前)

前言

上一篇中,我们搭建了基于Ubuntu(WSL2)下JAVA的开发环境,今天我们基于这个环境,来实现一个实际需求:

为特定方法的首尾分别插入Binder.clearCallingIdentity()Binder.restoreCallingIdentity()

产生这样需求的原因是如下这样一个情况:

某些功能需要system_server内部各个服务间相互调用,但是开发人员在设计API时,无法考虑到调用方是否具备调用其他服务的权限,因此如果不在调用之前执行Binder.clearCallingIdentity(),会导致调用失败,从而引起功能异常;

举个例子:

  1. 功能a需要借助NetworkStatsManager的API,获取一些网络统计数据;
  2. NetworkStatsManager的大部分API需要调用方为SYSTEM_UID,否则报错,或者返回空数据;
  3. 功能a本身实现为system_server内部的服务,uid是SYSTEM_UID,因此理应可以正常获取到数据;
  4. 但是由于system_server内部的服务的相互调用不会走Binder,因此Binder.getCallingUid()依旧为调用功能a时的调用方;
  5. 设计功能a的开发者无法确保所有调用方都是SYSTEM_UID,且从功能设计上来说,这是SYSTEM_UID在请求网络统计数据,因此不应该被限制;

因此,如果在需要在方法首行进行Binder.clearCallingIdentity()调用,在末尾通过调用Binder.restoreCallingIdentity()恢复的方法,如果可以在编译时自动插入,那么既可以减少开发人员的代码量,也可以避免由于疏忽导致的功能异常;

但是,这个需求看似一句话,实际上实现起来并不容易;

接下来,我们对该需求进行一个拆解:

需求拆解

回到这个需求本身:

为特定方法的首尾分别插入Binder.clearCallingIdentity()Binder.restoreCallingIdentity()

要实现这个需求,需要明确:

  1. 特定方法如何界定?
  2. ASM是否可以实现?

关于第一点,比较简单,只需要与开发人员约定好一个注解即可(例如:@NoCallingIdentity),当ASM解析字节码时,如果检测到该方法被@NoCallingIdentity修饰,那么才进行操作,其余方法一律不作任何操作;

至于第二点,则比较麻烦,我们需要明确插入的这两行代码包含了哪些JVM指令;
首先,我们将需要实现的需求,用伪代码的形式展现出来:

import android.os.Binder;
public class Test{

	public void test() {
		long token = Binder.clearCallingIdentity;
		//Do something
		Binder.restoreCallingIdentity(token);
	}
}

不难看出,要实现这个需求,需要在原有逻辑上添加:

  1. android.os.Binder包引入,这个在字节码阶段已经无法(也无需)实现,直接用类的完整名即可;
  2. 方法首行添加一个long型变量,名字随意,但是为了避免与方法内出现的变量名冲突,需要做排重,或取一个生僻的变量名;
  3. 调用Binder.clearCallingIdentity并将结果赋值给token
  4. 在方法结束前,调用Binder.restoreCallingIdentity并将token以参数传入;

在上一篇中,我们已经指导,方法内的逻辑都是可以在MethodNode类中,名为instructions,类型为InsnList的成员变量中记录的,那么往里面插入新的指令,理论上是可行的;

至于变量的添加,这里需要使用、操作上一篇中沒有提到的本地变量表(LocalVariableTable),而这个区域仅会在使用javac编译时,传入参数-g时才会生成,因此如果要评估该项需求是否可行,需要确认AOSP编译环境下,编译services.core.unboosted/services.core.priorityboosted时的编译参数是否包含-g

通过确认,可确认是有的:(Android P,分支名android-9.0.0_r46,其余基线请自行确认)

//build/soong/java/config/config.go

func init() {
	...
	pctx.StaticVariable("CommonJdkFlags", strings.Join([]string{
		`-Xmaxerrs 9999999`,
		`-encoding UTF-8`,
		`-sourcepath ""`,
		`-g`,
		// Turbine leaves out bridges which can cause javac to unnecessarily insert them into
		// subclasses (b/65645120).  Setting this flag causes our custom javac to assume that
		// the missing bridges will exist at runtime and not recreate them in subclasses.
		// If a different javac is used the flag will be ignored and extra bridges will be inserted.
		// The flag is implemented by https://android-review.googlesource.com/c/486427
		`-XDskipDuplicateBridges=true`,

		// b/65004097: prevent using java.lang.invoke.StringConcatFactory when using -target 1.9
		`-XDstringConcat=inline`,
	}, " "))
	...
}

确认可行后,我们就可以继续使用上一篇用的环境编写demo了;

开发步骤

源码端

import java.util.Random;

public class Test2 {
    
    private static final Random R = new Random();

    public static void main(String[] args) {
        Test2 instance = new Test2();
        instance.call(R.nextInt());
    }

    private void call(int arg) {
        System.out.println("call-->" + arg + 1);
    }
}

然后,由于这个环境没有Android SDK,因此我们先手动创建一个Binder.java来模拟:

package android.os;

import java.util.Random;

public class Binder {
    private static final Random R = new Random();
    public static final long clearCallingIdentity() {
        long token = R.nextLong();
        System.out.println("clearCallingIdentity-->" + token);
        return token;
    }
    public static final void restoreCallingIdentity(long token) {
        System.out.println("restoreCallingIdentity-->" + token);
    }
}

存放目录结构如下:

ryan ~/workspace/src $ tree
.
├── Test.java #这是上一篇Hello World使用的,此处无用
├── Test2.java
└── android
    └── os
        └── Binder.java

2 directories, 3 files

编译:

ryan ~/workspace/src $ javac -g Test2.java android/os/Binder.java
ryan ~/workspace/src $ tree ./
./
├── Test.java
├── Test2.class
├── Test2.java
└── android
    └── os
        ├── Binder.class
        └── Binder.java

2 directories, 5 files

运行一下:

ryan ~/workspace/src $ java Test2
call-->-18479534511

工具端

//tools/MethodRegionInjector.java
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.List;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

public class MethodRegionInjector {
	//与之前StringModifier.java基本一致,仅改变输入、输出文件名
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("../src/Test2.class");
        FileOutputStream fos = new FileOutputStream("../out/Test2.class");
        convert(fis, fos);
        fos.flush();
        fis.close();
        fos.close();
    }

	//与之前StringModifier.java基本一致,仅改变visitor类型
    private static void convert(InputStream in, OutputStream out)
            throws IOException {
        ClassReader reader = new ClassReader(in);
        ClassWriter writer = new ClassWriter(0);
        MethodRegionInjectorClassVisitor visitor = new MethodRegionInjectorClassVisitor(writer);
        reader.accept(visitor, 0);
        byte[] data = writer.toByteArray();
        out.write(data);
    }

	//与之前StringModifier.StringModificationClassVisitor基本一致,仅改变visitMethod的实现
    private static class MethodRegionInjectorClassVisitor extends ClassVisitor {

        private String currentClassName = null;

        private MethodRegionInjectorClassVisitor(ClassWriter writer) {
            super(Opcodes.ASM6, writer);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName,
                String[] interfaces) {
            currentClassName = name;
            super.visit(version, access, name, signature, superName, interfaces);
        }

        
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                String[] exceptions) {
            MethodVisitor chain = super.visitMethod(access, name, desc, signature, exceptions);
            //过滤类名、方法名,忽略构造方法、静态代码块
            if ("Test2".equals(currentClassName) && !"<init>".equals(name) && !"<clinit>".equals(name) && (access & Opcodes.ACC_STATIC) == 0) {
                MethodNode mn = new MethodNode(Opcodes.ASM6, access, name, desc, signature, exceptions);
                return new MethodRegionInjectorMethodVisitor(currentClassName, chain, mn);
            } else {
                return chain;
            }
        }

    }

	//visitEnd方法是这个类与StringModifier.java的主要差异,也是核心代码实现的地方
    private static class MethodRegionInjectorMethodVisitor extends MethodVisitor {

        private MethodVisitor chain;
        private String owner;

        private MethodRegionInjectorMethodVisitor(String owner, MethodVisitor chain, MethodNode mn) {
            super(Opcodes.ASM6, mn);
            this.owner = owner;
            this.chain = chain;
        }

        public void visitEnd() {
            MethodNode mn = (MethodNode) mv;                    
            //instructions为指令集合
            InsnList instructions = mn.instructions;
            //localVariables为局部变量集合,如果源码编译时javac没有带-g参数,此处列表内没有任何元素(空集合)
            List<LocalVariableNode> variables = mn.localVariables;
            int varIndex = -1;
            //确认需要新加的局部变量在列表中的位置
            if (variables != null) {
                varIndex = variables.size();
                //本地变量表中,double与long数据类型需要占用两个slot,因此此处需要避开
                for (LocalVariableNode var : variables) {
                    if ("J".equals(var.desc) || "D".equals(var.desc)) {
                        varIndex ++;
                    } 
                }
            }
            if (varIndex > 0) {
                //插入局部变量asmLocalToken的声明,变量名可以修改为更生僻,或者使用其他方法来避免重复,此处仅做展示用,所以就不写复杂逻辑了;
                //注意,参数4与参数5决定了该参数的作用域,此处为了在方法内全局有效,选用了方法内首位两个标签;
                LocalVariableNode varNode = new LocalVariableNode(
                        "asmLocalToken","J", null, getFirstLabel(mn), getLastLabel(mn), varIndex);

                //将变量声明添加到MethodNode中;
                variables.add(varNode);
                //添加变量后,如下字段也需要增加,同理,long与double占用两个slot,因此需要+2
				mn.maxLocals += 2;
                mn.maxStack += 2;

                //插入Binder.clearCallingIdentity()的函数调用指令
                MethodInsnNode preCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
                        "android/os/Binder","clearCallingIdentity", "()J", false);
                //不要使用insertBefore插入到第一条指令之前,否则上方声明的局部变量作用域可能会被改变,导致运行错误;
                instructions.insert(instructions.getFirst(), preCall);
                //将函数调用结果赋值给局部变量asmLocalToken,由于是长整形long,所以使用LSTORE
                VarInsnNode varStoreCall = new VarInsnNode(Opcodes.LSTORE, varIndex);
                instructions.insert(preCalocalVariablesll, varStoreCall);
                //遍历所有指令
                AbstractInsnNode targetNode = null;
                //由于需要定位return的语句,因此从末尾开始效率更高
                for (int i = instructions.size() - 1; i >= 0; i--) {
                    AbstractInsnNode node = instruct前ions.get(i);
                    int opCode = node.getOpcode();
                    //如果遇到无参的返回指令,则在此之前插入两条指令:LLOAD与INVOKESTATIC
                    if (opCode == Opcodes.RETURN) {
                        VarInsnNode varLoadCall = new VarInsnNode(Opcodes.LLOAD, varIndex);
                        instructions.insertBefore(node, varLoadCall);
                        MethodInsnNode postCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
                                "android/os/Binder","restoreCallingIdentity", "(J)V", false);
                        instructions.insert(varLoadCall, postCall);
                        break;
                    }
                }
            }
            super.visitEnd();
            mn.accept(chain);
        }

        private LabelNode getFirstLabel(MethodNode mn) {
            for (int i = 0; i < mn.instructions.size(); i++) {
                AbstractInsnNode s = mn.instructions.get(i);
                if (s instanceof LabelNode) {
                    return (LabelNode)s;
                }
            }
            return null;
        }
    
        private LabelNode getLastLabel(MethodNode mn) {
            for (int i = mn.instructions.size() - 1; i >= 0 ; i--) {
                AbstractInsnNode s = mn.instructions.get(i);
                if (s instanceof LabelNode) {
                    return (LabelNode)s;
                }
            }
            return null;
        }
    }
}

与上一篇编写的StringModifier.java主要的差异在MethodVisitor.visitEnd()方法的实现上,主要有如下几个关键逻辑:

  1. mn.localVariables用于存放本地变量(局部变量)的信息,新加变量需要往里面添加元素,添加时需要注意以下几点:
    a. 长整形long与双精度浮点double需要占用两个slot,因此计算时下标需要间隔一个;
    b. 添加新变量需要构造LocalVariableNode对象,并明确作用域;
    c. 明确作用域后,不要再在作用域之外插入其他需要使用该变量的指令;
  2. mn.instructions在上一篇已经操作过了,这里需要注意的是方法调用后,如果需要赋值给某个变量,需要再插入一条xSTORE指令(x表示类型,如果是长整形long,此处为LSTORE;如果为双精度浮点double,此处即为DSTORE,以此类推,具体定义详见API文档

验证

编译工具:

ryan ~/workspace/tools $ javac MethodRegionInjector.java -cp ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar

运行工具:

ryan ~/workspace/tools $ java -cp ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar:./ MethodRegionInjector

查看产物,并拷贝依赖:

ryan ~/workspace/tools $ cd ../out/
ryan ~/workspace/out $ mkdir android/os/ -p
ryan ~/workspace/out $ cp ../src/android/os/Binder.class android/os/Binder.class

运行产物:

ryan ~/workspace/out $ java Test2
clearCallingIdentity-->-3947030970469356260
call-->-8347958541
restoreCallingIdentity-->-3947030970469356260

后记

这只是实现了最简单的一种情况,但是事实上,还有如下情况需要考虑:

  1. 方法内有多个return;
  2. 方法具备返回值,返回值由分基本数据类型与引用数据类型;
  3. 方法内有try-catch-finally代码结构;

因此,预计还有两篇才能完全实现…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值