-
-
[原创]Intel 酷睿 CPU Management Engine 固件研究与分析逆向 (二) 实战DCI链接与ME解锁尝试
-
发表于: 20小时前 449
-
前言
此帖是记录笔者研究Intel Management Engine 以及 Intel-SA-00086 安全漏洞的记录。笔者也不知道能研究多深入,所以研究一点是一点,顺便发出来也给各位大佬参考以及讨论。说结论:当前只能说解锁了一半,暂时还没有找到一个能完全解锁ME的路径。我会把我研究的所有过程和思路都展示出来,各位师傅如有兴趣,可以一起来研究。(零AI注水放心阅读)
平台
平台选择我更换了成了Z270但是实际上Z370也是一样得没区别,所以没必要去追低版本。之前Z370平台被我弄坏了也可能是二手质量太差。
主板:华硕 Z270 - A
CPU: I5 7400
展示一下新的平台:

DCI链接
上一章主要讲解了ME解包以及Bios固件得DCI解锁手法但是由于没有展示完全得实战过程,有些师傅对我得手法和思路有些质疑,所以我重点讲解一下我是如何链接DCI。
软环搭建
我们要调试CPU需要安装Intel System Studio 2019,这个版本在看雪有人发过。(如有需要可以私信我我发你网盘)。
第一个坑:这个软件实际上是基于win7开发的,最好是在win7环境下运行。如果在win10上需要打开安装软件请设置兼容模式。

软件只需要安装这个即可:

需要注意安装Intel System Debugger 2019 需要安装Net3.5可以去微软官网下载这里就不介绍了。
在win11环境下安装会提示没有安装win7的安全补丁导致无法进行,我通过逆向分析发现有一个参数可以关闭这个检查。

如图会弹出这个提示,在运行的时候加 --noprereq 不会有问题了。
过掉环境检测后他会有一个License的校验

虽然公开资料里面附带一个License文件但是window版本是没法用的失效状态,这里我逆向了一下他的校验机制。核心的校验代码在issa.dll 中,
| # | 函数 | VA | 文件偏移 | 原始字节 | Patch |
|---|---|---|---|---|---|
| 1 | issa_set_existing_license_0 |
0x1001BDF0 |
0x1B3F0 |
55 8B EC |
31 C0 C3 |
| 2 | issa_existing_license_is_valid_info_0 |
0x1001E5B0 |
0x1DBB0 |
55 8B EC |
31 C0 C3 |
| 3 | issa_get_existing_license_info_0 |
0x1001EDBC |
0x1E3BC |
55 8B EC |
31 C0 C3 |

如图我拿其中一个展示一下,主要就是这三个函数用来校验,都是导出函数没什么好介绍的直接patch后,替换进去校验就行了。
安装完我们的调试软件后还需要安装我们的驱动组件,我这里直接从iso里面提取出来,我会发到附件里面去。
配置链接
安装完会生成如下两个目录:

首先需要打开 ConfigConsole 软件用来配置DCI链接:

首先根据你的平台和版本的不同进行配置:

根据配置名分析:
KBL是Kaby Lake CPU
OpenDCI也就是DCI线缆链接
SPT 指的是200系主板
DBC 指的就是 Debug Class 也就是我们这种线缆链接。
选择完后就可以直接点右上角链接即可。

CLI调试
在DAL_1.1839.428.110目录下找到:
PythonConsole.cmd,运行就可以打开我们的DCI调试的界面了。这个主要是Python2.7作为CLI的基础。

通过help()函数和dir的帮助。以及AI辅助后大概了解了一些基础命令:
# 逻辑设备
itp.cores
itp.boxes
itp.chipsets
itp.threads
# halt 测试
itp.halt()
itp.halt() 在当前状态下,直接调用这个函数会直接超时,cpu会拒绝。
The requested operation has timed out
UTOK
这个发现是我之前逆向ME固件的时候的一个意外发现。
ME 启动流程
这里我整理了一个图展示一下
上电
│
▼
┌─────────────────────┐
│ ROM (PCH 内固化) │
│ 验签 FTPR → 解压 RBE │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ RBE │
│ 初始化 PCH/SPI │
│ UTOK 校验 / boot_cfg │
│ 加载 kernel+syslib │
│ +bup 到 SRAM │
│ Hash 验证 │
└──────────┬──────────┘
│ jmp kernel entry
▼
┌─────────────────────┐
│ kernel │
│ ThreadX RTOS 启动 │
│ 启动 user process #1 │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ BUP (user proc #1) │
│ 硬件 bringup │
│ 读 boot_cfg → 开 DCI │
│ HECI / DID / PTT │
└──────────┬──────────┘
│
▼
pm / vfs / heci /
crypto / storage / ...
(38 个 user processes)
在这个里面我发现了UTOK模块,根据AI查询后的解释是:
UTOK = Unlock Token ← Intel 为 OEM 留的"后门"解锁机制,作用:无需硬件熔丝,通过 SPI Flash 写入数据 → 解锁 DFX 调试接口
,位置:SPI Flash 的 FPT 分区表中一个独立分区。这方面我确实不是很了解暂时先信任一下。
根据分析来看:
我整理一下UTOK的整个流程
| 编号 | 执行顺序 | 函数/位置 | 文件偏移 | VA (base 0x40000) | 说明 |
|---|---|---|---|---|---|
| 1 | ① | rbe_main |
0x20A |
0x4020A |
RBE 主入口,handoff 时读 0x60454E4/E0 → 跳 kernel |
| 2 | ② | rbe_detect_boot_config |
0x1042 |
0x41042 |
构造 boot_cfg,调用 UTOK 校验 |
| 3 | ③ | rbe_dir_lookup |
0xE78 |
0x40E78 |
按名查 dir,定位 "boot_fpt" 条目 |
| 4 | ④ | rbe_copy_flash_to_sram |
0xBFB |
0x40BFB |
DMA 读 FPT 条目表到栈 |
| 5 | ⑤ | rbe_utok_check: 魔数匹配 |
0xF08 |
0x40F08 |
cmp [edx], 0x4B4F5455 遍历 FPT 找 "UTOK" |
| 6 | ⑥ | rbe_copy_flash_to_sram |
0xBFB |
0x40BFB |
DMA 读 UTOK 分区 696 字节 |
| 7 | ⑦ | rbe_utok_check: 核心判断 |
0xF31 |
0x40F31 |
cmp [esi+0x298], 1 检查 valid_flag |
| 8 | ⑧ | rbe_utok_check: 门控检查 |
0xF3C |
0x40F3C |
cmp [0x6041001], 0 SRAM 调试门控 |
| 9 | ⑨ | rbe_utok_check: 调试 MMIO |
0xF45 |
0x40F45 |
写 5 个状态码到 0xF7010080/90/B0 |
| 10 | ⑩ | rbe_detect_boot_config: boot_mode判断 |
0x10BA |
0x410BA |
((boot_cfg & 0xF) - 2) & 0xFD == 0 → set bit4 |
| 11 | ⑪ | rbe_publish_boot_config |
0x11A1 |
0x411A1 |
第二次调 UTOK:mft[0x17F]==0x19 → bit3 |
我拿出最核心的检测调试区判断作为例子如下图:

UTKO区域定位
通过逆向分析,rbe_utok_check一开始是会拿到fpt区域

me的运行base是0x4000所以说字符串是2ad4如下图,UTKO首先会定位fpt区域


函数内部是一个链表,链表结构如下:

上面是运行时的结构
固件静态解析Patch调试位
先介绍一下 FPT(Flash Partition Table) 表:Flash 上的物理分区目录,主要是启动时候需要加载的一张路由表,所以这个里面存储了我们刚刚上面介绍的信息。
它位于 ME 区域的固定偏移处,记录了 ME 内每个分区的位置、大小和类型。这张表的作用很简单:ROM 和 RBE 启动时没有文件系统,不知道 UTOK 在哪、FTPR 在哪、kernel 在哪——全靠查 FPT 来找到对应分区的物理位置。
FPT 头部结构:
| 偏移 | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0x10 | 4B | 魔数 | 固定为 $FPT(0x24465054),用于定位 FPT 表的起始 |
| 0x14 | 4B | 条目数 | 表中有几个分区条目 |
| 0x1B | 1B | 条目大小 | 每条固定 32 字节(0x20) |
上面表格信息从UEFIExtract 源码中提取:解析FPT流程如下:

010如下图:

UTOK区域偏移:

UTOK区域

根据逆向分析的结果:

我们需要的标志位就是0X298,吧这个标志位置1后就启动了部分调试能力,厂家默认这部分是FF,所以是不启动的。
CSME认证链路
这是我逆向后整理一个图,从读取UTOK以及后面干了什么,UTOK标志位影响什么,还有哪些会影响到我们的调试接口的权限开放 :

根据逆向分析一共分成9层校验:
第一层:UTOK[0x298] == 1
; rbe_utok_check — Z270 RBE 文件偏移 0xF31
0x40F31: cmp [esi+0x298], 1 ; ★ 比较 UTOK 数据 +0x298 处是否为 1
0x40F38: jnz loc_40F76 ; 不等于 1 → 失败
0x40F3A: xor al, al ; al=0 返回成功
第二层:boot_mode == 2
; rbe_detect_boot_config — Z270 RBE 文件偏移 0x10BA
0x410AA: mov al, [0x60410A4] ; 读当前 boot_cfg[0]
0x410AF: mov dl, al
0x410B1: and edx, 0x0F ; 提取低 4 位 = boot_mode
0x410B4: sub edx, 2 ; boot_mode - 2
0x410B7: and dl, 0xFD
0x410BA: jnz loc_410C4 ; ★ 不等于 0 → 不是 mode=2, 跳过 bit4
0x410BC: or eax, 0x10 ; 是 mode=2 → bit4=1
; 第二处检查 (不管 UTOK)
0x410D4: jnz loc_410E5 ; ★ 不是 mode=2 → 跳过 bit7
0x410D6: or [0x60410A5], 1 ; b1 bit0 = mfg_flag
0x410DD: or eax, 0x80 ; boot_cfg[0] bit7=1
boot_mode 来自 0xF00B1050 寄存器的 >> 4 位(PCH 硬件 strap),代码在 0x4109C。
第三层:boot config 解析成功
; bup_cfg_handler — Z270 BUP 文件偏移 0x1CCA0
0x1CCF1: lea edx, [ebp-0xCCh] ; 输出缓冲区
0x1CCF7: mov eax, 5D521h ; 输入数据地址
0x1CCFC: call sub_19CB0 ; ★ 调用配置解析器
; sub_19CB0 内部:
0x19CFA: call ROM_SVC_MEMCMP ; memcmp(10B, magic_key, input_data)
0x19D21: call ROM_SVC_MEMCMP ; memcmp(9B, field_key, input_data)
; 解析成功后:
0x1CD63: mov [0x8416Eh], 1 ; ★ 配置加载标志置 1
; sub_4515 字段查找:
0x452E: push 0x0C ; 比较 12 字节
0x4532: call ROM_SVC_MEMCMP ; memcmp(12, field, table_entry)
0x4537: add edi, 0x18 ; 下一表项 (stride=24)
0x4541: imul ebx, 0x18
0x4544: mov eax, [ebx+0x5A4D8] ; 返回类型码
解析器逐字段匹配 "utknknobs"、"uniquepid" 等字符串,通过 0x5A4E4 表(35 项,每个 stride 24 字节)映射字段名到类型码。
第四层:0x8416C == 1
; Z270 BUP 文件偏移 0x1D187
0x1D199: mov [0x8416Ch], 1 ; ★ 无条件置 DCI CT 标志为 1
0x1D1A0: call sub_161D9 ; NOTIFY(3, &var, 1)
loc_1D187 是 bup_cfg_handler 中链表搜索失败后的兜底路径。只要走到这里,0x8416C 就被无条件设为 1。
第五层:0x8416D == 1
; Z270 BUP 文件偏移 0x1D1A8
0x1D1A8: mov [0x8416Dh], 1 ; ★ 无条件置 CT 总门控为 1
0x1D1AF: call sub_27049 ; 后续 CT 处理
紧跟在第四层之后,0x8416D 也被无条件设为 1。
第六层:flag_84884 == 0
; sub_A38B — Z270 BUP 文件偏移 0xA38B
0xA38B: push ebp
0xA38F: cmp [0x84884h], 0 ; ★ 检查硬件加密标志
0xA3AA: jz loc_A3B5 ; ==0 → 走软件回退
0xA3B0: jmp ROM_SVC_CRYPTO ; ==1 → ROM 硬件加密验证
; sub_A14C 软件回退:
0xA2B3: cmp ecx, 0x40 ; arg_4=1, 与 0x40 比较
0xA2B6: ja loc_A2C9 ; arg_4 > 0x40 才做验证
0xA2B8: push ecx ; arg_4 = 1
0xA2BD: call ROM_SVC_MEMCPY ; ← 不是验证, 只是内存拷贝
0xA2C5: xor eax, eax ; ★ return 0 = 验证通过
flag_84884 == 0 时,sub_A14C 中 arg_4=1 < 0x40,走不到加密验证逻辑,直接 return 0。
第七层:CT Handler
; CT Handler — Z270 BUP 文件偏移 0x264A2
0x264CD: call ROM_SVC_DID_PHASE ; DID 状态检查
0x2651E: call sub_1A71B ; ★ 解析 CT 配置表
; sub_1A595 门控:
0x1A5DB: cmp [0x8416Dh], 0 ; ★ 检查总门控
0x1A5E8: cmp [0x8416Ch], 0 ; ★ 检查 DCI 标志
; sub_1A71B 循环:
0x2652C: movzx eax, word [ebp-0x32Eh] ; 循环上限
0x26537: cmp byte [entry_type], 1 ; type=1
0x26557: call ROM_SVC_NOTIFY ; ★ NOTIFY(0xC7, addr, val)
0x2655F: cmp byte [entry_type], 2 ; type=2
0x2657F: call ROM_SVC_NOTIFY ; ★ NOTIFY(0xCF, addr, val)
0x26587: inc ebx ; 下一项
0x26588: jmp loop ; 继续循环
CT Handler 遍历配置表(stride=8B),将 type=1 条目的值和地址写入 state 0xC7,type=2 条目的值写入 state 0xCF。这些状态通过 ME 内部总线控制 PCH 的 DCI 传输层。
第八层:var_B7 == 0x36
; sub_27049 — Z270 BUP 文件偏移 0x27049
0x2707F: lea ebx, [var_B7] ; ebx = &var_B7
0x27094: call sub_1B0E4 ; sub_1B0E4(5E6C2h, var_B7, 0, 6, ...)
; sub_1B0E4 → sub_1A141:
0x1A248: call sub_19CB0 ; 解析 BSS 全零数据 → 退化结果
; 非 VFS 路径不清除 sub_1A141 的 edx:
; sub_1A141 不把 edx 写回 *edx (= var_B7)
; 回到 sub_27049:
0x270B4: cmp [var_B7], 36h ; ★ 检查 var_B7 是否等于 0x36
0x270BB: jnz loc_2711D ; 不等于 → 跳过 state 写入
; 如果等于:
0x270CF: call sub_26817 ; ★ write_state(0x50, 4, 1)
0x27115: call sub_26817 ; ★ write_state(0x58, 8, 1)
var_B7 是在栈上分配的一块内存。走非 VFS 路径时(flag_84204 bit0 == 0),sub_1A141 不会写入 *edx(即 var_B7)。var_B7 未被初始化,其值为栈上之前代码路径残留的数据。
第九层:FPF Debug Auth
; sub_16388 — Z270 BUP 文件偏移 0x16388
0x16388: mov eax, [0x83E38h] ; ★ 读取 DCI 上下文指针
0x16391: mov eax, [eax+0x48h] ; ★ 读取 +0x48 偏移处的值
0x16394: retn
; 调用方 — Z270 BUP 文件偏移 0x1F873
0x1F873: call sub_16388 ; 读状态
0x1F875: test al, 2 ; ★ 检查 bit1
0x1F877: jz loc_1F8BD ; bit1=0 → 跳过 sub_1D64C
; bit1=1 → sub_1D64C → sub_1D1DA → 6 次 NOTIFY
0x83E38 是 ThreadX 在加载 BUP 时通过 copy-down 初始化的上下文指针。消费板的初始化数据中该指针指向的结构体的 +0x48 处 bit1 为 0。
UTok固件格式解析
这里通过分析整个DCI认证的时候发现的一些校验的字段与对应的变量关系。整个UTok就是从本地拉去到内存后进行解析最后映射到不同的变量上去。
| 偏移 | 大小 | 内容 | 说明 |
|---|---|---|---|
0x000 |
12B | key = utknknobs\0\0\0 |
TLV 条目1 字段名 |
0x00C |
2B | flags = 0000 |
标志位 |
0x00E |
2B | total_len | = 0x12 + payload长度 |
0x010 |
2B | pad = 0000 |
填充 |
0x012 |
N B | payload | 调试开关数据 · [0x2C]=boot_cfg_byte |
| ... | [TLV 条目2-4] | ||
0x___ |
12B | key = uniquepid\0\0\0 |
TLV 条目2 |
0x___ |
12B | key = UTOK\0\0\0\0\0\0\0\0 |
TLV 条目3 |
0x___ |
12B | key = Nonces\0\0\0\0\0\0 |
TLV 条目4 |
| ... | 2B | total_len = 0000 |
链表终止 |
0x298 |
4B | valid_flag = 0x00000001 |
RBE唯一检查 |
0x29C |
28B | tail | 未知 |
0x2B8 |
7496B | unused | BUP VFS 可访问 · RBE 不读 |
Intel-SA-00086 定位
这里我定位了一下 Intel-SA-00086 公开泄露的这个位置:
传统的一个溢出漏洞:


核心就是这个数组,这个实际上是一个CT文件的缓冲区,但是没有对CT文件进行校验吗,所以可以构造畸形CT文件来让他直接溢出,当我们的V12超过100的时候这个数组就溢出了。
CT文件格式如下:
偏移 内容
┌───────┬──────────────────────────────────────┐
│ 0x00 │ 00 00 00 00 00 00 ← 6 bytes 保留 │
│ 0x06 │ C8 00 ← uint16 = 200 │ ← 畸形的关键!
├───────┼──────────────────────────────────────┤
│ 0x08 │ Record 0 (DFX 写入) │ 安全区
│ 0x10 │ Record 1 (DFX 写入) │
│ ... │ ... │ v13[0..799]
│ 0x320 │ Record 99 (最后一个安全 record) │
├───────┼──────────────────────────────────────┤
│ 0x328 │ Record 100 → 覆写 cookie │ 越界!
│ 0x330 │ Record 101 → 覆写 saved esi + ebp │
│ 0x338 │ Record 102 → 覆写 return address │
│ ... │ ... │
│ 0x648 │ Record 199 │
└───────┴──────────────────────────────────────┘
这个函数就是我们的从缓冲区拷贝函数:

这里的v11和v12实际上可以认为是一个结构体
typedef struct {
uint8_t reserved[6]; // +0x00
uint16_t num_records; // +0x06
ct_record_t records[]; // +0x08
} ct_file_t;

但是这个里面存在一个巨大的问题也是无法在酷睿上在这里操作的原因,就是有Cookie,在kaokao选择的TXE平台实际上Cookie可以利用Tls机制绕过,但是我们这个没有办法,我们的cookie是直接从Rom里面生成的,无法避免。
这里我贴一个我整理的分析链路感兴趣的自己可以去看看:
ROM (mask ROM, 不可提取)
│
├─ 早期初始化: 在 SRAM 预填充进程上下文表 @ 0x97B58
│ 每个表项 +8 处包含该进程的 cookie 值 (随机/派生)
│
├─ 0xFFF810E7: ROM 函数, kernel 调用后返回表索引 (0~31)
│
└─ 0xFFFF85FA: __security_check_cookie, cookie 不匹配时调用
Kernel (0x80000)
│
├─ loc_E54D → call ROM 0xFFF810E7 → 取索引 → 查表 → 返回上下文
├─ sub_60DE → push [上下文+8] → call sub_23B2
└─ sub_23B2 → mov [0x100C], eax
BUP (0x20000)
│
└─ CT handler: mov eax, [0x100C]
一个失败的ROP:
import struct
HEADER_SIZE = 8
RECORD_SIZE = 8
SAFE = (0x334 - HEADER_SIZE) // RECORD_SIZE # 101
DID_PHASE_VA = 0x35560 # sub_35560: SVC(state=0x19F) -> DCI enable
DCI_VAL = 0x1010 # DCI activation value
COOKIE_GUESS = 0 # must match SRAM [0x100C]
BSS_SAFE = 0x84200 # zero-initialized, fake ebp
def record(ty, off20, val):
return struct.pack('<II', (ty << 24) | (off20 & 0xFFFFF), val & 0xFFFFFFFF)
recs = []
recs += [record(2, 0x00E0, 0x01000000)] # DFX latch
recs += [record(2, 0x0010, 0x00000888)] # DCI bits
recs += [record(2, 0x0003, 0x00000000)] # TraceHub
recs += [record(0, 0, 0) for _ in range(3, SAFE)]
# overflow: [ebp-0xC]=cookie, [ebp-8]=ebx, [ebp-4]=esi, [ebp]=ebp, [ebp+4]=ret, [ebp+8]=arg
recs += [struct.pack('<II', COOKIE_GUESS, 0)]
recs += [struct.pack('<II', 0, BSS_SAFE)]
recs += [struct.pack('<II', DID_PHASE_VA, DCI_VAL)]
payload = struct.pack('<6sH', b'\x00'*6, len(recs)) + b''.join(recs)
open('payload_rop.bin', 'wb').write(payload)
最后的效果
由于只解锁了物理链接的端口限制,所以我们发送的命令CPU会相应,但是很多命令是拒绝的。

总结
感谢您看到这里了。如果你看到这里觉得好玩,或者对你有帮助请点个赞,有啥建议或者想说的可以留言,文章不做任何限制要求。后续可能还会继续研究,如果有其他进展我在继续发。其他师傅有什么想法可以在评论区一起讨论一下。
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!