找回密码
 加入我们

QQ登录

只需一步,快速开始

搜索
查看: 12067|回复: 5

NtOpenProcess inline hook

  [复制链接]

90

主题

473

回帖

2

精华

钻石会员

积分
3261
发表于 2010-9-28 19:20:27 | 显示全部楼层 |阅读模式
#include <ntddk.h>
#include <windef.h>
#include <ntstatus.h>
ULONG  CR0VALUE;
BYTE  OriginalBytes[5]={0};             //保存原始函数前五个字节           
BYTE JmpAddress[5]={0xE9,0,0,0,0};       //跳转到HOOK函数的地址
extern POBJECT_TYPE *PsProcessType;
typedef     NTSTATUS     (__stdcall *NTOPENPROCESS)(

  OUT PHANDLE             ProcessHandle,
  IN ACCESS_MASK          AccessMask,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN PCLIENT_ID           ClientId );

//HOOK函数

NTSTATUS __stdcall MyNtOpenProcess(
                                                                                   
  OUT PHANDLE             ProcessHandle,
  IN ACCESS_MASK          AccessMask,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN PCLIENT_ID           ClientId );



void  HookNtOpenProcess()

{
        
        //赋值前面定义的数组
        KIRQL Irql;
        KdPrint(("[NtOpenProcess111] :0x%x",NtOpenProcess));  //地址验证
    KdPrint(("[MyNtOpenProcess111] :0x%x",MyNtOpenProcess));  //地址验证
        //保存函数前五个字节内容
        RtlCopyMemory(OriginalBytes,(BYTE *)NtOpenProcess,5);
        //保存新函数五个字节之后偏移
        *(ULONG *)(JmpAddress+1)=(ULONG)MyNtOpenProcess-((ULONG)NtOpenProcess+5);
        //开始inline hook
        //关闭内存写保护
        _asm
               
        {
                push eax
                        
                        mov eax, cr0
                        mov CR0VALUE, eax
                        and eax, 0fffeffffh  
                        mov cr0, eax
                        pop eax
        }
        
        //提升IRQL中断级
        Irql=KeRaiseIrqlToDpcLevel();
        //函数开头五个字节写JMP
        RtlCopyMemory((BYTE *)NtOpenProcess,JmpAddress,5);
    KdPrint(("JmpAddress :0x%x",JmpAddress));
    KdPrint(("[MyNtOpenProcess222] :0x%x",MyNtOpenProcess));
        //恢复Irql
        KeLowerIrql(Irql);
        //开启内存写保护
        
        __asm
               
        {      
               
                    push eax
                        
                        mov eax, CR0VALUE
                        
                        mov cr0, eax
                        
                        pop eax
                        
        }
        
}


  NTSTATUS __stdcall OriginalNtOpenProcess(
  OUT PHANDLE             ProcessHandle,
  IN ACCESS_MASK          AccessMask,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN PCLIENT_ID           ClientId )

                                                                                                                        
{
        
/*_asm
     {nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
     }*/

return STATUS_SUCCESS;
        
}


NTSTATUS  MyNtOpenProcess(
                                                                                   
  OUT PHANDLE             ProcessHandle,
  IN ACCESS_MASK          AccessMask,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN PCLIENT_ID           ClientId )
                                                                                   
{
        
    NTSTATUS     rc;
    ULONG       PID;
   
    //DbgPrint( "NtOpenProcess() called.\n" );
   
    rc = (NTSTATUS)OriginalNtOpenProcess(
          ProcessHandle,
          AccessMask,
          ObjectAttributes,
          ClientId );
   
    if( (ClientId != NULL) )
    {
      PID = (ULONG)ClientId->UniqueProcess;
      //DbgPrint( "%d was opened,Handle is %d.\n", PID, (ULONG)ProcessHandle );
      
      // 如果进程PID是1908,直接返回权限不足,并将句柄设置为空
      if( PID == 1124 )
      {
         DbgPrint( "Some want to open pid 1520!\n" ); //调试输出 类似C语言的 Printf
            
             ProcessHandle = NULL;
                     
         rc = STATUS_ACCESS_DENIED;
      }
    }
   
    return rc;
   
}

void UnHookNtOpenProcess()

{
        
        //把五个字节再写回到原函数
        
        KIRQL Irql;
        
    //关闭写保护
        
        _asm
               
        {
               
                push eax
                        
                        mov eax, cr0
                        
                        mov CR0VALUE, eax
                        
                        and eax, 0fffeffffh  
                        
                        mov cr0, eax
                        
                        pop eax
                        
        }
        
    //提升IRQL到Dpc
        
    Irql=KeRaiseIrqlToDpcLevel();
        
        RtlCopyMemory((BYTE *)NtOpenProcess,OriginalBytes,5);
        
        KeLowerIrql(Irql);
        
    //开启写保护
        
        __asm
               
        {      
               
                    push eax
                        mov eax, CR0VALUE
                        mov cr0, eax
                        
                        pop eax
                        
        }
}


VOID Unload(IN PDRIVER_OBJECT pDriverObj)
{

UnHookNtOpenProcess();

KdPrint(("[UnHookNtOpenProcess] Unloaded\n"));

}


NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj,PUNICODE_STRING      pRegistryString)

{

pDriverObj->DriverUnload = Unload;

HookNtOpenProcess();

return STATUS_SUCCESS;

}

857

主题

2632

回帖

2

精华

管理员

此生无悔入华夏,  长居日耳曼尼亚。  

积分
36130
发表于 2010-9-28 20:18:09 | 显示全部楼层
本帖最后由 Tesla.Angela 于 2010-9-28 20:20 编辑

恭喜OK大哥学会Kernel Inline Hook!!加亮加精鼓励!!

90

主题

473

回帖

2

精华

钻石会员

积分
3261
 楼主| 发表于 2010-9-28 20:35:44 | 显示全部楼层
转个贴














这段代码是针对KiInsertQueueApc函数。那么inline hook的流程是什么呢?我们知道,inline hook的目的就是修改内核函数的代码。为什么要修改呢?因为原始函数的代码我不是很满意,或者说不符合我的想法。

我们还知道,函数的代码的处理对象其实就是传进来的参数。如果说我修改KiInsertQueueApc函数的开头几个字节,使其原来的代码变成一个跳转指令,跳到我自己的函数地址上并且执行我自己构建的函数,执行完成之后再跳回到原函数代码的下条指令。那么inline hook技术就实现了。而我自己构建的函数就可以事先验证参数信息。举个例子或许就非常容易理解。现在很多杀毒软件具有自我保护能力(比如保护自身进程),他会inline hook NtTerminateProcess()。具体原理是一样的,杀毒软件会修改这个函数开头的几个字节使其变为一个跳转指令,跳到杀毒软件作者自己写的函数地址上,之后这个自定义函数开始分析传进来的参数,如果发现是自己进程,那么返回失败,如果是其他进程,那么不做任何反映直接跳回去执行原始函数以后的代码。这样杀毒软件的进程就不会被关闭。

现在分析代码。一开始肯定是驱动程序的入口函数

NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject, IN PUNICODE_STRING theRegistryPath )
{
DbgPrint("My Driver Loaded!");
theDriverObject->DriverUnload = OnUnload;

g_KiInsertQueueApc = FindKiInsertQueueApcAddress();
DetourFunctionKiInsertQueueApc();

return STATUS_SUCCESS;
}

没啥特别的地方,和普通的驱动程序一样。由于不用处理IRP,那么分发函数就没有必要去定义。

需要注意的是,驱动程序不一定必须要创建设备。你可以发现,这个驱动程序就没有创建设备对象,因为此驱动程序不跟应用程序打交道。读者要正确理解驱动程序的概念

我们来看看FindKiInsertQueueApcAddress();

ULONG FindKiInsertQueueApcAddress()
{
char * Addr_KeInsertQueueApc = 0; (1)
int i = 0;
char Findcode[] = { 0xE8, 0xcc, 0x29, 0x00, 0x00 }; (2)
ULONG Addr_KiInsertQueueApc = 0;
Addr_KeInsertQueueApc = (char *) GetFunctionAddr(L"KeInsertQueueApc"); (3)
for(i = 0; i < 100; i ++) (4)
{
if( Addr_KeInsertQueueApc == Findcode[0] &&
Addr_KeInsertQueueApc[i + 1] == Findcode[1] &&
Addr_KeInsertQueueApc[i + 2] == Findcode[2] &&
Addr_KeInsertQueueApc[i + 3] == Findcode[3] &&
Addr_KeInsertQueueApc[i + 4] == Findcode[4]
)
{
Addr_KiInsertQueueApc = (ULONG)&Addr_KeInsertQueueApc + 0x29cc + 5; (5)
break;
}
}
return Addr_KiInsertQueueApc;
}
具体分析:这个函数的作用是找到KiInsertQueueApc()的首地址。具体看代码:

(1)定义一个指针变量。此指针的跳跃数为1字节。

(2)你必须用windbg分析KeInsertQueueApc()函数,由于这个函数必定调用KiInsertQueueApc(),那么“调用代码”必定会出现在函数中。那么我们选出这个“调用代码”作为特征值就可以定位到KiInsertQueueApc()的首地址。具体是这样的:{ 0xE8, 0xcc, 0x29, 0x00, 0x00 }这是在内存中的样子。而从程序的角度来讲是:000029ccE8,那么这样的机器码对应的汇编代码是 ADD BYTE PTR DS:[EAX],AL
SUB ESP,ECX
CALL

CALL对应的机器码是0xE8.那么既然CALL出现了,后面跟的东西肯定为一个地址值。通过用windbg分析可知,这个地址就是KiInsertQueueApc()的首地址。不过需要注意的是,汇编代码和机器码并不是呆板的一一对应的关系,这点千万要注意!!

(3)调用一个自定义函数。

ULONG GetFunctionAddr( IN PCWSTR FunctionName)
{
UNICODE_STRING UniCodeFunctionName;
RtlInitUnicodeString( &UniCodeFunctionName, FunctionName );
return (ULONG)MmGetSystemRoutineAddress( &UniCodeFunctionName );

}

这个函数会调用MmGetSystemRoutineAddress()内核函数,其目的是得到KeInsertQueueApc()的首地址。需要注意的是KeInsertQueueApc()是导出函数,那么可以用MmGetSystemRoutineAddress()得到其首地址。KiInsertQueueApc()为非导出函数,因此不能通过调用MmGetSystemRoutineAddress()直接得到其首地址。

(4)通过循环的方式查找特征码从哪个地方开始出现。

(5)Addr_KiInsertQueueApc = (ULONG)&Addr_KeInsertQueueApc + 0x29cc + 5。这句话我感觉有点错误,我把它改成Addr_KiInsertQueueApc = (ULONG)&Addr_KeInsertQueueApc + 5。(其实这样表达是错误的)

{ 0xE8, 0xcc, 0x29, 0x00, 0x00 } 后面是偏移量 CALL 后面真实的地址是 偏移量+偏移量后面的真实地址=call真实地址

举个例子吧:

004011FC |. E8 CF000000 CALL chapter1.004012D0
00401201 |. 83C4 04 ADD ESP,4

CALL chapter1.004012D0=是E8后面的 000000CF+00401201=004012D0 大家可以算下

Addr_KiInsertQueueApc = (ULONG)&Addr_KeInsertQueueApc + 0x29cc + 5 是正确的 相当于

(ULONG)&Addr_KeInsertQueueApc+5=上面call下一句的地址(就好比:00401201)

0x29cc=000000CF 所以Addr_KiInsertQueueApc = (ULONG)&Addr_KeInsertQueueApc + 0x29cc + 5就是CALL的真实地址

个人理解 有错误的地方 欢迎大家仍鸡蛋


这句话中有个指针的高级用法,请看(ULONG)&Addr_KeInsertQueueApc 。它的组合顺序是这样的:

(ULONG)(&(Addr_KeInsertQueueApc ))

其中Addr_KeInsertQueueApc代表一个跳跃数为1字节的指针。

Addr_KeInsertQueueApc表示这个指针跳跃i次后所指向的内存空间对应的变量。(死记)

&(Addr_KeInsertQueueApc 变量再次加上&之后又变成了地址值。

(ULONG)(&(Addr_KeInsertQueueApc ))最后加上ULONG强制转换类型。为4字节(32位地址)

为什么最后要+5呢?因为这5个字节特征码必须跳过才能真正定位到函数的地址值。你好好分析上面的循环代码,仔细算下就会知道必须+5。这个问题不难理解。

关于上面指针的用法,估计有些读者还是很晕,其实是这样的,指针的表示方式很多,上面这个表示方式是其中的一种,你如果不能理解,那么好好去理解,如果实在理解不了,那么死记。请看下面我的例子,或许能帮助你理解:

int * p;

int a=5;

p=&a;

上面这个3行代码很好理解,很基础。其实那样写麻烦,可以这样:

int *p =(int *)&(5)

哈哈,具体如何理解笔者不打算去讲解,靠读者自己去领悟。反正这样方法有些书上会用到。并且将来的代码都会涉及。


再来看看我们的自定义函数:

__declspec(naked) my_function_detour_KiInsertQueueApc() (1)
{
__asm
{
mov edi,edi (2)
push ebp (2)
mov ebp, esp (2)
push ecx
mov eax,ecx
_emit 0xEA (5)
_emit 0xAA (3)
_emit 0xAA (3)
_emit 0xAA (3)
_emit 0xAA (3)
_emit 0x08 (4)
_emit 0x00 (4)
}
}

这个函数为自定义函数,将来运行kiInsertQueueApc()函数之后会立刻跳转到这个函数上来。(1)(2)和函数堆栈框架有关,请读者仔细研究《天书夜读》这本书。

我们知道这个自定义函数执行完之后还必须跳回原来的kiInsertQueueApc(),那么(3)这个地方就是跳回去的地址。这里的_emit的意思是强制产生一个字节的数据作为代码,因此在mov eax,ecx 后面就多出了一段代码:EA,AA,AA,AA,AA,08,00。这7个字节的数据作为机器码而存在,那么翻译成汇编代码就是JMP FAR 0008:AAAAAAAA。其中0008是选择子。AAAAAAAA正好32位,为一个地址值。那么为什么必须这样做呢?为什么不能直接用JMP FAR指令呢?因为我们现在编写的是驱动程序,那么最后必须通过DDK编译器来编译,而DDK编译器压根没有指出段间跳转的指令语法,既然人家语法都没有去定义,那么你怎么能用呢。总之在DDK编译器眼里,它根本不认识JMP FAR这个指令。关于选择子的介绍等我考试好了再说,你现在只要知道:大多数内核代码用的都是08选择子。(4)就是选择子,以后重点会讲到

以上这个函数的作用仅仅只是跳回去。你可以在这个函数里写入自己想要写入的代码。


之后驱动程序调用DetourFunctionKiInsertQueueApc();

VOID DetourFunctionKiInsertQueueApc()
{

char *actual_function = (char *)g_KiInsertQueueApc; (1)
unsigned long detour_address;
unsigned long reentry_address; //从自定义函数条跳到原函数的偏移地址。
KIRQL oldIrql;
int i = 0;

char newcode[] = { 0xEA, 0x44, 0x33, 0x22, 0x11, 0x08, 0x00, 0x90 };

reentry_address = ((unsigned long)g_KiInsertQueueApc) + 8;

non_paged_memory = ExAllocatePool(NonPagedPool, 256);

for(i=0;i<256;i++)
{
((unsigned char *)non_paged_memory) = ((unsigned char *)my_function_detour_KiInsertQueueApc);
}

detour_address = (unsigned long)non_paged_memory;

*( (unsigned long *)(&newcode[1]) ) = detour_address;

for(i=0;i<200;i++)
{
if( (0xAA == ((unsigned char *)non_paged_memory)) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+1]) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+2]) &&
(0xAA == ((unsigned char *)non_paged_memory)[i+3]))
{
*( (unsigned long *)(&non_paged_memory) ) = reentry_address;
break;
}
}


oldIrql = KeRaiseIrqlToDpcLevel();
for(i=0;i < 8;i++)
{
g_oricode = actual_function;
actual_function = newcode;
}
KeLowerIrql(oldIrql);
}

哈哈,如果你对上面指针用法理解了。那么这段代码很容易理解。当然,如果你对指针的用法还是不理解,那么你通过这个函数的代码继续来理解。笔者不再解释。笔者用红字表示的部分非常之重要。这是inline hook的关键。还是句老话,代码理解很容易,前提是你理解指针的原理和表示手段。自己去好好思考吧,一定要理解透了。


void WPOFF()
{

ULONG uAttr;

_asm
{

cli /阻止任何中断响应。
push eax;
mov eax, cr0;
mov uAttr, eax;
and eax, 0FFFEFFFFh; // CR0 16 BIT = 0
mov cr0, eax;
pop eax;
sti /恢复中断响应
};

g_uCr0 = uAttr; //保存原有的 CRO 屬性

}

上面内容在讲解保护模式和实模式的区别的时候已经涉及。CR0寄存器的第16位为内存保护开关位。当你把这位置0,那么内存保护功能就关闭了,如果为1,那么内存保护功能就开启。读者会问为什么我要关闭内存保护呢?很简单,inline hook修改的可是内核代码,如果不关闭保护功能,你就不能修改。又有读者会问,关闭内存保护功能难道如此的简单?是的,就这么的简单!由于这个手段导致内存保护在某个时间段彻底的瓦解,因此微软不是很支持这样的做法。不过这样做是确实有效地。当然,修改内存保护属性可以通过其他方式实现,这个方式等我考试好了送上,并且这个方式是受微软支持的。

上面代码中你还会看到cli和sti这两个指令。他们都和中断有关,很明显,上面红色部分执行的时候你可不希望中途来个中断,那么你可以用cli暂时关闭中断,等执行完了之后记得要用sti开就OK了。


可以看到,网上这段代码并不是完美无缺,还是有少许的错误在里面。不过作者表达的意思已经足够的明了。这也就够了。接下来笔者还会再举2个例子来分析inline hook 。这些例子代码都是从网下或者书上搞到的。咱们先研究人家的思路和一些技巧,为以后独立完成一个inline hook做好基础。


OK,inline hook的初步介绍就到这里。窝可以去安心准备考试了。

补充说下,任何API函数的参数值都是32位值!这个很重要。对你研究堆栈很有帮助。

275

主题

3017

回帖

1

精华

管理员

嗷嗷叫的老马

积分
17064

论坛牛人贡献奖关注奖最佳版主进步奖人气王疯狂作品奖精英奖赞助论坛勋章乐于助人勋章

QQ
发表于 2010-9-29 09:41:47 | 显示全部楼层
补充说下,任何API函数的参数值都是32位值!这个很重要。对你研究堆栈很有帮助。 ...
ok100fen 发表于 2010-9-28 20:35


这个确实.

参数要么是表示的直接值,要么是指向某个结构或别的缓冲区的指针.但都是32位的一个值.

0

主题

2

回帖

0

精华

初来乍到

积分
21
发表于 2012-7-27 00:37:12 | 显示全部楼层
这代码有问题 死循环

0

主题

8

回帖

0

精华

初来乍到

积分
21
发表于 2012-8-6 11:45:18 | 显示全部楼层
学习了,谢谢!!!
您需要登录后才可以回帖 登录 | 加入我们

本版积分规则

快速回复 返回顶部 返回列表