查看原文
其他

x64内核中的HOOK技术|拦截进程、拦截线程、拦截模块(思路)

2018-02-17 张新琪 看雪学院



一、为什么讲解HOOK技术?


在32系统下,例如我们要HOOK SSDT表,那么直接讲CR0的内存保护属性去掉,直接讲表的地址修改即可。


但是在64位系统下,不可以这样操作了。


第一是因为 SSDT表加密了。第二是 SSDT表你就算解密了,那么你的API的地址也会放的很远。SSDT表放不下4G以外的地址,所以也不能放。


而现在试想一下,杀毒软件也要在内核中做点手脚。如果不能HOOK了,它们该怎么发展那?


所以到了win7 (64位)系统下,微软被玩怕了。直接告诉你,你想HOOK那个API,我帮你做,你不用动手做,而且我的还稳定,不会蓝屏。所以x64下的HOOK都是很简单的。


比如 xxx杀毒软件要监视进程的创建,那么微软就说,我给你API调用,你提供回调。当创建的时候我就通知你,返回值也是你说了算。这样我们就可以使用它的API了,线程、模块都是这样的。



二丶进程监控以及拦截HOOK


API:

PsSetCreateProcessNotifyRoutineEx


函数原型

NTSTATUS

  PsSetCreateProcessNotifyRoutineEx(

    IN PCREATE_PROCESS_NOTIFY_ROUTINE_EX  NotifyRoutine,    //你提供的回调函数地址.

    IN BOOLEAN  Remove                        //是否删除回调. 如果为False则是注册回调,如果为TRUE则是删除回调.

    );


这个函数的作用就是 当进程创建的时候会通知你。


参数一回调函数原型

VOID

  CreateProcessNotifyEx(

    __inout PEPROCESS  Process,                //进程的EPROCESS会提供给你.

    __in HANDLE  ProcessId,                  //进程ID会提供给你.

    __in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo      //进程信息的额外信息会提供给你. 注意参数是可操作的.也就是说可能为NULL

    );


进程额外信息结构体

typedef struct _PS_CREATE_NOTIFY_INFO {

  __in SIZE_T  Size;                    //大小.

  union {

    __in ULONG  Flags;

    struct {

      __in ULONG  FileOpenNameAvailable : 1;

      __in ULONG  Reserved : 31;

    };

  };

  __in HANDLE  ParentProcessId;            //父进程ID

  __in CLIENT_ID  CreatingThreadId;      

  __inout struct _FILE_OBJECT  *FileObject;     //文件对象

  __in PCUNICODE_STRING  ImageFileName;       //进程路径

  __in_opt PCUNICODE_STRING  CommandLine;      //命令行参数

  __inout NTSTATUS  CreationStatus;          //返回状态.

} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;


我们如果对返回状态进行操作,那么如果你返回值给失败,那么进程就不会创建,也就实现了我们阻止进程的创建了。


内核 代码实际操作 阻止calc计算器的创建。


initHook函数代码.


void InstallHook()

{  

  NTSTATUS status;                                  //定义NT状态                                                    

  status = PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx, FALSE);//注册回调

  if (NT_SUCCESS(status))                              //判断状态是否成功.

    dprintf("[Hello] PsSetCreateProcessNotifyRoutineEx CreateProcessNotifyEx=%pOK\r\n", CreateProcessNotifyEx);

  else

    dprintf("[Hello] PsSetCreateProcessNotifyRoutineEx status=%d\r\n", status);

}


回调函数代码


VOID CreateProcessNotifyEx( __inout PEPROCESS  Process,

                           __in HANDLE  ProcessId,

                           __in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo)

{

 

  UNICODE_STRING ustrCalc;                              //定义内核函数字符串

  RtlInitUnicodeString(&ustrCalc, L"\\??\\C:\\Windows\\system32\\calc.exe");//初始化计算器名字. 注意,这个要带上符号连接.  

  if (CreateInfo != NULL)                               //参数有可能为NULL

  {

    dprintf("[Hello] Create Porcess pid=%d ImageFileName=%wZ\r\n",

            ProcessId,

            CreateInfo->ImageFileName);

 

    if (RtlCompareUnicodeString(CreateInfo->ImageFileName, &ustrCalc, TRUE) == 0) //比较字符串.如果成功

    {

      //阻止创建

      CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;                           //标志给失败,则计算器不能运行.

    }

  }

  else

    dprintf("[Hello] Exit Porcess pid=%d\r\n", ProcessId);

}


回调卸载函数

PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx, TRUE);        //卸载回调


代码图片:



其中UnInstallHook则是调用卸载回调的函数。

驱动入口点调用initHook即可。


拦截图片:


编译好我们的驱动.去x64加载,打开计算机则会被拦截。



原理:

  

如果想知道原理,那么就要看Wrk源码。而在Wrk中,并没有这个API,这个API是64位才有的。但是在32下有一个,有一个不带EX版本的,我们可以参考一下。


 

通过源码可以发现.是操作数组,且这个数组里面是我们填写的回调,操作系统会依次调用回调。


跟随数组查看:



跟随查看,发现是个定长数组,里面只有八项。但是在32位,没人会使用这个API,所以8项正好。但是在64位系统下,数组变为了64。


也就是说:我们可以通过找到数组首地址,进行HOOK里面的回调。那么我们的就会执行,当然我们也可以自己注册。



三、线程监控以及拦截HOOK


线程拦截和进程拦截相似。但是在64位下,微软提供的HOOK函数没有这么强大。并没有带有EX的,所以我们只能用原来的。


线程监控API

  监控:    PsSetCreateThreadNotifyRoutine

  取消监控:  PsRemoveCreateThreadNotifyRoutine


监控原型:

NTSTATUS

  PsSetCreateThreadNotifyRoutine(

    IN PCREATE_THREAD_NOTIFY_ROUTINE  NotifyRoutine  //回调函数地址.当线程创建完毕,但还没运行的时候会调用你的回调.

    );


卸载函数原型:

NTSTATUS

  PsRemoveCreateThreadNotifyRoutine(

    IN PCREATE_THREAD_NOTIFY_ROUTINE  NotifyRoutine  //回调地址,你刚才注册的回调会取消.

    );


本质也是在数组里面注册函数地址。


这两个函数只能看,但是不能进行操作。但是很多时候看看就已经可以了。


线程回调原型:

VOID

(*PCREATE_THREAD_NOTIFY_ROUTINE) (

    IN HANDLE  ProcessId,          //进程ID

    IN HANDLE  ThreadId,           //线程ID

    IN BOOLEAN  Create            

    );


可以看到:线程HOOK后,信息很少,就一个进程ID、一个线程ID,还有一个创建标志。没有什么了,现在我们要进行HOOK。


HOOK思路:

  1. 通过进程ID,找到EPROCESS 对象

  2. 通过线程ID,找到ETHREAD   对象

  3. 通过EPROCESS对象,获得进程路径

  4. 通过进程路径,找到对应进程名

  5. 判断进程名是否相同

    

相同: 通过ETHREAD + 偏移,找到线程回调函数的地址. 并修改回调地址的内容为ret。

    

不相同:  不相同退出。


在上面HOOK思路中,第五不很重要.ETHREAD + 偏移,那么找到是那个成员?


那么我们要熟悉ETHREAD中那个成员重要



通过分析ETHREAD,可以得出:在偏移410的位置,存放这ring3下线程回调函数的地址,那么我们可以操作这个偏移,使其里面变为ret,变相了拦截了线程的创建。


实战代码:

  

initHook函数

void InstallHook()

{

  NTSTATUS status;

  status = PsSetCreateThreadNotifyRoutine(CreateThreadNotify);      //注册线程回调

  if (NT_SUCCESS(status))

    dprintf("[Hello] PsSetCreateThreadNotifyRoutine CreateThreadNotify=%p\r\n", CreateProcessNotifyEx);

  else

    dprintf("[Hello] PsSetCreateThreadNotifyRoutine status=%d\r\n", status);

}


回调函数中的代码

VOID CreateThreadNotify(

                                  IN HANDLE  ProcessId,

                                  IN HANDLE  ThreadId,

                                  IN BOOLEAN  Create)

{

  PEPROCESS Process = NULL;

  PETHREAD  Thread = NULL;

  UCHAR *pszImageName = NULL;

  NTSTATUS status;

  UCHAR *pWin32Address = NULL;

 

  status = PsLookupProcessByProcessId(ProcessId, &Process);  //1.通过进程ID,获取EPROCESS

  if (!NT_SUCCESS(status))

    return;

 

  status = PsLookupThreadByThreadId(ThreadId, &Thread);    //2.通过线程ID,获取ETHREAD

 

  pszImageName = PsGetProcessImageFileName(Process);      //3.通过EPROCESS获取进程名

 

 

  if (Create)

  {

    dprintf("[Hello] Create Thread pid=%d tid=%d ImageName=%s\r\n", ProcessId, ThreadId, pszImageName);

    if (strstr(pszImageName, "calc") != NULL)          //4.判断进程名是否是计算器

    {

      //KdBreakPoint();

 

      //修改回调函数代码

      pWin32Address = *(UCHAR**)((UCHAR*)Thread + 0x410);   //5.是的话.找到回调函数地址.并改为C3

      //KeAttachProcess();

      if (MmIsAddressValid(pWin32Address))

      {

        KdBreakPoint();

        //*pWin32Address = 0xC3;                 //修改为C3,但是注意,你修改的是否内存保护属性需要去掉.但是在64位下,不允许使用内联汇编了.不过你可以写二进制进行更改.或者汇编生成obj,驱动来使用

                                        //这里直接手动更改为C3.

      }

      //KeUnstackDetachProcess();

    }

  }

  else

    dprintf("[Hello] Exit Thread pid=%d tid=%d\r\n", ProcessId, ThreadId);

 

 

  if( Process )

    ObDereferenceObject (Process);              //引用计数--

  if( Thread )

    ObDereferenceObject (Thread);

 

}


PS:


上面通过进程ID获取EPROCESS等等的API,都是微软未公开的。但是导出了,我们只需要声明一下即可。但是既然是未公开那么就要看微软怎么使用,<br>微软使用之后,会对其引用计数--,所以我们也要这样做。


 内存中都改为C3,则线程都不能创建了。

 


四丶模块拦截,以及阻止思路


模块拦截以及HOOK也是和上面一样,提供回调即可。


但模块是给的ImageBae,也就是模块基址。所以我们只需要解析PE找到OEP,把OEP代码改成ret即可。


API:


监控:

  PsSetLoadImageNotifyRoutine

取消监控:

  PsRemoveLoadImageNotifyRoutine


函数原型:

NTSTATUS

  PsSetLoadImageNotifyRoutine(

    IN PLOAD_IMAGE_NOTIFY_ROUTINE  NotifyRoutine

    );


回调函数原型:

VOID

(*PLOAD_IMAGE_NOTIFY_ROUTINE) (

    IN PUNICODE_STRING  FullImageName,

    IN HANDLE  ProcessId, // where image is mapped

    IN PIMAGE_INFO  ImageInfo

    );


对于模块,并没有写实现代码。但是我觉着,思路很重要。仿照前面两个例子,自己扩展一下思路,自己实现也可以。


在这里先给大家拜个年。学习逆向方面的技术,第一是自己的兴趣;第二是自己的自学能力;第三就是找一个可靠的师兄。所谓前人栽树,后人乘凉。





本文由看雪论坛 张新琪 原创

转载请注明来自看雪社区


热门阅读


点击阅读原文/read,

更多干货等着你~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存