驱动学习---RootKits--inline hook 小插曲(

驱动学习---RootKits--inline hook 小插曲

(2010-05-09 03:14:02)

inline hook 初次见面(1)

RootKits中比较IMBA的技术就是inline hook,那么不妨来研究研究。

inline hook是病毒和杀毒软件必用的技术。这个技术通俗点讲就是修改内核代码,更直接点讲就是修改内核函数。需要重视的是,这个技术和其他hook的本质区别是它修改的是代码,并不仅仅是函数的地址。因此inline hook相对比较的难。具体体现在:

1,必须掌握汇编

2,必须掌握函数调用流程

3,必须掌握线程堆栈以及函数堆栈框架

4,必须掌握高级指针应用

5,清醒的头脑,踏实的态度

6,熟练使用windbg工具来分析某个函数的代码

inline hook其实不是难到无法接受这个地步。这点还请读者放心!但是,在我看来,计算机技术是靠自己的理解才能真正掌握,那么如果你有足够的兴趣,那么从现在开始,你必须努力攻克以上我谈到的所有东西!!在inline hook眼里,以上这些只是基础。

关于汇编的学习,请读者放弃大学课本里的16位汇编的学习!那个学了也没有什么用。反而会给你带来无限的困扰。我推荐给大家的书是《琢石成器32位汇编》。这本书非常棒。

函数的调用流程以前讲过,自己再参考相关书籍

函数堆栈框架这个知识很是重要。你可以参考《天书夜读》这本书。

高级指针的用法我一会就讲

由于用windbg看到的代码都是汇编代码,那么必须头脑清醒,不慌不忙,踏实认真。不然我不敢保证你能看得懂代码,除非你小子是天才。

OK,如果你是个初学者,那么以上的要求够你忙一阵子呢。当然你也可以选择放弃和驱动学习说88。

对于初学者来说,就目前而言可以暂时忘记我上面的要求继续往下看。请看下面一个完整的代码:

这个例子是我从网上随便找的,我搜索了一下,几乎都是用这个例子来讲inline hook。不过令人伤心的是,关于这样的代码,其解释要么很少要么全没。不过笔者会把解释送上。目的就是让读者在脑子里有这个思路。

#include <ntddk.h>
#include <ntifs.h>
ULONG g_KiInsertQueueApc;
char g_oricode[8];
ULONG g_uCr0;
char *non_paged_memory;

void WPOFF()
{

ULONG uAttr;

_asm
{
push eax;
mov eax, cr0;
mov uAttr, eax;
and eax, 0FFFEFFFFh; // CR0 16 BIT = 0
mov cr0, eax;
pop eax;
cli
};

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

}

VOID WPON()
{

_asm
{
sti
push eax;
mov eax, g_uCr0; //恢復原有 CR0 屬性
mov cr0, eax;
pop eax;
};

}

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

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

}

//根据特征值,从KeInsertQueueApc搜索中搜索KiInsertQueueApc
ULONG FindKiInsertQueueApcAddress()
{
char * Addr_KeInsertQueueApc = 0;
int i = 0;
char Findcode[] = { 0xE8, 0xcc, 0x29, 0x00, 0x00 };
ULONG Addr_KiInsertQueueApc = 0;
Addr_KeInsertQueueApc = (char *) GetFunctionAddr(L"KeInsertQueueApc");
for(i = 0; i < 100; i ++)
{
if( Addr_KeInsertQueueApc[i] == 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[i] + 0x29cc + 5;
break;
}
}
return Addr_KiInsertQueueApc;
}

VOID DetourFunctionKiInsertQueueApc()
{

char *actual_function = (char *)g_KiInsertQueueApc;
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)[i] = ((unsigned char *)my_function_detour_KiInsertQueueApc)[i];
}

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)[i]) &&
(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[i]) ) = reentry_address;
break;
}
}


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

VOID UnDetourFunction()
{
char *actual_function = (char *)g_KiInsertQueueApc;
KIRQL oldIrql;
int i = 0;

WPOFF();
oldIrql = KeRaiseIrqlToDpcLevel();

for(i=0;i < 8;i++)
{
actual_function[i] = g_oricode[i];
}
KeLowerIrql(oldIrql);
WPON();
ExFreePool(non_paged_memory);
}

VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("My Driver Unloaded!");
UnDetourFunction();
}

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

g_KiInsertQueueApc = FindKiInsertQueueApcAddress();
DetourFunctionKiInsertQueueApc();

return STATUS_SUCCESS;
}

*********************************下节分段分析************************************************* 

驱动学习---RootKits--inline hook 小插曲

(2010-05-09 22:21:38)

inline hook 初次见面(2)

这段代码是针对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[i] == 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[i] + 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[i] + 0x29cc + 5。其中29CC是个偏移。笔者分析这段代码感觉有点小疑惑,很明显上面已经用i进行一个循环,那么直接+i呀。当然作者的意图也是对的,我们完全可以对着windbg把这个偏移数出来,方法呆一点罢了。(在此感谢纠正我错误的朋友)

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

(ULONG)(&(Addr_KeInsertQueueApc[i] ))

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

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

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

(ULONG)(&(Addr_KeInsertQueueApc[i] ))最后加上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)[i] = ((unsigned char *)my_function_detour_KiInsertQueueApc)[i];
}

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)[i]) &&
(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[i]) ) = reentry_address;
break;
}
}


oldIrql = KeRaiseIrqlToDpcLevel();
for(i=0;i < 8;i++)
{
g_oricode[i] = actual_function[i];
actual_function[i] = newcode[i];
}
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位值!这个很重要。对你研究堆栈很有帮助。

***************************inline hook初步介绍就到这里*********************************


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值