1、HOOK SERVICE TABLE:HOOK SSDT
这种方法对于拦截 NATIVE API 来说用的比较多。
SSDT hook,一句话——Windows把需要调用的内核API地址全都存在了
一个表中(System Service Dispatch Table),要想hook一个内核API,比较简单的办法就是把
该内核API在表(SSDT)中保存的地址修改为自己撰写的函数地址。
2、HOOK INT 2E 方法:IDT HOOK
IDT是中断描述表,可以替换其中的中断处理程序。
这种方法对于跟踪、分析系统调用来说用的比较多。原理是通过替换 IDT
表中的 INT 2E 中断,使之指向我们自己的中断服务处理例程来实现的。掌握
此方法需要你对保护模式有一定的基础。
3、 HOOK PE 方法:EAT HOOK
这种方法对于拦截、分析其他内核驱动的函数调用来说用的比较多。原理
是根据替换 PE 格式导出表中的相应函数来实现的。
EAT是可执行文件的导出表,记录DLL中可供其他程序使用的函数,可执行文件装载时会使用相应DLL的EAT表来初始化IAT表,通过替换EAT表中的函数地址,就可以使依赖于本DLL的程序得到一个假的地址。
4IAT HOOK (ring3 用)
IAT是可执行文件的导入表,记录可执行文件使用的其它DLL中的函数,通过替换IAT表中的函数地址,可以hook相应DLL中的函数调用。
5、Inline Hook方法 (ring 0和ring3 都可以用)
Inline hook的工作流程:
1)验证内核API的版本(特征码匹配)。
2)撰写自己的函数,要完成以上三项任务。
2)获取自己函数的地址,覆盖内核API内存,供跳转。
Inline Hook的缺点:
1) 不够通用。各个windows版本中,内核API的开始一段不尽相同,要想通吃,就要多写几个版
本或者做一个特征码搜索(因为有的内核API在各个版本中非常相似,只是在“特征码”之前或之
后加一点东西)。
2) 已被一些检测工具列入检测范围,如果直接从内核API第一个字节开始覆盖,那么很容易被检
测,如果把覆盖范围往后推,并加以变形,也许能抵挡一气。具体情况,我才疏学浅,尚未试验
6SYSENTRY hook
为了性能的考虑,xp后的系统都改用sysentry命令来进入ring0,去调用SSDT中的服务,不再是通过IDT中的 int 2E。这也使得我们hook也变得相对容易了。
首先获得sysentry的地址,然后改之,不用再考虑IDT了
7)IRP hook
IRP是 I/O request packets,驱动程序中有一系列分发例程来处理请求,这些例程保存在驱动设备对象的数据结构中的一个表中,也很容易替换。
Windows的设备驱动框架中的上层与下层模块
在Windows的设备驱动框架中,下层模块向上层模块提供一个数据结构指针。但是,上层模块并不直接从这个数据结构获取具体的函数指针,更不直接使用这些函数指针调用下层模块中的函数;而是通过一些由内核提供的函数下达“I/O请求包”即IRP,间接地调用下层模块提供的函数,要求其执行某种 *** 作。这就好像是向内核下一个定单,定单中告诉内核要由哪一个下层模块执行何种 *** 作。另一方面,对于建立了形式“堆叠”的设备驱动,上层模块在运行中通常也没有如何“找到”下层模块的问题,甚至根本就不必知道其下一层是什么模块或什么设备,模块之间已在建立形式堆叠的时候固定连接好了。此时上层模块所获得的是哪一个下层模块的指针,取决于同一个堆叠中各个模块的装载次序,实际上取决于系统的配置,而相关的配置信息则最终来自相关的inf文件,这些信息保存在集中的数据库“注册表(Registry)”中。这样就为通过系统配置改变具体设备驱动堆叠的结构提供了更大的灵活性,主要体现在:
更容易在堆叠的下层实现“重定向”,即把上层模块嫁接到不同的下层模块上;
更容易在堆叠内部插入以“过滤驱动对象(FiDO)”为代表的“过滤模块”。
最后,设备驱动模块不是在真空中运行,需要得到内核的支持,需要由内核为其构筑起一个运行环境,这个环境的主体就是内核导出函数,此外还有一些全局的变量和数据结构。这就是Windows的“设备驱动开发包”DDK中所定义(更准确地说是“声明”)的函数和变量。
事物都是在发展的,Windows的设备驱动框架也不是一开始就这样,更不是永远这样。前面所讲的是为实现“即插即用”所必须要有的要素,主要就是模块的动态装载以及模块堆叠的形成。有了这些要素,包括即插即用在内的分层设备驱动就可以实现了,但是当然还可以有一些附加的要求。从Windows 98和Windows 2000开始,微软定义了一种(在当时是)新的设备驱动框架,称为WDM即“Windows设备驱动模型(Windows Driver Model)”。WDM要求设备驱动模块除满足PnP的需要外,还必须提供两方面的功能支持:
对于WMI的支持。WMI是“Windows管理手段(Windows Management Instrumentation)”的缩写。WMI与“简单网络管理规程”SNMP相似,要求每台Windows主机都能应“管理器(Manager)”的要求提供包括设备驱动在内的各种状态和统计信息。这些信息从哪儿来呢?对于设备驱动,当然得要由相应的设备驱动模块提供。
对于电源管理的支持。有些外设能耗不小,如果有一段较长的时间没有实际使用,就没有理由不将其转入某种“省电模式”。即使是能耗不大的外设,在节能成为一个环保问题的今天,也应该在不用时使其转入省电模式。这就是电源管理要达到的目的之一。所以,微软把支持电源管理列为WDM的要素之一。
总之,“老式”的设备驱动(在形式上)是不分层、不堆叠的;如果形式上分层并堆叠,在微软的术语中就称为“PnP设备驱动”。而WDM设备驱动,则是至少在形式上满足了上述两项附加条件的PnP设备驱动。对WMI的支持和电源管理的重要性当然不容低估,但是对于我们理解Windows的设备驱动框架却并非技术关键,所以后面的叙述将集中在框架的构成与实现,而忽略这两个方面。正因为这样,我们将称之为“Windows设备驱动框架”而不是“WDM”,以免混淆。
只有在内核中使用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出的符号才能在内核模块中直接使用。然而,内核并没有导出所有的符号。例如,在380的内核中,do_page_fault就没有被导出。 而我的内核模块中需要使用do_page_fault,那么有那些方法呢?这些方法分别有什么优劣呢? 下面以do_page_fault为例,一一进行分析: 修改内核,添加EXPORT_SYMBOL(do_page_fault)或EXPORT_SYMBOL_GPL(do_page_fault)。这种方法适用于可以修改内核的情形。在可以修改内核的情况下,这是最简单的方式。 使用kallsyms_lookup_name读取kallsyms_lookup_name本身也是一个内核符号,如果这个符号被导出了,那么就可以在内核模块中调用kallsyms_lookup_name("do_page_fault")来获得do_page_fault的符号地址。这种方法的局限性在于kallsyms_lookup_name本身不一定被导出。 读取/boot/Systemmap-,再使用内核模块参数传入内核模块Systemmap-是编译内核时产生的,它里面记录了编译时内核符号的地址。如果能够保证当前使用的内核与Systemmap-是一一对应的,那么从Systemmap-中读出的符号地址就是正确的。其中,kernel-version可以通过'uname -r'获得。但是这种方法也有局限性,在模块运行的时候,Systemmap-文件不一定存在,即使存在也不能保证与当前内核是正确对应的。 读取/proc/kallsyms,再使用内核模块参数传入内核模块/proc/kallsyms是一个特殊的文件,它并不是存储在磁盘上的文件。这个文件只有被读取的时候,才会由内核产生内容。因为这些内容是内核动态生成的,所以可以保证其中读到的地址是正确的,不会有Systemmap-的问题。需要注意的是,从内核2637开始,普通用户是没有办法从/proc/kallsyms中读到正确的值。在某些版本中,该文件为空。在较新的版本中,该文件中所有符号的地址均为0(除非/porc/sys/kernel/kptr_restrict 的值被设为0)。但是root用户是可以从/proc/kallsyms中读到正确的值的。好在加载模块也需要root权限,可以在加载模块时用脚本获取符号的地址。
一般在内核SSDT HOOK的时候就是直接钩住SSDT表替换NtOpenProcess的地址来达到保护进程的目的。而在InlineHook中,侧需要更进一步的了解NtOpenProcess函数,才能更好的做inlinehook。
首先说说Windows中用户层 OpenProces,WIN32函数OpenProces执行后,调用NTDLLDLL中NtOpenProcess函数,然后此函数INT2E自陷进入内核,开始从SSDT表中查找NtOpenProcess函数地址,继而在内核中执行NtOpenProcess函数。这是OpenProcess调用的路径。如图。
那么就这样说,在Windows中,OpenProcess函数是对NtOpenProcess调用的一个包装。NtOpenProcess包含在内核模块NTOSKRNLEXE当中。
内核中NtOpenProcess函数结构如下:
NTSTATUS NtOpenProcess (
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL);
ClientID参数是OpenProcess传递的实际PID。这个参数是可选的,但是根据我们的观察,OpenProcess在调用NtOpenProcess的时候总是使用这个参数。
在内核中执行的时候,NtOpenProcess主要实现3个功能:
1 它通过调用PsLookupProcessByProcessId函数来验证进程是否存在。
2 它通过调用ObOpenObjectByPointer来打开进程的句柄。
3 如果打开进程句柄成功,就将句柄返回给调用者。
那么很自然的,PsLookupProcessByProcessId函数和ObOpenObjectByPointer函数就成为我们inlinehook的目标
先看PsLookupProcessByProcessId函数:
lkd> u 805caefa
nt!NtOpenProcess+0x1fc:
805caefa 45 inc ebp
805caefb dc50ff fcom qword ptr [eax-1]
805caefe 75d4 jne nt!NtOpenProcess+0x1d6 (805caed4)
805caf00 e8617a0000 call nt!PsLookupProcessByProcessId (805d2966)
805caf05 ebde jmp nt!NtOpenProcess+0x1e7 (805caee5)
805caf07 8d45e0 lea eax,[ebp-20h]
805caf0a 50 push eax
805caf0b ff75cc push dword ptr [ebp-34h]
lkd> u nt!PsLookupProcessByProcessId
nt!PsLookupProcessByProcessId:
805d2966 8bff mov edi,edi
805d2968 55 push ebp
805d2969 8bec mov ebp,esp
805d296b 53 push ebx
805d296c 56 push esi
805d296d 64a124010000 mov eax,dword ptr fs:[00000124h]
805d2973 ff7508 push dword ptr [ebp+8]
805d2976 8bf0 mov esi,eax
805d2978 ff8ed4000000 dec dword ptr [esi+0D4h]
805d297e ff35c0385680 push dword ptr [nt!PsThreadType+0x4 (805638c0)]
805d2984 e803ad0300 call nt!ExEnumHandleTable+0x408 (8060d68c)
805d2989 8bd8 mov ebx,eax
805d298b 85db test ebx,ebx
805d298d c745080d0000c0 mov dword ptr [ebp+8],0C000000Dh
805d2994 7432 je nt!PsLookupProcessByProcessId+0x62 (805d29c8)
再看ObOpenObjectByPointer函数:
lkd> u 805caf15
nt!NtOpenProcess+0x217:
805caf15 8d8548ffffff lea eax,[ebp-0B8h]
805caf1b 50 push eax
805caf1c ff75c8 push dword ptr [ebp-38h]
805caf1f ff75dc push dword ptr [ebp-24h]
805caf22 e8bd07ffff call nt!ObOpenObjectByPointer (805bb6e4)
805caf27 8bf8 mov edi,eax
805caf29 8d8548ffffff lea eax,[ebp-0B8h]
805caf2f 50 push eax
lkd> u nt!ObOpenObjectByPointer
nt!ObOpenObjectByPointer:
805bb6e4 8bff mov edi,edi
805bb6e6 55 push ebp
805bb6e7 8bec mov ebp,esp
805bb6e9 81ec94000000 sub esp,94h
805bb6ef 53 push ebx
805bb6f0 8b5d08 mov ebx,dword ptr [ebp+8]
805bb6f3 56 push esi
805bb6f4 57 push edi
知道NtOpenProcess执行的过程后,为防止打开某特定进程的句柄,可以直接inlinehook ObOpenObjectByPointer函数,Hook 这个函数有一大好处,那就是进程线程都一起保护了
ObOpenObjectByPointer(
IN PVOID Object,
IN ULONG HandleAttributes,
IN PACCESS_STATE PassedAccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE Handle
)
NTSTATUS
T_ObOpenObjectByPointer(
IN PVOID Object,
IN ULONG HandleAttributes,
IN PACCESS_STATE PassedAccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PHANDLE Handle
)
{
PEPROCESS EPROCESS;
if ((Object != NULL) && (MmIsAddressValid(Object))) // 地址有效性验证
{
if (OBJECT_TO_OBJECT_HEADER(Object) -> Type == PsProcessType) // 若为进程对象
{
if ((PsGetCurrentProcess() != ProtectedProcess)) // 若 *** 作者不是受保护的进程自己
{
if (Object == ProtectedProcess) // 若被 *** 作进程是受保护进程
{
return STATUS_ACCESS_DENIED; // 拒绝访问
}
}
}
else
if (OBJECT_TO_OBJECT_HEADER(Object) -> Type == PsThreadType) // 若为线程对象
{
EPROCESS = IoThreadToProcess(Object); // 获取线程对应进程的 EPROCESS
if (EPROCESS == ProtectedProcess) // 若是受保护进程
{
if ((PsGetCurrentProcess() != ProtectedProcess)) // 若 *** 作者不是受保护进程自己
{
return STATUS_ACCESS_DENIED; // 拒绝访问
}
}
}
}
// 正常调用,执行原 API
return My_ObOpenObjectByPointer(
Object,
HandleAttributes,
PassedAccessState,
DesiredAccess,
ObjectType,
AccessMode,
Handle
);
}
My_ObOpenObjectByPointer(
Object,
HandleAttributes,
PassedAccessState,
DesiredAccess,
ObjectType,
AccessMode,
Handle
);
函数中,执行完ObOpenObjectByPointer函数前几字节后JMP到ObOpenObjectByPointer函数内部。
在系统内核有一个内核文件的加载的镜像(内核模块),NTOSKRNL或ntkrnlpa及ntkrnlmp、ntkrnlup、ntkrpamp都是OS内核文件,它提供了一整套核心态函数,供用户态和核心态调用,我们在用户态调用的函数大多数在最后都进入了核心态如kernel32dll的OpenProcess,在调用OpenProcess,以后会进入Ntdlldll中的NtOpenProcess然后调用系统函数调用号进入核心态的NtOpenProcess,所以核心态的钩子才是王道,核心态的钩子有SSDT HOOK和SDT INLINE HOOK,上面说了,内核导出了一系列的函数供用户态调用,那么导出的这个函数表就是SSDT(系统服务描述表),它的地址可由内核导出的一个变量 KeServiceDescriptorTable 查询,每4个字节(long 大小)为一个地址,在SSDT HOOK时我们只需要修改SSDT表中的地址为我们的地址即可,相当简单
例:修改NtOpenProcess:
(ULONG)(KeServiceDescriptorTable+(0x7A4))=(ULONG)MyOpenProcess
0x7A为NtOpenProcess的系统调用号,每个调用号占4个字节,KeServiceDescriptorTable+(0x7A4)就代表了NtOpenProcess地址的存放位置,MyOpenProcess为我们自定义的函数,这样系统在执行NtOpenProcess的时候就会跳到MyOpenProcess里。还有一种就是Inline Hook它和用户态的一样直接修改函数前5个字节即可,这里不再赘述。但是在核心态的inline hook比较危险,这涉及到多线程调用,有兴趣的朋友可以去查阅该方面资料。
众所周知,DLL导出函数有两种模式:按函数名或按序号
经编译DLL生成后,如果想隐藏函数名不需要任何工具,可以直接改二进制内容进行处理;
1、用二进制编辑器打开DLL文件,然后查找“DLL的文件名”。
2、文件名后紧跟着就是各个函数名,以00作为分隔符
3、文件名前面就是函数的具体信息,包括入口地址、函数顺序号、函数名地址、函数数量等
4、找到函数的数量修改为0000000即可,如果想要更彻底可以把函数名也全置为零。
附件是我修改后的内核kernel32dll和一个testdldll文件,经测试testdldll使用完全没有问题,但是用Dependency查看时,所有函数名变为“N/A”
具体二进制格式基本如下:
00 00 00 00
dll文件名存储地址
01 00 00 00
函数数量(8位)
函数名称的数量(8位)
函数入口地址的存储地址(8位)
函数名称地址的存储地址(8位)
函数序号的存储地址(8位)
函数入口地址列表(8位)
函数名称地址列表(8位)
函数序号列表(4位)
dll文件名称
函数名称列表
在内核中通过/proc/kallsyms获得符号的地址
Linux内核符号表/proc/kallsyms的形成过程
/scripts/kallsymsc负责生成Systemmap
/kernel/kallsymsc负责生成/proc/kallsyms
/scripts/kallsymsc解析vmlinux(tmp_vmlinux)生成kallsymsS(tmp_kallsymsS),然后内核编译过程中将kallsymsS(内核符号表)编入内核镜像uImage
内核启动后/kernel/kallsymsc解析uImage形成/proc/kallsyms
/proc/kallsyms包含了内核中的函数符号(包括没有EXPORT_SYMBOL)、全局变量(用EXPORT_SYMBOL导出的全局变量)
如何将内核中的函数、全局变量、静态变量都导出到/proc/kallsyms
查看内核 使用 uname -a
Windows内核分三层,与硬件直接打交道的是硬件抽象层HAL,这一层把所有与硬件相关代码逻辑隔离到一个专门模块中,从而是上层尽可能独立于硬件平台。HAL是一个独立动态链接库,windows带了多个如Haldll,halacpidl等,这是根据高级配置和电源接口高级可编程中断控制器之类的区别,只有一个会被选中选中之后拷贝改名为haldll,hal才是真正的硬件抽象层,例如自旋锁和中断是在hal实现。内核只需简单实用其导出函数。
HAL之上是内核层,有时候成为微内核,这是大内核中的小内核,是ntoskrnlexe的下层部分,上传是执行体,接近HAL层,这层是包含了基本的 *** 作系统原语和功能,如进程线程,线程调度,中断和异常处理,同步对象和各种机制,还负责同步处理器直接行为。windows内核实现了抢占式线程调度机制,就是线程按优先顺序,分配到处理器上,每个线程有基本优先级,也有动态优先级,高优先级线程可抢断低优先级线程,windows内核按面向对象思想,管理两种对象,分发器对象(dispatcher object)和控制对象,分发器对象实现各种同步功能,这些对象状态会影响线程电镀。分发器对象包括event,mutant,Semaphore,process,thread,queue,gate,timer。控制设备对象被用于控制内核 *** 作,不影响线程调度,包括异步过程调用APC,延迟过程调用DPC,中断对象等。
在内核层之上是执行体层,这一层是提供上层应用程序或内核驱动程序直接调用的功能和语义,Windows内核的执行体包含一个对象管理器,用于一致地管理执行体中的对象。执行体层和内核层位于同一个二进制模块中,即内核基本模块,其名称为ntoskrnlexe。执行体是ntoskrnl上层,包含进程线程管理器,内存管理器,安全引用监视器,IO管理器,缓存管理器。配置管理器。即插即用管理器。电源管理器。
既然在同一个模块,内核层和执行体层分工是,内核层实现 *** 作系统基本机制,而所有策略决定则留给执行体。执行体中的对象绝大多数封装了一个或者多个内核对象,并通过某种方式比如对象句柄,暴露给应用程序。这种设计体现了机制与策略分离的思想。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)