一种简单的java函数hook思路

一种简单的java函数hook思路

总结

本文取自《深入理解android java虚拟机art》第10章,在读书过程中总结的一些能让自己豁然开朗的点,通过这些点我发现了hook java函数的方法,有了此文。

LinkCode

LinkCode函数当中有个重要的过程,就是GetEntryPointFromQuickCompiledCode函数,他的返回值也就是quick_code为空的话代表该方法没有进行编译,在oat文件当中没有对应的汇编代码,需要进入解释执行模式而不是jit,这里有一个分支,如果ArtMethod是jni方法的话会将类似于elf文件的ENTRYPOINT 设置为art_quick_generic_jni_trampoline,普通方法会设置为art_quick_to_interpreter_bridge,如下表(取自该书565页),这里是关键代表着我们可以通过修改这里来完成java或jni函数的跳转,也就是每次art虚拟机解释执行某个java函数的时候,都会跳转到该java函数的EntryPoint,那么只要把这里替换成我们自己的c函数的地址,就可以实现拦截java函数了,当然也要保留原函数的 执行过程。

方案思考

了解了java函数大致的执行流程,下一补就是实操了,那么怎么更改一个java函数的EntryPoint呢?这是一个困难的问题,首先提出一点
1:是否可以通过hook关键art函数的方式来获得ArtMethod?
当然可以,但是我们要hook的是自身的dex,它的加载时机要早于我们的so加载进内存中,所以像LinkCode或者LoadMethod这种在dex加载流程上的函数是不行的,那就需要找一个系统函数,既有我们的函数的ArtMethod,又是在dex加载完成之后才进行的,这个就很难办了,首先通过函数名过滤ArtMethod就很难,其次dex加载完成之后也很难找到覆盖所有ArtMethod的函数,所以这种方法不可行
2:是否可以像frida一样Java choose内存搜索ArtMethod?
倒是可以通过java.lang.reflect.Method这个类来进行内存漫游,直接把Method类的实例全找出来,高版本可以再通过它父类的方法getArtMethod将它转化为ArtMethod的指针,但是内存漫游太难实现,可能一会会参考frida实现一下,目前不考虑,所以这种也不行
3:最终方案
最终的方案其实和frida差不多,回顾一下frida如何hook的java函数,先Java.use拿到类,然后用implementation直接覆盖原函数,那么我们也可以参考jni反射执行java函数的思路,通过FindClass拿到类,再通过GetMethodID的方式拿到一个jmethodID

        var MainActivity = Java.use("xxxxx");
        MainActivity.xxxxx.implementation=function(){
            return xxxx;        
        }

失败的案例

那么已经拿到jmethodID了,要如何转化为ArtMethod呢,这一点可以通过安卓源码来查看在http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.h目录,可以看到,它只是做了一个强制类型转换,那么就相当于我们拿到了我们想hook函数对应的ArtMethod

static inline ArtMethod* DecodeArtMethod(jmethodID method_id) {
  return reinterpret_cast<ArtMethod*>(method_id);
}

现在把他的EntryPoint改成我们想要的函数就好了,那么就又产生了2个问题
1.EntryPoint在哪我如何能找到它
庆幸的是安卓源码中有提供函数,也就是上面提到的GetEntryPointFromQuickCompiledCode那么只要通过ida查看它的实现就好了,遗憾的是这是一个inline函数,没有办法使用ida查看,只能通过自己修改源码,来查看偏移了,然后直接脱出libart.so,查看导出函数getmynative就好了

//ArtMethod.cc
extern "C"  const void * getmynative(ArtMethod* m)REQUIRES_SHARED(Locks::mutator_lock_){

m->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
m->UnregisterNative();
const void* entry_point = m->GetEntryPointFromQuickCompiledCode();
if(m->IsFastNative())
return GetQuickGenericJniStub();
return entry_point;

}

这样就知道了,偏移是40,那么直接更改就好了

2.直接改会少很多系统函数的调用能否成功
这个从理论上来说,是一定不成功的,看书上后面还有许多的过程,包括参数、栈的保存,都没有做,一定不可能成功,但是也不妨试一下,没准歪打正着了呢,第一版代码如下

void* func(void* a,void* b,int c,int d){

    __android_log_print(6,"r0ysue","i am success");
    return reinterpret_cast<void *>(5);

}


jclass a=env->FindClass(classname);
jmethodID b=env->GetMethodID(a,methodname, shoty);
*((long *)a1 + 5)= reinterpret_cast<long>(func);

居然没拉稀,和之前猜的不太一样,虽然成功执行了我们自己的函数,但是要思考一个问题就是如何执行原函数,这里如果通过反射直接执行的话,我们拿不到参数所以这种方案有点垃圾,我们还需要继续改进

进阶

既然上面的方案有瑕疵,就是有很多函数没有执行,导致我们拿不到参数,那么可以采取下一个,就是模仿Native函数的方式,将本函数改成Native函数,然后再使用将Native函数注册就好了,那么根据上面第一节的内容只要将EntryPoint改成art_quick_generic_jni_trampoline,就好了,再次用ida打开libart.so,发现art_quick_generic_jni_trampoline是一个函数,但它不是一个导出函数,使用导出表没有办法找到它,那么就用上篇文章提到的节头表索引发来搞定,最后再用RegisterNatives注册函数地址就好了

    int so=findsym("/system/lib64/libart.so","art_quick_generic_jni_trampoline");
    *((long *)a1 + 5)= reinterpret_cast<long>((char *) startr +so - 0x25000);//需要-0x25000是因为libart.so的程序头在偏移为0x25000这里没遍历偷懒了
    env->RegisterNatives(a,getMethods,1);

这样就完成了Java函数的Native化,那么试验一下好不好使,果然不行提示不是native函数无法完成注册,那么就看一下如何的判断是否是Native函数,之前编译rom的时候我就加入了,Native相关的判断,可以看到,公式(~*(_DWORD *)(a1 + 4) & 0x80100),就是判断是否是native函数的,只要它不等0,那么就可以认为我们的函数是Native函数.

那么我们就可以构造了,直接做异或就好了

 *(_QWORD *)(a1 + 4)= *(_QWORD *)(a1 + 4)^0x80100 ;

这样准备就完成了,我们就可以像动态注册的jni函数一样,去实现我们的c层的add函数
,执行就正常了,这里不给大家贴图了,因为还有最后一个问题没解决,就是如何执行原函数。

实现原函数

c调用java函数只有一种方式,那就是反射,所以我们只能用反射,那么就要收集参数了,比如我下面的这个函数,我需要一个实例,一个env,2个参数

   public int add2(int a,int b){
        return 7;
    }

幸运的是,我们是动态注册的,动态注册的实例函数会自带env和this实例,所以我们直接调用就好了

void* add(void* a,void* b,int c,int d){

    JNIEnv *st=(JNIEnv *)a;
    jclass a2=st->FindClass("com/r0ysue/myjavahook/MainActivity");
    jmethodID b2=st->GetMethodID(a2,"add2", "(II)I");
   int yy= st->CallIntMethod(static_cast<jobject>(b), b2, c, d);
   __android_log_print(6,"r0ysue","%x",yy);
    return reinterpret_cast<void *>(5);

}

直接死循环爆栈了…,太垃圾了,还是思路没找好,想想也是,我没有将函数恢复就直接反射,肯定会再次调用的,所以要想一个办法将ArtMethod恢复为当初java函数的样子,这里我又用了全局变量(没错就是inlinehook 时候坑了我那么久的东西,所以一会还要解决多函数的hook问题)

这里方案是这样的,将hook函数中保存的jump和nativ,直接复原,但是这里我没想好怎么搞,所以baocun函数要一个参数,就是ArtMethod指针,保存完还要回复到执行Native函数的状态


void hook(){

....
   nativ=*(_QWORD *)(a1 + 4);//isNative()?
    jump=*((long *)a1 + 5);//EntryPoint
....

}

void huifu(__int64 a,int n){

    *((long *)a + 5)=old1;
    *(_QWORD *)(a + 4)=old2;

}

void baocun(__int64 a){
 old1=*((long *)a + 5);
 old2=*(_QWORD *)(a + 4);
    *((long *)a + 5)= jump;
    *(_QWORD *)(a + 4)= nativ|0x80100;
}
void* add(void* a,void* b,int c,int d){

    JNIEnv *st=(JNIEnv *)a;
    jclass a2=st->FindClass("com/r0ysue/myjavahook/MainActivity");
    jmethodID b2=st->GetMethodID(a2,"add2", "(II)I");
  baocun((__int64)b2);
   int yy= st->CallIntMethod(static_cast<jobject>(b), b2, c, d);
   __android_log_print(6,"r0ysue","%x",yy);
   huifu((__int64)b2)
    return reinterpret_cast<void *>(5);

}


这样就很完美了,不管我怎么hook都没有问题了,也不好奔溃

hook 多个函数

但是当hook多个函数的时候就有问题了,我用的是全局变量,第二次hook的时候会覆盖第一次的baocun()和huifu()函数,所以我又采用了数组的方式来保存,这部分代码很简单就直接贴了

void baocun(__int64 a,int n){


 old1[n]=*((long *)a + 5);
 old2[n]=*(_QWORD *)(a + 4);

    *((long *)a + 5)= jump[n];
//    *(_QWORD *)(a + 4)=0x1d8ee408000101;
    *(_QWORD *)(a + 4)= nativ[n]|0x80100;
}

void huifu(__int64 a,int n){

    *((long *)a + 5)=old1[n];
    *(_QWORD *)(a + 4)=old2[n];

}
void hook(){
...
    nativ[ns]=*(_QWORD *)(a1 + 4);
    jump[ns]=*((long *)a1 + 5);

...
}

总结

*((long *)a + 5)=old1[n];
*(_QWORD *)(a + 4)=old2[n];

}
void hook(){

nativ[ns]=*(_QWORD )(a1 + 4);
jump[ns]=
((long *)a1 + 5);


}

## 总结
这样一个java层的hook框架就完成了,开始写的时候感觉比inlinehook难,完成之后发现代码量好少,原理就是从书上得到的,比较简单,主要获得ArtMethod不需要hook就节省了许多,jni方法太好用了
<think>好的,用户现在需要Hook打印类的成员变量。首先,我需要明确用户的具体需求。他们可能是在Android开发中遇到了问题,想要动态地获取或修改某个类的成员变量,比如用于调试、监控或者修改应用行为。用户之前询问过Pine框架,所以可能已经在使用或考虑使用这个工具。 接下来,我要考虑用户的技术背景。他们可能有一定的Android开发经验,但对Hook框架的具体使用还不够熟悉,特别是如何操作成员变量。需要分步骤解释,确保他们能理解并实施。 然后,回顾之前关于Pine框架的介绍,Pine主要用于方法拦截,但用户现在需要的是成员变量的Hook。这时候需要确认Pine是否支持直接Hook成员变量,或者是否需要通过其他方法间接实现。根据知识库,Pine主要针对方法进行Hook,因此可能需要通过Hook对应的getter和setter方法来访问成员变量,或者使用反射来直接访问。 另外,可能需要比较其他Hook框架如Xposed或Frida是否有更直接的方式,但用户已经提到Pine,所以应优先考虑在该框架下的解决方案。需要给出具体的代码示例,比如如何通过Hook方法或反射来获取变量值,并说明注意事项,如权限、兼容性、性能影响等。 还需要考虑用户可能遇到的问题,比如成员变量是私有的或者被混淆了,这时候反射可能需要处理访问权限,或者使用其他工具如Unidbg来辅助。同时,提醒用户注意稳定性,避免在关键代码中频繁Hook导致崩溃。 最后,总结使用Pine的步骤,并给出替代方案,让用户有更多选择。确保回答结构清晰,逐步引导用户解决问题,同时保持内容准确可靠。</think>--- ### **Hook 类成员变量的核心思路** Hook 类成员变量(即字段)的核心是通过 **拦截字段的读写操作** 来实现。由于 Java/Kotlin 的字段访问通常通过 `getter`/`setter` 方法或直接操作完成,常见的实现方式有两种: 1. **直接通过反射修改字段值(无需 Hook)** 适用于简单场景,但需绕过访问权限检查。 2. **Hook 字段的 `getter`/`setter` 方法** 通过拦截相关方法间接控制字段读写。 --- ### **使用 Pine 框架实现字段 Hook 的步骤** 以 Hook 一个类的私有成员变量 `private int value` 为例: #### **1. 确定字段的访问方法** 若字段通过 `getValue()` 和 `setValue(int)` 方法暴露,可直接 Hook 这些方法: ```java // 拦截 getValue() 方法 Pine.hook(MyClass.class, "getValue", new MethodHook() { @Override protected void beforeCall(MethodParam param) { // 修改返回值:假设强制返回 100 param.setResult(100); } }); // 拦截 setValue(int) 方法 Pine.hook(MyClass.class, "setValue", new MethodHook() { @Override protected void beforeCall(MethodParam param) { // 修改输入参数:将设置的值翻倍 int originalValue = (int) param.args[0]; param.args[0] = originalValue * 2; } }); ``` #### **2. 直接 Hook 字段(需结合反射)** 若字段没有通过方法暴露,需通过反射访问字段内存地址(**需 Root 权限或动态修改字节码**): ```java // 通过反射获取字段 Field field = MyClass.class.getDeclaredField("value"); field.setAccessible(true); // 绕过私有权限 // 动态修改字段值(需目标对象实例) MyClass obj = new MyClass(); field.setInt(obj, 999); // 直接设置值 ``` #### **3. 打印字段值(无需 Hook)** 若仅需打印字段值,可直接通过反射读取: ```java // 反射获取字段值 Field field = MyClass.class.getDeclaredField("value"); field.setAccessible(true); int value = (int) field.get(obj); // obj 是目标类的实例 Log.d("HookDemo", "当前 value 值: " + value); ``` --- ### **注意事项** 1. **权限问题** - 反射访问私有字段需调用 `setAccessible(true)`,但在 Android 9+ 可能受系统限制。 - 直接操作内存需 Root 权限或使用 Magisk 模块。 2. **兼容性** - 字段偏移量随 Android 版本变化,直接内存操作需适配不同设备。 3. **性能影响** - 频繁反射或 Hook 可能降低性能,建议仅在调试时使用。 --- ### **替代方案对比** | 方法 | 优点 | 缺点 | |-------------------|--------------------------|------------------------------| | **反射直接读写** | 简单直接,无需 Hook | 无法监控实时变化,需实例对象 | | **Hook getter/setter** | 动态监控字段变化 | 依赖字段是否通过方法暴露 | | **Xposed 框架** | 系统级支持,稳定性高 | 需 Root 和安装模块 | | **Frida 内存修改**| 跨进程动态修改 | 需常驻进程,逆向分析门槛高 | --- ### **实际场景示例** 假设需要监控 `UserData` 类的 `private String username` 字段: 1. **Hook 构造函数**(确保获取实例对象): ```java Pine.hook(UserData.class, "<init>", new MethodHook() { @Override protected void afterCall(MethodParam param) { UserData instance = (UserData) param.thisObject; String username = instance.getUsername(); // 假设有 getter Log.d("UserTracker", "新用户创建: " + username); } }); ``` 2. **Hook `setUsername` 方法**: ```java Pine.hook(UserData.class, "setUsername", new MethodHook() { @Override protected void beforeCall(MethodParam param) { String newName = (String) param.args[0]; Log.d("UserTracker", "用户名被修改为: " + newName); } }); ``` --- ### **总结** - 若仅需 **读取/修改字段值**,优先使用 **反射**。 - 若需 **动态监控字段变化**,需 Hook 相关的 `getter`/`setter` 方法。 - 对系统级字段(如系统服务)建议结合 Xposed 或 Frida 实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值