一句话结论
RuntimeHelpers.PrepareMethod不读任何m_invocationFlags(无条件强制 JIT);HookInfo 的「清 0x80 设 0x20(mdcStatic)」位手术影响的是 CLR 原生侧TryGetMultiCallableAddrOfCode/MayHaveNativeCode/MayBeRemotingIntercepted对 MethodDesc+6 字节的判定分支;取真实函数头靠md + ReadInt32(md+8) + 8偏移跟随,不靠手术。2026-09-10 重大更新(x86 4.8.1 9181 实锤):手术的真正作用在 x86 上复现成功——Chanjet FastCGI 环境(System.dll IL 副本)中,MD+6 从 0x80 改为 0x20 后
PrepareMethod会真 JIT(A/B 对比:+3 0x0B→0x39、md+8 回填 code-8、CodeAddr 真实函数头 vs 不手术仅登记不 JIT)。机制:MD+6 的 0x80 位(RequiresInheritanceCheck)使 PrepareMethodDesc 管线拦住 0x80 方法(只登记不 JIT),0x20(mdcStatic)方法则被 ExecuteGate 放行真 JIT。顺序约束:手术必须在首次 PrepareMethod 之前。 servicing 差异:9181 拦 0x80,9337+ 实测不拦(不手术也 JIT)——详见第六章。
关联文章:clr.dll逆向-GetFunctionPointer与Precode(GetFunctionPointer 决策链与 Precode 体系)
一、分析背景
某个 .NET Hook 框架的初始化组件(下文统称 HookInfo)在 x64 分支有一段位手术:PrepareOldMethodForHook:
byte bTargetMDC = bOriginMDC;
bTargetMDC &= 0x0F; // 保留低 4 位
bTargetMDC |= mdcStatic; // 0x20 —— 清掉 0x80(mdcRequiresInheritanceCheck),或上 0x20(mdcStatic)
SetInvocationFlags(OldMethod, bTargetMDC);
RuntimeHelpers.PrepareMethod(OldMethod.MethodHandle);
SetInvocationFlags(OldMethod, bOriginMDC); // 还原本次分析回答三个问题:
PrepareMethod是否通过m_invocationFlags判断是否执行?(答:否)GetInvocationFlags能否改为属性访问?(答:读可以,写不行)- 当年加
mdcStatic到底为什么?(答:见第四节,复现后修正)
二、CLR 逆向结论(clr.dll 4.8.4795 x64,IDA 已加 22 条注释)
2.1 PrepareMethod(FCall,0x18065eca0)—— 与 InvocationFlags 无关
ReflectionInvocation::PrepareMethod(ReflectMethodObject*, inst, count)
├─ GetAttrs() & 0x400 (abstract) → 抛 Argument_CannotPrepareAbstract
├─ CheckRestore / 泛型 instantiation 校验 / FindOrCreateAssociatedMethodDesc
└─ PrepareMethodDesc(md, ...)
└─ MethodCallGraphPreparer::Run() ← CER 准备 + IL 调用图遍历- 全程不读 mscorlib 侧
m_invocationFlags—— 那是托管对象(RuntimeMethodInfo)上的字段,native FCall 只持有MethodDesc*,物理上无法读到 - 无条件执行(内部 CER 检查去重),不存在”根据 InvocationFlags 跳过”的分支
2.2 m_invocationFlags 是纯托管概念
| 侧 | 位置 | 作用 |
|---|---|---|
| mscorlib 托管 | RuntimeMethodInfo.m_invocationFlags 字段 | MethodInfo.Invoke 的准入校验缓存(NoInvoke/ContainsStackPointers → 抛异常) |
| clr.dll 原生 | 不存在 | grep 全部 VM 代码零命中 |
托管 get_InvocationFlags IL 结构(net48 实测反汇编):读 m_invocationFlags 字段 → 测 INITIALIZED 位 → 未初始化则懒计算(GetSecurityFlags + 逐位 OR)→ 写回缓存。消费者:InvokeArgumentsCheck/UnsafeInvoke/RuntimeConstructorInfo。与 JIT 准备无关。
2.3 真正消费 MethodDesc+6 字节的函数(★ = 手术影响点)
| 函数 | 地址 | 读取点 | 作用 |
|---|---|---|---|
TryGetMultiCallableAddrOfCode | 0x18003de10 | (b6 & 7) == 1 分类位;b[3]&1 HasStableEntryPoint | GetFunctionPointer 返回真代码 vs precode 的核心决策 |
MayHaveNativeCode | 0x180010b70(冷路径 0x18040408f)★ | test [rbx+6], 0x20 | mdcStatic 位参与”可直达 native code”判定 |
MayBeRemotingIntercepted | 0x18003dd90 | 开头即 test [rcx+6], 0x20 → return 0 | mdcStatic 短路 remoting 拦截 |
IsVtableMethod | 0x1800070c0(冷路径 0x180108568) | 同读 0x20 与 0x80 | 虚表判定 |
IDA 中已按 [HookInfo 分析] 前缀添加注释(11 处函数级 + 11 处函数内关键指令),Ctrl+F 可搜。
2.4 TryGetMultiCallableAddrOfCode 完整决策树(0x18003de10)
GetFunctionPointer() → QCall RuntimeMethodHandle::GetFunctionPointer(0x180066080)→ 本函数。入参 accessFlags(GetFunctionPointer 传 CORINFO_ACCESS_UNMANAGED_CALLER_MAYBE|LDFTN=16,v5 = a2 & 1 即 UNMANAGED_CALLER 位)。
决策顺序(每条分支 = 一种方法形态拿 entrypoint 的方式):
TryGetMultiCallableAddrOfCode(md, accessFlags)
│
├─① IsGenericMethodDefinition(md) → 抛 InvalidOperationException(未实例化泛型不可调)
│
├─② IsWrapperStub(md) || IsEnCAddedMethod(md)
│ → GetOrCreatePrecode() → GetMethodEntryPoint()
│ 【wrapper stub(unboxing/实例化 stub)与 EnC 新增方法:先确保有 precode,返回稳定入口】
│
├─③ accessFlags 无 UNMANAGED_CALLER 且 IsRemotingInterceptedViaVirtualDispatch(md)
│ → CRemotingServices::GetNonVirtualEntryPointForVirtualMethod(md)
│ 【Remoting 拦截:虚分发被拦截的虚方法,返回非虚入口点(remoting 桩)】
│
├─④ Module::IsEditAndContinueEnabled(md 的模块)
│ → GetOrCreatePrecode() → GetMethodEntryPoint()
│ 【EnC 启用的模块:必须用可回 patch 的 precode 入口,保证热更新时可改写】
│
├─⑤ (b6 & 7) == mcFCall(1) ←★ byte+6 分类位(HookInfo WriteByte 影响的判定之一)
│ 且 非remoting拦截 且 MethodTable 无 (0xF0000)==0xC0000(?)且 (MT & 0x20000000)==0
│ → ECall::GetFCallImpl(md) ← 直接返回 FCall 的原生实现地址!
│ (GetFCallImpl 失败 → GetOrCreatePrecode)
│ 【FCall 方法:绕过 precode 直达 ECall 表里的 C++ 实现】
│
├─⑥ 否则 IsPointingToNativeCode(md) ← 读 b[3]&1(HasStableEntryPoint) 与 b[3]&2(HasNativeCodeSlot)
│ → GetNativeCode(md) ← 直接返回已 JIT 的真实代码
│ 【★ 最理想分支:已 JIT 且无 precode 间接,entry == realCode】
│
├─⑦ b[3]&1 HasStableEntryPoint
│ → GetMethodEntryPoint(md) ← 返回方法当前入口(可能是 precode)
│ 【已 JIT 但仍带 precode 的方法(如需 backpatch 的场景)】
│
├─⑧ MayHaveNativeCode(md)==0 或 IsRemotingInterceptedViaPrestub(md)
│ → GetOrCreatePrecode(md)
│ 【★ MayHaveNativeCode 读 byte+6 的 0x20(mdcStatic)——HookInfo 手术的机制命中点。
│ 判定失败 = 该方法"不可能有 native code"→ 只能给 precode(Hook 拿不到真实函数头的根因之一)】
│
└─⑨ md 所在 chunk 的 word >= 0(a1[-8*chunkIndex-6])
→ GetTemporaryEntryPoint(md) ← 临时入口点(PrecodeFixupThunk 场景)
否则 → return NULL2.5 各被调函数语义(clr.dll 实测反编译)
| 函数 | 地址 | 功能 |
|---|---|---|
IsGenericMethodDefinition | 0x18000e19c | 是否泛型方法定义(未实例化)。是 → 直接抛 InvalidOperationException, Hook 不能挂未实例化泛型 |
IsWrapperStub | 0x180029228 | 是否 unboxing stub 或 instantiating stub(泛型包装桩)。这类方法的”入口”语义是桩本身,须先 GetOrCreatePrecode |
IsEnCAddedMethod | 0x18002ad0c | 是否 EnC(Edit and Continue)运行期新增方法 |
IsRemotingInterceptedViaVirtualDispatch | 0x18003dd64 | MayBeRemotingIntercepted && IsVtableMethod——remoting 透明代理场景的虚方法拦截 |
IsRemotingInterceptedViaPrestub | 0x18001123c | MayBeRemotingIntercepted && !IsVtableMethod && !RequiresInstArg —— 非虚方法的 remoting 拦截走 prestub |
MayBeRemotingIntercepted | 0x18003dd90 | 开头 test [rcx+6],0x20(mdcStatic)→ 静态方法直接 return 0;再排除 MT flags 0xE0000==0x20000(context-bound?)及 Object ctor/finalizer |
IsPointingToNativeCode | 0x18003df00 | 读 b[3]&1(HasStableEntryPoint)+ b[3]&2(HasNativeCodeSlot)+ Precode::IsPointingTo(entry, nativeCode)——判断 entry 是否已指向真实 JIT 代码 |
MayHaveNativeCode | 0x180010b70 | 综合分类位 + MT flags(0xF0000==0xC0000 即 COM? / 0x80000030)+ byte+6 的 0x20 位,判定”此方法可能存在 native code”。失败 → GetOrCreatePrecode(Hook 拿不到函数头的根因场景) |
IsRemotingInterceptedViaPrestub | 0x18001123c | 见上 MayBeRemotingIntercepted |
GetNativeCode | 0x1800103dc | 读 b[3]&8(HasNativeCodeSlot)→ 按 s_ClassificationSizeTable[分类] 定位代码槽指针,& ~1(bit0 是标志位)。无 native code slot 返回 0 |
GetMethodEntryPoint | 0x180006c78 | 读方法当前入口点槽:有 native code slot(b[3]&8)走 ClassificationSizeTable 定位;否则从 MethodTable slot / chunk 间接解析。返回的可能是 precode 或真实代码 |
GetTemporaryEntryPoint | 0x18001df40 | 沿 MethodDescChunk 的 TempEntryPoints 表找/分配临时入口(PrecodeFixupThunk 形态的来源之一)。GetOrCreatePrecode 内部也依赖它 |
GetOrCreatePrecode | 0x1801736dc | 有 TempEntry 就地升级为 precode(AllocMemTracker 分配),否则新分配 Precode(StubPrecode/FixupPrecode)并回填 entrypoint |
ECall::GetFCallImpl | 0x180011578 | 查 ECall 表拿 FCall 的 C++ 实现地址(Console.WriteLine、String.Compare 这类内部方法不走 JIT) |
CRemotingServices::GetNonVirtualEntryPointForVirtualMethod | 0x18003e364 | remoting 场景:虚方法被透明代理拦截时返回非虚入口 |
IBCLogger::LogMethodDescAccessStatic | (多处) | IBC(Incremental Build Compatibility)日志探针,retail 关闭,无功能影响 |
一句话总结分支语义:
- ⑤ FCall → 直达 C++ 实现
- ⑥ 已 JIT 无 precode → 直达 native code(Hook 最理想)
- ⑦ 已 JIT 有 precode → 稳定入口(可能 E8/E9 跳板,需 HookInfo 的 md+8 跟随)
- ⑧⑨ 未 JIT / 异常场景 → precode / 临时入口(Hook 不能直接 patch 这里)
2.6 byte+6 位语义(net48 x64 实测)
| 位 | 含义 | 实测佐证 |
|---|---|---|
& 7 | 分类(mcIL=0/mcFCall=1/mcPInvoke=2/mcEEImpl=3/mcArray=4/mcInstantiated=5/mcDynamic=7) | String.ToUpper=0x08(mcIL+HasNonVtableSlot) |
0x08 | mdcHasNonVtableSlot | 实例方法 0x08、静态方法 0x28 |
0x20 | mdcStatic(静态缓存位) | IsNullOrEmpty(静态)=0x28、ToUpper(实例)=0x08;MayBeRemotingIntercepted 读此位 |
0x80 | mdcRequiresInheritanceCheck | 继承/override 链方法置位;注意: lordmilko/DebugTools 的 mdcStatic=0x0080 是 16 位 flags 新定义,对 net48 x64 的 byte+6 不适用 |
CoreCLR 现代源码(method.hpp)中对应的是 mdfStatic = 0x0080(16 位 m_wFlags),布局已完全重构 —— 不能拿新源码套 net48 x64 的字节偏移。
三、md+8 语义与真实函数头获取(复现定案)
3.1 md+8(int32)= 真实 JIT 代码相对 MethodDesc 的偏移
realCode = md + ReadInt32(md+8) + 8实测(ReproMd8Probe):StartWithShellExecuteEx md+8=0x82FEE0,md+0x82FEE0+8 = 0x7FF9A2F96A50 → 55 41 56 57(真实函数头)✓
HookInfo.Initialize() 的 E9/E8 跟随逻辑就是这个:Value = ReadInt32(md+8); Value = md + Value + 8,任何入口形态下都能拿到真实代码。
3.2 PrepareMethod 前后 entry 对比(无手术对照)
| 阶段 | entry | 形态 |
|---|---|---|
| 反射刚拿到(未 JIT) | 0x7FF94A550718 | E8 93 DE 7C 5F → clr.dll+0x18E5B0(PrecodeFixupThunk) |
| GetParameters 后 | 同上 | 无变化 |
| PrepareMethod 后 | 0x7FF94A44D080 | E9 63 C9 EF FF → 0x7FF94A44D080,bytes=48 83 EC 28 真实函数头 |
普通 PrepareMethod 就能让 entry 从 E8 回填成 E9 直达真实代码 —— 手术对这批样本非必需。
3.3 E8 跳板目标 = PrecodeFixupThunk
entry+5+rel32 = clr.dll+0x18E5B0(共享 PrecodeFixupThunk,所有未 JIT 方法指向同一地址)。GetJitFunPtrStatus 的 Injected 判定(b1 == 0xe9 || 0xe8)捕捉的就是这种形态。
四、复现实验(ReproMdcStatic / ReproMd8Probe / ReproSurgeryHunter)
配套复现工程 FunctionPointerDemo(net48,x64/x86 双构建)
4.1 广撒网扫描(ReproSurgeryHunter,21128 个 BCL 方法)
| 指标 | 数值 |
|---|---|
| 扫描方法 | 21128(mscorlib/System/System.Core/System.Data/System.Web) |
| 带 0x80 | 2171 |
| 无手术 PrepareMethod 后仍 thunk | 100(51 去重后) |
| 手术+PrepareMethod 后回填 | 0(★★ 实锤 = 0) |
手术在纯 BCL 环境未复现到”有效”场景。 仍 thunk 的方法全是 async/Task/IO 类(Stream.ReadAsync、TaskAwaiter.OnCompleted、CancellationToken.Equals…),by design(IL stub/delegate,md+8 编码 token 非偏移),手术天然无效。
4.2 md+8 两分类(仍 thunk 方法)
| 类别 | md+8 特征 | HookInfo 跟随落点 | 占比 |
|---|---|---|---|
| 正值偏移 | ~0xBC4E60(~12MB) | 0x7FF9A6BB****,bytes[0]=48/57 函数头形态 ✓ | 28 |
| 负值 token | 0x9101B44D 等(高位 1) | 0x7FF9370D**** 垃圾 | 23 |
→ HookInfo 的 md+md8+8 跟随对正值 md+8 有效,负值(token 类)落垃圾。
4.3 触发条件的真实语义
nOffsetPre8 < 0xFF(有符号 int32 比较):
Process.Startmd+8=0xA2F97230 → 负数 → 恒 < 0xFF → 手术触发 ✓(此时方法已 JIT,entry=真实函数头,手术前后无变化)StartWithShellExecuteExmd+8=0x82FEE0 → 正数且 ≥ 0xFF → 不触发- 未 JIT 方法 md+8 是小 offset(如 ReproDerived.Compute md+8=0)→ 触发检查
4.4 实验环境注意
- WSL interop 会把 32 位 exe 拉成 64 位宿主 → x86 验证必须 Windows 侧
run-net48-x86.bat - x86 进程下手术分支不走(
!Is64BitProcess只裸 PrepareMethod);x86 的 md+8 是m_codeOrIL指针(大值) MEMORY_BASIC_INFORMATIONx64 布局:RegionSize必须UIntPtr且 8 字节对齐(+24),否则 VirtualQuery 全误判
五、当年为什么加 mdcStatic —— 结论(2026-09-10 已实锤,见第六章)
- 不是”PrepareMethod 通过 m_invocationFlags 判断是否执行”——PrepareMethod 无条件执行
- 不是”让 GetFunctionPointer 返回真实函数头”——x64 普通 PrepareMethod 就能回填(E8→E9)
- 已实锤的真实作用(x86 4.8.1 9181,第六章):MD+6 的分类位决定 prepare 管线分流——0x80(继承检查)方法只登记不 JIT(等首次调用激活),0x20(mdcStatic)方法在 PrepareMethod 内部直接 JIT。手术 = 把方法从保守登记路径切到 JIT 路径。x64 的对应消费点在 GetFunctionPointer 决策链(MayHaveNativeCode),x86 的消费点在 PrepareMethodDesc 管线(ExecuteGate)
- 生产取头路径:
Initialize()的md + ReadInt32(md+8) + 8偏移跟随仍是主力;手术是让 PrepareMethod 真正生效的前置条件(x86 场景必需)
六、相关文件
| 文件 | 说明 |
|---|---|
FunctionPointerDemo/ReproMdcStatic.cs | HookInfo 手术复现(8 候选对比) |
ReproMd8Probe.cs | md+8 语义探针(三阶段 PrepareMethod 前后) |
ReproSurgeryHunter.cs | BCL 广撒网扫描(21128 方法) |
ReproClassificationProbe.cs | 2026-09-10 新增:分类判定位指纹矩阵(12 值安全集,entry 前后字节对比 + B0 状态桩识别)+ SurgeryTest(手术有效性,带 fresh md8!=0 不可归因警告)+ DumpFollowup(双形态:带槽 md+md8+8 / 无槽 precode)+ StickyTest(登记粘性:首次 PM 后手术是否仍有效)。模式:CV=主域矩阵 / H/HS=子域对照 / D/E/F=Mine 维度拆解 / ST=粘性测试;COMPLUS_ZapDisable=1 可复现 IL-only dump 场景 |
| IDA clr.dll 数据库 | 22 条 [HookInfo 分析] 注释(x64)+ 9181 x86 版 30 函数命名/21 注释 |
| Hook 框架初始化组件(HookInfo.cs) | PrepareOldMethodForHook 位手术与 md+md8+8 跟随逻辑(L97-247) |
七、改造建议(上轮可行性分析的落地)
| 环节 | 属性方案可行性 | 建议 |
|---|---|---|
| 读 INITIALIZED 位判断”反射缓存已暖” | ✅ | GetProperty("InvocationFlags") 优先,m_invocationFlags 字段回退,最后 +6 字节 |
| 写 MDC 分类位(0x20/0x80) | ❌ | 保留 Marshal.WriteByte(+6) —— native 侧直接消费的内存位,托管层无等价入口 |
| 取真实函数头 | — | 已有 md+md8+8 跟随,无需改动 |
八、x86 4.8.1(9181)实锤:手术如何让 PrepareMethod 真 JIT
本章环境:Chanjet.TP.WebServer.FastCgi.exe(Nginx 反代 32 位 FastCGI 自宿主),CLR 4.8.9181.0 x86,System.dll 以 per-domain IL 副本加载(非 NGEN)。IDA 逆向对象:该环境 clr.dll(已标注 13 个函数)。
8.1 一个前置问题:为什么 v4.0.30319 的 clr.dll 下载不到 PDB
msdl(https://msdl.microsoft.com/download/symbols)只托管微软公开发布的二进制对应 PDB,索引以 PE 内嵌的 GUID+age 为准。下载不到只有三种可能:
- 非公开构建:内部重编/实验版/非 shipping 版本不在公开符号服务上(
chk/retail内部号); - 公开但有条件:Win11 24H2/25H2 起 .NET Framework servicing 的符号改为随 Windows 更新分发,msdl 镜像滞后或只对部分 build 发布(实测本机 9337 GUID 直 404);
- GUID 输错:PDB 目录里取的 GUID 大小写/age 拼接错(正确格式
{32位大写HEX}{age十进制})。
排查口诀:!lmi clr(windbg,看 Symbol Type 与 PDB 路径)→ !sym noisy + .reload /f clr.dll(看 sym srv 实际请求 URL 与响应码)。本机 9337 即第 2 类(404);若 dump 里的 9181 能被 windbg 解析,说明 9181 在 msdl 有(或符号来自本地缓存/内部符号服务器)。
8.2 实测 A/B 对比(FastCGI 环境,System.dll IL 副本的 StartWithShellExecuteEx)
| 字段 | B:不改(原 0x80) | A:改 0x20 后 PrepareMethod |
|---|---|---|
| MD+3(prepare 状态机) | 0x08 → 0x0B(登记完成) | 0x08 → 0x39(JIT 完成) |
| MD+7 | 0x00 → 0x20(已登记标志) | 0x00 → 0x20 |
| md+8 | 保持 0(无代码) | 0 → 0xfae3096c(相对偏移,指向 code_real_addr-8) |
!name2ee IsJitted | no(CodeAddr=ffffffff) | yes |
| CodeAddr 内存 | —(无代码) | 55 8B EC 57 56 53 83 EC 30(真实函数头) |
8.3 x86 9181 PrepareMethod 完整调用流
分流本质:MD+6 的 classification 字节决定方法在 gate 链中的命运。0x80(mdcRequiresInheritanceCheck,方法在 override 链上)→ CLR 无法保证继承解析完成前 JIT 的代码不作废 → prepare 管线对它只挂账(+3 停在 0x0B、md+8 保持 0),等首次真实调用走常规 PreStubWorker 激活。0x20(mdcStatic,「无继承解析需求」)→ gate 全放行 → ExecuteGate 立即 JIT 并回填。
judge/gate 的消费位(指令级):judge1 = [MD+3]&4==0 + 协同谓词;judge2 与 ClsCmpGate = [MD+6]&7(低 3 位分类:0=IL)。0x20/0x80 高位不经这些点——分流发生在 gate 群内部的分散检查与 ExecuteGate 的 vtable 目标选择(详见 IDA 标注:PrepareMethodDesc 及其调用链 8 处注释)。
8.4 与 9337 的 servicing 差异(行为不同:9181 拦 / 9337 不拦)
本节为 2026-09-10 深夜修正版:早前发布的「9337 判定位漂移至 0x08」是错误归因——当时用「PM 后 md+8 是否变化(槽分配)」当 JIT 判据,实际上 md+8 变化只代表槽分配,JIT 与否要看 md+md8+8 是否出现真实函数头。
用 COMPLUS_ZapDisable=1(禁 NGEN,强制 IL-only,fresh 状态 md+8=0、+3=0x0B、b6=0x80 与 dump 完全同态)重跑后的修正结论:
| 场景 | 9181(A/B dump 实测) | 9337(ZapDisable 实测) |
|---|---|---|
| 0x80 不手术 + PrepareMethod | 不 JIT(+3=0x0B、md+8 恒 0、CodeAddr 不可用) | JIT(+3=0x3B、md+8 回填、md+md8+8=55-8B-EC-57) |
| 0x20 手术 + PrepareMethod | JIT(+3=0x39、md+8 回填) | JIT |
结论修正为:9181 的 PrepareMethodDesc 管线(Finalize 执行循环 gate 链)确实拦截 0x80 方法;9337 的同类管线不拦(0x80 也直接 JIT)。servicing 差异是「拦不拦」的行为差异,不是「判定位漂移」。9337 版本的 JIT 触发点不在我逆向到的 PrepareMethodDesc 主体内(该函数只有登记二分 + SetFlag6),具体实现在入口函数的前后段,待后续逆向。
另:9-4 旧文的 x64 结论(PrepareMethod 无条件 ForceJIT)与 9337 x86 行为同构——9337 代两个架构行为一致,9181 x86 的拦截是旧 servicing 特有行为。这也解释了 9-4 时「x64 手术在 BCL 扫描 0 命中」——x64 上不手术也 JIT,手术自然测不出差异。
附带方法论教训(写给自己):跨进程验证 JIT 状态,唯一可信判据是 md + ReadInt32(md+8) + 8 处的字节内容;entry 指针是 PM 前缓存的 precode 地址,JIT 回填不反映在 precode 体,不能作为判据。
8.5 生产方案(x86 HookInfo 修复)
★ 顺序约束(关鍵):手术必须发生在首次 PrepareMethod 之前——首次 PM 会把方法登记进 module 注册表,之后再 PM 走「已登记」分支(SetFlag6 + Register 直接返回),不会 JIT。9-4 版 HookInfo 的「先 PM 再手术再 PM」结构在 9181 上无效就是这个原因。
byte b6 = Marshal.ReadByte(md + 6);
if ((b6 & 0x80) != 0)
{
Marshal.WriteByte(md + 6, (byte)((b6 & 0x0F) | 0x20)); // 清 0x80,设 mdcStatic —— 与 x64 手术同公式
try { RuntimeHelpers.PrepareMethod(handle); /* 然后安装 hook */ }
finally { Marshal.WriteByte(md + 6, b6); } // hook 装完立即还原(instance 伪装 static 窗口期最小化)
}
else
{
RuntimeHelpers.PrepareMethod(handle);
}配套校验(防 servicing 静默失效):PrepareMethod 后读 +3——0x0B→0x39(bit0 置位)且 md+md8+8 处出现 55 8B 函数头即成功;否则 fallback(precode patch / 激活时 hook)。注:9337+ 上不手术也 JIT(8.4),手术是无害冗余——两代 servicing 下这套代码都工作。
8.6 IDA 落库清单(9181 clr,13 函数 + 8 注释)
| 地址 | 名称 | 说明 |
|---|---|---|
| 0x1043D380 | ReflectionInvocation__PrepareMethod | 202 条,特征码命中 |
| 0x1007D996 | PrepareMethodDesc | ecx=pMD, edx, 5栈参 retn 14h |
| 0x100DC451 | PrepMD_IsRegistered_BinSearch | 二分 [modstruct+368] 表 |
| 0x1001EFE6 | PrepMD_GetModuleStruct | |
| 0x100937FC / 0x100DC6A9 | PrepMD_Unreg_InitRecord / FillAndEnqueue | record 0x10 字节初始化 + 字段填充 |
| 0x1009387B | PrepMD_Unreg_Finalize | 队列执行循环 |
| 0x10038AD7 / 0x100D6B06 / 0x101838EC | ClsCmpGate / ExecuteGate / StateMachine | gate 链与执行动作 |
| 0x1015A768 | 泛型哈希 gate | 29 桶,仅泛型路径(edx≠0) |