首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
4
8
[原创]HybridCLR拆解记录 (Part. I)
发表于: 2026-2-10 16:35
3365
[原创]HybridCLR拆解记录 (Part. I)
fengmaple
2026-2-10 16:35
3365
第一次发帖,被迫造轮子但必须得手撕Asm的感觉不亚于屎里找饭,~~这种自虐的感觉真让人欲罢不能。~~ 原本在<a href="elink@d8fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3L8r3!0W2K9h3x3K6i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1k6U0L8$3c8W2M7Y4g2K6N6r3I4W2x3W2)9J5c8W2)9J5x3K6m8^5x3o6k6Q4x3X3c8Q4x3U0g2q4y4g2)9J5y4f1t1H3i4K6t1#2z5f1c8Q4x3U0g2q4z5q4)9J5y4f1q4r3i4K6t1#2z5e0g2Q4x3U0g2q4y4q4)9J5y4f1u0p5i4K6t1#2b7V1k6Q4x3U0g2q4y4#2)9J5y4e0V1@1i4K6t1#2b7e0S2t1P5h3u0J5K9h3c8o6e0q4u0Q4x3X3c8t1L8$3!0C8i4K6t1#2c8U0m8Q4x3U0f1&6c8W2)9J5y4e0V1^5i4K6t1#2z5e0f1`.">上一篇文章0x06</a>的尝试过后,我已经打定主意不再碰HybridCLR了,一是因为网络上可参考的内容确实不多,native hook方案非开源,~~抄都没地方抄~~;二是既然已经可以直接replace dll了,为啥还要自找麻烦呢?但实在是不甘心只能用Frida脚本反复试错,于是艰苦的旅程又双叒叕开始了。下文依旧围绕coderustle2展开。 APP: com.wingjoy.coderustle2 (ver 1.8.16) OS: Redmi K40 with HyperOS 1.0.6.0 (Android 13) / Xiaomi 14 with HyperOS 3.0.6.0 (Android 16) Tools: Android Studio / NDK r27 / SDK 36.0 ## 函数进入监听及入参修改 ### 0x00 原理分析 额外说明:不同版本的HybridCLR,相关重要函数参数及结构体定义不尽相同。以下内容均基于hybridclr-4.0.0。 不赘述,根据官方文档<a href="elink@f45K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2Z5P5h3u0J5K9h3c8U0L8s2u0Q4x3X3g2U0L8W2)9J5c8X3c8G2j5%4y4Q4x3V1k6T1j5i4y4A6j5#2)9J5c8X3#2W2N6r3S2G2k6r3u0J5K9h3c8Y4k6b7`.`.">桥接函数</a>、<a href="elink@2e7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2Z5P5h3u0J5K9h3c8U0L8s2u0Q4x3X3g2U0L8W2)9J5c8X3c8G2j5%4y4Q4x3V1k6T1j5i4y4A6j5#2)9J5c8Y4y4G2N6i4u0U0k6h3W2F1M7%4m8W2j5%4b7`.">HybridCLR源码结构及调试</a>及源码,可知`hybridclr/interpreter/interpreter_Execute.cpp`的`Interpreter::Execute`函数负责寄存器IR指令的解释执行,同时也是Native/反射进入解释器的唯一入口;`hybridclr/interpreter/Engine.h`的`EnterFrameFromInterpreter`函数负责解释器调用解释器(即解释器内部A方法调用内部B方法)时建立 frame,不复制参数;`hybridclr/interpreter/Engine.h`的`EnterFrameFromNative`函数负责Native调用解释器时建立 frame,并把参数搬进解释器栈。上述亦参考了其他分析文章:[UnityHybirdCLR Hook实现](https://bbs.kanxue.com/thread-281910.htm)、<a href="elink@6feK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3M7X3!0Y4i4K6u0V1k6$3q4E0k6g2)9J5k6h3N6A6N6r3S2#2j5W2)9J5k6h3W2G2i4K6u0r3M7r3!0K6N6s2y4Q4x3V1k6J5k6h3q4V1i4K6u0r3K9s2W2T1M7X3W2V1j5$3I4J5i4K6u0V1P5i4g2S2L8X3#2S2P5i4g2W2k6s2g2Q4x3V1k6Q4x3U0y4Q4x3U0g2W2y4q4)9J5y4h3t1^5i4K6t1#2z5o6m8Q4x3U0g2W2y4q4)9J5y4h3u0S2i4K6t1#2z5h3u0Q4x3U0g2W2y4g2)9J5y4e0R3&6i4K6t1#2z5r3c8Q4x3U0g2W2y4W2)9J5y4e0S2X3i4K6t1#2z5e0m8Q4x3U0g2W2y4#2)9J5y4e0W2X3i4K6t1#2j5e0g2Q4x3U0g2W2z5q4)9J5y4h3q4X3i4K6t1#2z5o6k6Q4x3U0g2W2y4#2)9J5y4e0R3J5i4K6t1#2j5U0V1`.">HybridCLR源码赏析</a> 结论显而易见,同时hook `EnterFrameFromNative`和`EnterFrameFromInterpreter`函数,根据第一个参数`InterpMethodInfo* imi`结构体定义,通过解析可获得`MethodInfo* method`,进而拿到所有进入解释器的方法信息。`InterpMethodInfo`定义如下: ```c++ // hybridclr/interpreter/InterpreterDefs.h #include <vector> struct InterpMethodInfo { const MethodInfo* method; MethodArgDesc* args; uint32_t argCount; uint32_t argStackObjectSize; byte* codes; uint32_t codeLength; uint32_t maxStackSize; // args + locals + evalstack size uint32_t localVarBaseOffset; uint32_t evalStackBaseOffset; uint32_t localStackSize; // args + locals StackObject size std::vector<uint64_t> resolveDatas; std::vector<InterpExceptionClause*> exClauses; bool initLocals; }; ``` 第二个参数`StackObject* argBase`中的`StackObject`本身并不是数据类型,而是作为解释器栈槽(stack slot),同时声明解释器的最小栈单位是8字节。每个参数、局部变量、临时值都可能占用 1个或多个`StackObject`: ```c++ // hybridclr/interpreter/InterpreterDefs.h union StackObject { uint64_t __u64; void* ptr; bool b; int8_t i8; uint8_t u8; int16_t i16; uint16_t u16; int32_t i32; uint32_t u32; int64_t i64; uint64_t u64; float f4; double f8; Il2CppObject* obj; Il2CppString* str; Il2CppObject** ptrObj; }; static_assert(sizeof(StackObject) == 8, "require 8 bytes"); ``` 其中**bool/int8/int16/int32/float**类型实际只用slot低位,高位清零,但仍占用8字节槽;**int64/double/指针**类型正好占满8字节;**Struct**类型将按大小拆成多个StackObject,如:12字节 -> 8字节+4字节 -> 2个slot。对于简单的入参类型,如`int A`,可以直接按上述定义读取值,例:`int valueA = argBase[i].i32`;写回值时要注意不要混用成员,同理:`argBase[i].i32 = newValue`;针对值传递结构体,取值时应按结构体大小,把连续的`StackObject`视同一块原始内存来读: ```c++ auto* value = reinterpret_cast<Struct*>(&argBase[start]); Struct v = *value; float f = value->someField; // 也可只读字段 ``` 写回时直接按内存拷贝: ```c++ auto* dst = reinterpret_cast<Struct*>(&argBase[start]); *dst = newValue; ``` 针对ref/out(引用传递/输出引用)结构体,栈上只放了1个指向真实Struct地址的指针,读写方式需要对应修改: ```c++ auto* p = reinterpret_cast<Struct*>(argBase[i].ptr); // 读 p->field = value; // 写 *p = newValue; // 写 ``` ### 0x01 函数进入监听 获得`EnterFrameFromNative`和`EnterFrameFromInterpreter`函数绝对地址的方法可参考<a href="elink@e4bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6u0d9f1W2u0L8h3#2E0P5i4W2&6i4K6u0r3d9s2W2T1M7X3W2V1b7@1I4d9i4K6u0V1d9r3!0G2K9H3`.`.">HybridCLR-Hook</a>,此处不赘述。基于Dobby的hook写法示例如下: ```c++ //0xF5D7C4 InterpFrame* EnterFrameFromNative(const InterpMethodInfo* imi, StackObject* argBase) //0xF5D8E0 InterpFrame* EnterFrameFromInterpreter(const InterpMethodInfo* imi, StackObject* argBase) HOOK_DEF(InterpFrame*, EnterFrameFromInterpreter, InterpFrame* self, const InterpMethodInfo* imi, StackObject* argBase) { InterpFrame* newFrame = orig_EnterFrameFromInterpreter(self, imi, argBase); const MethodInfo* method = imi->method; const char* name = method->name; LOGW("[Interp] %s called", name); return newFrame; } // Use absolute address for test void doHook() { //HOOK_FUNC((il2cpp_base + 0xF5D7C4), EnterFrameFromNative); HOOK_FUNC((il2cpp_base + 0xF5D8E0), EnterFrameFromInterpreter); } ``` ### 0x02 入参修改 函数原型: ```csharp // Token: 0x06000694 RID: 1684 RVA: 0x000260EC File Offset: 0x000242EC public void AddDropGoldCoins(int count) { this.m_TotalGoldCoins += count; } ``` hook示例如下: ```c++ HOOK_DEF(InterpFrame*, EnterFrameFromInterpreter, InterpFrame* self, const InterpMethodInfo* imi, StackObject* argBase) { const MethodInfo* method = imi->method; const char* name = method->name; if (strcmp(name, "AddDropGoldCoins") == 0) { int count = argBase[1].i32; LOGI("[Interp] orig count: %d", count); argBase[1].i32 = count * 50; } return orig_EnterFrameFromInterpreter(self, imi, argBase); } ``` ## 函数返回值读取及修改 #### 0x03 原理分析 首先查看`hybridclr/interpreter/Engine.h`的`LeaveFrame`函数,逐行解读: ```c++ // hybridclr/interpreter/Engine.h InterpFrame* LeaveFrame() { IL2CPP_ASSERT(_machineState.GetFrameTopIdx() > _frameBaseIdx); // 断言当前至少存在一个可弹出的解释器帧,防止栈下溢 POP_STACK_FRAME(); // 通知调试器/profiler当前方法即将退出 InterpFrame* frame = _machineState.GetTopFrame(); // 取得当前正在执行的callee帧 #if IL2CPP_ENABLE_PROFILER il2cpp_codegen_profiler_method_exit(frame->method->method); #endif if (frame->exFlowBase) { // 如果该方法内部使用过异常处理(try/catch) _machineState.SetExceptionFlowTop(frame->exFlowBase); // 恢复异常流栈到进入该方法之前的状态,防止异常信息泄漏到caller帧 } _machineState.PopFrame(); // 将当前callee Frame从frame栈中移除 _machineState.SetStackTop(frame->oldStackTop); // 恢复解释器操作数栈到进入该方法之前的高度 _machineState.SetLocalPoolBottomIdx(frame->oldLocalPoolBottomIdx); return _machineState.GetFrameTopIdx() > _frameBaseIdx ? _machineState.GetTopFrame() : nullptr; // 如果还有上层帧,则返回caller的InterpFrame*;如果已经回到解释器入口,则返回nullptr,解释器执行结束 } ``` 进一步查找`LeaveFrame`函数于何处被使用,进入到`hybridclr/interpreter/interpreter_Execute.cpp`: ```c++ // hybridclr/interpreter/interpreter_Execute.cpp #define LEAVE_FRAME() { \ frame = interpFrameGroup.LeaveFrame(); \ // 弹出当前解释器帧返回caller帧,或nullptr if (frame) \ // 如果为解释器内部调用返回(interp -> interp) { \ LOAD_PREV_FRAME(); \ // 恢复caller帧的执行上下文,继续解释执行 }\ else \ // 最外层返回(interp -> native) { \ goto ExitEvalLoop; \ // 跳出解释器主循环 } \ } #define SET_RET_AND_LEAVE_FRAME(nativeSize, interpSize) { \ void* _curRet = frame->ret; \ // 保存当前帧的返回值地址 frame = interpFrameGroup.LeaveFrame(); \ // 弹出当前解释器帧并返回caller帧,或nullptr if (frame) \ // 如果为解释器内部调用返回(interp -> interp) { \ Copy##interpSize(_curRet, (void*)(localVarBase + __ret)); \ // 使用解释器格式(StackObject)拷贝callee帧的返回值到_curRet,供caller帧读取 LOAD_PREV_FRAME(); \ // 恢复caller帧的执行上下文,继续解释执行 }\ else \ // 最外层返回(interp -> native),如:Runtime::Invoke { \ Copy##nativeSize(_curRet, (void*)(localVarBase + __ret)); \ // 使用native ABI约定拷贝返回值 goto ExitEvalLoop; \ // 跳出解释器主循环 } \ } ``` 可见此处定义了2个预处理宏,其中`LEAVE_FRAME()`用于在执行循环中弹出当前解释器帧,恢复上一帧上下文或在没有帧可返回时跳出解释器主循环,执行完毕后销毁当前帧;`SET_RET_AND_LEAVE_FRAME(nativeSize, interpSize)`用于处理有返回值的方法,这里我们结合`Execute`函数中的特定case以及`Copy##`函数源码重新阅读一遍,以`case RetVar_ret_1`为例: ```c++ // hybridclr/interpreter/interpreter_Execute.cpp case HiOpcodeEnum::RetVar_ret_1: { // 若返回值为1字节 uint16_t __ret = *(uint16_t*)(ip + 2); // 从当前解释器指令后2个字节中取uint16_t作为__ret,作为返回值的局部变量索引 SET_RET_AND_LEAVE_FRAME(1, 8); // 将返回值从localVarBase的ret slot拷贝到caller帧的ret slot,供caller读取 continue; } ``` ```c++ // hybridclr/interpreter/MemoryUtil.h inline void Copy1(void* dst, void* src) { // 如果最外层返回至native,遵循ABI按真实类型大小进行拷贝,即Copy1 *(uint8_t*)dst = *(uint8_t*)src; } inline void Copy8(void* dst, void* src) { // 如果解释器内部调用返回,执行Copy8,最终返回1个StackObject(8字节) *(uint64_t*)dst = *(uint64_t*)src; } ``` 那么,如果想要读取或者修改函数返回值,**最稳妥的载点应该是`Copy`函数执行之前,对`localVarBase + __ret`指向的内存区域进行读写**。但还有1个关键问题待确认:`Copy`执行之前,`LeaveFrame`已经执行完毕,在这中间阶段寄存器中是否还留存有`InterpFrame* callee`?相关信息是否已经被`LeaveFrame`销毁?这关乎到我们能否在此处获得callee帧method信息,以及进行特定method hook。 阅读源码`case RetVar_ret_n`,通过`memove`交叉引用定位到`LeaveFrame()`: ```c++ // hybridclr/interpreter/interpreter_Execute.cpp case HiOpcodeEnum::RetVar_ret_n: { uint16_t __ret = *(uint16_t*)(ip + 2); uint32_t __size = *(uint32_t*)(ip + 4); std::memmove(frame->ret, (void*)(localVarBase + __ret), __size); LEAVE_FRAME(); continue; } ``` 对应: ```assembly loc_F52F24: LDRH W8, [X22,#2] ; jumptable 0000000000F4AAC0 case 294 LDR W2, [X22,#4] ; size_t LDR X0, [X25,#0x18] ; void * ADD X1, X23, X8,LSL#3 ; void * BL .memmove ADD X0, SP, #0x1460+var_BF0 BL LeaveFrame ; sub_F5D954 B loc_F5445C ``` `Execute`共计调用`LeaveFrame`16次,与IDA所查看的交叉引用次数相符。此处画一个流程图便于理解:  顺带根据Copy的系列定义,对比Asm语义逐一标注出`SET_RET_AND_LEAVE_FRAME(1, 8)`至`SET_RET_AND_LEAVE_FRAME(32, 32)`入口:  以`case RetVar_ret_24`为例(`ObscuredFloat`就是24字节Struct,后面接着细说),先解读入口的几句Asm: ```assembly loc_F4B24C: ; jumptable 0000000000F4AAC0 case 291 RetVar_ret_24 LDRH W20, [X22,#2] ; X22 = ip,W20 = __ret = ip + 2 LDR X19, [X25,#0x18] ; X25 = InterpFrame* callee,X19 = frame->ret = dst ADD X0, SP, #0x1460+var_BF0 ; X0 = &interpFrameGroup(this) BL LeaveFrame ADD X8, X23, X20,LSL#3 ; X23 = localVarBase,X20 = __ret,X8 = src,等价于:void* src = localVarBase + __ret CMP X19, X8 B.HI loc_F50DCC LDR X9, [X8] ; 拷贝slot0 STR X9, [X19] ; 拷贝slot0 LDR X9, [X8,#8] ; 拷贝slot1 ADD X8, X8, #0x10 ; X8 -> slot3 STR X9, [X19,#8] ; 拷贝slot1 ADD X19, X19, #0x10 ; X19 -> slot3 B loc_F54454 ; slot3后续拷贝 ``` 因此,需要重点关注寄存器X25所保存的`InterpFrame* callee`在`LeaveFrame`执行完毕后是否被其他值覆盖。结合`MachineState`定义,把`LeaveFrame`的Asm读一遍: ```c++ // hybridclr/interpreter/Engine.h MachineState() { Config& hc = Config::GetIns(); _stackSize = -1; _stackBase = nullptr; _stackTopIdx = 0; _localPoolBottomIdx = -1; _frameBase = nullptr; _frameCount = -1; _frameTopIdx = 0; _exceptionFlowBase = nullptr; _exceptionFlowCount = -1; _exceptionFlowTopIdx = 0; } ``` ```assembly LeaveFrame: STP X20, X19, [SP,#-0x10]! ; 保存被调用者寄存器 X19/X20 STP X29, X30, [SP,#0x10] ; 保存 FP/LR ADD X29, SP, #0x10 ; 建立栈帧 LDR X8, [X0] ; X8 = this->_machineState MOV X19, X0 ; X19 = this (InterpFrameGroup*) LDRSW X9, [X8,#0x20] ; X9 = _frameTopIdx CMP W9, #1 B.LT loc_F5D984 ; 若 frameTopIdx < 1,则没有 frame LDR X8, [X8,#0x18] ; X8 = _frameBase ADD X8, X8, X9, LSL #6 ; X8 = _frameBase + frameTopIdx * sizeof(InterpFrame) SUB X20, X8, #0x40 ; X20 = GetTopFrame() = base + idx - 1 B loc_F5D988 loc_F5D984: MOV X20, XZR ; frame = nullptr loc_F5D988: LDR X8, [X20] ; X8 = frame->method LDR X0, [X8] ; X0 = frame->method->method BL sub_C4E428 ; profiler_method_exit(method) LDR X8, [X20,#0x28] ; X8 = frame->exFlowBase CBZ X8, loc_F5D9BC ; 若无异常流,跳过 LDR X9, [X19] ; X9 = _machineState LDR X10,[X9,#0x28] ; X10 = _exceptionFlowBase SUB X8, X8, X10 ; X8 = exFlowBase - exceptionFlowBase (字节差) LSR X8, X8, #3 ; /8 MOV W10,#0xAAAB MOVK W10,#0xAAAA,LSL#16 ; W10 = 0xAAAAAAAB MUL W8, W8, W10 ; /3 → 等价于 /24 STR W8, [X9,#0x30] ; _exceptionFlowTopIdx = (top - base) loc_F5D9BC: LDR X8, [X19] ; X8 = _machineState MOV X0, XZR ; 默认返回 nullptr LDR W9, [X8,#0x20] ; W9 = _frameTopIdx SUB W9, W9, #1 STR W9, [X8,#0x20] ; PopFrame(): --_frameTopIdx LDR X8, [X19] LDR W9, [X20,#0x10] ; frame->oldStackTop STR W9, [X8,#0x0C] ; _stackTopIdx = oldStackTop LDR X8, [X19] LDR W9, [X20,#0x38] ; frame->oldLocalPoolBottomIdx STR W9, [X8,#0x10] ; _localPoolBottomIdx = oldLocalPoolBottomIdx LDR X8, [X19] LDR W9, [X8,#0x20] ; W9 = _frameTopIdx CMP W9, #1 B.LT loc_F5DA14 ; 若无 frame,返回 nullptr LDR W10,[X19,#0x0C] ; W10 = _frameBaseIdx CMP W9, W10 B.LS loc_F5DA14 ; 若 <= baseIdx,返回 nullptr LDR X8, [X8,#0x18] ; X8 = _frameBase SXTW X9, W9 ADD X8, X8, X9, LSL #6 SUB X0, X8, #0x40 ; 返回新的 GetTopFrame(),即caller Frame loc_F5DA14: LDP X29, X30, [SP,#0x10] ; 恢复 FP/LR LDP X20, X19, [SP],#0x20 ; 恢复寄存器并回收栈 RET ``` 可见X25 在整个`LeaveFrame`执行期间保持不变,`LeaveFrame`最终返回`X0 = caller Frame`。现在可以确认:**在`CMP X19, X8`语句处观测X25寄存器,即可获得当前method;读写X8寄存器指向的内存,即可读写函数返回值。** ### 0x04 函数返回值读取 函数原型: ```csharp // Token: 0x170005D7 RID: 1495 // (get) Token: 0x06002918 RID: 10520 RVA: 0x000BA685 File Offset: 0x000B8885 public ObscuredFloat UpgradeReformSuccessRate { get { return 0.001f * (float)this.Level; } } ``` 这里需先在Github上找一份成品`ObscuredFloat`结构体定义并自行引用:<a href="elink@fc5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3L8%4S2U0L8$3!0C8K9h3g2Q4x3V1k6s2L8$3!0K6k6g2)9#2k6V1N6G2L8%4y4W2i4K6g2X3c8s2g2U0K9#2)9#2k6V1S2S2j5$3E0Q4x3V1k6T1L8r3!0T1i4K6u0r3z5e0l9$3k6X3f1H3z5o6V1&6j5$3q4W2y4K6b7J5j5e0k6U0k6o6j5$3y4U0V1^5x3$3b7J5y4h3q4X3z5o6g2W2j5X3x3^5j5e0u0W2y4W2)9J5c8W2y4@1M7Y4g2U0N6q4)9J5c8V1!0T1M7$3y4#2M7X3g2V1g2s2W2H3k6i4y4Q4x3X3g2Z5M7s2m8Q4x3U0y4x3z5o6l9`.">传送门</a> 可知`ObscuredFloat`结构体在内存中的真实布局为24字节: ```bash slot0 (0x00~0x07): currentCryptoKey | hiddenValue slot1 (0x08~0x0F): hiddenValueOldByte4 | inited + padding slot2 (0x10~0x17): fakeValue | fakeValueActive + padding ``` 示例如下: ```c++ //0xF5D8E0 InterpFrame* EnterFrameFromInterpreter(const InterpMethodInfo* imi, StackObject* argBase) HOOK_DEF(InterpFrame*, EnterFrameFromInterpreter, InterpFrame* self, const InterpMethodInfo* imi, StackObject* argBase) { InterpFrame* newFrame = orig_EnterFrameFromInterpreter(self, imi, argBase); const MethodInfo* method = imi->method; const char* name = method->name; if (strcmp(name, "get_UpgradeReformSuccessRate") == 0) { LOGW("[Interp] get_UpgradeReformSuccessRate called frame=%p method=%p", newFrame, method); } return newFrame; } static void on_ret24_cmp(void *address, DobbyRegisterContext *ctx) { uint64_t x8 = ctx->general.regs.x8; InterpFrame* frame = (InterpFrame*)ctx->general.regs.x25; const MethodInfo* method = frame->method->method; const char* name = frame->method->method->name; if (strcmp(name, "get_UpgradeReformSuccessRate") == 0) { LOGI("F4B260 hit:"); LOGI(" X8 (ret src) = %p", (void*)x8); LOGI("InterpFrame:"); LOGW(" self = %p", frame); LOGI(" imiMethod = %p", frame->method); LOGW(" method = %p", frame->method->method); LOGI(" stackBasePtr = %p", frame->stackBasePtr); LOGI(" oldStackTop = %d", frame->oldStackTop); LOGI(" ret = %p", frame->ret); LOGI(" ip = %p", frame->ip); // dump return slot memory uint8_t dump[24]; memcpy(dump, (void*)x8, sizeof(dump)); for (int i = 0; i < 24; i += 8) { LOGI(" ret +%02X: %02X %02X %02X %02X %02X %02X %02X %02X", i, dump[i+0], dump[i+1], dump[i+2], dump[i+3], dump[i+4], dump[i+5], dump[i+6], dump[i+7]); } auto* of = (ObscuredFloat*)x8; LOGI("Original ObscuredFloat:"); LOGW(" key=%d hidden=0x%08X inited=%d fake=%f fakeActive=%d", of->currentCryptoKey, (uint32_t)of->hiddenValue, of->inited, of->fakeValue, of->fakeValueActive); } } // Use absolute address for test void doHook() { HOOK_FUNC((il2cpp_base + 0xF5D8E0), EnterFrameFromInterpreter); // 双向观测 DobbyInstrument((void*)(il2cpp_base + 0xF4B260), on_ret24_cmp); } ``` 运行结果:  ### 0x05 函数返回值修改 示例如下: ```c++ static void on_ret24_cmp(void *address, DobbyRegisterContext *ctx) { uint64_t x8 = ctx->general.regs.x8; InterpFrame* frame = (InterpFrame*)ctx->general.regs.x25; const MethodInfo* method = frame->method->method; const char* name = frame->method->method->name; if (strcmp(name, "get_UpgradeReformSuccessRate") == 0) { LOGI("Original ObscuredFloat:"); LOGW(" key=%d hidden=0x%08X inited=%d fake=%f fakeActive=%d", of->currentCryptoKey, (uint32_t)of->hiddenValue, of->inited, of->fakeValue, of->fakeValueActive); of->fakeValueActive = true; of->Encrypt(0.25f); of->inited = true; LOGI("New ObscuredFloat:"); LOGE(" key=%d hidden=0x%08X inited=%d fake=%f fakeActive=%d", of->currentCryptoKey, (uint32_t)of->hiddenValue, of->inited, of->fakeValue, of->fakeValueActive); } } ``` 运行结果:  ## 下一步待实现 ~~画大饼时间到~~ 1.自动识别 EnterFrameFromNative、FromInterpreter 并hook 2.自动识别所有 Ret case 并插桩 3.高版本HybridCLR支持 4.区分数据类型,提供更多示例
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#逆向分析
#HOOK注入
收藏
・
4
点赞
・
8
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
Metzkerson
你的帖子非常有用,感谢分享!
2026-6-1 07:17
by_Lin
为你点赞!
2026-3-7 01:58
我的小拇指啊
感谢你的积极参与,期待更多精彩内容!
2026-2-13 05:41
清风jw
非常支持你的观点!
2026-2-12 16:28
doduhuang
期待更多优质内容的分享,论坛有你更精彩!
2026-2-11 10:09
saykao
感谢你的积极参与,期待更多精彩内容!
2026-2-10 19:51
梧桐生
这个讨论对我很有帮助,谢谢!
2026-2-10 19:49
孤独的街
感谢你分享这么好的资源!
2026-2-10 16:54
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
Zepp7289
雪 币:
200
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
137
粉丝
0
关注
私信
Zepp7289
2
楼
tql
2026-2-10 19:25
0
Imxz
雪 币:
112
活跃值:
(9405)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
750
粉丝
9
关注
私信
Imxz
3
楼
tql
2026-2-11 12:54
0
by_Lin
雪 币:
631
活跃值:
(1096)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
by_Lin
4
楼
tql
2026-3-7 01:58
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
fengmaple
1
发帖
6
回帖
10
RANK
关注
私信
他的文章
[原创]HybridCLR拆解记录 (Part. I)
3365
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部