|
吾爱专用:小白学习 大神勿喷 有错误忘指正 下一篇 内核里操作文件
今天,谈谈 WIN64 内核
编程的基本规则。在 WIN32 环境下,大家都能乱来,随便加载各种驱动,进行各种挂钩和 DKOM,
各种穷凶极恶的手段粉墨登场。把好端端的 WINDOWS 弄得连七八糟,严重影响了系统安全。
虽说为编程爱好者提供了表演的舞台,但是苦了那些把电脑当工具的人。于是那段时间,
“LINUX 和 MAC OS 比 WINDOWS 安全可靠”,大有把 WINDOWS
和不安全划上等号之势。
为此,微软很生气,后果很严重。从 WINDOWS 2003 X64 开始,微软开始对 WIN64 系统
增加限制,增强系统安全。总体来说有两条,一是 KPP(Kernel Patch Protection,内核补
丁保护),二是 DSE(Driver Signature Enforcement,驱动签名强制)。WINDOWS 2003 X64
只有 KPP,从 VISTA 开始有了 DSE。KPP 利用 PatchGuard 技术检查内核有没有被“打补丁”
(不仅检查内核函数有没有被 HOOK,也包括一些关键的内核结构体有没有被修改,比如进
程链表 PsActiveProcessLinks 有没有被摘链),如果发现被“打补丁”,则直接引发 0x109
蓝屏(CRITICAL_STRUCTURE_CORRUPTION,直译为关键结构损毁)。DSE 则是拒绝加载不包含
正确签名的驱动(包括伪造签名和测试签名)。多说一句,总有人把 KPP 把 PatchGuard 划等
号,其实二者是不等的。KPP 是机制,PatchGuard 是实现。就好比 CIA 是机构,CIA 的特工
才是一系列“黑色行动”的执行者。
实际上 KPP 和 DSE 并非铁板一块,二是各有漏洞可
钻的。KPP 保护不了内核所有的部分,只保护了几个驱动:NTOSKRNL.EXE、HAL.DLL、CI.DLL、
NDIS.SYS 等(当然,NTOS 部分包括了 IDT、GDT、MSR 等)。对一些较为上层的驱动,比如
FAT32 和 NTFS 作 IRP HOOK,PG 是不管的。而 DSE 则在某些条件下不启动,比如在 PE 环境
下;或者说有些时候管得不严格,比如在测试模式下,允许含有测试签名的驱动加载。总结
一句:进行 WIN64 内核编程的时候,别想用 API HOOK 解决问题;当发布含有 WIN64 驱动的
时候,记得给“证书签发机构”交保护费(购买正规数字签名)。不过,这两项限制让很多
黑客乃至安全公司大为不满,各种过 KPP 和 DSE 的方法层出不穷。目前,VISTA、WIN7、WIN8、
WIN8.1 的 KPP 和 DSE 已全部被攻破。
编程的时候,大家基本都是需要使用 API 的。在 RING3 下使用 WINAPI,在 RING0 下则
使用内核 API。特别注意的是,内核编程是无法使用 WINAPI 的(当然,也有特殊的方法调
用,不过非标准方法,这里略过不提)。什么叫做内核 API 呢,就是虚拟地址位于内核空间
的 API。不管是不是微软的内核模块,也不管导出没导出,只要知道地址和每个参数的含义,
就能调用。不过,我们写程序大多时候都是使用微软模块(NTOSKRNL、HAL 等)导出的 API。
例如:ZwOpenProcess,IoCreateFile 等。
内核编程用内核 API,而自然也有内核结构体。其实“内核结构体”这个说法不太妥当,
因为结构体是不分场合甚至不分系统的。但这么说大家也能理解是什么意思,就是内核编程
中常用的结构体。这种结构体又分为两种,一种是“万年不变”的,一种是每个系统都不同
的。“万年不变”的结构体通常能在 MSDN 上查到,比如 CLIENT_ID、IO_STATUS_BLOCK;每个
系统都不同的结构体通常在 MSDN 上查不到,但是存在于符号文件里,比如 EPROCESS、ETHREAD。
我们编程的时候,尽量只使用“万年不变”的结构体,不使用每个系统都不同的结构体。当
非要使用不可的时候,必须根据系统版本定义制定成员的偏移量。如果发现未知的系统版
本,则提示并退出。如果不这样做,等着 BSOD 吧。
WIN64 内核编程基础班
内核编程的基本规矩不是一篇能讲完的,下面几篇会细化讲解,这篇只是个引子。
-----------------------------------------------------------------
SCM 加载法。
加载驱动的方法其实有很多,在 2000/XP 这些古老的 NT5 系统上,公开的至少也有两
种,一种是直接使用 NtLoadDriver 函数(SCM 加载法最终也是调用了此函数,不过我这里
指的是不使用 SCM 系列函数,而是自己写入注册表项并直接调用 NtLoadDriver),另外一
种是使用 NtSetSystemInformation 来加载驱动。第二种加载驱动的方法非常“直接”,什
么注册表项都不用写,把参数填写正确就能加载了。在第二种方法没公开之前,很多病毒
都是用这种方法来对抗 HIPS(当然公开了之后很快就被和谐掉了)。除了这两种方法之
外,据说还存在“地下方法”加载驱动,就是利用一些极少人关注的函数(比如 GDIXXX 这
种看起来和加载驱动毫无关联的函数)来加载驱动。这些加载驱动的方法可以称为“漏
洞”,在黑市上能卖很多钱,在被封杀之前,一个好用的漏洞换一套在广州市中心的房产
绝对不是梦。所以大家想致富的话,就多挖掘一下驱动加载漏洞,挖到了好用漏洞就等于
挖到了房子。扯淡到此结束,下面详细讲解一下 SCM 加载驱动的要领。
使用 SCM 系列函数加载卸载驱动的过程并不复杂,总体流程是:打开 SCM 管理器(获
得 SCM 句柄)->创建驱动服务(获得服务句柄,如果服务已经存在,此步则变成打开服
务)->启动服务->停止服务->移除服务->关闭服务句柄->关闭 SCM 句柄。如果要与驱动通
信,则用 CreateFile 打开驱动的符号链接(可以理解成获得一个“通信句柄”),然后使
用 DeviceIoControl 与驱动进行信息交互。如果曾经打开过驱动的符号链接,则必须在卸
载驱动前关闭“通信句柄”,否则再次加载相同的驱动时会有一些麻烦。DeviceIoControl 的参数也很好理解,只有 5 个重要参数:控制码,输入缓冲区,输
入长度,输出缓冲区,输出长度。但这个控制码(以下简称 IOCTL)则是大有玄机的,这
里则必须讲清楚。仔细看回 KrnlHW64.sys 的代码,里面关于 IOCTL 的定义是这么写的:
#define IOCTL_IO_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED,
FILE_ANY_ACCESS)
#define IOCTL_SAY_HELLO CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED,
FILE_ANY_ACCESS)
--------------------------------------------------
这里面有一个宏,叫做 CTL_CODE,它用函数来表示就是:
DWORD CTL_CODE_GEN(DWORD lngFunction)
{
//const DWORD FILE_DEVICE_UNKNOWN = 0x22;
//const DWORD METHOD_BUFFERED = 0;
//const DWORD FILE_ANY_ACCESS = 0;
return (FILE_DEVICE_UNKNOWN * 65536) | (FILE_ANY_ACCESS * 16384) | (lngFunction * 4)
| METHOD_BUFFERED;
}-----------------------------------------------------------------
之所以要这么计算,是因为这个 32 位的 IOCTL 的每一位都有不同的含义(IOCTL 每一
位的具体含义如下图所示)。如果不遵守这个规则,随意指派控制码,那么在与驱动进行通
信时将会蓝屏。
------------------------------------------------------------------------------
程序的本质,就是内存里的一串串的数字(它们被 CPU 当作指令解析,才能够有意义,
否则就是一坨垃圾);因此在正式讲解驱动编程之前,首先讲解内存使用。
内存使用,无非就是申请、复制、设置、释放。在 C 语言里,它们对应的函数是:malloc、
memcpy、memset、free;在内核编程里,他们分别对应 ExAllocatePool、RtlMoveMemory、
RtlFillMemory、ExFreePool。它们的原型分别是:PVOID ExAllocatePool(POOL_TYPE PoolType, SIZE_T NumberOfBytes);
VOID RtlMoveMemory(PVOID Destination, PVOID Source, SIZE_T Length);
VOID RtlFillMemory(PVOID Destination, SIZE_T Length, UCHAR Fill);
VOID ExFreePool(PVOID P);
需要注意的是,RtlFillMemory 和 memset 的原型不同、ExAllocatePool 和 malloc 的原型
也不同。前者只是参数前后调换了一下位置,但是后者则多了一个参数:PoolType。这个
PoolType 也是必须了解的。PoolType 在 MSDN 的介绍上有 N 种,其实常用的只有 2 种:
PagedPool 和 NonPagedPool。PagedPool 是分页内存,简单来说就是物理内存不够时,会把
这片内存移动到硬盘上,而 NonPagedPool 是无论物理内存如何紧缺,都绝对不把这片内存
的内容移动到硬盘上。在往下讲之前,先补充一个知识,就是我们操作的内存,都是虚拟内
存,和物理内存是两码事。但虚拟内存的数据是放在物理内存上的,两者存在映射关系,一
般来说,一片物理内存可以映射为多片虚拟内存,但一片虚拟内存必定只对应一片物理内
存。假设虚拟内存是 0Xfffff80001234567 在物理内存的地址是 0x123456,当物理内存不够用
时,物理内存 0x123456 的原始内容就挪到硬盘上,然后把另外一片急需要用的内容移到物
理内存里。此时,当你再读取 0Xfffff80001234567 的内容时,就会引发缺页异常,系统就会
把在硬盘上的内容再次放到物理内存中(如果这个过程失败,一般就死机了)。以上说了这
么多废话,总结两句:1.NonPagedPool 的总量是有限的(具体大小和你物理内存的大小相
关),而 PagedPool 的总量较多。申请了内存忘记释放都会造成内存泄漏,但是很明显忘记
释放 NonPagedPool 的后果要严重得多;2.一般来说,PagedPool 用来放数据(比如你用
ZwQuerySystemInformation 枚举内核模块,可以申请一大片 PagedPool 存放返回的数据),
而 NonPagedPool 用来放代码(你写内核 shellcode 并需要执行时,必须使用 NonPagedPool
存放 shellcode)。以我的经验来说,访问到切换出去的内存没事,但是执行到切换出去的内
存必然蓝屏(这只是我的经验,正确性待定)。3.在用户态,内存是有属性的,有的内存片
只能读 不 能 写 不 能 执 行 ( PAGE_READ ), 有 的 内 存 片 可 以 读 可 以 写也可以执行
(PAGE_READ_WRITE_EXECUTE)。在内核里,PagedPool 和 NonPagedPool 都是可读可写可执
行的,而且没有类似 VirtualProtect 之类的函数:
void test()
{
PVOID ptr1 = ExAllocatePool(PagedPool,0x100);
PVOID ptr2 = ExAllocatePool(NonPagedPool,0x200);
RtlFillMemory(ptr2,0x200,0x90);
RtlMoveMemory(ptr1,ptr2,0x50);
ExFreePool(ptr1);
ExFreePool(ptr2);
}
----------------------------------------
在内核里想要写入“别人的”内存(一般指 NTOS 等系统模块的内存空间),
这里又涉及到另外两个概念:IRQL 和内存保护。IRQL 成为中断请求级别,从 0~31 共
32 个级别;内存保护可以打开和关闭,如果在内存处于保护状态时写入,会导致蓝屏。一般
来说,要写入“别人的”内核内存,必须关闭内存写保护,并把 IRQL 提升到 2 才行(绝大多
数时候 IRQL 都为 0,当 IRQL=2 时,会阻断大部分线程执行,防止执行出错)。内存是否处
于写保护的状态记录在 CR0 寄存器上,因此直接修改 CR0 寄存器的值即可;而提升或降低
IRQL 则使用 KeRaiseIrqlToDpcLevel 和 KeLowerIrql 实现(WIN64 的 IRQL 值记录在 CR8 寄存器
上,而 WIN32 的 IRQL 值记录在 KPCR 上)。代码如下:
KIRQL WPOFFx64()
{
KIRQL irql=KeRaiseIrqlToDpcLevel();
UINT64 cr0=__readcr0();
cr0 &= 0xfffffffffffeffff;
__writecr0(cr0);
_disable();
return irql;
}
void WPONx64(KIRQL irql)
{
UINT64 cr0=__readcr0();
cr0 |= 0x10000;
_enable();
__writecr0(cr0);
KeLowerIrql(irql);
}
void test()
{
KIRQL irql=WPOFF();
RtlMoveMemory(NtOpenProcess,HookCode,15);
WPON(irql);
}
----------------------------------------------
至于写入“别人的”内存,还有一种微软推荐的安全方式,就是 MDL 映射内存的方式。
这个比较麻烦,大概方法是申请一个 MDL(类似句柄的玩意),然后尝试锁定页面,如果成
功,则让系统分配一个“安全”的虚拟地址再行写入,写入完毕后解锁页面并释放掉 MDL。
SafeCopyMemory:
BOOLEAN SafeCopyMemory( PVOID pDestination, PVOID pSourceAddress, SIZE_T SizeOfCopy )
{
PMDL pMdl = NULL;
PVOID pSafeAddress = NULL;
if( !MmIsAddressValid(pDestination) || !MmIsAddressValid(pSourceAddress) )
return FALSE;
pMdl = IoAllocateMdl(pDestination, (ULONG)SizeOfCopy, FALSE, FALSE, NULL );
if( !pMdl )
return FALSE;
__try
{
MmProbeAndLockPages( pMdl, KernelMode, IoReadAccess );
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
IoFreeMdl( pMdl );
return FALSE;
}
pSafeAddress = MmGetSystemAddressForMdlSafe( pMdl, NormalPagePriority );
if( !pSafeAddress )
return FALSE;
__try
{
RtlMoveMemory(pSafeAddress, pSourceAddress, SizeOfCopy );
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
;
}
MmUnlockPages( pMdl );
IoFreeMdl( pMdl );
return TRUE;
}
-----------------------------------------------------------------------------
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
|