欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]SSDT InlineHook学习笔记

[原创]SSDT InlineHook学习笔记

最近学习SSDT InlineHook,看了很多论坛前辈的文章,收获很多,代码很长,也很完善,但觉得有点难理解,太完善的代码会禁锢人的思维,发现问题通过努力自己解决,岂不理解得更加深刻?纸上得来终觉浅,绝知此事要躬行。下面说说我对InlineHook的理解,以NtOpenProcess为例。先看下正常的NtOpenProcess的执行流程,如图1:

我们应该怎么Hook这个函数呢,最简单的方法只需要把函数头的push 0C4h改为jmp xxxxxxx就可以了,当调用NtOpenProcess的时候,就会跳到我们的Hook函数上,就可以为所欲为了。这就是InlineHook,改变函数内部执行流程的Hook就是InlineHook。Hook后的函数,如图2:

我们看到原函数的push 04Ch已经变为jmp f7c15600,这个跳转的地址就是HookedNtOpenProcess的地址。那么问题来了,我们如何把原函数头部的push指令改为jmp指令,使其跳转到我们的Hooked函数上呢。我们必须要取得两个函数的地址,由于SSDT表是导出的,所以取得NtOpenProcess非常简单.

typedef struct _SERVICE_DESCRIPTOR_TABLE

{

  PVOID    ServiceTableBase;

  PULONG   ServiceCounterTableBase;

  ULONG    NumberOfService;

  ULONG    ParamTableBase;

}SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE; 

Extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;       //KeServiceDescriptorTable为导出函数

Address=(ULONG)KeServiceDescriptorTable->ServiceTableBase+0x7A*4;

//存放NtOpenProcess的数组下标为7A,

OldServiceAddress=*(ULONG*)Address;  

//取得Address中的内容,为NtOpenProcess地址。

HookedNtOpenProcess函数是我们定义的,直接取就可以,这样jmp的地址就可以计算了。
jmpaddr=(ULONG)HookedNtOpenProcess-(ULONG)RealNtOpenProcess-5;为什么要这样计算跳转地址呢,直接jmp MyNtOpenProcess不可以吗,当然不行。在16位汇编下,jmp指令是分段内转移和段间转移的。32位下内存为flat模式,不再区分段内段外,可以理解为都是段内转移,所以jmp指令执行的时候不需要转移的目的地址,需要的是偏移。我们需要以原函数的第二条指令为基址计算偏移(push 04Ch占5字节),也就是图1中push offset nt!ObWatchHandles+0x25c (804daaa8)指令,写成如下格式可能更好理解
jmpaddr=(ULONG)HookedNtOpenProcess-((ULONG)RealNtOpenProcess+5);
Jmpaddr就是跳转位移,跟jmp的机器码e9组合起来,就是最关键的跳转指令了。如下图所示,这我Hook完之后的函数头,大家注意,e965336577这个字节码,e9为jmp指令的字节码,65336577倒过来看77653365为相对于原函数的偏移量。

我们可以反推一下Hooked函数=原函数+偏移量+5,我们用WinDbg看下是否正确,如下图:

果然,已经来到了Hooked函数头部,我们的计算是正确的。具体函数如下。
ULONG OldServiceAddress;                                         //原来NtOpenProcess的服务地址

VOID Hook(int *PID)

{  //HANDLE pid;

     ULONG temp;

  ULONG Address;

  ULONG jmpaddr;

  Address=(ULONG)KeServiceDescriptorTable->ServiceTableBase+0x7A*4;    //0x7A为NtOpenProcess服务ID

  DbgPrint("Address:0x%08X\n",Address);

  MyPID=(HANDLE)*PID;

  OldServiceAddress=*(ULONG*)Address;                                  //保存原来NtOpenProcess的地址

  RealNtOpenProcess=(NTOPENPROCESS)OldServiceAddress;

  DbgPrint("RealNtOpenProcess:0x%08X\n",RealNtOpenProcess);

  DbgPrint("MyNtOpenProcess:0x%08X\n",MyNtOpenProcess);

  __asm                                                                //去掉关内存保护

  {

        cli

      mov    eax,cr0

      and    eax,not 10000h

      mov    cr0,eax

  }

     temp=*((ULONG*)Address);

     DbgPrint("temp:%08X\n",temp);

     jmpaddr=(ULONG)MyNtOpenProcess-(ULONG)RealNtOpenProcess-5;

         __asm

         {

           mov ebx,temp

           mov eax,jmpaddr

           mov byte ptr ds:[ebx],0xE9           //在原来函数头加jmp 

           mov DWORD ptr ds:[ebx+1],eax

         }

  __asm                                                               //恢复开内存保护 

  {

        mov    eax,cr0

      or     eax,10000h

      mov    cr0,eax

      sti

  }

  DbgPrint("hook\n");

}

Hook函数完成了,接下来看下执行主体的实现方法。NtOpenProcess在内核中的调用非常频繁,所以我们需要判断哪些程序不能发现我们保护的进程。这个函数有多种写法, 我更喜欢用naked函数,这里又要普及一下naked关键字了。裸函数就是不为你添加任何汇编指令的函数,真正的全都要自己写的函数,使用这个关键字有几点需要特别注意:
1.使用 naked 关键字必须自己构建 EBP 指针
2.必须显示返回,用联汇编的ret不是c的return
3.不能直接引用参数,需要用寄存器如ESP+8
4.栈平衡要尤其注意

最简单的我们可以把它写成这样子,什么都不做,直接返回四个参数,这样也Hook NtOpenProcess。
NTSTATUS __declspec(naked)(__stdcall MyNtOpenProcess)(

                                              PHANDLE ProcessHandle,

                        ACCESS_MASK DesiredAccess,

                        POBJECT_ATTRIBUTES ObjectAttributes,

                        PCLIENT_ID ClientId) 

{

  __asm

  {

  

          retn 0x10   //执行保护后的返回

  }

我们现在把它扩展一下,根据进程ID来保护进程不被OD发现并且不能被任务管理器终结。
我们关注下第四个参数:
typedef struct _CLIENT_ID {

    HANDLE UniqueProcess;

    HANDLE UniqueThread;

} CLIENT_ID;

typedef CLIENT_ID *PCLIENT_ID;

UniqueProcess这个成员变量是调用者传入的进程PID,只需要根据这个PID来判断是否使我们要保护的进程就可以了,因为不能直接访问参数所以用寄存器访问。
__asm

  {

    push eax

    mov eax,[esp+0x14]  //获取ClientId,因为是第四个参数所以【esp+10】,但上面有push eax 所以再加4

    mov eax,[eax]       //取得UniqueProcess

    mov PID,eax        //不能在naked函数里定义局部变量

    pop eax

  }

PID和Access我定义的是全局变量,保存esp 用ebp来存放局部变量,应该也可以但我并没有试验。取到了PID之后呢与需要保护的PID对比
if(PID==MyPID)

    {

      __asm

        {

          mov [esp+4],0 //返回空句柄

          mov eax,0xC0000022L  //返回值STATUS_ACCESS_DENIED 无法发现进程

          retn 0x10   //执行保护后的返回

        }

    }

如果是的话ProcessHandle置零,如果函数成功会返回进程对象,我们不让他成功,返回值设置为0xC0000022L,含义如下
#define STATUS_ACCESS_DENIED             ((NTSTATUS)0xC0000022L)

这个函数相当于我们只把进程对象置零,并且返回无权限,这个Hookd函数就结束了。但这还没有完,当我们需要放行的时候,还需要调用原来的NtOpenProcess。
__asm

  {

    push  0x0C4

    mov edx,RealNtOpenProcess

    add edx,5

    jmp edx

  }

因为我们之前Hook的时候已经把push 04Ch改为了jmp xxxxxxx,所以调用原来函数的时候需要实现它的前五个字节也就是push 04Ch,然后跳到它的下一条指令继续执行。这样这个函数就写完了,可能有点不直观,再来个流程图,如图5

这样清晰多了。我们看下效果吧,driverMoniter载入。

我们已经无法通过任务管理器关闭计算器了。再看下OD能不能发现我们的进程。

OD看不到计算器了,我们的hook成功了。
文笔不好加之水平有限,错漏再算难免,只为做个记录,请大家批评指正。
代码如下:
inlineHook.rar

上传的附件 图片1.jpg
图片2.jpg
图片3.jpg
图片4.jpg
图片5.jpg
图片6.jpg
图片7.jpg
inlineHook.rar

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功