一种基于 Rosetta2 二进制翻译的 Syscall 拦截方法
目录
引言
随着 Wine 等兼容技术的持续发展,大量 Windows 游戏与应用程序已能流畅运行于 Linux 平台。然而,将这一生态迁移至 MacOS 时仍面临显著障碍。
由于 MacOS 内核缺失 Seccomp 机制,Wine 无法对进程的系统调用进行内核级拦截。这导致那些绕过 Windows API 直接执行 Syscall 指令的程序在 MacOS 上触发错误的系统调用号,最终引发崩溃或行为异常。
针对这一平台差异,本文提出了一种创新的解决方案,即利用 Apple Silicon 架构下的 Rosetta2 二进制翻译器,在指令转换层面实现对 x86 系统调用的捕获与重定向,从而在用户态构建起类 Seccomp 的拦截能力。
技术路线
传统方案
在 Linux 平台上,Wine 依赖 BPF 在内核态设立"陷阱门"。当进程执行 Syscall 指令时,内核先检查过滤器规则,若为 Windows 调用则返回用户态交由 Wine 模拟。
然而,MacOS 的 XNU 内核先天缺失 Seccomp 机制,且出于安全策略禁止第三方内核扩展介入系统调用路径。
Wine 自身的用户态 Hook 仅能覆盖通过 ntdll.dll 导出的接口,对于直接内联汇编执行 Syscall 的指令流无能为力。这类指令会直接陷入内核,触发错误的系统调用号,导致程序行为出现异常。
Rosetta2 拦截
Apple Silicon 架构下的 Rosetta2 作为 x86_64 到 ARM64 的动态二进制翻译器,强制所有 x86 指令经过其翻译。
这一架构特性创造了独特的干预窗口:无论原始 x86 代码如何编写,都必须经过 Rosetta2 的解码、翻译,最终生成 ARM64 代码才能执行。
并且 Rosetta2 在用户态运行,其内部数据结构、乃至生成的本地代码均处于用户态可访问的内存空间。
这为我们提供了在不触碰内核的前提下,修改其内部流程以拦截 Syscall。
Rosetta2 二进制翻译原理
Rosetta2 其大致分为三个部分包括 oahd / oahd-helper、runtime、libRosettaAot / libRosettaRuntime
oahd / oahd-helper
其中oahd为 Rosetta2 守护进程,其负责管理与创建 AOT 文件。
在启动 x86 程序时,翻译引擎会通过 XPC 向oahd请求 AOT 文件,此时oahd会检查/var/db/oah/*目录下是否存在对应程序的 AOT 文件,若 AOT 不存在,其会调用oahd-helper并传入文件句柄以创建对应的 AOT 文件。
oahd-helper中 AOT 流程大致分为以下几个步骤
读取 Mach-O 文件 -> 判断文件类型 -> 解析区段等信息 -> 转换为rosetta::librosetta_aot::ModuleResult -> rosetta::librosetta_aot::translate识别函数翻译并构建映射 -> 获取翻译后的 Data -> 应用各种 Fixup (如 runtime routines ) -> 构建新的 Mach-O 文件 -> 映射符号 -> 签名输出
其对程序进行了静态分析并调用了libRosettaAot进行 AOT
runtime
runtime为 Rosetta2 的运行时代码,在启动 x86 程序时系统首先加载runtime,再有runtime向oahd请求 AOT 文件。
runtime在请求到 AOT 文件后会解析验证 AOT 文件与映射信息并将其加载到内存之中,并移交控制权到 AOT 代码。
runtime 也大致由三个部分构成,分别是AOT Loader、JIT Engine、Runtime Routine。
其中AOT Loader用于验证签名并加载之前所属的 AOT 文件,JIT Engine则用于动态解码翻译某些之前静态分析所漏掉的函数,如:动态链接库中的函数、ShellCode、由于不透明谓词未能分析到的函数。
Runtime Routine则用于为翻译后的代码提供功能支持,其中 Syscall 其会在翻译过程中被转换为call indirect_jmp_dyld_stub类似的代码,Syscall 的功能将由Runtime Routine提供,其将 x86 的调用转换为符合当前系统的调用以模拟 Syscall。
此外JIT Engine也由Runtime Routine触发,其辅助函数中包含indirect_jmp_dyld_stub等触发函数,在第一次调用某个动态链接函数时,会通过此辅助函数获取欲调用函数翻译后的地址。
此函数调用后runtime将会从代码映射红黑树中搜索欲调用的函数是否被翻译(AOT 中文件中的映射也将被加载),若目标函数未被翻译(即未被 AOT 或首次调用),runtime则会调用libRosettaRuntime的rosetta::runtime::library::ir_create函数进行翻译(与 AOT 不同这里使用的另一个翻译函数,而且这里的libRosettaRuntime也是专门经过调整的翻译引擎),翻译后指令将被JIT Engine加载到内存之中。
JIT Engine辅助函数(Runtime Routine)大致流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | uint64_t ThreadContext::cache_slot_miss(Context context, uint64_t x86_address, uint64_t stubs_sh_address) {
Translator translator = context->translator;
uint32_t type = context->type;
uint64_t arm_addrress;
if (find_translation_in_tree_x86(translator, x86_address, &arm_addrress, 0, stubs_sh_address, type)) {
return arm_addrress;
} else {
return jit_translation_for_x86_address(translator, x86_address, 0, 0, stubs_sh_address, type);
}
}
uint64_t indirect_jmp_dyld_stub@<X0>(
uint64_t context@<x18>,
uint64_t x86_address@<x22>,
uint64_t stubs_sh_address@<x23>
) {
context->save_context();
context->cache_slot_miss(context, x86_address, stubs_sh_address);
runtime_restore()
}
|
Syscall 辅助函数(Runtime Routine)大致流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | uint64_t __usercall syscall_handler@<X0>(
uint32_t svc_code@<W0>,
uint64_t rcx@<X1>,
uint64_t rdx@<X2>,
uint64_t rbx@<X3>,
uint64_t rsp@<X4>,
uint64_t rbp@<X5>,
uint64_t rsi@<X6>,
uint64_t rdi@<X7>,
uint64_t r8@<X8>) {
switch(svc_code) {
}
}
|
目前已知的辅助函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 说明:
辅助函数偏移量存储于`runtime_pointers`节,顺序与名字一致。
在运行时`runtime`会使用`register_runtime_routine_offsets`函数将辅助函数注册到翻译引擎。
随后翻译引擎将会通过之前记录的 Fixup 地址填充实际的辅助函数地址。
"branch_slot"
"indirect_jmp" // JIT 间接跳转
"indirect_jmp_dyld_stub" // JIT 动态链接函数
"far_jmp"
"indirect_call" // JIT 间接调用
"far_call"
"return_stack_miss"
"far_ret"
"far_ret_single_step"
"syscall_handler" // Syscall 转换
"toggle_single_step"
"cpuid" // 模拟 Cpuid
"wide_udiv_64"
"wide_sdiv_64"
"pcmpestri"
"pcmpestrm"
"pcmpistri"
"pcmpistrm"
"mov_segment"
"mov_segment_and_reg"
"get_cpu_number"
"get_tls_base" // 模拟 TLS
"read_timer_nanoseconds"
"read_mxcsr" // x87 系列指令模拟
"mxcsr_to_fpcr_fpsr"
"f2xm1"
"fabs"
"fadd_f32"
"fadd_f64"
"fadd_ST"
"fbld"
"fbstp"
"fchs"
"fcmov"
"fcom_f32"
"fcom_f64"
"fcom_ST"
"fcomi"
"fcos"
"fdecstp"
"fdiv_f32"
"fdiv_f64"
"fdiv_ST"
"fdivr_f32"
"fdivr_f64"
"fdivr_ST"
"fiadd"
"ficom"
"fidiv"
"fidivr"
"fild"
"fimul"
"fincstp"
"fist_i16"
"fist_i32"
"fist_i64"
"fistt_i16"
"fistt_i32"
"fistt_i64"
"fisub"
"fisubr"
"fld_fp32"
"fld_fp64"
"fld_fp80"
"fld_constant"
"fld_STi"
"fmul_f32"
"fmul_f64"
"fmul_ST"
"fpatan"
"fprem"
"fprem1"
"fptan"
"frndint"
"fscale"
"fsin"
"fsincos"
"fsqrt"
"fst_fp32"
"fst_fp64"
"fst_fp80"
"fst_STi"
"fsub_f32"
"fsub_f64"
"fsub_ST"
"fsubr_f32"
"fsubr_f64"
"fsubr_ST"
"fucom"
"fucomi"
"fxam"
"fxch"
"fxtract"
"fyl2x"
"fyl2xp1"
"ffree"
"load_segment_limit"
"rdrand"
"xrstor"
"xsave"
|
libRosettaAot / libRosettaRuntime
libRosettaAot / libRosettaRuntime 为核心翻译引擎,其中包含了控制流提取,基本块/指令翻译,映射/重定位等多个功能。
注: libRosettaRuntime是经过严格地址布局过的运行时专用版翻译引擎,由于运行时环境不包含dyld无法动态链接所以需要单独制作一个运行时特化版翻译引擎。
根据其内部残留字符串可知其支持 x86 的下列指令集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Translator.cpp 普通 x86 指令集
TranslatorAes.cpp x86 aes 加速指令集
TranslatorArithmetic.cpp
TranslatorBitTest.cpp
TranslatorFlags.cpp
TranslatorFlagsSaveRestore.cpp
TranslatorHelpers.cpp
TranslatorMulDiv.cpp
TranslatorShifts.cpp
TranslatorSseConversions.cpp x86 sse 加速指令集
TranslatorSseFpComparisons.cpp
TranslatorSseMovesShuffles.cpp
TranslatorSseShifts.cpp
TranslatorSseStringOps.cpp
TranslatorStringOps.cpp
TranslatorX87.cpp x87 浮点数指令集
|
其内部寄存器映射(来自ProjectChampollion):
| x86_64 |
arm64 |
| RAX |
x0 |
| RCX |
x1 |
| RDX |
x2 |
| RBX |
x3 |
| RSP |
x4 |
| RBP |
x5 |
| RSI |
x6 |
| RDI |
x7 |
| R8 |
x8 |
| R9 |
x9 |
| R10 |
x10 |
| R11 |
x11 |
| R12 |
x12 |
| R13 |
x13 |
| R14 |
x14 |
| R15 |
x15 |
| XMM0~15 |
q0~15 |
拦截思路
基于以上翻译原理我们可以使用以下思路对特定地址或特定调用号的 Syscall 进行拦截。
首先我们将通过特征码搜索syscall_handler与ThreadContext::cache_slot_miss,获取到函数地址后分别在两个函数中放置 Hook。
此后通过cache_slot_miss中的 Hook 恢复 ARM 与 x86 地址的映射关系,并将欲拦截函数放入一个全局的黑名单。
最后在syscall_handler中通过返回地址识别欲拦截函数并通过信号将 Context 发送至 Wine 层。
注:MacOS 对 Rosetta2 代码进行了代码签名以及修改检查等保护,可使用 Debugger 的思路来绕过 MacOS 对 Rosetta2 层代码的保护,具体方法请参考rosettax87
鸣谢
ProjectChampollion
rosettax87
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 6天前
被EX呵呵编辑
,原因: 修改正确的函数名