您的位置:首页 > 其它

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

2012-01-01 22:21 302 查看

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

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


转载▼


标签:

rootkits

inline

hook

驱动学习

驱动

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)


转载▼


标签:

rootkits

inline

hook

驱动

驱动学习

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初步介绍就到这里*********************************
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: