首页
社区
课程
招聘
[原创] 让Detours支持ARM64EC
发表于: 3小时前 74

[原创] 让Detours支持ARM64EC

3小时前
74

背景

我看过Detours、MinHook、MHook等API Hook库后,认为稳定、兼容的API Hook库之位仍有空缺,于是有了KNSoft.SlimDetours,基于Detours改进而来,重要的稳定性、兼容性提升/修复在 [原创]浅探内联挂钩的水有多深 一文里有详细介绍,且我在持续维护。现在它虽然Star不多,但已被 ExplorerPatcher (33.5k⭐)、windhawk (8.6k⭐)这样的项目使用,Release Notes也提到换用SlimDetours后为它们提高了稳定性。

目前这些API Hook库在ARM64系统中存在兼容问题,最显著的是x64构建的Hook模块挂钩系统COM函数后,执行时会崩溃,如 Detours Issue: #292#355。根因是这些从COM对象的虚表里获取的函数地址不是FFS入口,而直接是ARM64EC代码(ARM64指令),Hook模块按自身构建向其写入x64指令,执行时即引发 STATUS_ILLEGAL_INSTRUCTION 异常。

即使后来Detours加入了ARM64EC构建目标(PR #338)也并不能解决此问题,目前正在打开状态的 PR #385 也只是为x64构建拒绝这样的场景(用返回错误取代崩溃),ARM64EC构建仍不兼容。x64构建要兼容ARM64是难了,但ARM64EC本身就为了提供x64与ARM64指令的互操作性,理论上能成。我在我的KNSoft.SlimDetours上实现了ARM64EC兼容,下文是随着实现一起写的完整技术文档内容(附库上文档链接:实现ARM64EC兼容性)。


实现ARM64EC兼容性

ARM64EC兼容问题与其它库的兼容性

Windows on Arm (WoA) 通过 ARM64EC ABI 实现了x64指令与ARM64指令的互操作性,使x64与ARM64EC进程可以同时加载这两种二进制模块:x64模块指令在模拟环境中运行,ARM64EC模块则用ARM64指令原生运行。

ARM64EC 提供 FFS (快进序列,x64函数) 来兼容强依赖于体系结构的内联挂钩场景,但目标地址不经过 FFS 直接指向ARM64EC代码时(如一些系统ARM64EC模块提供的COM接口虚表项),目前常见挂钩库在x64/ARM64EC构建下仍将目标指令当作x64处理,写入x64跳转并会在执行时触发 STATUS_ILLEGAL_INSTRUCTION 异常,见 Detours Issue: #292 #355

SlimDetours的实现

完整实现参考 KNSoft.SlimDetours #30,以下是其实现要点。

检测目标代码类型

RtlIsEcCode用于检测目标代码是否为ARM64仿真兼容:

  • 虽然官方文档写它由 kernel32.lib 导入,但它实际实现位于 ntdll.dll 中的同名导出函数。
  • 虽然在Windows SDK中它的声明被 #if defined(_M_ARM64EC) 守卫,但x64构建下的二进制仍可调用它,ARM64与x64 Win11中 ntdll.dll 均导出了此函数。
  • 它的内部读取了 PEB::EcCodeBitMap,实现可参考 KNSoft.NDK: [NT:RTL] Implement _Inline_RtlIsEcCode
__inline
BOOLEAN
NTAPI
_Inline_RtlIsEcCode(
    _In_ DWORD64 CodePointer)
{
    PCUCHAR EcCodeBitMap;
    DWORD64 PageIndex;
    UCHAR PageBitMask;

    if (SharedUserData->NativeProcessorArchitecture != PROCESSOR_ARCHITECTURE_ARM64 ||
        CodePointer < (DWORD64)MM_LOWEST_USER_ADDRESS)
    {
        return FALSE;
    }
    EcCodeBitMap = (PCUCHAR)NtCurrentPeb()->EcCodeBitMap;
    if (EcCodeBitMap == NULL)
    {
        return FALSE;
    }

    PageIndex = CodePointer >> PAGE_SHIFT;
    PageBitMask = (UCHAR)(1u << (PageIndex % CHAR_BIT));
    return BooleanFlagOn(EcCodeBitMap[PageIndex / CHAR_BIT], PageBitMask);
}

处理两种指令集

为使ARM64EC构建同时处理x64与ARM64代码:

  • 指令处理函数默认与编译目标平台匹配;处理ARM64指令的函数增加 _arm64 后缀,如detour_skip_jmp_arm64detour_copy_instruction_arm64detour_gen_jmp_immediate_arm64等,并在ARM64EC构建下参与编译。
  • 增加 detour_skip_jmp_arm64ec,按目标代码类型选择 detour_skip_jmpdetour_skip_jmp_arm64
  • SlimDetoursCopyInstructionSlimDetoursCodeFromPointer 则在事务外自行检测代码类型。

Trampoline的兼容

ARM64EC构建下,Trampoline要同时兼容x64和ARM64EC架构:

  • ARM64EC Trampoline由NtAllocateVirtualMemoryExMEM_EXTENDED_PARAMETER_EC_CODE参数分配,x64 Trampoline沿用普通可执行内存。
  • 目标补丁与Trampoline的跳回代码按代码类型生成:ARM64EC目标使用ARM64间接跳转,x64目标沿用x64跳转序列。

x64构建为何无法为ARM64EC目标函数生成通用的转接桥?

x64 Hook模块挂钩ARM64EC目标时,调用链需要两次ABI转换:

  • ARM64EC目标跳转到x64 Detour,需要Exit Thunk。
  • x64 Detour通过Trampoline调用原函数,需要Entry Thunk。

Entry/Exit Thunks的参数与返回值转换取决于函数签名;x64编译器不会生成ARM64EC Thunk,我们也无法仅凭函数地址生成它们。

ARM64EC Hook模块挂钩x64目标时同样存在两次ABI转换,但签名来自Hook代码:编译器根据ARM64EC Detour的签名生成Entry Thunk,并在通过有类型的原函数指针调用x64 Trampoline时生成Exit Thunk。挂钩ARM64EC目标时,目标、Detour与Trampoline均使用ARM64EC ABI,无需转换。

故x64构建Hook前要检测原始和解析后的目标地址,遇到ARM64EC代码即返回HRESULT_FROM_NT(STATUS_NOT_SUPPORTED)。挂钩此类目标应使用ARM64EC构建的Hook模块。若Windows未来使这些目标入口指向 FFS,x64 Hook模块才能更好地兼容这类目标。

兼容性验证

以下组合均使用COMHook测试逻辑在ARM64真机上挂钩IStream::Read。进程架构由EXE决定,Hook模块架构单独列出。

进程 Hook模块 目标代码 Microsoft Detours SlimDetours
x64 x64 ARM64EC 崩溃STATUS_ILLEGAL_INSTRUCTION 报错STATUS_NOT_SUPPORTED
x64 ARM64EC ARM64EC 崩溃STATUS_ILLEGAL_INSTRUCTION 通过
ARM64EC x64 ARM64EC 崩溃STATUS_ILLEGAL_INSTRUCTION 报错STATUS_NOT_SUPPORTED
ARM64EC ARM64EC ARM64EC 崩溃STATUS_ILLEGAL_INSTRUCTION 通过

参考资料



本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 (CC BY-NC-SA 4.0) 进行许可。


Ratin <ratin@knsoft.org>
中国国家认证系统架构设计师
ReactOS贡献者


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回