‘壹’ 如何提取BIOS固件中的代码
2 逆向系统管理中断(SMI)句柄
或许有人认为应该动用“硬件分析器”来参与我们的宏伟计划,其实这是一种误解。SMI处理程序是BIOS固件的一部分,和普通的BIOS代码一样,也可以类似的从中将其反汇编出来。Pinczakko的《BIOS Disassembly Ninjutsu Uncovered 》和《Guide to Award BIOS Reverse Engineering》二书,讲述了Award and AMI BIOS逆向方面的详细情况,有兴趣的读者可以到此参考。在这里,从BIOS中转储SMI处理函数,我们有两种方法可供选择。
1. 找一个漏洞,从保护模式进入SMRAM,并将SMRAM中的所有内容,尤其是TSEG,High SMRAM和0xA0000-0xBFFFF等区域转储出来。如果BIOS没有锁定D_LCK位,可以通过Duflot和BSDaemon介绍的修改SMRAMC PCI配置寄存器的方式转储。万一BIOS锁定了SMRAM,BIOS固件看上去余简也无懈可击,那就只有修改BIOS,令其不会设置D_LCK位这一条路了。将修改的程序重刷回BIOS的ROM,这样在启动时,SMRAM就不会被锁定了。不过这样做首先要确定BIOS不需要数字签名(digitally signed),而且目前几乎没有主板会使用带数字签名的非EFIBIOS固件。
我想在这里有必要提一下BIOS设置D_LCK位的方法。通常情况下,BIOS都倾向于使用0xCF8/0xCFC端口,通过合法的I/O访问,设置相关的PCI配置寄存器。BIOS首先将0x8000009C写入到0xCF8的地址端口,然后再将0x1A的数值写入到0xCFC的数据端口,设洞毁纯置SMRAMC寄存器就可以锁定SMRAM了。
2. 还有另外一种方法,相对前者来说要简单一些,不需要访问运行时的SMRAM数据。
2.1 从BIOS开发商的网站下载最新的,或者使用闪存编程器件(Flash Programmer)从BIOS的ROM中提取固件的二进制代码。我们针对的ASUS P5Q主板就是要下载P5Q-ASUS-PRO-1613.ROM文件。
2.2 大多数的BIOS固件都包含了主BIOS模块,压缩的SMI处理句柄就位于其中,利用开发商提供的提取/解压工具打开BIOS主模块。由于ASUS BIOS是基于AMI BIOS的,我们使用AMIBIOS BIOS Mole Manipulation Utility 和MMTool.exe从中抽取主模块。在MMTool中打开下载的.ROM文件,单击“Single Link Arch BIOS”抽取模块(ID=1Bh),然后检查“In uncompressed form”的选项,最后保存,就得到了包含SMI处理句柄的主BIOS模块。
2.3 主BIOS模块抽取完成,就可以用HIEW或IDA Pro等工具开始我们的SMI反汇编之旅了。
反汇编SMI句柄
我们注意到在ASUS/AMI BIOS中使用的是一个结构体数组来描述SMI的处理函数。数组中的每一个入口项都有“$SMIxx”的签名,其中“xx”字符指明了具体的SMI处理函数。图1显示的是基于P45芯片组ASUS P5Q SE主板的AMIBIOS 8所使用的SMI分配表(SMI dispatch table)数据。
反汇编SMI分配函数
BIOS中有一个特殊的SMI分配函数,我们将其命名为“dispatch_smi”,将遍历分配表中的所有入口,并调用handle_smi_ptr指向的处理函数。如果没有任何处理函数能够响应当前的SMI中断信号,它将调用最后的$DEF例程。下面的代码就是我们从ASUS P5Q主板反汇编得到纳咐的Handle_SMI BIOS函数。
钩挂SMI处理函数
基于上述的讨论,钩挂SMI处理函数的方法也有很多,可以添加一个新的SMI处理句柄,也可以给已有的句柄打个补丁,加上新的功能。两种方法在本质上并没有多大区别,所以两种情况我们都将做必要的介绍。
1.在SMI分配表中添加我们自己的SMI处理函数。
要加入新的函数,必须先在分配表中建立一个新的入口表项,我们将其取名为“$SMIaa”,如图3所示。
该入口包含了指向默认SMI处理句柄的指针,待会儿我们将修改这个默认的处理函数。或者我们也可以在SMRAM中找一块空闲区域,放上一段shellcode,将指向默认句柄的指针替换为指向shellcode。最后还要注意,当有新的处理函数加入到分配表中之后,保存在SMRAM数据段中的SMI代码计数值也要相应的加1,保持SMI处理函数数目的一致性。
上述这个“调试”处理函数只做了一件事情,就是将SMI分配表从0x0B428:[si]拷贝到0x07000:[di]的位置,看来我们可以放心大胆的用自己的SMI代码钩挂它了。随后,我们将实现一个键盘记录程序,将其注入到这个处理函数内部。不过在我们开始新的章节之前,还是有必要先来回顾一下可以用于在用户击键时,调用记录程序的相关技术。
1. 使用I/O APIC将键盘的硬件中断(IRQ #01)导向SMI。Shawn Embleton和Sherri Sparks采用的就是I/O高级可编程中断控制器,将键盘的IRQ #01号中断导向SMI,并在SMI处理程序中捕获击键事件。
2. 采用键盘控制器数据端口访问时的I/O陷阱机制。
我们在本文中使用了不同于前者的I/O陷阱技术,该项技术最初是BIOS模拟PS/2键盘的用途,在下一章节中我们将详细的解释其工作原理。
3 SMM键盘记录程序
3.1 硬件I/O陷阱机制
实现一个内核级的键盘记录程序,方法之一是钩挂中断描述符表(IDT)中的调试陷阱#DB处理函数,并设置调试寄存器DR0-DR3,用数据端口0x60捕获系统的击键事件。类似的,我们也可以采用通过键盘I/O端口60/64陷入SMI的方法。我们参考了AMI BIOS的设计白皮书《USB Support for AMIBIOS8》,里面有这样的一段叙述。
“2.5.4 60/64端口模拟(emulation)
该选项可以开启或者关闭60h/64h端口的陷阱功能。60h/64h端口陷阱允许BIOS为USB键盘和鼠标提供基于PS/2的完全支持,在Microsoft Windows NT操作系统和支持多语言键盘上尤为有用。该选项还为USB键盘提供了诸如键盘锁定,密码设置和扫描码选择等各项PS/2键盘的功能。”
该机制由硬件系统来完成,所以我们还要查看一下具体的硬件配置情况。好在Intel和AMD的CPU中都有I/O陷阱的相关机制。AMD开发手册《BIOS and Kernel's Developer's Guide for AMD Athlon 64 and AMD Opteron Processors》中的“SMM I/O Trap and I/O Restart”一节,和Intel手册《Intel IA-32 Architecture Software Developer's Manual》中的“I/O State Implementation and I/O INSTRUCTION RESTART”一节对该机制都有详细的介绍。
I/O陷阱机制允许陷入SMI后,在SMI处理程序内部使用IN和OUT指令来访问系统的任意I/O端口。之所以设计该机制的目的是为了在断电时,通过I/O端口来开启(power on)某些设备。除此之外,I/O陷阱当然也可以用于在SMI句柄中模拟60h/64h的键盘端口。在某种程度上说,它和上述的调试陷阱机制有些类似,用陷阱捕获对I/O端口的访问,但是并非调用OS内核的调试陷阱处理句柄,而是产生一个SMI中断,让CPU进入SMM模式,执行I/O陷阱的SMI处理函数。
当处理器陷入I/O指令,进入SMM模式时,它会将I/O指令陷入时的所有信息保存在SMM存储状态映射区(Saved State Map)的I/O状态域(I/O State Field)中,位于SMBASE+0x8000+0x7FA4的位置。图4是该区域的数据分布情况,待会儿我们的记录程序将会用到。
-设置了IO_SMI (bit 0),表示当前是一个I/O陷阱SMI。
- I/O 长度标志(bits [1:3])表示 I/O访问是byte(001b)、word(010b)或dword(100b)三者之一。
- I/O Type标志(bits [4:7])表示I/O指令的类型,IN imm(1001b),IN DX(0001b)等。
- I/O 端口(bits [16:31]),包含了当前访问的I/O端口号。
如果当前是通过IN DX指令,字节宽度来访问0x60端口, IO_SMI置位,SMM keylogger首先需要检测和更新SMM存储状态映射区中的EAX域,然后还要检测0x7FA4处的I/O状态域的值是否为0x00600013。
mov esi, SMBASE
mov ecx, dword ptr fs:[esi + 0xFFA4]
cmp ecx, 0x00600013
jnz _not_io_smi
上述是检测的简化形式,SMM keylogger还需要检测I/O状态域中I/OType和I/O Length等其他标志。因为我们是记录键盘的目的,所以只关心I/O陷阱,并不用理会I/O重启(I/O Restart)的相关设置。I/O重启和I/O陷阱构成了完整的SMI I/O处理方式,当SMM中的SMI执行完毕时,I/O重启允许IN或OUT指令从SMI中断处恢复并继续执行。
I/O陷阱机制允许我们在任意I/O端口的软硬件交互读写操作时陷入SMI的处理例程,现在关心的只是0x60数据端口,实现键盘击键时的I/O陷入的具体步骤如下:
1. 击键事件发生时,键盘控制器产生一个硬件中断,用I/O APIC将IRQ 1中断信号导向SMI的处理句柄。
2. 收到键盘中断之后,APIC调用IDT中的键盘中断处理程序,对PS/2键盘是0x93号中断向量。
3. 键盘中断处理程序通过端口0x60从键盘控制器的缓冲区中读取按键扫描码。正常情况下将清空扫描码,并将其显示在屏幕上。
4. 此时芯片组引起端口0x60的读取陷阱,产生信号通知I/O陷阱SMI。
5. 在SMM模式下,keylogger的SMI处理句柄响应SMI中断,处理I/O陷阱SMI。
6. 退出SMM时,keylogger的SMI处理句柄将结果(当前扫描码)返回给0x60端口的读取指令,交由内核的中断句柄作进一步处理。
上述的第6步操作,将扫描码返回到OS的键盘中断处理程序,在I/O陷阱和I/O APIC下的实现是有区别的。如果使用了APIC来触发SMI,SMI keylogger必须再次向键盘控制器的缓冲区中填充扫描码,以待操作系统再次读取,做进一步处理。
I/O陷阱下则是采用另外的方法。OS键盘中断处理程序使用的“IN al, 0x60”指令会引起SMM keylogger的I/O陷入,由于该IN指令产生了无穷的SMI陷入循环,将永远无法从SMM中恢复到原来的状态继续执行。此时,SMI句柄只要将IN指令的读取结果保存到AL/AX/EAX寄存器,表现得就像IN指令从来没有陷入过一样。
IA32体系,EAX寄存器位于SMRAM存储状态区偏移为0x7FD0的位置,即 SMBASE +0x8000+0x7FD0,在IA64下为SMBASE + 0x8000+0x7F5C。因此当上述的IO_SMI置位时,SMM keylogger需要将从0x60端口读取的扫描码更新至EAX域,下面就是更新EAX域的代码片段:
; 1.验证读取0x60端口时设置的IO_SMI位
; 2.更新SMM存储状态区的EAX域(SMBASE + 0x8000 + 0x7FD0)
mov esi, SMBASE
mov ecx, dword ptr fs:[esi + 0xFFA4]
cmp ecx, 0x00600013
jnz _not_io_smi
mov byte ptr fs:[esi + 0xFFD0], al
3.5 多处理器下的keylogger说明
我们的keylogger已经更新了系统SMRAM存储状态映射区中的EAX(RAX)寄存器,要是碰到了多处理器系统那又该怎么办呢?当多个逻辑处理器同时进入SMM模式时,它们在SMRAM中都要有自己的SMM存储状态映射区,这将由BIOS为每个处理器分配不同的SMRAM 基地址(SMBASE)来妥善解决,因此该项技术也被称为“SMBASE重定向”。
例如在双处理器系统中,两个逻辑处理器分别具有不同的SMBASE,SMBASE0和SMBASE0+0x300;第一个处理器的SMI处理句柄将从EIP = SMBASE0+0x8000处开始执行,而第二个则从EIP = SMBASE0+0x8000+0x300的地方开始;同理,它们各自的存储状态映射区也就分别位于(SMBASE0+0x8000+0x7F00)和(SMBASE0+0x8000+0x7F00+0x300)。
不只是0x300,BIOS也会为额外的处理器设置其他的SMBASE增量偏移。增量偏移虽然可变,但是其计算过程也不算复杂。在SMM存储状态映射区内部0x7EFC偏移处包含了一个SMM修正ID(Revision ID),对每个处理器来说都是同样的数值。例如SMM的修正ID可能为0x30100,在SMRAM中找到各处理器的修正ID,计算它们之间的差值也就得到了各SMBASE间的相对位移。
下面我们展示的是SMM keylogger在双处理器系统上的EAX更新代码。它将顺次检查I/O状态域是否和某个处理器的I/O陷阱匹配,确定的话则更新其SMM存储状态映射区中的EAX值。
; 在双处理器系统上更新EAX
mov esi, SMBASE
lea ecx, dword ptr [esi + SMM_MAP_IO_STATE_INFO]
cmp ecx, IOSMI_IN_60_BYTE
jne _skip_proc0:
mov byte ptr [esi + SMM_MAP_EAX], al
_skip_proc0:
lea ecx, dword ptr [esi + SMM_MAP_IO_STATE_INFO + 0x300]
cmp ecx, IOSMI_IN_60_BYTE
jne _skip_proc1:
mov byte ptr [esi + SMM_MAP_EAX + 0x300], al
_skip_proc1:
…
4 建议的检测方法
4.1 I/O陷阱机制检测
4.2 计时(timing)检测