对内核的直接挂钩

简介: 简介<br>          有时在开发中,会遇到这样一种情况,当非常需要对某些内核函数进行挂钩时,而常规基于PE的挂钩,往往达不到目的。在本文中将要探讨的,是怎样直接挂钩内核函数,另外,在示例中,还要演示在系统中显示为一个基本磁盘的可移动USB存储设备,并在其上创建及管理多个分区(因为这样或那样的原因,Windows既不允许,也不能识别可移动存储设备上的多个分区,所以我们要“欺骗”一
简介
         有时在开发中,会遇到这样一种情况,当非常需要对某些内核函数进行挂钩时,而常规基于PE的挂钩,往往达不到目的。在本文中将要探讨的,是怎样直接挂钩内核函数,另外,在示例中,还要演示在系统中显示为一个基本磁盘的可移动USB存储设备,并在其上创建及管理多个分区(因为这样或那样的原因,Windows既不允许,也不能识别可移动存储设备上的多个分区,所以我们要“欺骗”一下系统)。因为本文中的示例只用作演示目的,所以只对一个函数进行了挂钩,但可对文中阐述的方法进行扩展,以处理多个函数(例如,工程中可能需要直接挂钩好几个NDIS库中的函数)。再者,你应该清楚地认识到,本文是在讲述直接挂钩,而不是研究USB存储,所以,用作示例的问题当然还可有其他的方法来解决。
 
 
         我们的问题
         USB设备在系统中表示的方式,定义在STORAGE_DEVICE_DESCRIPTOR结构的RemovableMedia字段中,此结构通常会在USBSTOR.SYS响应IOCTL_STORAGE_QUERY_PROPERTY请求时返回。如果设备生产商想让此设备显示为一个基本磁盘,会在驱动程序中设置STORAGE_DEVICE_DESCRIPTOR 结构中RemovableMedia字段,并在响应IOCTL_STORAGE_QUERY_PROPERTY请求时返回FALSE。由此,设备在系统中就显示为一个基本磁盘,而DISK.SYS也不知道它实际上是在与硬盘,还是在与一个USB设备打交道。
         因此,如果我们挂钩USBSTOR.SYS中的IRP_MJ_DEVICE_CONTROL子程序,只需简单地修改IOCTL_STORAGE_QUERY_PROPERTY请求的返回值,就能在系统中把可移动磁盘显示为一个基本磁盘,这可通过以下的代码来完成:
 
typedef NTSTATUS (__stdcall*ProxyDispatch) (IN PDEVICE_OBJECT device,IN PIRP Irp);
ProxyDispatch realdispatcher;
 
//代理函数
NTSTATUS Dispatch(IN PDEVICE_OBJECT device,IN PIRP Irp)
{
    NTSTATUS status=0; ULONG a=0;PSTORAGE_PROPERTY_QUERY query;
    PSTORAGE_DEVICE_DESCRIPTOR descriptor;
 
    PIO_STACK_LOCATION loc= IoGetCurrentIrpStackLocation(Irp);
 
    if(loc->Parameters.DeviceIoControl.IoControlCode
                         ==IOCTL_STORAGE_QUERY_PROPERTY)
    {
        query=(PSTORAGE_PROPERTY_QUERY) Irp->AssociatedIrp.SystemBuffer;
        if(query->PropertyId==StorageDeviceProperty)
        {
            descriptor=(PSTORAGE_DEVICE_DESCRIPTOR) Irp->AssociatedIrp.SystemBuffer;
            status=realdispatcher(device,Irp);
            descriptor->RemovableMedia=FALSE;
            return status;
        }
    }
    return realdispatcher(device,Irp);
}
 
//代码中的其他地方……
realdispatcher=(ProxyDispatch) driver->MajorFunction[IRP_MJ_DEVICE_CONTROL];
driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]=Dispatch;
 
         正如你所看到的,一个可移动USB设备能非常简单地在系统中显示为一个基本磁盘,然而,还有一点小小的“并发症”——只有当你在USB接口中插入一个设备时,系统才会加载USBSTOR.SYS,直到拔出设备后,才会卸载它,因此,我们不能预先对USBSTOR.SYS进行挂钩——必须先插入一个设备。如果我们在USBSTOR.SYS已经处理了IOCTL_STORAGE_QUERY_PROPERTY请求之后,才对它进行挂钩,那么为时已晚了。我们也不能插入一个设备,挂钩USBSTOR.SYS,拔掉它,接着再插入;当你拔出设备时,USBSTOR.SYS也卸载了,挂钩只会白费力气。所以,要对USBSTOR.SYS进行挂钩,最适当的时机是在当它准备创建设备对象时,一方面,我们知道USBSTOR.SYS已经加载了,另一方面,此时IOCTL_STORAGE_QUERY_PROPERTY请求还并未被处理。如果我们能设法捕捉到USBSTOR.SYS对IoCreateDevice()的调用,那么接下来的事情就简单多了——IoCreateDevice()接受一个指向新创建设备的DRIVER_OBJECT的指针作为参数,因此,我们就可在驱动程序的MajorFunction[IRP_MJ_DEVICE_CONTROL]中替换掉一个指针。
 
         为了达到上述目的,我们准备在IoCreateDevice()的可执行代码中插入一些指令,以便直接挂钩,也就是所谓的“通过覆盖的挂钩”。事实上,只有通过挂钩ntoskrnl.exe的导出索引,才能完成此项任务,但是,本文要讲述的是有关直接挂钩,所以,我们准备对IoCreateDevice()进行直接挂钩。然而,知己知彼,百战百胜,先了解一下相关的事情,总是有好处的,那就先来了解一下中断挂钩吧。
 
 
         处理中断与异常
         为响应硬件中断或异常,CPU保存了当前运行线程的执行上下文,并把执行流程转到一个特殊的内核模式程序中——称为“处理程序”。执行上下文保存的方式,依赖于中断模式的特权级;如果中断代码是非特权级的,处理器必须切换到特权堆栈和代码段,以便可以执行一个内核模式的处理程序,因此,CPU在转换执行流程到相应的处理程序之前,会把用户模式的SS、ESP、EFLAGS、CS寄存器值(所有入栈均按上述顺序),加上返回地址,压入到内核堆栈上;另外,如果是发生异常,CPU也可以在栈顶的返回地址上,再压入一个错误代码。如果中断代码是特权级的,堆栈切换就没有必要了,因此,在这种情况下,只有EFLAGS、CS和返回地址,也许可能还有错误代码被压入到堆栈中;此时,SS和ESP寄存器不会保存在堆栈上。
         每一个中断及异常都有着与之关联的号码,称为向量,共有256个中断向量。所有中断与异常处理程序的地址,都存储在一个称为“中断描述符表”(IDT)的内核模式的数据结构中。通常,在一台对称多处理(SMP)计算机上,每个处理器都有其自己的IDT,但在整个系统中,所有中断与异常处理程序的地址,对所有CPU而言,都是一样的。每个IDT入口点关联到它对应的向量,且在每个IDT中,都可以保存中断门描述符、陷阱门描述符、任务门描述符。中断与陷阱门描述符的二进制形式,可用如下的结构来表示:
 
struct GATE
{
 WORD    OffsetLow;     
 WORD    Selector;      
 WORD    Unused:8;
 WORD    Type:5;
 WORD    DPL:2;
 WORD    Present:1;
 WORD    OffsetHigh;
} ;
 
         如上所示,中断与陷阱门描述符的二进制表示形式,与调用门描述符非常相似。而中断与陷阱门的不同之处,在于当中断或异常处理程序开始处理时,EFLAGS寄存器中IF标志的状态。如果中断或异常是通过一个中断门引发的,IF标志会被处理器自动清除;如果中断或异常是通过一个陷阱门引发的,则IF标志不会受到影响。在其他方面,中断与陷阱门是一样的——这也不足为奇,因为它们都是用同样的结构来描述的,但任务门描述符的二进制形式就不相同了。另外,因为性能的原因,在Windows NT中,所有的用户过程都运行于一个单任务的上下文中,所以在IDT中,还有一些任务门描述符,它们主要保留用于“异常的情况”,如系统崩溃;它们的任务是保证系统可以有足够长时间,在CPU重设自身之前,抛出一个蓝屏错误。
 
         现在,要来说一下异常了,IDT的头32个入口点负责与异常处理程序打交道(它们对特定向量的映射,已被Intel预先定义好了),异常在此可归类为陷阱(Trap)、错误(Fault)与异常终止(Abort)。异常终止类的异常不允许失败的任务继续执行下去,有关的一个典型例子就是机器检查异常(INT 0x12);而陷阱与错误则允许失败的任务在异常被处理之后,继续执行下去。陷阱与错误的不同之处,在于保存在堆栈上的返回地址不同;在错误类的异常情况下,这个地址指向导致异常的指令,也就是说,在异常处理程序返回控制之后,会试图执行前面失败的指令,有关的一个典型例子就是页面错误异常(INT 0xE);而在陷阱类的异常情况下,返回地址将指向紧跟在导致异常指令后的下一条指令,有关的典型例子如调试断点异常(INT 3)。
 
         一个调试异常(INT 1)就本身而言,是个非常有意思的异常——依据不同的异常原因,它可以被陷阱或错误异常抛出。通常,一个调试异常可被以下任一原因抛出:
 
Ø 执行时的断点
Ø 内存访问的断点
Ø IO端口访问的断点
Ø 一般侦测情况(会设置EFLAGS寄存器的TF标志,甚至于每条指令的执行,都可以抛出一个调试异常)
Ø 任务切换(此处与Windows的任务切换无关)
Ø INT 1指令
 
在1至4的情况中,INT 1是作为一个错误被抛出,而在其他情况中,它是作为一个陷阱被抛出,而一般可通过来自DR6寄存器的INT 1处理程序,来找出抛出异常的原因。一个调试异常能由多个原因产生,例如,设置了TF标志的执行断点,在这种情况下,执行断点比TF标志具有更高的优先级,因此,INT 1是作为一个错误抛出,而不是作为一个陷阱。
 
         那么,有了挂钩函数之后,上面这些东西都能做些什么呢?我们将要把目标函数开始处的头几个字节(8个字节就足够了),复制到从非分页池里分配的数组中,再挂钩INT 1与INT 3的处理程序,并写入一个0xCC操作码(其代表INT 3指令)至目标函数的开始处。这样,当目标函数准备执行它的第一条指令时,就会触发我们被代理过的INT 3处理程序,而我们INT 3处理程序开始执行时的堆栈布局,可用下面的结构来描述:
 
struct INTTERUPT_STACK
{
    ULONG InterruptReturnAddress;
    ULONG SavedCS;
    ULONG SavedFlags;
    ULONG FunctionReturnAddress;
    ULONG Argument;
};
 
         在堆栈顶部,CPU设置了一个帧,以用于响应一个INT 3指令,也就是一个INT 3处理程序应该返回控制,加上CS及EFLAGS寄存器标志的地址值;而目标函数应该返回控制的地址紧接其后;另外,函数参数的数组在堆栈上,正位于返回地址之下(所以从实践经验来说,把所有的参数当作ULONG,还是有道理的,这样我们就能

在需要时把它们转换成它们实际的类型)。在这一点上,我们就能做任何想做的事了——我们可以检查或修改函数参数、修改返回地址,也就是那些通常在挂钩函数之后可以做的事情。但对我们目前的任务来说,我们只对第一个参数感兴趣,也就是传递给IoCreateDevice()的PDRIVER_OBJECT。
         在被我们代理的INT 3处理程序返回之前,它将会把栈顶结构中的InterruptReturnAddress字段,修改为我们复制的带有指令的数组,并设置SaveFlags字段中的TF标志。我们的INT 3处理程序返回之后,保存在堆栈上的InterruptReturnAddress和SavedFlags字段,将会分别弹出至EIP与EFLAGS寄存器中。由此,执行流程将会从我们复制的指令数组处继续执行,而且,我们一旦修改了TF标志,它将会以单步模式继续下去,也就是说,在每条指令执行时,都会抛出INT 1。
         如果INT 1的抛出,是因为设置了TF标志,那它将会被当作一个陷阱来处理。因此,在数组中第一条指令执行之后,就会触发我们代理过的INT 1处理程序,而保存在堆栈上的EIP将会指向数组中的第二条指令。这样,从保存在栈顶的返回地址中,减去我们数组的地址,就可以得到执行过的指令大小,因此,在我们的INT 1处理程序返回前,它将会修改返回地址为目标函数起始地址(+)执行过的指令大小,并清除保存在堆栈上的EFLAGS中的TF标志。由此,执行流程将会从目标函数的第二条指令处开始继续,而我们的INT 1处理程序返回之后,TF标志也被清除了。换句话来说,目标函数将会继续执行下去,好像什么事也没有发生过一样。
 
         明显地,我们的方法似乎有点复杂了,让人难以理解,但实际上,我们只不过换了种方式来做而已。例如,我们可以复制目标函数起始处的一些指令到我们的数组中,并通过一个JMP指令覆盖掉目标函数的起始地址,这样,执行程序就能跳到我们的挂钩代码中来了。如果这样做的话,我们还要计算出目标函数内的偏移量,以确定我们的挂钩代码执行完后,从目标函数哪条指令开始恢复执行,所以,就还要算出指令大小。可是,说起来容易,做起来难啊,要像上述这样来做,将必须写一个完整的反汇编程序,而且,复杂的事还在后面,指令还可能涉及到与特定指令位置相关的内存,这种情况下,我们必须在重定位之后,调整指令的操作数。换句话来说,如果我们选择把函数开始处覆写为一个JMP,而不是INT 3指令,我们的程序将会非常大,95%的代码都要用于处理反汇编,而不是挂钩本身。因此,对INT 1与INT 3进行挂钩,是更加合情合理的事情,只要利用好INT 1与INT 3,想要CPU做什么,都不是问题了。
 
         现在,来看一下实际的工作。
 
 
         解决我们的问题
         针对我们特定的工程,可在DriverEntry()中进行所有与挂钩相关的工作,下面来看一下代码:
 
//这个子程序挂钩并恢复IDT,
//必须保证这个函数只运行在一个CPU上,
//因此我们在整个执行过程中屏蔽了中断以避免上下文切换。
 
void HookIDT()
{
    ULONG handler1,handler2,idtbase,tempidt,a;
    UCHAR idtr[8];
 
    //取得地址以便写入到IDT
    handler1=(ULONG)&replacementbuff[0];
    handler2=(ULONG)&replacementbuff[32];
 
    //分配临时的内存,这应该为我们的第一步,从此时开始,我们屏蔽了中断直到返回,
    //我们不想冒险调用任何不是我们自己编写的代码。
//(理论上来说,这个代码可能会在我们未知的情况下重新打开中断,那可就……)
 
    tempidt=(ULONG)ExAllocatePool(NonPagedPool,2048);
 
    _asm
    {
        cli
       
        sidt idtr
        lea ebx,idtr
        mov eax,dword ptr[ebx+2]
        mov idtbase,eax
    }
 
    //检查是否已挂钩IDT,
    //如果是,重新打开中断并返回。
    for(a=0;a<IdtsHooked;a++)
    {
        if(idtbases[a]==idtbase)
        {
            _asm sti
            ExFreePool((void*)tempidt);
            KeSetEvent(&event,0,0);
            PsTerminateSystemThread(0);
        }
    }


 
    _asm
    {
        //现在,将要加载IDT的副本到IDTR寄存器。
        //以个人的经验来看,修改内存,再由IDTR寄存器进行指向,是不安全的。
        mov edi,tempidt
        mov esi,idtbase
        mov ecx,2048
        rep movs
 
        lea ebx,idtr
        mov eax,tempidt
        mov dword ptr[ebx+2],eax
        lidt idtr
 
        //现在,我们能安全地修改IDT了,准备好。
        mov ecx,idtbase
 
        //挂钩INT 1
        add ecx,8
        mov ebx,handler1
 
        mov word ptr[ecx],bx
        shr ebx,16
        mov word ptr[ecx+6],bx
 
        //挂钩INT 3
        add ecx,16
        mov ebx,handler2
 
        mov word ptr[ecx],bx
        shr ebx,16
        mov word ptr[ecx+6],bx
 
        //重新加载原始IDT
        lea ebx,idtr
        mov eax,idtbase
        mov dword ptr[ebx+2],eax
        lidt idtr
        sti
    }
 
    //添加我们刚才挂钩的IDT地址至已挂钩的IDT列表
    idtbases[IdtsHooked]=idtbase;
    IdtsHooked++;
    ExFreePool((void*)tempidt);
    KeSetEvent(&event,0,0);
    PsTerminateSystemThread(0);
}
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT driver,IN PUNICODE_STRING path)
{
    ULONG a;PUCHAR pool=0;
    UCHAR idtr[8];HANDLE threadhandle=0;
 
    //以机器码填充数组
    replacementbuff[0]=255;replacementbuff[1]=37;
    a=(long)&replacementbuff[6];
    memmove(&replacementbuff[2],&a,4);
    a=(long)&INT1Proxy;
    memmove(&replacementbuff[6],&a,4);
 
    replacementbuff[32]=255;replacementbuff[33]=37;
    a=(long)&replacementbuff[38];
    memmove(&replacementbuff[34],&a,4);
    a=(long)&BPXProxy;
    memmove(&replacementbuff[38],&a,4);
 
    //保存INT 1与INT 3处理程序的原始地址
    _asm
    {
        sidt idtr
        lea ebx,idtr

mov ecx,dword ptr[ebx+2]
 
        //保存INT1
       add ecx,8
        mov ebx,0
        mov bx,word ptr[ecx+6]
        shl ebx,16
        mov bx,word ptr[ecx]
        mov Int1RealHandler,ebx
 
        //保存INT3
        add ecx,16
        mov ebx,0
        mov bx,word ptr[ecx+6]
        shl ebx,16
        mov bx,word ptr[ecx]
        mov BPXRealHandler,ebx
    }
 
    //挂钩INT 1与INT 3的处理程序,必须在覆写NDIS之前完成。
    //把HookUnhookIDT()作为一个单独的线程运行,直到所有的IDT都进行了挂钩。
    KeInitializeEvent(&event,SynchronizationEvent,0);
 
    RtlZeroMemory(&idtbases[0],64);
    a=KeNumberProcessors[0];
    while(1)
    {
        PsCreateSystemThread(&threadhandle,
                (ACCESS_MASK) 0L,0,0,0,
                (PKSTART_ROUTINE)HookIDT,0);
        KeWaitForSingleObject(&event,
           Executive,KernelMode,0,0);
        if(IdtsHooked==a)
            break;
    }
 
    KeSetEvent(&event,0,0);
 
    //填充结构
    a=(ULONG)&IoCreateDevice;
    HookedFunctionDescriptor.RealCode=a;
    pool=ExAllocatePool(NonPagedPool,8);
    memmove(pool,a,8);
    HookedFunctionDescriptor.ProxyCode=(ULONG)pool;
 
    //现在进行覆写内存
    _asm
    {
        //在覆写之前去掉保护
        mov eax,cr0
        push eax
        and eax,0xfffeffff
        mov cr0,eax
 
        //插入断点(0xCC操作码)
        mov ebx,a
        mov al,0xcc
        mov byte ptr[ebx],al
 
        //恢复保护
        pop eax
        mov cr0,eax
    }
 
    return 0;
}
 
         让我们先来解释一下上述动作,一开始,我们用非直接跳转指令,填充了两个内存块——在挂钩IDT之后将会用到。但有些东西似乎从逻辑上解释不了,当试图写入函数地址本身到IDT中时,总会产生蓝屏,然而,如果写入带有非直接跳转指令的数组地址到IDT中时,也就是说,使执行流程跳到我们的函数中,就一切正常,真是让人不解啊。接下来,把INT 1与INT 3实际处理程序的地址保存在全局变量中,再对IDT进行挂钩,此处需格外小心。
 
         正如前面所说过的,在一部SMP电脑上,每个处理器都有其自己的IDT,但随着Intel超线程技术的出现,一个支持超线程技术的CPU,会被系统当作两个独立的CPU,因此,不得不对系统中的所有IDT进行挂钩,所以要创建运行HookIDT()的线程,直到系统中所有IDT都被挂钩了。
         一开始,HookIDT()分配了内存,以便复制IDT的内容——但就个人经验来看,写入内存,再由IDTR寄存器进行指向,是不安全的,即使中断已被屏蔽。因此,我们复制IDT到分配的内存中,并使用LIDT指令,加载一个指向此内存的指针到IDTR寄存器中,这样,我们就能安全地修改原始IDT;完成之后,会用原始IDT地址来重新加载IDTR。从HookIDT()发现IDT还未被挂钩,到修改并重新加载IDT,它都运行在同一个CPU上,所以我们就可以屏蔽中断,以避免上下文切换。然而,所有的工作,都只应在为临时IDT分配内存之后进行,为什么呢?因为,在我们这个例子中,调用任何不是我们自己编写的代码,都是不明智的行为——如果这些代码重新打开中断,很可能会把我们搅得一团糟。因此,我们要避免调用任何不是我们自己编写的代码——正如大家所看到的,甚至我们在分配用于复制原始IDT内容的内存时,都用的是REP MOVS指令,而不是常用的memcpy()。
 
         在对IDT中的INT 1与INT 3处理程序进行挂钩之后,我们把目标函数(即IoCreateDevice())的头八个字节,复制到我们从非分页池中分配的内存中,并在目标函数的起始处插入0xCC操作码。在此目标函数的可执行代码存放于只读内存中,因此,在我们可覆写函数之前,要么在页表中修改页面保护,要么清除CR0寄存器中的WP标志(此处为简单起见,我们选择清除WP标志)。以上操作完成之后,当每次有对IoCreateDevice()的调用发生时,我们挂钩于INT 3的代码就会执行了。
 
         现在,让我们来看一下挂钩INT 1与INT 3的代码。
 
//此函数保证我们的挂钩工作正常
ULONG __stdcall INT1check(INTTERUPT_STACK * savedstack)
{
    ULONG offset=0,stepping=savedstack->SavedFlags&0x100;
 
    //如果INT 1是因为单步之外的其他原因被抛出,返回0。
    //因为执行流程最终仍会到达真正的INT 1处理程序。
    if(!stepping)return 0;
 
    //检查单步是否与我们的挂钩有关,否则,返回0。
    if(savedstack->InterruptReturnAddress<=
            HookedFunctionDescriptor.ProxyCode)
        return 0;
    if(savedstack->InterruptReturnAddress>=
            HookedFunctionDescriptor.ProxyCode+8)
        return 0;
 
    //在堆栈上修改返回地址,清除TF标志。
    offset=savedstack->InterruptReturnAddress-
              HookedFunctionDescriptor.ProxyCode;
    savedstack->InterruptReturnAddress=
              HookedFunctionDescriptor.RealCode+offset;
    savedstack->SavedFlags &=0xfffffeff;
 
    //清除DR6
    _asm
    {
        mov eax,0
        mov dr6,eax
    }
 
    return 1;
}
 
ULONG __stdcall BPXcheck(INTTERUPT_STACK * savedstack)
{
    PDRIVER_OBJECT driver;char buff[1024]; HANDLE handle=0;
    PUNICODE_STRING unistr=(PUNICODE_STRING)&buff[0];ULONG a=0;
 
    //如果断点与我们的挂钩无关,返回0。
    if(savedstack->InterruptReturnAddress!= HookedFunctionDescriptor.RealCode+1)
return 0;
   
    //使INT 1返回到我们复制的代码,并设置TF标志。
    savedstack->SavedFlags|=0x100;

savedstack->InterruptReturnAddress=
       HookedFunctionDescriptor.ProxyCode;
 
    //所有x86相关的工作都已完成,
    //现在来进行实际的工作。
 
    driver=(PDRIVER_OBJECT)savedstack->Arg;
 
    if(ObOpenObjectByPointer(driver,0, NULL, 0,
                0,KernelMode,&handle))return 1;
    ZwQueryObject(handle,1,buff,256,&a);
    if(!unistr->Buffer){ZwClose(handle);return 1;}
    if(_wcsicmp(unistr->Buffer,L"\\Driver\\USBSTOR"))
        {ZwClose(handle);return 1;}
 
    ZwClose(handle);
 
    a=(ULONG)driver->MajorFunction[IRP_MJ_DEVICE_CONTROL];
 
    if(a==(ULONG)Dispatch)return 1;
 
    realdispatcher=(ProxyDispatch)a;
    driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]=Dispatch;
    return 1;
}
 
_declspec(naked) INT1Proxy()
{
    _asm   
    {
        pushfd
        pushad
        mov ebx,esp
        add ebx,36
        push ebx
        call INT1check
 
        cmp eax,0
        je fin
 
        popad
        popfd
        iretd
 
        fin: popad
             popfd
             jmp Int1RealHandler
    }
}
 
_declspec(naked) BPXProxy()
{
 
    _asm   
    {
        pushfd
        pushad
        mov ebx,esp
        add ebx,36
        push ebx
        call BPXcheck
 
        cmp eax,0
        je fin
   
        popad
        popfd
        iretd
 
        fin: popad
             popfd
             jmp BPXRealHandler
 
    }
}
 
         当有一个对IoCreateDevice()的调用发生时,会触发BPXProxy()函数。函数BPXProxy()保存了寄存器与标志值,并在开始执行时把ESP值压入栈,接着调用BpxCheck(),因此,BpxCheck()收到一个指向我们前面所提过的INTTERUPT_STACK结构的指针作为参数。首先,通过把结构的InterruptReturnAddress与目标函数的地址进行对比,BpxCheck()将会检查INT 3的调用,是否与我们的挂钩有关;如果不是,它返回0;否则,它把InterruptReturnAddress修改为我们复制过去的带有指令的数组,并设置SavedFlags字段中的TF标志。至此,我们就可以做与挂钩相关的工作了,在我们的例子中,将检查传递给IoCreateDevice()的PDEVICE_OBJECT是否为\\Driver\\USBSTOR(其意味着USBSTOR.SYS已经加载)的其中一个,并把IRP_MJ_DEVICE_CONTROL处理程序替换为我们函数的地址——当然,是在它还未被替换时。现在,我们已可以监视由系统发送给USBSTOR的所有IRP_MJ_DEVICE_CONTROL请求了,也即完成了我们的最初目标。在BpxCheck()返回之后,中断的处理方式依赖于它的返回值,如果它回返0,我们把控制传给INT 3真正的处理程序,否则,我们仅仅带着IRETD指令返回,因此,执行流程将会从带有指令数组的开始处恢复执行。一旦我们修改了TF标志,它将会以单步模式恢复执行,也就是说,INT1Proxy()得到了调用。
 
         有关INT1Proxy()的实现,几乎与BPXProxy()一样,唯一的不同之处,是它调用了INT1Check(),而不是BpxCheck()。首先,INT1Check()检查保存在堆栈上的EFLAGS寄存器中的TF标志,如果它发现INT 1是因为单步之外的其他原因被抛出的,它将返回0(因为前面也提到,INT 1可由多种原因抛出);否则,它将检查返回地址是否位于我们复制的指令数组中某处,如果也不是,还是返回0——毕竟,其他程序在调试时,也会打开TF标志;如果是,从堆栈上的返回地址中减去数组中地址,就得到了目标函数的第一条指令大小(也就是刚执行过的那条指令),紧接着修改堆栈上的返回地址为目标函数起始地址(+)它的第一条指令大小,清除保存在堆栈上的DR6寄存器和EFLAGS中的TF标志,并返回1。这样一来,如果INT 1是因为其他原因抛出的,那么与我们的挂钩无关,INT1Proxy()会将控制传到INT 1真正的处理程序中,否则,它带着IRETD指令返回,所以,目标函数(IoCreateDevice())将会继续执行,好像什么事也没发生过一样。
 
         要运行示例的代码,你必须创建一个按需启动的服务,并在命令行中手工启动它。当你在这个服务运行期间插入一个USB存储设备时,你将看到一个基本磁盘标志,而不是一个可移动磁盘,因此,如果打开控制面板中的磁盘管理,将可以在其上创建多个分区了。
 
         注意:此示例程序使用了Windows 2000 DDK来构建,因此,它会把KeNumberProcessors导出符号当作一个指针;如果你在使用XP DDK,KeNumberProcessors会被当作一个变量,这样,示例程序就通不过编译了。然而,这些问题只存在于编译期间,示例程序在Windows 2000与Windows XP上都工作正常,而不管你用的是什么DDK版本。
 
 
         结论
         尽管在我们的例子中,只挂钩了一个函数,但可扩展这种方法以用于处理多个函数,此外,我们也作了一个大胆的假设——目标函数的首指令不为JMP。但在实际应用中,还是觉得有必要检查一下——如果目标函数首指令刚好为JMP呢,以对代码作出调整(所做的只是计算出将要跳转到的指令位置,并在此进行挂钩),换句话来说,你可以按自己的想法对示例代码进行调整,以满足现实工作中工程的特定需要。

相关文章
|
4月前
|
监控 安全 API
5.9 Windows驱动开发:内核InlineHook挂钩技术
在上一章`《内核LDE64引擎计算汇编长度》`中,`LyShark`教大家如何通过`LDE64`引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的`InlineHook`函数挂钩其实与应用层一致,都是使用`劫持执行流`并跳转到我们自己的函数上来做处理,唯一的不同的是内核`Hook`只针对`内核API`函数,但由于其身处在`最底层`所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。
40 1
5.9 Windows驱动开发:内核InlineHook挂钩技术
|
17天前
|
监控 安全 API
7.3 Windows驱动开发:内核监视LoadImage映像回调
在笔者上一篇文章`《内核注册并监控对象回调》`介绍了如何运用`ObRegisterCallbacks`注册`进程与线程`回调,并通过该回调实现了`拦截`指定进行运行的效果,本章`LyShark`将带大家继续探索一个新的回调注册函数,`PsSetLoadImageNotifyRoutine`常用于注册`LoadImage`映像监视,当有模块被系统加载时则可以第一时间获取到加载模块信息,需要注意的是该回调函数内无法进行拦截,如需要拦截则需写入返回指令这部分内容将在下一章进行讲解,本章将主要实现对模块的监视功能。
33 0
7.3 Windows驱动开发:内核监视LoadImage映像回调
|
4月前
|
监控 Windows
4.4 Windows驱动开发:内核监控进程与线程创建
当你需要在Windows操作系统中监控进程的启动和退出时,可以使用`PsSetCreateProcessNotifyRoutineEx`函数来创建一个`MyCreateProcessNotifyEx`回调函数,该回调函数将在每个进程的创建和退出时被调用。PsSetCreateProcessNotifyRoutineEx 用于在系统启动后向内核注册一个回调函数,以监视新进程的创建和退出,
40 0
4.4 Windows驱动开发:内核监控进程与线程创建
|
4月前
|
监控 安全 API
7.1 Windows驱动开发:内核监控进程与线程回调
在前面的文章中`LyShark`一直在重复的实现对系统底层模块的枚举,今天我们将展开一个新的话题,内核监控,我们以`监控进程线程`创建为例,在`Win10`系统中监控进程与线程可以使用微软提供给我们的两个新函数来实现,此类函数的原理是创建一个回调事件,当有进程或线程被创建或者注销时,系统会通过回调机制将该进程相关信息优先返回给我们自己的函数待处理结束后再转向系统层。
59 0
7.1 Windows驱动开发:内核监控进程与线程回调
|
4月前
|
Windows
5.4 Windows驱动开发:内核通过PEB取进程参数
PEB结构`(Process Envirorment Block Structure)`其中文名是进程环境块信息,进程环境块内部包含了进程运行的详细参数信息,每一个进程在运行后都会存在一个特有的PEB结构,通过附加进程并遍历这段结构即可得到非常多的有用信息。在应用层下,如果想要得到PEB的基地址只需要取`fs:[0x30]`即可,TEB线程环境块则是`fs:[0x18]`,如果在内核层想要得到应用层进程的PEB信息我们需要调用特定的内核函数来获取。
48 0
5.4 Windows驱动开发:内核通过PEB取进程参数
|
4月前
|
监控 安全 Windows
4.3 Windows驱动开发:监控进程与线程对象操作
在内核中,可以使用`ObRegisterCallbacks`这个内核回调函数来实现监控进程和线程对象操作。通过注册一个`OB_CALLBACK_REGISTRATION`回调结构体,可以指定所需的回调函数和回调的监控类型。这个回调结构体包含了回调函数和监控的对象类型,还有一个`Altitude`字段,用于指定回调函数的优先级。优先级越高的回调函数会先被调用,如果某个回调函数返回了一个非NULL值,后续的回调函数就不会被调用。当有进程或线程对象创建、删除、复制或重命名时,内核会调用注册的回调函数。回调函数可以访问被监控对象的信息,如句柄、进程ID等,并可以采取相应的操作,如打印日志、记录信息等。
28 0
4.3 Windows驱动开发:监控进程与线程对象操作
|
10月前
|
Windows
驱动开发:内核扫描SSDT挂钩状态
在笔者上一篇文章`《驱动开发:内核实现SSDT挂钩与摘钩》`中介绍了如何对`SSDT`函数进行`Hook`挂钩与摘钩的,本章将继续实现一个新功能,如何`检测SSDT`函数是否挂钩,要实现检测`挂钩状态`有两种方式,第一种方式则是类似于`《驱动开发:摘除InlineHook内核钩子》`文章中所演示的通过读取函数的前16个字节与`原始字节`做对比来判断挂钩状态,另一种方式则是通过对比函数的`当前地址`与`起源地址`进行判断,为了提高检测准确性本章将采用两种方式混合检测。
179 0
|
10月前
驱动开发:内核实现SSDT挂钩与摘钩
在前面的文章`《驱动开发:内核解析PE结构导出表》`中我们封装了两个函数`KernelMapFile()`函数可用来读取内核文件,`GetAddressFromFunction()`函数可用来在导出表中寻找指定函数的导出地址,本章将以此为基础实现对特定`SSDT`函数的`Hook`挂钩操作,与`《驱动开发:内核层InlineHook挂钩函数》`所使用的挂钩技术基本一致,不同点是前者使用了`CR3`的方式改写内存,而今天所讲的是通过`MDL映射`实现,此外前者挂钩中所取到的地址是通过`GetProcessAddress()`取到的动态地址,而今天所使用的方式是通过读取导出表寻找。
11240 0
|
安全 API
驱动开发:内核层InlineHook挂钩函数
在上一章`《驱动开发:内核LDE64引擎计算汇编长度》`中,`LyShark`教大家如何通过`LDE64`引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的`InlineHook`函数挂钩其实与应用层一致,都是使用`劫持执行流`并跳转到我们自己的函数上来做处理,唯一的不同的是内核`Hook`只针对`内核API`函数,但由于其身处在`最底层`所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。
417 0
驱动开发:内核层InlineHook挂钩函数