Author:Sandman
Date:2013.11.20
前几天一个做硬件的朋友向我求助,他开发的一款磁盘数据加密卡在测试的过程中出现了兼容性问题。通过调试发现问题是由软件的问题造成的。通过一下午的调试,最终使用CPU提供的调试机制完美的解决了兼容性问题。希望本文可以对相关开发人员有所帮助。
PS:虽然本文标题含有“PCI、ROM”相关字样,但是本文不涉及复杂的数字电路理论,解决问题的过程全部是由软件代码完成的,请读者不必恐慌...
出现兼容性问题硬件的故障现象:部分机型插卡开机以后,扩展卡初始化界面没有出现,直接进入OS。但是在OS中可以看到安装的新硬件。
出现故障的机型:
1.以联想天开S4250为代表的联想OEM品牌机
2.HP DC5700等机型
3.部分H61、Q75主板的兼容机
出现兼容性问题硬件的基本原理:
问题硬件基于PCI扩展ROM机制,利用在开机引导过程中PCI扩展ROM中的代码会被BIOS执行的特性,挂钩INT19h拦截OS引导,并在INT19h的中断钩子中挂钩INT13h,INT13h钩子做OS读盘时的数据解密工作。PC的引导过程可以概括为如下6点:
1.开机,BIOS代码被加载至F000h:0000h处执行;BIOS POST自检
2.初始化显示设备
3.初始化外围硬件设备(PCI扩展ROM会在此时执行)
4.内存测试、设备测试等过程
5.调用INT19h加载OS
6.INT19h调用INT13h读取引导磁盘的0磁道0柱面1扇区(MBR)到0000h:7C00h处,并jmp far 0000h:7C00h跳转至MBR所在内存,最终MBR完成指定OS的加载。
在PCI规范中提供了一种机制,使PCI设备可以带一个扩展ROM。通过执行ROM中存放的代码来完成与设备有关的初始化的操作,同时也有可能辅助系统的引导功能,例如挂接中断等操作。问题硬件就利用了这个特性挂钩INT19H,抢在OS加载前获取执行权限。很多还原卡也是利用了这个特性抢在操作系统之前执行。
但是,执行扩展ROM的任务是由BIOS来完成的,不同的主板厂商使用的BIOS不同,极少数的主板甚至不会去执行扩展ROM中的代码。因此就要考虑一个问题:我们的扩展ROM代码会不会被BIOS执行?如果答案是否定的话,解决这个问题的办法只有一个:联系主板厂商升级BIOS。
如果ROM代码被部分执行了,只是在某个阶段出现了问题,这样就要通过调试的方式来解决。大厂商可以使用类似Arium ECM-XDP3这样的硬件调试器来进行实时调试。但是由于本人没有硬件调试器,所以只能使用黑箱排查的方式一步步寻找问题所在。
本文的调试过程基于WCH CH360S扩展ROM调试板,如图所示。
其中J1是外部输入引脚引出,可以通过读写I/O寄存器读出其状态。
根据CH360S的DataSheet,J1引脚为通用输入引脚,内置上拉电阻,默认为高电平,若将J1短接,则GPI1引脚接地,输入为低电平。可以利用这个特性在扩展ROM加载时作判断,如果被短接则不执行后面的代码。这样做的目的是为了方便调试,防止由于程序的错误导致无法进入系统。
在ROM的初始化代码段中,我加入了向屏幕打印输出INT19h中断地址的代码,同时HOOK INT19h中断,在HOOK以后再次打印INT19h中断地址的代码查看是否挂钩成功。HOOK处理例程中设置一个死循环,造成一个黑屏死机的假象,证明我们的INT19h被BIOS调用了。详细代码如下。
BITS 16
CONST_VID EQU 1A00H
CONST_DID EQU 8088H
ROM_START:
ROM_HEADER:
DB 55H ;; 00H: SIGNATURE 1
DB 0AAH ;; 01H: SIGNATURE 2
DB (ROM_END-ROM_START) / 512 ;; 02H: IMAGE_SIZE
JMP ROM_ENTRY ;; 03H: JUMP INSTRUCTION
times 12H DB 00H ;; 06H: PADDING
DW (PCI_DATA_STRUC-ROM_START) ;; 18H: PCI_DATA_STRUC POINTER
DW 0000H
PCI_DATA_STRUC:
DB 'PCIR' ;; 00H: device flag
DW CONST_VID ;; 04H: vendor ID
DW CONST_DID ;; 06H: device ID
DW 0000H ;; 08H: VPD pointer
DW 0018H ;; 0AH: PCI_DATA_STRUC's length
DB 00H ;; 0CH: PCI_DATA_STRUC's version
DB 00H,80H,01H ;; 0DH: device type.Class Code
DW (ROM_END-ROM_START) / 512 ;; 10H: Image length
DW 0000H ;; 12H: the version of the code data
DB 00H ;; 14H: the code type of ROM
DB 80H ;; 15H: the flag used to make sure if or not the last ROM image
DW 0000H ;; 16H: Reserved
Int19h_Hook_Hanlder:
jmp $ ;卡在这里,证明INT19H确实被执行了
int 89h
iret
Install_Int19h_Hook:
push ds
xor ax,ax
mov ds,ax
mov eax,[ds:(19h*4)]
mov [ds:(89h)*4],eax
mov word [ds:(19h*4)],Int19h_Hook_Hanlder
mov word [ds:(19h*4)+2],cs
pop ds
ret
ROM_ENTRY:
push ax ;AX=由BIOS传来的总线号/设备号/功能号
pusha
mov bx,ax
mov ax,0b109h
mov di,PC_BASE_ADDR ;读取扩展ROM卡的IO地址
int 1ah
and cx,0FFF0h
mov dx,cx
mov dl,CH367_GPIR
in al,dx ;读取J1引脚的状态,如果被短接则退出
and al,02h
jz Exit_Init_Rom
call PrintInt19h ;打印初始的INT19H地址
call Install_Int19h_Hook ;安装INT19h钩子
call PrintInt19h ;打印修改后的INT19H地址
Exit_Init_Rom:
popa
pop ax
retf
%include "CH360DEF.ASM"
%include "PCIE_CFG.ASM"
%include "ShowIVT.asm"
times 32*1024-($-$$) db 0
ROM_END:
以上代码使用NASM编译,编译后写入调试卡,插卡开机后的结果如下图所示:
可以看到,原始INT19h的地址为F000:E6F2 我们的INT19h处理例程为:CC80:0034
但是,在显示延时结束后,并没有达到造成黑屏死机的效果,而是直接进入了OS引导过程。难道这些主板不是用INT19h进行启动的(UEFI主板我开启了CSM支持)?为了证实这个问题,我编写了一个简单的驱动程序,映射物理地址0~0x400(IVT所在的物理地址内存区域),将BIOS的IVT打印输出。代码如下:
PULONG MapPhysicalMemory(ULONG StartAddress,ULONG RegionSize)
{
PHYSICAL_ADDRESS PhyAddr;
PhyAddr.HighPart = 0;
PhyAddr.LowPart = StartAddress;
return MmMapIoSpace(PhyAddr,RegionSize,FALSE);
}
VOID UnMapPhysicalMemory(PVOID StartAddress,ULONG RegionSize)
{
MmUnmapIoSpace((ULONG)StartAddress,RegionSize);
}
VOID PrintInterruptVectorTable(PULONG IVTBuffer)
{
ULONG i;
USHORT Segment;
USHORT Offset;
for(i=0;i<0xFF;i++)
{
Segment = (USHORT)((IVTBuffer[i] & 0xFFFF0000) >> 16);
Offset = (USHORT)(IVTBuffer[i] & 0xFFFF);
DbgPrint("INT 0x%.2X:%.4X:%.4X\n",i,Segment,Offset);
}
}
VOID DriverUnload(IN PDRIVER_OBJECT pDriverObj)
{
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pRegistryString)
{
PULONG IVTBuffer;
IVTBuffer = MapPhysicalMemory(0,0x100000);
if(IVTBuffer)
{
DbgPrint("Mapped IVT:%.p\n",IVTBuffer);
PrintInterruptVectorTable(IVTBuffer);
UnMapPhysicalMemory(IVTBuffer,0x100000);
}
pDriverObj->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
下图为输出结果
结果非常出人意料,竟然INT19h的处理函地址又变回了F000:E6F2!!难怪后续的初始化不成功,原来是BIOS在某个时刻又重载了INT19h的处理函数地址。
如果INT19h会被重载,那我们应该如何在OS加载以前获取控制权限?
我把还把思路放回到中断上。继续在ROM代码的执行过程中尝试挂钩INT 13h,INT 15h,INT18h等开机必执行的中断,结果发现这些关键的中断处理例程都会在操作系统启动前的某一个时刻被一起重载,难道BIOS会重载整个IVT?
为了验证这个猜想,我想到了漏洞挖掘中FUZZ的思路:写一段代码,在ROM执行的过程中,从0地址开始,按照16字节的长度,将IVT中的项目填0,然后进入到Windows中查看那些地址被重载了,挑出从那些没有被重载的中断,查阅相关手册,看那些可以利用。代码如下:
xor ax,ax
mov es,ax
mov di,ax
mov cx,8
cld
rep stosw
经过一次的填充发现,INT 0 INT1 INT2 INT3中断不会被重载。我立刻把目光转向INT 1中断。INT 1是x86构架CPU用于处理调试异常的中断,用于响应单步异常和硬件断点。
这是我有了一个想法:设置硬件断点到0x7C00,装入我们自己的INT 1处理例程,在硬件断点被触发时装入INT13h钩子后删除硬件断点,返回到MBR继续执行,让MBR加载操作系统,此时硬件的中断钩子也随之就生效了。同时保持原有的INT19h钩子不变,用于兼容老系统。这样既达到了在操作系统加载前获取执行权限,又可以最小限度的修改产品的代码。
实现代码如下:
Install_HW_BreakPoint:
pushf
push ds
push eax
xor eax,eax
mov ds,ax
mov word [ds:(1h*4)],Int1h_BreakPoint_Handler ;安装INT 1h钩子
mov word [ds:(1h*4)+2],cs
mov ax,7C00h
mov dr0,eax ;设置DR0=0x7C00
mov eax,dr7
bts eax,1
mov dr7,eax ;启用DR0断点;TYPE:EXCUTE LEN=1 BYTE
pop eax
pop ds
popf
ret
Clear_HW_BreakPoint:
pushf
push eax
mov eax,dr7
btr eax,1 ;取消DR0断点
mov dr7,eax
xor eax,eax
mov dr0,eax ;DR0清零
pop eax
popf
ret
Int1h_BreakPoint_Handler:
pusha
call Setup_Self
popa
iret
Setup_Self:
call Clear_HW_BreakPoint ;取消断点
;这里可以安装INT13h钩子或者做一些其它的事情
ret
Install_Int19h_Hook:
push ds
xor ax,ax
mov ds,ax
mov eax,[ds:(19h*4)]
mov [ds:(89h)*4],eax
mov word [ds:(19h*4)],Int19h_Hook_Hanlder
mov word [ds:(19h*4)+2],cs
pop ds
ret
Restore_Int19h_Hook:
push ds
xor ax,ax
mov eax,[ds:(89h)*4]
mov [ds:(19h)*4],eax
pop ds
ret
Int19h_Hook_Hanlder:
pusha
call Setup_Self
pushf
cli
call Restore_Int19h_Hook
popf
popa
int 89h
iret
ROM_Entry:
pushf
cli
call Install_Int19h_Hook ;安装INT 19h钩子
call Install_HW_BreakPoint ;设置硬件断点
popf
将修改后的代码写入到板卡中,开机测试,产品的一切功能全部正常。同时利用本方法,可以一次性完美解决上面提到的那些硬件的兼容性问题。下图为加密卡成功挂钩INT13h的截图
[课程]FART 脱壳王!加量不加价!FART作者讲授!
上传的附件: