• NDIS HOOK实现方法


    转载

    探索NDIS HOOK新的实现方法(1)

    NDIS HOOK是专业级防火墙使用的一种拦截技术,NDIS HOOK的重点是如何获得特定协议对应NDIS_PROTOCOL_BLOCK指针,获得了该指针,接下来就可以替换该协议所注册的收发函数,而达到拦截网络数据的目的。
         获 得NDIS_PROTOCOL_BLOCK指针的方法一般是用NdisRegisterProtocol注册一个新的协议,所获得的协议句柄实际上就是一 个NDIS_PROTOCOL_BLOCK指针,顺着该指针遍历NDIS_PROTOCOL_BLOCK链表,就可以找到你所要挂钩的协议所对应的 NDIS_PROTOCOL_BLOCK.之所以可以这样做,是因为每注册一个协议,系统都会把该协议对应的NDIS_PROTOCOL_BLOCK放置 在协议链表的开头,该协议链表每个元素都是NDIS_PROTOCOL_BLOCK类型,代表一个已经注册的协议。
         事 实上我们需要的只是TCPIP协议族的NDIS_PROTOCOL_BLOCK指针,毕竟TCP,IP,ARP,ICMP等等几乎所有我们感兴趣的协议, 都是在tcpip.sys协议驱动里面实现的。如果我们只需要TCPIP协议所对应的NDIS_PROTOCOL_BLOCK,那么上面的方法就有点繁琐 了。我们可以试着寻找更简便的方法来获得TCPIP协议的NDIS_PROTOCOL_BLOCK.
    于 是我对tcpip.sys驱动进行了反汇编,发现NDIS_PROTOCOL_BLOCK指针存放在一个名为_ARPHandle的全局变量里面,所以如 果能找到_ARPHandle的地址,我们就成功了,我们完全可以把该全局变量的偏移量作为一个常量来使用,但这里纯粹为了拓宽思路,我介绍另一种找到该 全局变量的方法。
         Tcpip.sys有个导出函数叫IPDelayedNdisReEnumerateBindings,该函数内部曾经出现过_ARPHandle 的地址,为什么会出现它的地址呢,因为该函数内部调用过NdisReEnumerateProtocolBindings函数,懂得反汇编的应该知道,在 用call指令调用函数之前,必然会用到push指令将函数的参数压到栈里面去,不巧的是, NdisReEnumerateProtocolBindings函数只有一个参数,而该参数恰恰是一个NDIS_PROTOCOL_BLOCK指针类 型,在这里,实际上就是把_ARPHandle当作参数传给了
    NdisReEnumerateProtocolBindings,所以_ARPHandle的地址必然会出现在push指令的后面,说具体一点,紧跟push指令的四个字节就是_ARPHandle的地址。
          所以具体的思路就是这样,先找到IPDelayedNdisReEnumerateBindings函数的地址,然后从该函数的地址开始搜索push指令 的特征码,搜到了以后,把紧跟push指令的四个字节作为指向NDIS_PROTOCOL_BLOCK指针的指针返回。
         也许有的人会问,如果IPDelayedNdisReEnumerateBindings函数体内部出现过多次push指令,岂不是会搜出不正确的地址, 事实上,虽然都叫push指令,然而在机器码级别是不同的,push指令的机器码表示有十几种之多,用来区别不同的寻址方式,调用 NdisReEnumerateProtocolBindings 时用的push指令字节序列是0xff35,这个push指令表示后面紧跟的四个字节是一个内存地址,而不是一个立即数或者寄存器之类的。知道了这些,我 们就可以清楚,在一个有限的地址范围,0xff35的唯一性是可以得到满足的。根据我的观察,在win2000,winxp,win2003上 面,IPDelayedNdisReEnumerateBindings本身是一个很短的函数,0xff35指令确实只出现过一次,所以该方法是很可靠 的。
    思路已经出来了,下面我把详细的代码给大家贴出来,理解这些代码需要对windows Pe格式有所了解,如果你不想理解也行,代码可以直接拿来用。
           以下是我写的一个 获取内核模块某个导出函数地址的 通用例程。这里主要是为了获取tcpip.sys模块的导出函数IPDelayedNdisReEnumerateBindings
       void* GetRoutineAddress(char* ModuleName,char* RoutineName)
    {
           PIMAGE_DOS_HEADER dos_hdr;
        PIMAGE_NT_HEADERS nt_hdr;
        PIMAGE_EXPORT_DIRECTORY export_dir;
        ULONG *fn_name, *fn_addr, i;
          char* base;
          base=(char*)FindModule(ModuleName);//该函数用来获得内核模块的基地址
        if(!base)
            return NULL;
        DbgPrint("tcpip address:%p",base);
        dos_hdr = (PIMAGE_DOS_HEADER)base;
        if (dos_hdr->e_magic != IMAGE_DOS_SIGNATURE)
            return NULL;
        nt_hdr = (PIMAGE_NT_HEADERS)(base + dos_hdr->e_lfanew);
        export_dir = (PIMAGE_EXPORT_DIRECTORY)(base + nt_hdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
        fn_name = (ULONG *)(base + export_dir->AddressOfNames);
        fn_addr = (ULONG *)(base + export_dir->AddressOfFunctions);
        for (i = 0; i < export_dir->NumberOfNames; i++, fn_name++, fn_addr++)
        {
            if (strcmp(RoutineName, base + *fn_name) == 0)
            {
                return base + *fn_addr;
            }
        }
        return NULL;
    }
    以下是FindModule函数的实现:
    void *
    FindModule(char *name)
    {
        ULONG i, n, *q;
        PSYSTEM_MODULE_INFORMATION p;
        void *base;
        ZwQuerySystemInformation(SystemModuleInformation, &n, 0, &n);
        q = (ULONG *)ExAllocatePool(PagedPool, n);
        ZwQuerySystemInformation(SystemModuleInformation, q, n * sizeof (*q), 0);
        p = (PSYSTEM_MODULE_INFORMATION)(q + 1);
        base = NULL;
        for (i = 0; i < *q; i++) {
            if (_stricmp(p.ImageName + p.ModuleNameOffset, name) == 0) {
                base = p.Base;
                break;
            }
        }
        ExFreePool(q);
        return base;
    }
    以下是获取tcpip协议的NDIS_PROTOCOL_BLOCK指针的函数
       void* GetProtocolBlock()
    {
        char* base;
        char bytes[]={0xff,0x35};
        base=GetRoutineAddress("tcpip.sys","IPDelayedNdisReEnumerateBindings");
         while(RtlCompareMemory(base,bytes,2)!=2)
        {
           base++;
        }
        return **((void***)(base+2));

    }

    探索NDIS HOOK新的实现方法(2)

    ---INLINE HOOK实现NDIS HOOK
    前 面讲述了如何通过获取NDIS_PROTOCOL_BLOCK来实现NDIS HOOK,这里讲述第二种方法,那就是inline hook方法。说起inline hook,也不是什么新鲜玩意,无非是在一个函数的首部嵌入一个jmp机器指令,在该函数执行有效代码前就跳到我们的代理函数,在我们的代理函数里做了必 要的处理以后,再跳回原来的函数,接着执行原函数的指令。
    既然tcpip.sys是标准的NDIS协议驱动,那么收包函数显然应该是在 tcpip.sys内部实现的,我们直接找到这两个收包函数,然后对其inline hook不就可以了吗?经过逆向分析,我找到了这两个函数,本人安装了两个XP系统,其中一个导出了这两个函数,另一个系统却没导出,所以我们仍然需要用 特征码搜索这两个函数,这两个函数声明如下:
    NDIS_STATUS
    ARPRcv (NDIS_HANDLE BindContext,
    NDIS_HANDLE MacContext,
    UCHAR* HeadBuffer,
    ULONG HeadSize,
    UCHAR* Buffer,
    ULONG BufferSize,
    ULONG PacketSize);
    INT
    ARPRcvPacket (NDIS_HANDLE BindContext,
    PNDIS_PACKET Packet);
    搜索这两个函数地址的代码如下:
    //以下全局变量保存两个函数的地址
    void* ARPRcv=NULL;
    void* ARPRcvPacket=NULL;
    void SearchProtocolRoutine()
    {
    //以下分别为两个收包函数的特征码
    UCHAR ARPRcvBytes[] ={0x8b,0xff,0x55,0x8b,0xec,0x56,0x8b,0x75,0x08,0x33};
    UCHAR ARPRcvPacketBytes[]={0x8b,0xff,0x55,0x8b,0xec,0x51,0x53,0x56,0x57,0x8b};
    //获取tcpip.sys模块的基地址,该函数在前一节已经提供给大家
    char* base=FindModule("tcpip.sys");
    while(ARPRcv==NULL||ARPRcvPacket==NULL)
    {
    if(ARPRcv==NULL&&
    RtlCompareMemory(ARPRcvBytes,base,10)==10)
    {
    ARPRcv=base;
    }
    else if(ARPRcvPacket==NULL&&
    RtlCompareMemory(ARPRcvPacketBytes,base,10)==10)
    {
    ARPRcvPacket=base;
    }
    base++;
    }
    }
    各种编译器所编译的函数,前几个指令都是几乎一样的,用来建立堆栈帧,这些指令叫函数的序言。
    在win2000上是三字节
    push ebp
    mov ebp, esp
    到了winxp以及后续系统上,则变成了五字节
    mov edi, edi
    push ebp
    mov ebp, esp
    而一个近跳转指令刚好是五字节,在xp上刚好覆盖了函数的序言,所以在XP上挂钩也相对容易一点,这里着重说明如何对ARPRcv进行挂钩,我们在ARPRcv内部插入一个jmp指令,将跳到ARPRcvProx函数,该函数是个裸函数,函数实现如下:
    _declspec(naked) ARPRcvProx()//跳板函数
    {
    _asm
    {
    mov edi, edi
    push ebp
    mov ebp ,esp
    //七个参数开始压栈
    push [ebp+20h]
    push [ebp+1ch]
    push [ebp+18h]
    push [ebp+14h]
    push [ebp+10h]
    push [ebp+0ch]
    push [ebp+8]
    call NewARPRcv //调用NewARPRcv函数
    cmp eax,0x10003 //判断函数返回值是否NDIS_STATUS_NOT_ACCEPTED
    jz end //如果是NDIS_STATUS_NOT_ACCEPTED,直接结束本函数
    //而不跳回到ARPRcv函数
    mov eax,ARPRcv //如果返回的不是NDIS_STATUS_NOT_ACCEPTED,将会
    //执行到这条指令,该指令将 ARPRcv函数的地址装入eax
    add eax,5 //将ARPRcv地址值加上5,存入eax,表示即将跳转的//地址
    jmp eax //开始跳回ARPRcv体内
    end:
    pop ebp
    retn 1ch
    }
    }
    在该函数内部,又调用了NewARPRcv函数,原型和ARPRcv保持一致,也必须由我们自己实现:
    NDIS_STATUS
    NewARPRcv(
    IN NDIS_HANDLE ProtocolBindingContext,
    IN NDIS_HANDLE MacReceiveContext,
    IN PVOID HeaderBuffer,
    IN UINT HeaderBufferSize,
    IN PVOID LookAheadBuffer,
    IN UINT LookaheadBufferSize,
    IN UINT PacketSize
    )
    {
    /*
    在这里加入你的判断逻辑代码,是否拦截该数据
    如果要拦截,则返回 NDIS_STATUS_NOT_ACCEPTED
    否则返回NDIS_STATUS_SUCCESS,把数据交给ARPRcv处理
    */
    return NDIS_STATUS_SUCCESS;
    }
    同样的原理,我们在ARPRcvPacket里面插入jmp指令,将跳转到ARPRcvPacketProx裸函数,该函数实现如下:
    _declspec(naked) ARPRcvPacketProx()
    {
    _asm
    {
    mov edi, edi
    push ebp
    mov ebp ,esp
    //两个参数开始压栈
    push [ebp+0ch]
    push [ebp+8]
    call NewARPRcvPacket//调用NewARPRcvPacket
    cmp eax,0 //如果返回0则表示拒绝该数据包
    jz end //直接返回本函数
    mov eax ,ARPRcvPacket
    add eax ,5
    jmp eax //跳回ARPRcvPacket函数第六个字节
    end: pop ebp
    retn 8
    }
    }
    在该函数内部,将会调用NewARPRcvPacket,函数实现如下:
    INT
    NewARPRcvPacket(NDIS_HANDLE BindContext,
    PNDIS_PACKET ndisPacket)
    {
    /*
    在这里加入你的判断逻辑,是否拦截该数据,如果要拦截,则返回0,
    否则返回非0
    */
    DbgPrint("RcvPacket");
    return 1;
    }
    请仔细阅读以上代码的注释,接下来,我们还必须提供一个函数实现安装和卸载挂钩功能
    void PatchARPRcv(BOOLEAN isPatch)//isPatch为TRUE表示安装挂钩,为FALSE表示卸载挂钩。
    {
    /*即将用以下五个字节覆盖ARPRcv函数前五个字节
    这5个字节就是jmp XXXX指令的机器码,因为跳转的相对地址还需要
    进一步计算,所以暂时用零填充
    */
    UCHAR patchBytes[5]={0xe9,0x00,0x00,0x00,0x00};
    //即将用以下五个字节覆盖ARPRcvPacket函数前五个字节
    UCHAR patchBytes2[5]={0xe9,0x00,0x00,0x00,0x00};
    //保存原始函数的前五个字节,方便以后恢复挂钩
    UCHAR restoreBytes[5]={0x8b,0xff,0x55,0x8b,0xec};
    /*
    以下两行代码计算跳转的偏移量
    */
    int offset=(char*)ARPRcvProx-(char*)ARPRcv-5;
    int offset2=(char*)ARPRcvPacketProx-(char*)ARPRcvPacket-5;
    //修正patchBytes和patchBytes2中的相对地址
    memcpy(patchBytes+1,&offset,4);
    memcpy(patchBytes2+1,&offset2,4);
    if(isPatch)
    {
    DisableWriteProtect();//禁止写保护
    memcpy(ARPRcv,patchBytes,5);
    memcpy(ARPRcvPacket,patchBytes2,5);
    EnableWriteProtect(); //开启写保护
    }
    else
    {
    DisableWriteProtect();
    memcpy(ARPRcv,restoreBytes,5);
    memcpy(ARPRcvPacket,restoreBytes,5);
    EnableWriteProtect();
    }
    }
    因为ARPRcv和ARPRcvPacket函数处于只读页,所以必须先禁用写保护才能向其中插入代码,禁用写保护和开启写保护代码如下:
    void
    DisableWriteProtect()
    {
    _asm{
    cli
    mov eax, cr0
    and eax, 0FFFEFFFFh
    mov cr0, eax
    }
    }
    void
    EnableWriteProtect()
    {
    _asm{
    mov eax, cr0
    or eax, not 0FFFEFFFFh
    mov cr0, eax
    sti
    }
    }
    注意这些代码暂时只适用XP系统,在win2000和win2003上都需要少许改动。

  • 相关阅读:
    设计模式-装饰模式(Decorator Pattern)
    死锁分析与解决
    事务原理与开发
    SQL注入与防范
    数据库连接池
    JDBC基础知识
    算法复习
    计算机网络基础知识
    Caused by: org.gradle.api.internal.plugins.PluginApplicationException: Faile
    手写算法
  • 原文地址:https://www.cnblogs.com/chenhs/p/1434429.html
Copyright © 2020-2023  润新知