1 前言
Just for fun, won't be as big and professional as GNU. -- Linus Torvalds
2020 年的美好的一天.
当时是疫情, 我正在悠哉悠哉地上着学校的网课(摸鱼bushi). 正好当时我着迷黑苹果, 于是就打算在我家那台电脑上试试.
不出所料 KP 了(当时还不知道啥叫 KP), 无论是在物理机还是 VMWare.
忘了说了, 当时我家那台电脑的 CPU 是 Intel Pentium G4400.
然后绝望的我就在百度知道上发了几条帖子.
后面经过我的不懈努力 , 也是终于找到了一个方法 -- 仿冒 CPUID, 也就是图中我圈起来的部分.
但是这个问题并没有消失, 而是一直存在了我的肚子里 -- 为什么加入仿冒 CPUID 后, 它就可以正常工作?
几年之后, 我也算成为了半个业余内核开发者, 至少能看懂一些基本的错误日志.
然后, 最近我又翻到了这篇帖子. 在看了一下这张 KP 图后, 一看堆栈, 我就感觉不太对劲...
RAX: 0x00007ffeefa04a70, RBX: 0x00000000ffffffe4, RCX: 0x0000000000000048, RDX: 0x0000000000000020
RSP: 0x00007ffeefa048e0, RBP: 0x00007ffeefa048e0, RSI: 0x00007ffeefa04928, RDI: 0x00007ffeefa04a70
R8: 0x0000000000000403, R9: 0x0000000000000000, R10: 0x0000000000000140, R11: 0x0000000000000148
R12: 0x0000000000000008, R13: 0x00007ffeefa04b58, R14: 0x00007ffeefa04a70, R15: 0x00007ffeefa04acc
RFL: 0x0000000000010202, RIP: 0x0000000101079d0d, CS: 0x000000000000002b, SS: 0x0000000000000023
这不对啊, 这 rsp 怎么是 0x00007ff 开头? 还有这诡异的 CS, 0x2b? 用户态???
这压根就不是因为内核态或者驱动程序崩溃而引起的 KP, 而是一个用户程序! (亏我当时还疯狂地换 Lilu.kext, WhateverGreen.kext...)
那正常的用户态程序挂了就挂了, 发个 SIGSEGV 就好了呀, 凭啥这个用户程序挂掉能直接让整个内核 KP 呢? 答案可能只有一个 -- launchd(换句话说, init).
那这到底是怎么回事? 为什么会出现这种情况呢?
2 问题推测
以下有几个线索, 供我进行初步的原因推断:
- 在低于等于
OS X 10.10上的系统中不会出现这个Kernel Panic,VMWare上能正常运行这个系统, 物理机上则会报Still Waiting For Root Device(当然, 那是另外一个故事了), 只有EI Capitan以上的系统会出现.
- 补充:
OS X 10.10发布年份是2014, 那个时候最新的硬件是Broadwell,OS X 10.11发布时间是2015, 这个版本开始支持Skylake.- 在我家的另一台装有
H61 + Celeron G2030的电脑, 安装任意版本的macOS的时候都不会出现这个Kernel Panic.- 加入
306A0(Ivy Bridge) 的仿冒CPUID后, 系统就能正常启动.
因此我认为问题的根源不在于 Apple 恶意 ban 了 Pentium 和 Celeron, 更像是一个意外.
初步推测出问题的调用链
c
//macOS 内核初始化阶段:
mov eax, 1
cpuid
switch(eax) {
case 0x506E3:
AVX=1; SSE42=1;...
case 0x306A0:
AVX=0; SSE42=1; ...
} // 压根没管 ECX 和 EDX, 直接对 CPU Capabilities 进行 Hardcode.
//macOS launchd 准备阶段:
if(AVX)
avx_unzip(...)
//Ivy Bridge 不存在 AVX 相关的指令集, 不会走对应的 avx_unzip.
//Skylake 在 XNU 的视角中应该是存在 AVX 指令集的(毕竟硬编码了).
//但是 G4400 是个特例, 偏偏不存在 AVX.
//-> 执行相关的 AVX 指令后, 系统直接 #UD
//-> launchd 被 XNU kill
//-> Aiee, attempt to kill init!
//-> Kernel Panic
//-> 若仿冒成 Ivy Bridge, 则不会走 AVX 路径.
//正常的系统初始化:
mov eax, 1
cpuid
avx = ((ECX >> 28)&1)
3 实验环境
虽然这张图并没有给我们 macOS 的具体版本号(万恶的 Not yet set), 但是我们可以通过下面的 Kernel Version 反推出来.
Darwin Kernel Version 18.7.0: Thu Jun 20 18:42:21 PDT 2019; root:xnu-4903.270.47~4/RELEASE_X86_64
上网搜了一下, 这个 Kernel 大概对应着这个版本
macOS Version: 10.14.6
Build: 18G84 或者 18G87
这台机子的主板我们也可以通过 KP 最底下的日志看到, 是微星的 MS-7996, H110M PRO-VD. 这个板子之前是给我用的, 后面被我刷了新的 BIOS, 然后换了个 i3-9100F 给我爸用. 我只记得当时改 BIOS 的时候用 CoffeeTime 删了几个微码, 但忘了删掉了哪几个, 所以我还不太确定这个板子能不能装 G4400 这个U(不过那是后话了).
不过还好, 我家里还有一块闲置的 H610 + 12100F, 刚好趁着这个机会给我爸升级一下配件(其实是为了方便给我弟打游戏, 不对这不能说), 然后那个主板拿来给我做研究. 这样我只需要花 10 块钱就能买到 G4400, 就可以开始进行实验 (双赢这一块算是被我玩明白了bushi)
话不多说, 开整!
实验环境:
- 主板: H110M PRO-VD/VMWare
- CPU: Intel Pentium G4400
- VMWare 版本: 14
- 宿主机系统: Windows 10 20H2
- 虚拟机系统: macOS 10.14.6 (Build 18G84)
- 引导: VMWare + unlocker 4.2.6
- 引导参数 :
-v debug=0x100
KDK 下载地址: 点击下载
Unlocker 4.2.6 下载地址: jb51
4 开始实验
4.1 问题的复现和猜想的验证
首先我们先屏蔽 AVX 位, 在 vmx 文件末尾屏蔽 CPUID Leaf 1 的 ECX 的 AVX 位和 CPUID Leaf 7 的 AVX2 位 (Bit 5):
cpuid.1.ecx="---0:----:----:----:----:----:----:----"
cpuid.7.ebx="----:----:----:----:----:----:--0-:----"
接下来, 分别设置实验组和对照组的 FakeCPUID
cpuid.1.eax="0000:0000:0000:0011:0000:0110:1010:0000" #0306A0
cpuid.1.eax="0000:0000:0000:0101:0000:0110:1110:0011" #0506E3
实验结果如图所示:
在 FakeCPUID 为 0x0506E3 (Skylake) 的时候, 会出现这个 Kernel Panic.
在 FakeCPUID 为 0x0306A0 (Ivy Bridge) 的时候, 反而不会出现这个 Kernel Panic, 系统能正常启动.
同时, 我也通过录像, 录到了 Panic 的上半部分:
panic(cpu 1 caller 0xffffff880a6bb8ef): initproc exited -- exit reason namespace 2 subcode 0x4
经资料查阅, subcode = 0x4 代表 SIGILL, 也就是 UD.
猜想被证实了, 确实是因为 SIGILL 导致 launchd 挂掉, 最终 Attempt to kill init!.
那么, 到底是哪一行代码导致的 UD 呢? 进入 4.2 节 -- 翻调用栈.
4.2 判断 KP 的原因
首先我们得明确一下这个 Kernel Panic 的结构. 这是我从 VM 上截取的 KP 图:
上面的
uuid = 代表加载的 dylib 的信息. 其中:
- addr 代表 dylib 被加载到的位置
- uuid 代表 dylib 的加载文件
下面的 Thread 0 类似 Linux 的 Call Stack
但是第一个很明显是内核地址(ffff开头的), 估计是跟 task_kill 有关的函数, 这儿不深究.
所以第二行才是我们想要的用户地址: 0x110837d0d.
根据上面的 uuid 对应的表格来看, 可以看到距离这个地址最近的 dylib 的位置为:
0x110836000 uuid <9d1fe5e4-eb7d-3b3f-a8d1-a96d9cf1348c>
接下来我们可以得到这些信息:
UUID=9d1fe5e4-eb7d-3b3f-a8d1-a96d9cf1348c对应的dylib文件就是导致KP的真凶.- 出问题的指令在
dylib中的Offset=0x110837d0d-0x110836000=0x1d0d
挂载 10.14.6 的 BaseSystem.dmg, 然后去 /usr/lib/system 目录下找对应的 .dylib:
可以看到对应的文件是 libsystem_platform.dylib, 揪出它, 丢到 Cutter 里面看看是什么情况.
按 offset 排序, 找到距离 0x1d0d 最近的函数 SA, 最终定位在 0x1cc0 这个位置.
看 Signature, 这是一个针对 Haswell 专门优化的内存移动函数:
c
void memmove_haswell(unsigned long dst, unsigned long src, unsigned long size);
就是它了! 0x1d0d vmovups xmm1, xxmmword [rsi+rdx*1-0x10].
一条典型的 AVX 指令. Pentium G4400 看到这脑子直接宕机: 我 AVX 不是被阉割了吗? 这是什么鬼?
然后就直接一个 #UD 砸下来了, 最终 launchd 被 kill. 问题调用链算是找到了.
然后接下来就是对下一层的地址 0x1107ee705 进行 disas, 一样的操作:
- 找到
uuid=4195838c-efef-3cc9-b459-75032af7eala,offset=0x1705 - 根据
uuid找到上层调用memmove函数的文件是libsystem_kernel.dylib - 丢
Cutter后定位到0x1705.
emmm... 虽然 0x1705 只是一条人畜无害的 mov 指令, 但是它上面刚好就有一个 _memcpy 啊! 干他!
一个间接跳转指令, rax 寻找了两次, 第二次很明显是个 offset, 还有这个全局变量 __platform_string_functions 是什么?
首先顺藤摸瓜找到了一个叫做 libkernel_memmove 的函数. 但这显然和我们之前的 libplatform_memmove 对不上. 这到底是怎么回事?
4.4 探寻 __platform_string_functions 的初始化过程
百思不得其解, 于是我问了一下 ai, 它提示我去看看 libsystem 和 libplatform 的初始化函数. 这一查还真查出了一点东西.
这 init 函数有点东西啊, 它传递了一个 __platform_string_functions 到 rdi 中, 然后调用 __libkernel_platform_init.
然后我们再来看一下 __libkernel_platform_init 的实现:
nasm
;-- func.00001555:
___libkernel_platform_init(int64_t arg1);
; arg int64_t arg1 @ rdi
0x00001555 mov qword [__libkernel_string_functions], rdi ; 0x2b3f0 ; arg1
; __libkernel_string_functions = rdi(__platform_string_functions)
0x0000155c xor eax, eax
0x0000155e ret
对, 它往**__libkernel_string_functions** 写入了这个参数!
继续, 我们再看一下 libsystem_string_functions 的这个 __platform_string_functions 是什么.
很好, 这个类似的结构体在哪出现过? 对, 上面的 memcpy 中的汇编代码, 我们探究 __libkernel_string_functions 的结构的时候.
nasm
;-- func.00001816:
void *_memcpy(void *s1, const void *s2, size_t n);
0x00001816 mov rax, qword [__libkernel_string_functions] ; 0x2b3f0 ; [0x2b3f0] = 0x029010
0x0000181d mov rax, qword [rax+0x20] ; 0x99ea
0x00001821 jmp rax
那么我们就看一下 __platform_string_functions+20 对应的地址是什么:
一个 stub, 它名字都写好了, 就叫 memmove!
现在一切都好像有答案了:
- 这个
__platform_string_functions是一个存函数指针的结构体, 为了方便针对不同平台进行优化, Apple 采用了这种设计, 方便解耦. - 为了防止空指针,
__platform_string_functions在libsystem_kernel的编译期先硬编码为libkernel内部的字符串操作函数.- 这也解释了我们在反汇编软件看到这个函数的时候, 它指向
libkernel_memmove而不是libplatform_memmove函数的原因
- 这也解释了我们在反汇编软件看到这个函数的时候, 它指向
- 在
libplatform初始化的时候, 它会重新初始化这个变量, 指向libplatform内部的结构体.
4.5 探究 platform_memmove 的实现
好的, 线索到这已经很明朗了, 接下来看看 platform_memmove 的实现.
Ghidra 反编译结果如下:
c
code * __platform_memmove(void)
{
if (*(int32_t *)0x7fffffe00040 < 0x573b5eec) {
if (*(int32_t *)0x7fffffe00040 == 0x1f65e835) {
return __platform_memmove$VARIANT$Ivybridge;
}
if (*(int32_t *)0x7fffffe00040 != 0x5490b78c) {
return __platform_memmove$VARIANT$Haswell;
}
} else if (*(int32_t *)0x7fffffe00040 != 0x573b5eec) {
if (*(int32_t *)0x7fffffe00040 == 0x78ea4fbc) {
return __platform_memmove$VARIANT$Base;
}
if (*(int32_t *)0x7fffffe00040 != 0x6b5a4cd2) {
return __platform_memmove$VARIANT$Haswell;
}
}
return __platform_memmove$VARIANT$Nehalem;
}
果然, 硬编码这一块, 实锤了. 这个 memmove 它返回一个函数指针, 所以上面的逻辑闭环了:
nasm
mov rax, [rax+20] ;memmove
jmp rax
接下来看看 0x7fffffe00040 这个地址到底藏了什么东西. 查阅资料发现, 这个地址其实是 XNU 的 commpage 的一部分, 为了实现 zerocopy. 它对应的 offset 是 _COMM_PAGE_CPU_FAMILY.
OK, 翻 XNU 源码.
c
//osfmk/mach/machine.h
#define CPUFAMILY_INTEL_6_13 0xaa33392b
#define CPUFAMILY_INTEL_PENRYN 0x78ea4fbc
#define CPUFAMILY_INTEL_NEHALEM 0x6b5a4cd2
#define CPUFAMILY_INTEL_WESTMERE 0x573b5eec
#define CPUFAMILY_INTEL_SANDYBRIDGE 0x5490b78c
#define CPUFAMILY_INTEL_IVYBRIDGE 0x1f65e835
#define CPUFAMILY_INTEL_HASWELL 0x10b282dc
#define CPUFAMILY_INTEL_BROADWELL 0x582ed09c
#define CPUFAMILY_INTEL_SKYLAKE 0x37fc219f
#define CPUFAMILY_INTEL_KABYLAKE 0x0f817246
#define CPUFAMILY_INTEL_ICELAKE 0x38435547
#define CPUFAMILY_INTEL_COMETLAKE 0x1cf8a03e
好嘛, 这果然是一堆 magic_number.
然后根据这个头文件搜, 发现了这么一个函数:
c
static uint32_t
cpuid_set_cpufamily(i386_cpu_info_t *info_p)
{
uint32_t cpufamily = CPUFAMILY_UNKNOWN;
switch (info_p->cpuid_family) {
case 6:
switch (info_p->cpuid_model) {
case 23:
cpufamily = CPUFAMILY_INTEL_PENRYN;
break;
case CPUID_MODEL_NEHALEM:
case CPUID_MODEL_FIELDS:
case CPUID_MODEL_DALES:
case CPUID_MODEL_NEHALEM_EX:
cpufamily = CPUFAMILY_INTEL_NEHALEM;
break;
case CPUID_MODEL_DALES_32NM:
case CPUID_MODEL_WESTMERE:
case CPUID_MODEL_WESTMERE_EX:
cpufamily = CPUFAMILY_INTEL_WESTMERE;
break;
case CPUID_MODEL_SANDYBRIDGE:
case CPUID_MODEL_JAKETOWN:
cpufamily = CPUFAMILY_INTEL_SANDYBRIDGE;
break;
case CPUID_MODEL_IVYBRIDGE:
case CPUID_MODEL_IVYBRIDGE_EP:
cpufamily = CPUFAMILY_INTEL_IVYBRIDGE;
break;
case CPUID_MODEL_HASWELL:
case CPUID_MODEL_HASWELL_EP:
case CPUID_MODEL_HASWELL_ULT:
case CPUID_MODEL_CRYSTALWELL:
cpufamily = CPUFAMILY_INTEL_HASWELL;
break;
case CPUID_MODEL_BROADWELL:
case CPUID_MODEL_BRYSTALWELL:
cpufamily = CPUFAMILY_INTEL_BROADWELL;
break;
case CPUID_MODEL_SKYLAKE:
case CPUID_MODEL_SKYLAKE_DT:
case CPUID_MODEL_SKYLAKE_W:
cpufamily = CPUFAMILY_INTEL_SKYLAKE;
break;
case CPUID_MODEL_KABYLAKE:
case CPUID_MODEL_KABYLAKE_DT:
cpufamily = CPUFAMILY_INTEL_KABYLAKE;
break;
case CPUID_MODEL_ICELAKE:
case CPUID_MODEL_ICELAKE_H:
case CPUID_MODEL_ICELAKE_DT:
cpufamily = CPUFAMILY_INTEL_ICELAKE;
break;
case CPUID_MODEL_COMETLAKE_DT:
cpufamily = CPUFAMILY_INTEL_COMETLAKE;
break;
}
break;
}
info_p->cpuid_cpufamily = cpufamily;
DBG("cpuid_set_cpufamily(%p) returning 0x%x\n", info_p, cpufamily);
return cpufamily;
}
好, 继续翻 CPUID_MODEL_* 的宏定义.
c
//osfmk/i386/cpuid.h
#define CPUID_MODEL_PENRYN 0x17
#define CPUID_MODEL_NEHALEM 0x1A
#define CPUID_MODEL_FIELDS 0x1E /* Lynnfield, Clarksfield */
#define CPUID_MODEL_DALES 0x1F /* Havendale, Auburndale */
#define CPUID_MODEL_NEHALEM_EX 0x2E
#define CPUID_MODEL_DALES_32NM 0x25 /* Clarkdale, Arrandale */
#define CPUID_MODEL_WESTMERE 0x2C /* Gulftown, Westmere-EP/-WS */
#define CPUID_MODEL_WESTMERE_EX 0x2F
//...
然后继续看 i386_cpu_info_t, 找到了这么一个获取 cpuid_info 的函数:
c
i386_cpu_info_t *
cpuid_info(void)
{
/* Set-up the cpuid_info stucture lazily */
if (cpuid_cpu_infop == NULL) {
PE_parse_boot_argn("-cpuid", &cpuid_dbg, sizeof(cpuid_dbg));
cpuid_set_info();
cpuid_cpu_infop = &cpuid_cpu_info;
}
return cpuid_cpu_infop;
}
继续看:
c
void
cpuid_set_info(void)
{
i386_cpu_info_t *info_p = &cpuid_cpu_info;
boolean_t enable_x86_64h = TRUE;
/* Perform pre-cpuid workarounds (since their effects impact values returned via cpuid) */
cpuid_do_precpuid_was();
cpuid_set_generic_info(info_p); //重点!
/* verify we are running on a supported CPU */
if ((strncmp(CPUID_VID_INTEL, info_p->cpuid_vendor,
min(strlen(CPUID_STRING_UNKNOWN) + 1,
sizeof(info_p->cpuid_vendor)))) ||
(cpuid_set_cpufamily(info_p) == CPUFAMILY_UNKNOWN)) {
panic("Unsupported CPU");
//AMD Hackintosh 玩家表示很淦
}
//...
}
static void
cpuid_set_generic_info(i386_cpu_info_t *info_p) {
uint32_t reg[4];
char str[128], *p;
DBG("cpuid_set_generic_info(%p)\n", info_p);
/* do cpuid 0 to get vendor */
cpuid_fn(0, reg);
info_p->cpuid_max_basic = reg[eax];
bcopy((char *)®[ebx], &info_p->cpuid_vendor[0], 4); /* ug */
bcopy((char *)®[ecx], &info_p->cpuid_vendor[8], 4);
bcopy((char *)®[edx], &info_p->cpuid_vendor[4], 4);
info_p->cpuid_vendor[12] = 0;
/* get extended cpuid results */
cpuid_fn(0x80000000, reg);
info_p->cpuid_max_ext = reg[eax];
//...
wrmsr64(MSR_IA32_BIOS_SIGN_ID, 0);
cpuid_fn(1, reg);
info_p->cpuid_microcode_version =
(uint32_t) (rdmsr64(MSR_IA32_BIOS_SIGN_ID) >> 32);
info_p->cpuid_signature = reg[eax]; //演都不演了
info_p->cpuid_stepping = bitfield32(reg[eax], 3, 0);
info_p->cpuid_model = bitfield32(reg[eax], 7, 4);
info_p->cpuid_family = bitfield32(reg[eax], 11, 8);
info_p->cpuid_type = bitfield32(reg[eax], 13, 12);
info_p->cpuid_extmodel = bitfield32(reg[eax], 19, 16);
info_p->cpuid_extfamily = bitfield32(reg[eax], 27, 20);
info_p->cpuid_brand = bitfield32(reg[ebx], 7, 0);
info_p->cpuid_features = quad(reg[ecx], reg[edx]);
//...
}
然后我们看一下它是怎么写入 commpage 里面的:
c
//osfmk/i386/cpu_capabilities.h
#define _COMM_PAGE_CPUFAMILY (_COMM_PAGE_START_ADDRESS+0x040) /* uint32_t hw.cpufamily, x86*/
//osfmk/i386/commpage/commpage.c
static void
commpage_populate_one(
vm_map_t submap, // commpage32_map or compage64_map
char ** kernAddressPtr, // &commPagePtr32 or &commPagePtr64
size_t area_used, // _COMM_PAGE32_AREA_USED or _COMM_PAGE64_AREA_USED
commpage_address_t base_offset, // will become commPageBaseOffset
commpage_time_data** time_data, // &time_data32 or &time_data64
new_commpage_timeofday_data_t** gtod_time_data, // >od_time_data32 or >od_time_data64
const char* signature, // "commpage 32-bit" or "commpage 64-bit"
vm_prot_t uperm)
{
//...
cfamily = cpuid_info()->cpuid_cpufamily;
commpage_stuff(_COMM_PAGE_CPUFAMILY, &cfamily, 4);
}
这部分代码负责将 cpufamily 写入 commpage 中.
OK, 实锤完毕. 这些代码, 足以证明, 我之前的所有猜想都是正确的.
- 苹果根据
cpuid的eax来判断cpuid_model,cpuid_family. - 根据
cpuid_family来确定0x7fffffe00040(_COMM_PAGE_CPU_FAMILY) 的值. - 到这一步都正常, 但是烂苹果在写
libplatform的时候, 只根据_COMM_PAGE_CPU_FAMILY来判断返回哪一个memmove优化函数! - 偏偏
G4400没AVX指令集, 一执行,launchd都直接炸! KP!
到此, 实验结束.
5 实验结果 & 感慨
一个牙膏厂,一个烂苹果,一个往死里阉割,一个往死里"忧化",两个神人公司叠加在一起,总之都不把低端奔腾和赛扬的用户当人😷卧龙凤雏了属于是😧😧
只能说受不了一点, 两个神人公司加起来, 组合成的 KP, 给当时 2020 年在上网课的 13 岁的我卡了三个月...
只能说, "强强联手"啊!
还有.
- 致敬当年的黑苹果时光.
- 致敬当年"百机齐放"的折腾时光.
- 致敬 13 岁那个懵懂无知, 但是愿意花 3 个月, 仅仅为了解决一个黑苹果无法启动的, 乐于折腾的我.
- 成长不是亲手杀死,或者禁闭掉自己的那个内心小孩,而是将内心的小孩和现在的大人进行一次整合,让强大的能力,保护住那个内心的小孩不受伤害。
在这个厂商限制越来越严格的时代, 在这个 BL 锁已经让 Root 和刷机销声匿迹的时代, 在这个黑苹果已经落幕, Apple 全面转向 Apple Silicon 的时代, 愿我们折腾的初心不变.
The End
本期文章写到这, 感谢大家的观看哦~萌新初涉系统编程, 有错误也请多多指正~
版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
作者: Sudo-su-Bash (Alien-Bash)
发布时间: 2026-08-01