风控对抗与设备指纹伪装

从内核 sysctl 到设备树、硬件包、网络接口与传感器的高保真伪装方案

风控对抗与设备指纹伪装

现代风控 SDK 判定「这台设备是不是虚拟机 / 模拟器 / 云真机」的探针遍布全栈:内核 sysctl、Metal / GPU 驱动、网络接口、摄像头、运动传感器、文件系统路径、launchd 服务与开放端口。任何一处露出破绽,整套伪装即告失效。

vphone 在固件补丁与 CFW 安装两个阶段内置了一整套高保真伪装方案,覆盖四个层面:

层面 典型探针 vphone 对策
内核与环境 kern.hv_vmm_presentkern.versionhw.machine 等几十个 sysctl 读侧 caller-aware 伪装 + 版本串改写
硬件与驱动 加载镜像列表扫描 GPU 驱动名、MTLDevice.namegetifaddrs()、相机枚举 GPU 驱动包改名、蜂窝网卡伪造、虚拟摄像头真机化
传感器 加速度计 / 陀螺仪 / 气压计可用性与数据死值检测 高保真噪声注入(漂移 + 白噪声)
文件系统与服务 越狱工具路径扫描、launchd.plist 内容、端口扫描 隐蔽路径 + 伪装命名 + 默认零 TCP 暴露

本文按层面逐一拆解各项伪装的原理与实现。各项能力在不同变体(dev / jb / exp)中的归属见文末「变体归属」。

① 内核与环境伪装

内核层伪装以 devOnly 补丁组为核心(devjbexp 变体均生效),核心设计原则是 caller-aware(按调用者区分):系统组件(launchd、root 守护进程、白名单进程)读真实值以保住系统稳定性,普通 App 读伪装值以骗过探针——一刀切的伪造会导致 launchd panic、USB 激活链路中断等启动回归。

kern.hv_vmm_present:虚拟化标志屏蔽

sysctlbyname("kern.hv_vmm_present") 是最经典的 VM 探针。vphone 用一组共五个协同补丁做「按调用者屏蔽」:

  • 内核 sysctl 读口 hook(code cave v22):静态 48 项进程名白名单 + pid ≤ 1(内核 / launchd)+ uid ≤ 500(root / 系统守护)放行真实值;其余调用者(即普通 App)一律读到 0,并经 os_log_printf 记录 pid/uid/comm 便于审计。
  • AMFI 内部查询旁路:AMFI kext 自己也会查 kern.hv_vmm_present 决定 hasAVP 分支——若放行 0 会走 PMGR latch 并 panic,故 NOP 掉该分支保证 AMFI 安全路径。
  • Sandbox isVM 标志修正:Sandbox kext 依同一 sysctl 设置 launch constraint 用的 is-vm 标志,NOP 后标志恒置,避免带 is-vm 启动约束的平台二进制被 SIGTRAP。
  • 两处 CS_CHECK_EXPIRATION SIGTRAP 门_exec_spawnattr_getmacpolicyinfo_exec_mach_imgact):伪装改变了时序敏感的 csflags,把这两个可能误杀 watchdogd 的信号门改为无条件跳过,否则会引发启动期 kernel panic。

kern.bootargs:对普通 App 返回 EPERM

真机上 App 沙盒读 kern.bootargs 得到的是 EPERM(OID 存在但被沙盒拒绝),而 root / launchd 正常读取。VM 里 userspace 以 root 运行,直接全局拒绝会打断 launchd 启动与 USB 配对链。vphone 复用 hv_vmm_present 同款 caller-aware 框架(48 项白名单 + uid/pid 门):普通 App 读到 EPERM,系统守护读真实值——与真机的错误码语义完全一致。

kern.hv_support:错误码本身也是指纹

真机读 kern.hv_support 得 EPERM(节点存在、沙盒拒绝);未打补丁的研究内核没有注册这个 OID,返回 ENOENT——「查无此节点」反而是云真机检测的银弹。vphone 在 sysctl 名字解析层(name2oid handler 与 sys_sysctlbyname 直呼路径,两条都 hook)把仅针对 kern.hv_support 的 ENOENT 翻译成 EPERM,其他名字原样放行。注意这里刻意不「返回一个值」——真机从不暴露值,成功读取本身就会露馅。

hw.* 与时间类 sysctl:真实 iPhone 映射

sysctl 真实值(VM) 伪装值 说明
hw.machine iPhone99,11(研究平台) iPhone17,3 code cave 写栈缓冲区,读侧恒返回
hw.cpufamily 0(无真实 CPU family) 0x6F5129AC iPhone17,3 真机同值
kern.boottime 真实启动时刻 真实值 − 30 天 uptime 更自然,抹掉「刚开机的全新设备」信号;tv_usec 保留
security.mac.amfi.developer_mode_status 恒 1(开发者模式 ON) App 读 0、系统守护读真实值 与可读的 lsd 标志组合会暴露「开发者模式」特征;返回值而非 EPERM(真机 App 能读到值)

hw.ncpu不用补丁硬改:直接以 cpus=6 boot-arg 在源级限制 XNU CPU 拓扑,让 hw.ncpuhw.logicalcpuhw.activecpu 与 commpage 的 activeProcessorCount 天然一致地等于 6——旧的 sysctl 硬改只覆盖 hw.ncpu,active 数仍是宿主 8,active > total 反而触发检测。

kern.version:构建标识改写

研究内核的 kern.versionRELEASE_ARM64_VRESEARCH1 结尾——一次 sysctl 读取即判死刑;RELEASE_ARM64_ 后缀不以 T 开头是第二重信号。补丁把 kernelcache 中所有 RELEASE_ARM64_VRESEARCH1(24 字节,含 version[] 全局与 SCCS ident 两处)等长替换RELEASE_ARM64_T8140(19 字节 + 5 个 NUL 填充)——不移动任何字节、不漂移相邻符号,kern.version 读回 .../RELEASE_ARM64_T8140,即 iPhone17,3(A18 = T8140)的真机构建标识。版本串其余部分(xnu revision、Darwin 版本、构建日期)保持原样。

DeviceTree 身份属性

dev 变体的 CFW 阶段对 preboot 卷的 devicetree.img4 做身份改写(patch-devicetree --full-identity):root/model → iPhone17,3root/target-type → D47compatible 首项翻成 D47AP(保留 VPHONE600AP 在列表中,IOKit 平台绑定不回归,而 userland 查 hw.model 看到的是 D47AP)、target-sub-type / fdr-product-type / unique-model / arm-iot8140-io / H17 SoC 代号等全套属性对齐真机。这些属性在 restore 阶段是致命的(会与 BuildManifest 签名身份冲突),所以必须在 restore 完成后、首次启动前经 host-mount 离线改写。配套的 patch-build-version 把 SystemVersion 伪装为指定 build(默认 23F77)。

② 硬件与驱动伪装

GPU 驱动包改名:AppleParavirtGPUMetalIOGPUFamily → AppleGFXHW

检测方扫描 _dyld_get_image_name() 加载镜像列表,查找 AppleParavirtGPUMetalIOGPUFamily 这个一眼假的驱动名。vphone 做的是整套改名

  • .bundle 目录、内部可执行文件、Info.plistCFBundleExecutable / CFBundleName / CFBundleIdentifier(→ com.apple.driver.AppleGFXHW)全部改为 AppleGFXHWNSPrincipalClass 刻意不改——它是 IOKit 匹配用的 IOClass,改了驱动就加载不了。
  • Metal 设备名MTLDevice.name 原本返回 Apple Paravirtual device GPU。补丁把驱动二进制内的产品名三件套改为 vendorName "Apple" + familyName "A18" + productName "",拼出真机输出 Apple A18 GPU;改动页经代码签名 re-attest 后通过 TXM 校验。
  • kernelcache 侧配套:kernelcache __PRELINK_INFO(未压缩明文 XML)里的 MetalPluginName 是 Metal 插件加载器查找 bundle 的依据,必须与文件系统上的新名一致。替换为 <string>AppleGFXHW</string> 后用 23 个尾随空格补齐到原 51 字节——空格是合法 XML 空白,字节等长意味着整个 img4 无需解压重打包。

入口:make gpu_patch--auto 对已装系统 host-mount 改名,或 host-only 模式改 CFW 输入 tar)+ make kernel_patch(MetalPluginName)。

pdp_ip0~4:让 App 感知到真实蜂窝网络

无基带的 VM 里 getifaddrs() 看不到任何蜂窝接口,云真机检测一查一个准。内核补丁 hook _bsd_init 网络初始化序列中的 _loopattach 调用点,在其后注册 五个 pdp_ip0~pdp_ip4 虚假 ifnet(类型 IFT_CELLULAR、family IFNET_FAMILY_CELLULAR)——App 枚举网络接口时看到的是与真机一致的蜂窝接口族。

配套的用户态补丁(DSC):CTTelephonyNetworkInfo.serviceSubscriberCellularProviders 在无 SIM 描述符时永远为空,补丁将其替换为返回 2 个 CTCarrier 的 mock(对齐国行双物理 SIM 机型预期):serviceID 为运行时构造的 16 位十六进制真机格式,carrierName --、MCC/MNC 65535,能通过 allSlotsBlank 检查;另有无线接入技术(radio access technology)补丁补齐网络制式上报。

虚拟摄像头:伪装成 iPhone 真实镜头组

宿主侧可以把测试图案或视频文件推给 guest 当相机画面(vsock 1338 推帧),但风控 SDK 更在意的是摄像头枚举出来的形态。注入 cameracapturedlibvcamcaptured 注册 5 个合成 FigCaptureSource,与 iPhone17,3(iPhone 16 标准版)真机 dump 完全对齐:

合成设备 deviceType 位置
后置广角相机 WideAngle back
前置广角相机 WideAngle front
后置超广角相机 UltraWide back
后置双广角融合镜头 DualWide(虚拟融合设备) back
前置原深感摄像头 TrueDepth front

所有属性按真机 dump 复刻:uniqueID == modelIDcom.apple.avfoundation.avcapturedevice.built-in_video:N 下划线格式)、本地化设备名(「后置相机」「后置超广角相机」……)、Manufacturer = "Apple Inc."、Flash / Torch 能力位。基于 localizedName / modelID 关键字扫描、设备数量检查的相机探测,看到的是一台标准 iPhone 16。配套 DSC 补丁把 +[AVCaptureDevice authorizationStatusForMediaType:] 恒置 Authorized,探测相机授权的进程不再卡在授权检查。

③ 传感器高真仿真

风控 SDK 对传感器的探针通常两级:可用性isAccelerometerAvailable 等)与数据质量(连续采样求和的标准差——真机传感器永远有热噪声,恒 0 或恒定值即为模拟器铁证)。

vphone 的 CoreMotion DSC 补丁一次覆盖四类传感器(陀螺仪 / 加速度计 / 磁力计 / 气压计)外加融合运动(DeviceMotion):

  • 可用性isGyroAvailableisAccelerometerAvailableisMagnetometerAvailableisRelativeAltitudeAvailable(及 CMPedometer 一族)直接改写 IMP 为 MOV W0,#1; RET
  • 数据通路startGyroUpdatesToQueue: 等 API 整函数替换为 dispatch timer——每个 tick 构造 CMGyroData / CMAccelerometerData / CMMagnetometerData / CMAltitudeData 对象并同步回调 handler。刻意绕开 CoreMotion 原生 MotionThread 路径:原生路径无硬件事件源时会立即推一个全 0 初始回调,检测代码在第一个回调就 stop 并判定模拟器——替换后第一个回调即是带噪声的真实感数据。
  • 高保真噪声:数据 = 三角波随机游走漂移(模拟人手持手机的自然缓慢晃动)+ 逐轴白噪声,而非围绕固定基值的纯白噪声。白噪声发生器参数(K=178500)实测标定 std_sum ≈ 5.37e-3,稳定通过风控 SDK 常见的 std_sum > 1e-7 静态死值探针,且量级贴近真实陀螺仪温漂。
  • 融合运动(DeviceMotion)isDeviceMotionAvailable / availableAttitudeReferenceFrames 走三层过滤——root(getuid()==0)、SpringBoard、可执行路径不含 /var/containers/ 的进程读真实值,只对第三方 App 伪装,系统组件零感知。
  • 权限语义仿真:位置类传感器带 NSError(CMErrorDomain, 105) 错误回调路径,复刻真机 TCC 拒绝语义;气压计走公开 startRelativeAltitudeUpdatesToQueue:(绕过 TCC),1 Hz 注入。

④ 文件系统与服务指纹

设备「看起来像不像被人动过」,很大程度取决于文件系统里有没有越狱工具的痕迹、launchd 配置里有没有显眼的第三方服务、网络上有没有不该开的端口。

路径与进程伪装

  • iosbinpack64 → /usr/libexec/.sysbinpack:整套命令行工具包(bash、launchctl、dpkg 依赖等)不以 /iosbinpack64 这类显眼路径落盘,而是整体移入 /usr/libexec/ 下的隐藏目录,所有 daemon plist 里的路径引用同步改写。
  • vphoned 伪装为 Apple 守护进程:guest 控制守护进程以 /usr/libexec/assetd 之名部署,LaunchDaemon 注册为 com.apple.mobile.assetd——进程列表与 /System/Library/LaunchDaemons/ 目录里看到的都是 Apple 自家服务的形态。
  • daemon 一律 com.apple.* 风格命名:com.apple.security.shell(bash)等,避开 bash / dropbear / vphoned 这类一眼穿的名字;dylib 也用短路径(/usr/.v/b)。

launchd.plist 痕迹清理

CFW 安装时注入 daemon 会顺手清理历史版本残留的显眼 keybashdropbeartrollvncrpcserver_iosvphoned、旧 sshd 名字)——旧 key 若残留,既是指纹,又会让 launchd 反复尝试加载已删除的 plist 报错。

VNC 默认关闭:不给端口扫描留指纹

旧方案在 guest 内开 VNC(trollvnc 监听 5901、rpcserver_ios 监听 5910,且绑 0.0.0.0)——对外网络扫描这两个端口即为云真机指纹。现在的方案默认不注入任何 VNC daemon,vphone 自有原生截屏 / 控制通道;确有需要时以 VPHONE_VNC_DAEMONS=1 显式开启。

SSH:vsock 桥,guest 零 TCP 暴露

guest 的 SSH 不监听任何 TCP 端口:vphoned 在 vsock 2222 上 accept 后以 inetd 模式按需 spawn dropbear,宿主经 vsock 桥转发。从 guest 网络侧看,不存在任何「开着等连接」的服务端口。

变体归属

能力 dev jb exp standalone 叠加
内核 sysctl spoof / 版本串 / pdp_ip(devOnly 组) make kernel_patch KERNEL_ARGS='--dev'
DeviceTree 全量身份改写 + SystemVersion 伪装 ✅(CFW --full-identity make dt_patch DTPATCH_ARGS='--full-identity'
CoreMotion 传感器仿真 make sensor_patch SENSOR_ARGS='--auto'
CoreTelephony SIM / 接入技术 make sim_patch SIM_ARGS='--auto'
GPU 驱动包改名 + MetalPluginName make gpu_patch / make kernel_patch
相机注入(合成摄像头) ✅(dev 版 dylib) ✅(MobileSubstrate 版)
iosbinpack64 / vphoned / launchd 隐蔽化、VNC 默认关
hv_vmm OID 改名 + DSC 黑名单翻转 + watchdogd 缓存字节(反 VM 实验补丁)

验证与排障

  • 每个内核补丁都记录进 VM bundle 的 patch_report.json(offset 与前后状态),可审计、可比对回归。
  • standalone 补丁支持 --dry-run(如 make sensor_patch SENSOR_ARGS='--dry-run'),先看会改哪些站点再落盘。
  • 补丁全部幂等:检测到「已是补丁后状态」即跳过,重跑安全;gpu_patch / dt_patch / kernel_patch 均带 --restore 可回退。
  • guest 内验证 sysctl 伪装时注意用普通 App 身份读(root shell 属于 uid≤500 放行组,读到的是真实值,属预期行为)。

深入阅读

  • 变体定义与选型:本帮助中心「固件变体:DEV 与 JB」一篇
  • 补丁管线与 patch 报告机制:本帮助中心「固件补丁管线」一篇
  • 逐 patch 二进制细节:仓库 research/0_binary_patch_comparison.mdresearch/ 专题文档