风控对抗与设备指纹伪装
风控对抗与设备指纹伪装
现代风控 SDK 判定「这台设备是不是虚拟机 / 模拟器 / 云真机」的探针遍布全栈:内核 sysctl、Metal / GPU 驱动、网络接口、摄像头、运动传感器、文件系统路径、launchd 服务与开放端口。任何一处露出破绽,整套伪装即告失效。
vphone 在固件补丁与 CFW 安装两个阶段内置了一整套高保真伪装方案,覆盖四个层面:
| 层面 | 典型探针 | vphone 对策 |
|---|---|---|
| 内核与环境 | kern.hv_vmm_present、kern.version、hw.machine 等几十个 sysctl |
读侧 caller-aware 伪装 + 版本串改写 |
| 硬件与驱动 | 加载镜像列表扫描 GPU 驱动名、MTLDevice.name、getifaddrs()、相机枚举 |
GPU 驱动包改名、蜂窝网卡伪造、虚拟摄像头真机化 |
| 传感器 | 加速度计 / 陀螺仪 / 气压计可用性与数据死值检测 | 高保真噪声注入(漂移 + 白噪声) |
| 文件系统与服务 | 越狱工具路径扫描、launchd.plist 内容、端口扫描 | 隐蔽路径 + 伪装命名 + 默认零 TCP 暴露 |
本文按层面逐一拆解各项伪装的原理与实现。各项能力在不同变体(dev / jb / exp)中的归属见文末「变体归属」。
① 内核与环境伪装
内核层伪装以 devOnly 补丁组为核心(dev、jb、exp 变体均生效),核心设计原则是 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.ncpu、hw.logicalcpu、hw.activecpu 与 commpage 的 activeProcessorCount 天然一致地等于 6——旧的 sysctl 硬改只覆盖 hw.ncpu,active 数仍是宿主 8,active > total 反而触发检测。
kern.version:构建标识改写
研究内核的 kern.version 以 RELEASE_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,3、root/target-type → D47、compatible 首项翻成 D47AP(保留 VPHONE600AP 在列表中,IOKit 平台绑定不回归,而 userland 查 hw.model 看到的是 D47AP)、target-sub-type / fdr-product-type / unique-model / arm-io 的 t8140-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.plist的CFBundleExecutable/CFBundleName/CFBundleIdentifier(→com.apple.driver.AppleGFXHW)全部改为AppleGFXHW;NSPrincipalClass刻意不改——它是 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 更在意的是摄像头枚举出来的形态。注入 cameracaptured 的 libvcamcaptured 注册 5 个合成 FigCaptureSource,与 iPhone17,3(iPhone 16 标准版)真机 dump 完全对齐:
| 合成设备 | deviceType | 位置 |
|---|---|---|
| 后置广角相机 | WideAngle | back |
| 前置广角相机 | WideAngle | front |
| 后置超广角相机 | UltraWide | back |
| 后置双广角融合镜头 | DualWide(虚拟融合设备) | back |
| 前置原深感摄像头 | TrueDepth | front |
所有属性按真机 dump 复刻:uniqueID == modelID(com.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):
- 可用性:
isGyroAvailable、isAccelerometerAvailable、isMagnetometerAvailable、isRelativeAltitudeAvailable(及 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 会顺手清理历史版本残留的显眼 key(bash、dropbear、trollvnc、rpcserver_ios、vphoned、旧 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.md及research/专题文档