技术原理:
IOAPIC是可以用于多个核心CPU的新型中断控制器,可以通过编程来对其控制,可以理解为可编程硬件。根据《寒江独钓》的说明,目前不管是单核还是双核都拥有IOAPIC机制。
对于IOAPIC的简单处理过程,我的理解如下(如果有错误,请路过的大牛一定要指正啊!!):

其中,上面用黄色笔做了标记的就是这个技术的核心。
因为硬中断IRQ最终都会交给中断处理程序进行,而Windows会根据中断向量号来转移到相应的中断处理程序。因此,就必然存在IRQ与中断向量号的映射关系。其实,Windows在初始化系统服务的时候就在IOAPIC中描述好这种映射关系了,注意我在上面画的图中,橙色标注的部分。
所以,整个技术的核心就修改在IOAPIC中,已经填写好的中断向量号。这样也就要求我们事先应该在IDT中插入一个中断门。
如何访问IOAPIC:
IOAPIC是通过读写其一系列的寄存器来实现编程控制的。根据《寒江独钓》提供的关于IOAPIC的Intel手册资料来看,要访问IOAPIC,不能直接使用它的内部寄存器,而是要通过2个内存映射寄存器来访问一个是IOREGSEL,一个是IOWIN。其中Intel手册的说明如下:

这两个内存映射寄存器的描述如下:

按照手册的说明是寄存器,但实际上是两个物理内存地址,而且是制定了格式的了,就是FEC0XY00H和FEC0XY10H,其中X,Y是自定义值,也指定了取值范围,就是X = 0-FH,Y = 0,4,8,CH。根据《寒江独钓》里的说明,Windows已经规定了,IOREGSEL:FEC00000H;IOWIN:FEC00010H。
因为这里谈的都是真正的物理地址,是不能够直接访问的,在Windows下访问则需要对物理地址进行一个映射(实际上就是添加一个页表项来描述这个映射关系),在wdk中提供了一个这样的映射函数:
PVOID
MmMapIoSpace(
IN PHYSICAL_ADDRESS PhysicalAddress,
IN SIZE_T NumberOfBytes,
IN MEMORY_CACHING_TYPE CacheType
);
经过映射后,我们就可以对其进行操作了。
首先是IOREGSEL映射寄存器。这个寄存器的作用是:让你指定要访问哪一个IOAPIC的内部寄存器。可以指定的内部寄存器有:

IOREGSEL映射寄存器的描述符如下:

可以看出只有低8位是有效位(在编程时候要注意的)。这里根据Intel手册的说明是,指定的是内部寄存器对应的地址偏移,为了方便理解,我们可以理解为编号,就是指定每个寄存器对应的编号。
第二个是IOWIN映射寄存器。这个寄存器的作用的作用是:显示IOREGSEL指定的内部寄存器的内容,你可以读取或修改IOWIN这个映射寄存器的值,然后映射机制会修改真正的IOAPIC内部寄存器的值。
IOAPIC的各内部寄存器对应的描述如下(一下资料均来自intel手册):
IOAPICVERIOAPIC VERSION REGISTER:

IOAPICVERIOAPIC VERSION REGISTER:

IOAPICARBIOAPIC ARBITRATION REGISTER:

IOREDTBL[23:0]I/O REDIRECTION TABLE REGISTERS:
这就是重定向表,每个IRQ与中断向量号的映射关系就记录在里面的0~7这个位段,这个表是一个数组,24项,也就说明了为什么在《寒江独钓》中说是有24个可编程的IRQ。

每个IRQ描述项都是8个字节,64位,描述如下(其实无必要,只是作为资料):



所以,我们要做的就是将0~7这个位段修改成我们的预先插入的中断处理的向量号。
参考了上面资料后就开始编程:
一些数据结构的定义:
#define IOAPIC_REGSEL_PHYSICAL_ADDRESS 0xfec00000
#define IOAPIC_WIN_PHYSICAL_ADDRESS \
IOAPIC_REGSEL_PHYSICAL_ADDRESS + 0x10
//根据中断描述表来定义个中断门的结构
typedef struct _INTGATE
{
USHORT OffsetLow;
USHORT Selector;
UCHAR Reserved; //这个与是保留值,需要设置成NULL
UCHAR Attributes;
USHORT OffsetHigh;
}INTGATE,*PINTGATE;
NTSTATUS
InsertNewInterruptRoutine(PVOID pNewRoutine,PULONG pulInterruptVectorNumber)
{
NTSTATUS ReturnStatus;
ULONG ulInterruptVectorNumber;
PVOID pInterruptTableAddress;
PINTGATE pstNewIntGate;
//检测参数的合法性
if(!MmIsAddressValid(pNewRoutine))
return STATUS_INVALID_PARAMETER;
//获取idt表的地址
ReturnStatus = GetIdtServiceTableAddress(&pInterruptTableAddress);
if(!NT_SUCCESS(ReturnStatus))
return ReturnStatus;
//枚举idt表
for(ulInterruptVectorNumber=1;ulInterruptVectorNumber<INT_VECTOR_MAX_COUNT+1;ulInterruptVectorNumber++)
{
//判断中断门是否已经存在
if(!IsInterruptVectorExisted(pInterruptTableAddress,ulInterruptVectorNumber))
break;
}
if(ulInterruptVectorNumber > INT_VECTOR_MAX_COUNT)
return STATUS_UNSUCCESSFUL;
//保存中断向量号
*pulInterruptVectorNumber = ulInterruptVectorNumber;
//填写中断门结构
pstNewIntGate = &((PINTGATE)pInterruptTableAddress)[ulInterruptVectorNumber];
pstNewIntGate->Reserved = NULL;
pstNewIntGate->Selector = 8; //在内核中的段选择子
pstNewIntGate->Attributes = 0x80 | 0x08 | 0x06;
pstNewIntGate->OffsetLow = (USHORT)pNewRoutine;
pstNewIntGate->OffsetHigh = (USHORT)((ULONG)pNewRoutine >> sizeof(USHORT)*8);
return STATUS_SUCCESS;
}
BOOLEAN //判断中断门是否存在
IsInterruptVectorExisted(PVOID pIdtAddress,ULONG ulInterruptVectorNumber)
{
PINTGATE pstIntGate;
//检测idt地址是否有效
if(!MmIsAddressValid(pIdtAddress))
return STATUS_INVALID_PARAMETER;
pstIntGate = &((PINTGATE)pIdtAddress)[ulInterruptVectorNumber];
//判断中断门的P位,也就是判断该调用们是否有效
if(pstIntGate->Attributes & 0x80)
return TRUE;
return FALSE;
}
//获取IDT表的地址
NTSTATUS
GetIdtServiceTableAddress(PVOID pIdtServiceTableAddressPointer)
{
USHORT wSelectorAndOffset[3];
ULONG ulUshortSize;
ulUshortSize = sizeof(USHORT);
__asm
{
pushad;
pushfd;
sidt [wSelectorAndOffset];
lea esi,dword ptr [wSelectorAndOffset];
add esi,[ulUshortSize]; //将指针移到段内偏移值上
mov ebx,[pIdtServiceTableAddressPointer];
mov eax,dword ptr [esi];
mov dword ptr [ebx],eax;
popfd;
popad;
}
if(!(*(PULONG)pIdtServiceTableAddressPointer)) //如果获取失败的话,就返回不成功
return STATUS_UNSUCCESSFUL;
return STATUS_SUCCESS;
}
然后就是修改IOAPIC的重定位表:
NTSTATUS
ChangeInterruptVectorNumber(ULONG ulIrqVectorNumber,ULONG ulNewNumber,PULONG pulPerviousNumber)
{
PHYSICAL_ADDRESS TempPhysicalAddress;
PUCHAR pIoRegSelAddress;
PULONG pIoWinAddress;
//检查指针地址是否合法
if(!MmIsAddressValid(pulPerviousNumber))
return STATUS_INVALID_PARAMETER;
RtlZeroMemory(&TempPhysicalAddress,sizeof(TempPhysicalAddress));
TempPhysicalAddress.LowPart = IOAPIC_REGSEL_PHYSICAL_ADDRESS;
pIoRegSelAddress = (PUCHAR)MmMapIoSpace(TempPhysicalAddress,0x10+sizeof(ULONG),MmNonCached);
//如果映射失败的话,就返回
if(!MmIsAddressValid(pIoRegSelAddress))
return STATUS_UNSUCCESSFUL
pIoWinAddress = (PULONG)(pIoRegSelAddress + 0x10); //IOWIN的映射地址
//指定获取IRQ1的项,参考intel手册
*pIoRegSelAddress = 0x10 + (UCHAR)ulIrqVectorNumber * sizeof(UCHAR)*2;
//保存原来的向量号
__asm
{
mov eax,[pIoWinAddress];
movzx eax,byte ptr [eax];
push eax;
mov eax,[pulPerviousNumber];
pop dword ptr [eax];
}
//设置新的中断向量号
__asm
{
mov eax,[pIoWinAddress];
mov ecx,[ulNewNumber];
mov byte ptr [eax],cl;
}
//解除地址映射
MmUnmapIoSpace(pIoRegSelAddress,0x10+sizeof(ULONG));
return STATUS_SUCCESS;
}
编写一个总的调用函数:
NTSTATUS
HookIOAPIC(
ULONG ulIrqVectorNumber,
PVOID pNewRoutine,
PULONG pulNewInterruptVectorNumber,
PULONG pulPerviousVectorNumber
)
{
NTSTATUS ReturnStatus;
ReturnStatus = InsertNewInterruptRoutine(pNewRoutine,pulNewInterruptVectorNumber);
if(NT_SUCCESS(ReturnStatus))
{
ReturnStatus = ChangeInterruptVectorNumber(ulIrqVectorNumber,*pulNewInterruptVectorNumber,pulPerviousVectorNumber);
if(!NT_SUCCESS(ReturnStatus)) DeleteInterruptRoutine(*pulNewInterruptVectorNumber);
}
return ReturnStatus;
}
NTSTATUS
UnHookIOAPIC(ULONG ulIrqVectorNumber,ULONG ulCurrentInterruptVectorNumber,ULONG ulOldInterruptVectorNumber)
{
NTSTATUS ReturnStatus;
ULONG ulTempNumber;
ReturnStatus = ChangeInterruptVectorNumber(ulIrqVectorNumber,ulOldInterruptVectorNumber,&ulTempNumber);
if(NT_SUCCESS(ReturnStatus))
{
ReturnStatus = DeleteInterruptRoutine(ulCurrentInterruptVectorNumber);
}
return ReturnStatus;
}
NTSTATUS //删除中断门
DeleteInterruptRoutine(ULONG ulInterruptVectorNumber)
{
NTSTATUS ReturnStatus;
PVOID pInterruptTableAddress;
PINTGATE pstIntGate;
//获取idt表的地址
ReturnStatus = GetIdtServiceTableAddress(&pInterruptTableAddress);
if(!NT_SUCCESS(ReturnStatus))
return ReturnStatus;
pstIntGate = & ((PINTGATE)pInterruptTableAddress)[ulInterruptVectorNumber];
if(!MmIsAddressValid(pstIntGate))
return STATUS_INVALID_PARAMETER;
RtlZeroMemory(pstIntGate,sizeof(INTGATE));
pstIntGate->Selector = 8;
return STATUS_SUCCESS;
}