开源字节码插装工具 HiBeaver 介绍与原理详解

HartLouise 发布于1年前

 

介绍

插装技术是一个古老却又强大的技术,简单来说就是在目标程序代码中某些位置插入一些代码或者修改成一些代码,从而在目标程序运行过程中获取某些程序状态并加以分析。这样说可能有点抽象,我来举个栗子。大家在不同语言的开发过程中可能都用到过一类叫Profiler的工具,开启了Profiler之后跑程序结束后会给一个表,统计哪些函数执行的次数最多,耗时最长,用来性能优化。

Profiler工具的实现方法就是在目标程序里的函数开头和结尾都插入监控代码,在运行过程中,这些监控代码会记录目标函数被调用的次数,并且通过一头一尾的时间戳计算被监控的函数的执行时间。收集到的数据经过算算算,就可以出来统计的图表。

有人说:这不就是inline hook技术吗?是的。那我用OllyDbg不是一下子就搞定了?是却不是。插装的工具有很多很多(真的很多,每种语言可能有不止一个),插装的方式也有不少(动态的,静态的,VM的,系统的,硬件的),手工插装在简单分析程序中可能短平快,自动插装却是个学问。如果要写个插装程序去自动插装另一个程序,要考虑到插哪里,插装什么代码,怎么确保目标程序插装后能正常跑起来,怎么确保插装代码不会带来太多性能开销等等等等。这里每一项都是一个小研究领域,避免大家瞌睡,本文暂时略过,如果大家有兴趣之后可以再写一篇学术一点的介绍代码插装的前世今生。

好,回到主题, Android应用的插装 。通常开发者以及安全分析人员都希望能够监控APK中某些函数,比如开发人员会关心APP执行到了哪个生命周期函数,消耗多少时间。安全分析人员可能更感兴趣这个APP访问了哪些敏感的信息,比如IMEI和网络。考虑到Android App有native code,字节码,甚至还有Javascript代码,所以设计一个成熟的Android的插装工具要求很高。 今天来介绍一下近期开源的字节码插装工具HiBeaver。

Github项目网址是:

https://github.com/BryanSharp/hibeaver  

目前版本是1.2.3作者更新还挺勤快的。

 

HiBeaver使用案例

首先HiBeaver是一个Gradle插件,什么鬼?!好吧,HiBeaver被设计成在一个APP编译中后期接入,使用大名鼎鼎的ASM.jar(不知道的去末尾的扩展阅读看看)对Java字节码进行修改的Gradle插件。

Gradle被小伙伴誉为: 一个看上去很简单,但深入写起来发现要完蛋的玩意” ,没办法,Gradle是Google Android钦定的构建系统。Gradle是一个主要用来构造Java类程序的构建系统(类似make),构建规则由Groovy代码提供,Gradle提供了一个框架,比如要编译一个Java文件可以用Groovy代码去继承Gradle提供的一个类去做。

等等,等等, 什么是Groovy?

好吧,一种比较恶心的Java方言,我们先略过,下面看了就会知道。Google Android给Gradle贡献了一个用于构造Android APP的插件,然后集成在Android Studio里面。下图是完整构建一个Android APP的流程图,好复杂,简单来说就是把Java文件编译成class之后,在用Android的dx工具编译成Dex文件,然后和编译后的资源文件打包成一个APK文件,并进行签名。

开源字节码插装工具 HiBeaver 介绍与原理详解

HiBeaver同学就是在这个过程中,在dx之前,通过Gradle提供的Transform API(大概在上图的橘黄色的proguard前面),截获产生的.class文件,并且对其中的Java字节码进行修改(通过ASM.jar)。

话不多说,来看看怎么用。需要使用HiBeaver首先要配置Android APP工程的build.gradle文件,添加:

buildscript {
    dependencies {
        classpath 'com.bryansharp:HiBeaver:1.2.3' // 增加这个,HiBeaver已经发布
    }
}

然后在build.gradle开头加上配置信息:

apply plugin: 'hiBeaver'
hiBeaver {
    // 是否打印帮助信息,默认为true
    showHelp = true
    // 是否打印插装log,默认是false就是打印
    keepQuiet = false
    // 统计插装时间
    watchTimeConsume = false
    // 插装规则,重点,下详
    modifyMatchMaps = [:]
}

然后调用 gradle build 构造APK的过程中,可以看到 Applied HiBeaver 意味着配置成功了,总体来说配置还是非常简单的。

modifyMatchMaps 是这里的重点,也就是插装规则数组,每一条规则包含匹配和动作。“匹配”可以匹配到某个/些类的某个/些函数,然后动作就是具体用ASM.jar插入怎样的代码了,例如以下的例子:

'*Activity|*Receiver|!android*' : [ // 首先匹配类名字,以Activity或者Receiver结尾,并且不是以android开头,注意这里是class全名,也就是前面包含包名
            // 然后匹配成员方法
            // 方法名也可以用通配符模糊匹配,比如这里匹配以on开头的方法
            // methodDesc匹配方法的原型,设置成null代表不限制
            // 最后adapter是插装动作函数
            ['methodName': 'on**', 'methodDesc': null, 'adapter': {
                // 该动作函数输入参数包括ClassVisitor,也就是代表了匹配到的类
                // access是类的访问权限,例如public private,详细见java反射的Modifier类
                // name, desc, signature不讲了,表示原型;exceptions 是函数定义中的throw
                ClassVisitor cv, int access, String name, String desc, String signature, String[] exceptions ->
                    // 这个->是一个Groovy闭包定义,接触过Javascript的应该不难理解,没接触过的就认为是一个函数定义吧
                    // 上来第一步用cv对象找到我们指代的method
                    MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
                    // 然后定义一个修改规则
                    MethodVisitor adapter = new MethodLogAdapter(methodVisitor) {
                        // visitCode是对这个目标函数的代码做点什么的意思
                        @Override
                        void visitCode() {
                            super.visitCode();
                            // visitXXX是用来产生注入的一句Java字节码
                            // ldcInsn的意思是加载常量到堆栈,相当于x86汇编里面push参数入栈
                            methodVisitor.visitLdcInsn(desc);
                            methodVisitor.visitLdcInsn(name);
                            // 这句是插入一句函数调用语句,调用一个钩子函数,invoke_static表示这个钩子函数是一个static函数,这样也就不用传入this指针了,这里由于是底层字节码,所以要给全名,包括包名,hook函数名,还有参数定义;这里的参数定义说明传入两个Object,返回void,这两个object就是前面提到的desc和name
                            methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, 
                                "bruce/com/testhibeaver/MainActivity", 
                                    "hookXM", "(Ljava/lang/Object;Ljava/lang/Object;)V");
                        }
                    }
                    // 把这个动作交给插装框架
                    return adapter;
            }]
    ]

用起来就这么“简单”,对于比较熟悉ASM.jar的朋友们可能比较直观。这和我们以前用OllyDbg inline hook有什么差别呢?

首先 前面定义的匹配规则能够选出整个程序中满足条件的函数进行插装,远比人工寻找要高效,随着规则不断升级,匹配规则可以匹配类的继承关系等。

其次 ,使用ASM.jar进行插装,会自动调节指令位置,还能保证调试信息指向的行数依然有效,省去了很多麻烦。

上面这个例子摘自作者Github项目的README,具体例子APP也可以在Github上找到。这个例子就是在APK中用户自己写的Activity, Service的所有生命周期函数 (onXXX) 开头插入调用 hookXM(name, desc ) 。聪明的安全人员一定能够想到怎么跳过APK中某些代码(插一条return嘛),怎么偷梁换柱(找到某个method我就自己重写),怎么hook系统调用(找到调用getIMEI的地方,来调用钩子函数)等等。

HiBeaver的一些好处如下:


1. 插装规则面对的是整个APK,所以不止包括用户自己写的代码,还包括用户使用的一些SDK,比如广告库,推送SDK,加密库等等,聪明的你肯定想到了什么。


2. 在Gradle构造流程中,在proguard混淆前,当然也在加固前,只要注入的代码稳定,应该不会有很大的正确性问题。


3. 灵活性,原则上聪明的使用者可以扮演一个编译器的角色,给APK增加各种各样的功能,而且钩子函数(例如这里的hookXM)依旧能用java实现。

 

原理分析

接下来我们来深入HiBeaver内部来看看它的原理:

首先 apply 'hiBeaver' 是将所有HiBeaver代码引入Gradle构造过程的入口。

代码在:

src/main/groovy/com/bryansharp/gralde/hibeaver/HiBeaverPluginImpl.groovy , HiBeaverPluginImpl1.apply——

就是apply做的实际动作。Gralde里面,所谓apply就是传给Gradle插件正在构建的工程对象(Project),这个工程对象里面包含了所有已经有的过程(其他插件的),然后当前插件自己再往里面加。HiBeaver这里很简单,就是利用了Gradle的Transform接口,注册到工程上,这样 Gradle每次产生了一个class文件就会丢给HiBeaver指定的这个InjectTransform对象了。

代码虽然是 Groovy ,但是相当 Java ,可读性尚可。同时可以看到,配置中如果 watchTimeConsume 是true,那么会用工程完成处理后 project.afterEvaluate ,

去调用:

src/main/groovy/com/bryansharp/gralde/hibeaver/TimeListener.groovy  ——

进行计时,代码非常直观,也不是重点,不再赘述。

HiBeaver的重点代码在:

src/main/groovy/com/bryansharp/gralde/hibeaver/InjectTransform.groovy ,重点是这个覆盖掉Gradle提供的父类Transform的transform函数。

首先 modifyMatchMaps 被从配置中读出,并简单处理一下。

然后作者写到“获取所有依赖的classPaths,仅做备用”,这里需要稍微解释一下:APK构造后,会产生用户代码,但是用户代码需要依赖的jar文件,还有Android编程框架(android.jar)才能找到所有函数的链接,所以classPaths就是找全所有jar文件。

然后HiBeaver开始遍历所有的输入文件(即Gradle构建出来的所有Jar文件以及class文件),这边值得注意的是工程可能会产生两个同名的Jar文件但是输入路径不同,所以这里用了hexName进行区分,统一输出文件是输入文件名+hexName。

这里 isJarNeedModify 简单来说就是到规则库里面去看这个jar文件是否需要被插装(匹配),如果需要,则通过 modifyJarFile 来对其进行插装(也就是执行动作)。遍历Jar主要会编译用户工程依赖的所有第三方Jar文件,而紧跟着的遍历目录,一般就是用户代码产生的.class文件,也就是遍历用户代码,重写原理类似。

isJarNeedModify 遍历了jar里面所有的class文件,然后利用 shouldModifyClass 判断是否需要修改某个class文件。 shouldModifyClass 就是匹配规则,可以看到大大的switch里面目前支持了三种匹配,全名匹配,简单字符串比较;正则比较,那就是用正则表达式比较;wildcard比较,也就是 *Activity 这种,用 Util.wildcardMatchPro 进行比较。

modifyJarFile 和 modifyClassFile 功能类似,一个遍历jar文件中所有class文件,另一个对一个class文件而言;最终调用了 ModifyClassUtil.modifyClasses 干实际的活,这个函数传入了整个规则组。

ModifyClassUtil.modifyClasses 是最里层的逻辑,继承了ASM.jar的 ClassAdapter 用来对class文件里面的方法进行修改。可以看到,现在的修改仅限于方法的代码,所以对annotation,attributes, fields全部是略过的。最终 visitMethod 方法遍历了所有规则,匹配,并使用规则提供的Adapter对代码进行修改。分析到此结束。

 

总结

1. 基于ASM.jar还是一个比较底层的技术,大家可以学习。在传统Java界,ASM.jar被广泛用于实现各种Profiler,内存泄露检测、程序分析工具以及Lint查错工具等。


2. 嵌入到安卓应用编译过程中是一个不错的想法,但是用Groovy语言开发是在太蛋疼。


3. HiBeaver功能比较简单,具体插装需要使用者对ASM.jar非常熟悉,可能对开发新的插装内容比较麻烦。

 

扩展阅读

1. objectweb的ASM.jar:

http://asm.ow2.org/

2. Java字节码解析:

http://ifeve.com/javacode2bytecode/

3. Gradle Transform API:

http://tools.android.com/tech-docs/new-build-system/transform-api

4. Gradle入(cai)门(keng):

http://www.infoq.com/cn/articles/android-in-depth-gradle

5. Gradle插件开(xian)发(jing):

http://git.bookislife.com/post/2015/how-to-develop-gradle-plugin/

6. HiBeaver作者教你怎么用: 

https://segmentfault.com/a/1190000008491823

- End -

开源字节码插装工具 HiBeaver 介绍与原理详解

看雪ID: kenmark                                     

https://bbs.pediy.com/user-22148.htm

本文由  kenmark  原创

转载请注明来自看雪社区

开源字节码插装工具 HiBeaver 介绍与原理详解

好书推荐:

 

开源字节码插装工具 HiBeaver 介绍与原理详解 立即购买!

热门技术文章推荐:

开源字节码插装工具 HiBeaver 介绍与原理详解

公众号ID:ikanxue
官方微博:看雪安全

商务合作:wsc@kanxue.com

查看原文: 开源字节码插装工具 HiBeaver 介绍与原理详解

  • silverrabbit
  • beautifulostrich
  • tinygorilla
  • bigostrich
  • redrabbit
  • purplegoose
  • whiteostrich
  • greenpeacock