在前两篇文章中,我们已经做到了attach和spawn两种模式的注入,你是否还记得,我们在做传统spawn注入的时候用到了一个叫做dobby的框架,当时并没有深入介绍,从这一篇文章开始,我们就将进入真正的Hook部分,这里先从Java世界开始。有描述不对的或者值得改进的欢迎在评论区提出!项目地址:78fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5x3h3q4G2L8U0q4F1k6#2)9J5c8V1&6G2L8$3D9`.暂时我们只关心Java Hook的核心部分实现,因此这里的成品只是一个粗糙的hook框架,其他的后面会慢慢再补充上来。在读完这篇文章后,相信你可以问答下面这些问题:我们从一个非常简单的Java方法来理解Java Hook如果我们想要把他的返回值从111改为999,表面上看起来非常简单,但从运行视角来看,它背后发生了这些事情:在这个过程中需要解决的是两个核心问题:首先是方法接管即解决怎么让目标方法执行时先到我这里来的问题然后是调用还原即解决“来到我这里”之后,我怎么知道原方法的参数、返回值怎么处理、原方法还能不能继续调用的问题所以在框架外看起来可能只是一个简单的数字替换,在内部实现往往会牵扯到ArtMethod、trampoline、ABI、参数解析、backup、调用桥接等问题,解决这些问题是实现Java Hook的最低门槛。上文我们已经知道了实现一次hook需要经历哪些流程,由此我们可以总结出一个最小可用的Java Hook框架至少需要下面这些部件。方法定位第一步首先是需要先找到目标方法,他需要解决的问题是如何从类名、方法名、签名定位到Java方法,又如何从Java方法进一步拿到ART内部的方法。即:用户需要给出class_name, method_name, signature, is_static;首先需要JNIEnv,因为后面无论是FindClass还是GetMethodID都依赖于JNIEnv*,然后是FindClass,思路是先直接通过env->FindClass,失败后通过ActivityThread.currentApplication()拿到当前应用(这里选择android.app.ActivityThread,因为他是系统类,能拿到当前进程里的Application对象),找到currentApplication方法,调用它拿到当前Application,此时我们拿到了当前目标进程里的Application实例,他是App运行时环境的核心对象之一,通过它可以继续拿到真正属于这个App的ClassLoader,最后调用ClassLoader.loadClass()方法真正加载目标类。再接着是FindMethod,其实就是获取methodID,我们此时已经拿到了jclass,并且知道了方法名、方法签名,直接通过env->GetMethodID获取即可,顺便把签名转化为一种更简单的格式,后面记作shorty。在ART中,jmethodID只是一个中间桥梁,真正需要改写的目标是ArtMethod。运行时结构识别我们最终要改的不是Java对象,而是ART内部结构和入口字段,因此需要识别出ArtMethod大小、access_flags偏移、entry_point_from_compiled_code偏移等关键信息这个运行时结构信息我们一部分通过解析libart符号获取,一部分通过运行时探测结构偏移获取,最后把这些结构统一存储到ArtInternals中供后续Hook使用我们可以设计几个结构体来存储相关运行时布局信息其中ArtRuntimeSpecOffsets描述的是Runtime里几个关键成员的偏移,比如heap、threadList、classLinker等;ClassLinkerSpecOffsets描述的是ClassLinker里几个关键trampoline的偏移,ArtMethodSpec描述的是ArtMethod里真正要改写的字段偏移:access_flags,entry_jni,entry_quick,ArtMethod大小。ArtInternals的命名空间记录了结果的存储大致是靠这几种方法找到的:原方法备份如果我们没有做原方法的备份,仅仅只是做了修改目标方法入口,当后面再想调用原方法的时候,可能就无从找起了,因此需要提前做好backup。先读原始字段然后放入HookInfo中然后按探测到的ArtMethod大小分配一块新内存,把当前目标ArtMethod整块复制过去但我们最终想要的backup不是目标方法某一时刻的机械拷贝,而是一份可以代表原方法执行路径、并且可以被Invoke稳定调用的ArtMethod,所以需要调用recover_artmethod方法,恢复原始access_flags、entry_quick、entry_jni,并且还加上了一个kAccCompileDontBother标志,可以让其更稳定,减少运行时/JIT对他做额外处理入口改写这里是真正让hook生效的一步,Java Hook的本质就是接管执行入口,这一步通常有两种方案,一个是replacement,即直接修改ArtMethod里的入口字段,一个是inline hook式的,直接patch编译代码入口处的机器码。我们这里先尝试replacement方案,读出原始access_flags/entry_jni/entry_quick,然后构造HookInfo,最后把目标ArtMethod改写掉。这里改的核心就三点:flag + entry_jni + entry_quick。用access_flags把方法伪装成native,用entry_jni塞进自己的trampoline,用entry_quick接到ART的quick JNI bridge,这样形成一个完整路径trampoline即中间跳板,框架不会直接把目标方法的入口改成某个高层回调函数,而是通常会先进入一小段我们自己控制的机器码,这段机器码做的是“现场接管”的工作:保存关键寄存器,带上当前Hook标识、跳到统一的native handler。他是链接ART调用现场和Hook逻辑的桥梁ABI参数解析一个Hook回调想要好用,最终回是这样的一个类似效果但是ART在调用方法时,并不会主动帮我们把参数打包成args[],他只会按照ABI规则把参数放进x0-x7、v0-v7、栈空间当中,因此框架必须自己完成一次还原,先知道目标方法参数类型,再按ABI规则从寄存器/栈中读回来,最后整理成统一的回调参数表示。比如一个方法他在解析时候就需要知道:参数个数为2,两个参数都是对象,对象参数在ART调用现场要按对象引用规则处理。先看签名转换然后是参数还原,其实就是从寄存器取,超过8个就从栈里读。比较特殊的就对象参数和this,从寄存器或栈里拿出来的不能直接作为jobject,需要转换成JNI本地引用/jobjectHook回调分发当trampoline把执行流交给统一handler之后框架还要回答一个问题:当前这次的调
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290505.htm
[原创]从0到1构建一个Hook工具之Java Hook篇(三)
227 浏览
8 回复
感谢分享
感谢分享
感谢分享
感谢分享
mark
看看
感谢分享
tql