IDT系统中断描述表以及绕过Xurtr检测的HOOK姿势
阅读原文时间:2023年07月11日阅读:1

什么是中断?

指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程。即在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止程序的执行转而处理这个新的情况的过程就叫做中断。

比如:除零(0号中断)、断点(3号中断)、系统调用(2e号中断)、以及异常处理等都会引发中断,所以自然需要相应的中断例程去进行处理。

这样操作系统就会用数据结构来维护这些中断例程,这个数据结构就是IDT(Interrupt Descriptor Table)。

中断描述表

IDT表的长度与地址是由CPU的IDTR寄存器来描述的。IDTR寄存器共有48位,高32位是IDT表的基地址,低16位是IDT的长度。

typedef struct _IDTR{
USHORT IDT_limit;
USHORT IDT_LOWbase;
USHORT IDT_HIGbase;
}IDTR,*PIDTR;

IDTR idtr;

__asm SIDT idtr;

可以通过以上SIDT指令可以读取IDTR寄存器。然后通过MAKEWORD宏把高位与地位组合起来就可以获得IDT表的基地址了。

简单来说,IDT表是一张位于物理内存的线性表,共有256个表项。在32位模式下,每个IDT表项的长度是8个字节(64 bit),IDT表的总长度是2048字节。

kd> r idtr
idtr=8003f400
kd> r idtl
idtl=000007ff

通过Windbg命令 r idtr、r idtl可以读取IDT表的基地址与边界。

如图可以清晰的看见每一个表项了,可是每一个表项8字节都代表什么意思呢?

IDT表中每一项也称为“门描述符”,之所以这样称呼,是因为IDT表项的基本用途就是引领CPU从一个空间到另一个空间去执行,每个表项好像是一个空间到另一个空间的大门。

IDT表中可以包含以下3种门描述符:

任务门描述符:用于任务切换,里面包含用于选择任务状态段(TSS)的段选择子。可以使用JMP或CALL指令通过任务门来切换到任务门所指向的任务,当CPU因为中断或异常转移到任务门时,也会切换到指定任务。

中断门描述符:用于描述中断例程的入口。

陷阱门描述符:用于描述异常处理例程的入口。

以下为三种门描述符的内存布局:

结构体定义为

typedef struct _IDTENTRY
{
unsigned short LowOffset;
unsigned short selector;
unsigned char retention:5;
unsigned char zero1:3;
unsigned char gate_type:1;
unsigned char zero2:1;
unsigned char interrupt_gate_size:1;
unsigned char zero3:1;
unsigned char zero4:1;
unsigned char DPL:2;
unsigned char P:1;
unsigned short HiOffset;
} IDTENTRY,*PIDTENTRY;

其中DPL代表描述符优先级,用于优先级控制,P是段存在标志,段选择子用来选择一个段描述符(LDT或GDT)偏移部分用来指定段中偏移。两者共同准确的定义一个内存地址,对于中断门和陷阱门,他们指定的就是中断或异常处理例程的地址,对于任务门它们指定的就是任务状态段地址。

也就是说段选择子提供一个所谓的段基地址,那么处理例程 = 段基址 + 段偏移。

那么HOOK方式就有两种了:更改段偏移或者更改段基址。

我们先忽略段选择子提供的段基址,先简单的认为门描述符提供的高16位和低16位的段内偏移就是该中断的处理例程(其实本来就是,因为32位操作系统中,已经弱化了段基址的概念,运用了平坦模型,也就是说段基址就是0)。

然后我们看看HOOK代码:

#ifndef CXX_IDTHOOK_H

include "IDTHook.h"

#endif

#define WORD USHORT
#define DWORD ULONG

ULONG g_InterruptFun = 0;

#define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) \
| ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))

NTKERNELAPI VOID KeSetSystemAffinityThread ( KAFFINITY Affinity );
NTKERNELAPI VOID KeRevertToUserAffinityThread ( VOID );

PULONG GetKiProcessorBlock()
{
ULONG* KiProcessorBlock = 0;

KeSetSystemAffinityThread(1); //使当前线程运行在第一个处理器上  

\_asm  
{  
    push eax  
    mov  eax,FS:\[0x34\]  
    add  eax,20h  
    mov  eax,\[eax\]  
    mov  eax,\[eax\]  
    mov  eax,\[eax+218h\]  
    mov  KiProcessorBlock,eax  
    pop  eax  
}

KeRevertToUserAffinityThread();

return KiProcessorBlock ; 

}
void PageProtectOn()
{
__asm{//恢复内存保护
mov eax,cr0
or eax,10000h
mov cr0,eax
sti
}
}

void PageProtectOff()
{
__asm{//去掉内存保护
cli
mov eax,cr0
and eax,not 10000h
mov cr0,eax
}
}

void _stdcall FilterInterruptFun()
{
DbgPrint("CurrentProcess : %s",(char*)PsGetCurrentProcess()+0x174);
}

_declspec(naked)
void Fake_InterruptFun()
{
_asm{
pushad
pushfd

    push fs  
    push 0x30  
    pop  fs

    call FilterInterruptFun;  
    pop  fs

    popfd  
    popad

    jmp g\_InterruptFun  
}  

};

NTSTATUS
DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pRegistryString)
{
IDTR Idtr;
PIDTENTRY pIdtEntry;
ULONG ulIndex = 0 ;
ULONG* KiProcessorBlock;

pDriverObj->DriverUnload = DriverUnload;

KiProcessorBlock = GetKiProcessorBlock();

DbgPrint("%X\\r\\n",KiProcessorBlock);

while (KiProcessorBlock\[ulIndex\])  
{  
    pIdtEntry = \*(PIDTENTRY\*)(KiProcessorBlock\[ulIndex\] - 0x120 + 0x38) ;

    DbgPrint("IDT Base:%X\\r\\n",pIdtEntry);

    g\_InterruptFun = MAKELONG(pIdtEntry\[3\].LowOffset,pIdtEntry\[3\].HiOffset);

    DbgPrint("InterruptFun3:%X\\r\\n",g\_InterruptFun);

     PageProtectOff();  
    pIdtEntry\[3\].LowOffset = (unsigned short)((ULONG)Fake\_InterruptFun & 0xffff);  
     pIdtEntry\[3\].HiOffset  = (unsigned short)((ULONG)Fake\_InterruptFun >> 16);  
     PageProtectOn();

    ulIndex++;  
}

return STATUS\_SUCCESS;  

}

VOID
DriverUnload(IN PDRIVER_OBJECT pDriverObj)
{

return;  

}

稍微解释一下,里面获得IDT表的时候没有通过寄存器IDTR进行读取,是因为对于多核CPU来说不一定只有一个IDT表,而通过IDTR来读取只能读到一份表,所以HOOK IDT的时候一定要注意多核问题

系统维护了一个全局的处理器数组KiProcessorBlock,其中每个元素对应于一个处理器的KPRCB 对象。

kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB

在0X120处,有个_KPRCB结构。再来看看这个结构。

kd> dt _KPCR
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 DebugActive : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB
kd> dt _PRCB
Symbol _PRCB not found.
kd> dt _KPRCB
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD
+0x008 NextThread : Ptr32 _KTHREAD
+0x00c IdleThread : Ptr32 _KTHREAD
+0x010 Number : Char
+0x011 Reserved : Char
+0x012 BuildType : Uint2B
+0x014 SetMember : Uint4B
+0x018 CpuType : Char
+0x019 CpuID : Char
+0x01a CpuStep : Uint2B
+0x01c ProcessorState : _KPROCESSOR_STATE
+0x33c KernelReserved : [16] Uint4B
+0x37c HalReserved : [16] Uint4B
+0x3bc PrcbPad0 : [92] UChar
+0x418 LockQueue : [16] _KSPIN_LOCK_QUEUE
+0x498 PrcbPad1 : [8] UChar
+0x4a0 NpxThread : Ptr32 _KTHREAD
+0x4a4 InterruptCount : Uint4B
+0x4a8 KernelTime : Uint4B
+0x4ac UserTime : Uint4B
+0x4b0 DpcTime : Uint4B
+0x4b4 DebugDpcTime : Uint4B
+0x4b8 InterruptTime : Uint4B
+0x4bc AdjustDpcThreshold : Uint4B
+0x4c0 PageColor : Uint4B
+0x4c4 SkipTick : Uint4B
+0x4c8 MultiThreadSetBusy : UChar
+0x4c9 Spare2 : [3] UChar
+0x4cc ParentNode : Ptr32 _KNODE
+0x4d0 MultiThreadProcessorSet : Uint4B
+0x4d4 MultiThreadSetMaster : Ptr32 _KPRCB
+0x4d8 ThreadStartCount : [2] Uint4B
+0x4e0 CcFastReadNoWait : Uint4B
+0x4e4 CcFastReadWait : Uint4B
+0x4e8 CcFastReadNotPossible : Uint4B
+0x4ec CcCopyReadNoWait : Uint4B
+0x4f0 CcCopyReadWait : Uint4B
+0x4f4 CcCopyReadNoWaitMiss : Uint4B
+0x4f8 KeAlignmentFixupCount : Uint4B
+0x4fc KeContextSwitches : Uint4B
+0x500 KeDcacheFlushCount : Uint4B
+0x504 KeExceptionDispatchCount : Uint4B
+0x508 KeFirstLevelTbFills : Uint4B
+0x50c KeFloatingEmulationCount : Uint4B
+0x510 KeIcacheFlushCount : Uint4B
+0x514 KeSecondLevelTbFills : Uint4B
+0x518 KeSystemCalls : Uint4B
+0x51c SpareCounter0 : [1] Uint4B
+0x520 PPLookasideList : [16] _PP_LOOKASIDE_LIST
+0x5a0 PPNPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x6a0 PPPagedLookasideList : [32] _PP_LOOKASIDE_LIST
+0x7a0 PacketBarrier : Uint4B
+0x7a4 ReverseStall : Uint4B
+0x7a8 IpiFrame : Ptr32 Void
+0x7ac PrcbPad2 : [52] UChar
+0x7e0 CurrentPacket : [3] Ptr32 Void
+0x7ec TargetSet : Uint4B
+0x7f0 WorkerRoutine : Ptr32 void
+0x7f4 IpiFrozen : Uint4B
+0x7f8 PrcbPad3 : [40] UChar
+0x820 RequestSummary : Uint4B
+0x824 SignalDone : Ptr32 _KPRCB
+0x828 PrcbPad4 : [56] UChar
+0x860 DpcListHead : _LIST_ENTRY
+0x868 DpcStack : Ptr32 Void
+0x86c DpcCount : Uint4B
+0x870 DpcQueueDepth : Uint4B
+0x874 DpcRoutineActive : Uint4B
+0x878 DpcInterruptRequested : Uint4B
+0x87c DpcLastCount : Uint4B
+0x880 DpcRequestRate : Uint4B
+0x884 MaximumDpcQueueDepth : Uint4B
+0x888 MinimumDpcRate : Uint4B
+0x88c QuantumEnd : Uint4B
+0x890 PrcbPad5 : [16] UChar
+0x8a0 DpcLock : Uint4B
+0x8a4 PrcbPad6 : [28] UChar
+0x8c0 CallDpc : _KDPC
+0x8e0 ChainedInterruptList : Ptr32 Void
+0x8e4 LookasideIrpFloat : Int4B
+0x8e8 SpareFields0 : [6] Uint4B
+0x900 VendorString : [13] UChar
+0x90d InitialApicId : UChar
+0x90e LogicalProcessorsPerPhysicalProcessor : UChar
+0x910 MHz : Uint4B
+0x914 FeatureBits : Uint4B
+0x918 UpdateSignature : _LARGE_INTEGER
+0x920 NpxSaveArea : _FX_SAVE_AREA
+0xb30 PowerState : _PROCESSOR_POWER_STATE

在0x38的地方是不是看到了我们熟悉的IDT表。

我们在Windbg下dd KiProcessBlock

kd> dd KiProcessorBlock
80553e40 ffdff120 00000000 00000000 00000000
80553e50 00000000 00000000 00000000 00000000
80553e60 00000000 00000000 00000000 00000000
80553e70 00000000 00000000 00000000 00000000

因为是虚拟机里面做的测试,所以只是单核CPU,那么问题来了,如何获得KiProcessBlock?

在Win732位与XP中它是未导出的,那么可以用IDA在ntosknl导出表搜索,找到哪个函数中用了这个变量就可以用这个函数加硬编码的方式进行强行定位,上次看到一篇帖子是如何获得系统未导出的全局变量,是通过遍历未公开的结构体,我就试了试,没想到还成功了,然后就用了这种方法,其实解决多核问题还有好多种方法,不一定局限于哪一种。

下面着重讲解下,如何修改段基址的方法实现HOOK,然后躲过xuetr的检测。

IA-32处理器有三种描述符表:全局描述符表GDT,局部描述符表LDT,中断描述符表IDT。

GDT表是全局的,一个系统中通常只有一个GDT表,供系统中所有程序和任务进行使用。LDT与任务相关,每个任务可以有一个LDT,也可以让多个任务共享一个LDT。

我们用WINDBG观察下GDT

kd> r gdtr
gdtr=8003f000

获得GDT的基地址之后观察下它的内存

是不是和IDT表一样啊,这时候里面的一项叫做段描述符。

这个图就是段描述符的内存结构。有点看不清,将就一下,详情可以参考张银奎老师的《软件调试》。

typedef struct _KGDTENTRY {
USHORT LimitLow;
USHORT BaseLow;
union {
struct {
UCHAR BaseMid;
UCHAR Flags1; // Declare as bytes to avoid alignment
UCHAR Flags2; // Problems.
UCHAR BaseHi;
} Bytes;
struct {
ULONG BaseMid : 8;
ULONG Type : 5;
ULONG Dpl : 2;
ULONG Pres : 1;

        ULONG   LimitHi : 4;  
        ULONG   Sys : 1;  
        ULONG   Reserved\_0 : 1;  
        ULONG   Default\_Big : 1;  
        ULONG   Granularity : 1;  
        ULONG   BaseHi : 8;  
    } Bits;  
} HighWord;  

} KGDTENTRY, *PKGDTENTRY;

段选择子:

还记得门描述符里的选择子吗?选择子的作用就是选择一个段描述符,相当于索引。

段选择子的T1位代表要索引的段描述符表,T1=0表示全局描述符表,T1=1表示局部描述符表。

段选择子的高13位是描述符索引,即要选择的段描述符在T1所表示的段描述符表中的索引号。因为这里使用的是13位,意味着最多可索引8192个描述符,所以GDT和LDT表的最大表项数都是8192.因为X86CPU最多支持256个中断向量,所以IDT 表的最多表项数是256.

然后我们来看看如何修改段基址来实现IDT HOOK。

要是段内偏移不变,那么必须满足:原始偏移 + newbase = newfuntion

那么newbase  = newfuntion - 原始偏移****

然后把newbase分解了再填到段描述符中就可以了吗?答案是不可以,因为一个段描述符可能会有很多程序在用,如果无故修改段描述符那么就会产生不可预料的错误,那么我们如何修改段基址呢?

我们可以变通一下,因为段描述符表里有很多空的未利用的,我们可以把相应的门描述符里的段选择子改掉,让它去选择一个空的段描述符,我们把原先段选择符内容拷贝过来再修改这个段描述符就可以达到目的了!

#include "ntifs.h"

#define WORD USHORT
#define DWORD ULONG

#define MAKELONG(a, b) ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) \
| ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))

typedef struct _IDTR{
USHORT IDT_limit;
USHORT IDT_LOWbase;
USHORT IDT_HIGbase;
}IDTR,*PIDTR;

typedef struct _IDTENTRY
{
unsigned short LowOffset;
unsigned short selector;
unsigned char retention:5;
unsigned char zero1:3;
unsigned char gate_type:1;
unsigned char zero2:1;
unsigned char interrupt_gate_size:1;
unsigned char zero3:1;
unsigned char zero4:1;
unsigned char DPL:2;
unsigned char P:1;
unsigned short HiOffset;
} IDTENTRY,*PIDTENTRY;

typedef struct _KGDTENTRY {
USHORT LimitLow;
USHORT BaseLow;
union {
struct {
UCHAR BaseMid;
UCHAR Flags1; // Declare as bytes to avoid alignment
UCHAR Flags2; // Problems.
UCHAR BaseHi;
} Bytes;
struct {
ULONG BaseMid : 8;
ULONG Type : 5;
ULONG Dpl : 2;
ULONG Pres : 1;

        ULONG   LimitHi : 4;  
        ULONG   Sys : 1;  
        ULONG   Reserved\_0 : 1;  
        ULONG   Default\_Big : 1;  
        ULONG   Granularity : 1;  
        ULONG   BaseHi : 8;  
    } Bits;  
} HighWord;  

} KGDTENTRY, *PKGDTENTRY;

//global
USHORT g_FilterJmp[3];
ULONG g_uOrigInterruptFunc;

void PageProtectOn()
{
__asm{//恢复内存保护
mov eax,cr0
or eax,10000h
mov cr0,eax
sti
}
}

void PageProtectOff()
{
__asm{//去掉内存保护
cli
mov eax,cr0
and eax,not 10000h
mov cr0,eax
}
}

USHORT g_u_cs;

void __stdcall FilterInterrupt()
{
KdPrint(("%s---%X",(char*)PsGetCurrentProcess()+0x16c,g_u_cs));
}

__declspec(naked)
void NewInterrupt3OfOrigBase()
{
__asm{
pushad
pushfd

    push    fs  
    push    0x30  
    pop        fs

    call    FilterInterrupt

    pop        fs

    popfd  
    popad

    jmp        g\_uOrigInterruptFunc  
}  

}

__declspec(naked)
void NewInterrupt3()
{
__asm{
mov g_u_cs,cs
jmp fword ptr[g_FilterJmp]
}
}

ULONG GetInterruptFuncAddress(ULONG InterruptIndex)
{
IDTR idtr;
IDTENTRY *pIdtEntry;

\_\_asm    SIDT    idtr;

pIdtEntry = (IDTENTRY \*)MAKELONG(idtr.IDT\_LOWbase,idtr.IDT\_HIGbase);

return MAKELONG(pIdtEntry\[InterruptIndex\].LowOffset,pIdtEntry\[InterruptIndex\].HiOffset);  

}

ULONG GetNewBase(ULONG NewInterruptFunc,ULONG OrigInterruptOffset)
{
return (NewInterruptFunc - OrigInterruptOffset);
}

VOID SetInterrupt(ULONG InterruptIndex,ULONG uNewBase,BOOLEAN bIsNew)
{
ULONG u_fnKeSetTimeIncrement;
UNICODE_STRING usFuncName;
ULONG u_index;
ULONG *u_KiProcessorBlock;

IDTENTRY        \*pIdtEntry;  
PKGDTENTRY        pGdt;

RtlInitUnicodeString(&usFuncName,L"KeSetTimeIncrement");

u\_fnKeSetTimeIncrement = (ULONG)MmGetSystemRoutineAddress(&usFuncName);  
if (!MmIsAddressValid((PVOID)u\_fnKeSetTimeIncrement))  
{  
    return;  
}

u\_KiProcessorBlock = \*(ULONG\*\*)(u\_fnKeSetTimeIncrement + 44);

u\_index = 0;  
while (u\_KiProcessorBlock\[u\_index\])  
{  
    pIdtEntry = \*(IDTENTRY\*\*)(u\_KiProcessorBlock\[u\_index\] - 0xE8);  
    pGdt = \*(PKGDTENTRY\*)(u\_KiProcessorBlock\[u\_index\] - 0xE4);

    PageProtectOff();

    if (bIsNew)  
    {  
        pIdtEntry\[InterruptIndex\].selector = 0xA8;        //10101 000   //低1 2位  RPL用于检测权限  低 3 位用于选择 GDT 或者 LDT   高五位用于代表表中索引号  这个索引是21  
        RtlCopyMemory(&pGdt\[21\],&pGdt\[1\],sizeof(KGDTENTRY));  
        pGdt\[21\].BaseLow = (USHORT)(uNewBase&0xffff);  
        pGdt\[21\].HighWord.Bytes.BaseMid = (UCHAR)((uNewBase>>16)&0xff);  
        pGdt\[21\].HighWord.Bytes.BaseHi = (UCHAR)(uNewBase>>24);         //把原来的段描述符拷过来  修改段描述符的基址  
    }else{  
        pIdtEntry\[InterruptIndex\].selector = 0x8;  
        memset(&pGdt\[21\],0,sizeof(KGDTENTRY));  
    }

    PageProtectOn();

    u\_index++;  
}  

}

VOID HookInterruptFunc(ULONG InterruptIndex,ULONG NewInterruptFunc)
{
ULONG uNewBase;

g\_uOrigInterruptFunc = GetInterruptFuncAddress(InterruptIndex);  
uNewBase = NewInterruptFunc - g\_uOrigInterruptFunc;    //段基地址 + g\_uOrigInterruptFunc = NewInterruptFunc

\*(ULONG\*)g\_FilterJmp = (ULONG)NewInterrupt3OfOrigBase;  
g\_FilterJmp\[2\] = 0x8;

SetInterrupt(InterruptIndex,uNewBase,TRUE);  

}

void UnHookInterruptFunc(ULONG InterruptIndex)
{
SetInterrupt(InterruptIndex,0,FALSE);
}

VOID MyUnload(PDRIVER_OBJECT pDriverObject)
{
UnHookInterruptFunc(3);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING Reg_Path)
{
HookInterruptFunc(3,(ULONG)NewInterrupt3);
pDriverObject->DriverUnload = MyUnload;
return STATUS_SUCCESS;
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器