【梅哥的Ring0湿润插入教程】第一课Windows内核/驱动编程概述及应用、商业驱动保护软件原理分析

【梅哥的Ring0湿润插入教程】

Email:mlkui@163.com 转载请注明出处,谢绝喷子记者等,如引起各类不适请自觉滚J8蛋!

第一课Windows内核/驱动编程概述及应用、 商业驱动保护软件原理简单分析

【湿润前言】

       随着驱动保护技术的逐步成熟,诸如网络游戏公司等越来越多的商业软件公司开始使用Ring0级保护技术保护自己的产品,以起到反用户级调试、反RootKit、反各类钩子、反各类远程注入等作用。目前,使用驱动保护技术的代表产品主要有上海盛大网络发展有限公司开发的GPK(Game Protect Kit)、深圳市腾讯计算机系统有限公司开发的TenSafe及韩国neople棒子公司开发的nProtect等。显然,进入到Ring0级进行某些操作是应对这些驱动保护技术的较好办法之一,而运行于核心态即Ring0级的Windows设备驱动程序则可以说是进入到Ring0的唯一方法。

       微软在其WDK,即所谓Windows Driver Kit的官方文档《Getting Started with the Windows Driver Development Environment》一文中开篇指出:“Even for experienced developers, getting started with Windows® device drivers can be difficult. You have a new programming model to learn and a new set of tools for building and debugging”。

       正如上文所述,Windows内核编程与Win32编程区别较大,其调试方法也与Win32下的调试方法有极大不同;同时,Ring0级的反汇编与Ring3反汇编环境也有不小的差异;而大量计算机专业从业人员在没有点拨的情况下也难以意识到内核编程化御姐为萝莉、化少年为怪蜀黍的威力(以下省略一万字)。正是出于以上及其他的各种原因,梅哥将在学习Windows内核编程的同时写下本系列的Ring0湿润插入大法,旨在分享Windows内核编程经验、并说明Windows内核编程在绕过/破解商业驱动保护软件中的重要地位和神奇作用

       好了,下面就抛开用户级的桎梏,跟梅哥一起开始深入Ring0的湿润之旅吧~

============================我是湿润的昏割线=============================

【商业驱动保护软件的原理简述】

       盛大公司的GPK驱动保护产品目前在其内部及外部的多家公司游戏产品中使用,例如由网易运营的网络游戏《梦幻西游》、由深圳市迅雷网络技术有限公司运营的网络游戏《仙剑神曲》等众多游戏都采用了GPK驱动保护产品。下面梅哥将以由深圳市迅雷网络技术有限公司运营的网络游戏《仙剑神曲》为例分析商业驱动保护软件的原理。

       盛大公司的GPK将驱动伪装成1394总线集线器设备驱动在内核中注册,利用相关工具可在内核驱动模块中找到1394hub.sys,如下图所示:

       进一步查看内核钩子,可观察到被各种SSDT Hook及Inline Hook等。例如,该驱动利用内核内核钩子勾住了函数NtProtectVirtualMemory:

 

      Hook该函数即在某种程度上阻止几种常见三方非法软件,例如:

      1)需要使用ExAllocateVirtualMemory在目标进程内部开辟空间的远程线程注入方法等;

      2)Cheat Engineer等通过向目标进程空间读取/写入数据的软件;

再如Shadow SSDT中的NtUserPrintWindow函数被Hook,可以实现阻止应用层进程在游戏窗体内进行绘图操作等;除此以外还有防止输入法注入的NtUserSetAppImeLevel等:

      在上述的多种驱动保护下,GPK实现了在OD中隐藏被保护游戏进程以避免其在被Ring3上调试、阻止CE等常见内存搜索工具附加游戏进程等等等等等功能。当然,商业驱动保护软件所采取的措施远非上文所述,还有各种反调试等等等等等等策略,梅哥将逐渐深入分析;而上文中提到的SSDT、Native API等的概念也将在后续逐渐说明。

【真正的4G虚拟地址】

       首先必须明确,操作系统课本中所谓每个进程占有232=4G虚拟内存法并不确切。例如Windows实际将4G虚拟内存划分为用户空间及内核空间两部分,其中用户空间为低端2G(0x0-0x79999999)而内核空间为高端2G(0x80000000-0xFFFFFFFF-1),可以认为前者即所谓操作系统的Ring3而后者即为Ring0。X86架构的CPU利用Ring0-Ring3标记各内存页面等,以起到保护内存防止越权访问等作用。对Windows操作系统而言,高端2G内核空间只有一份并由内核独立占有,而低端2G为每个进程一份,各进程之间互相独立,如下图所示:

       用户空间不能访问高端2G的内核空间,同时,当涉及进程调度等时,内核只切换低端2G的虚拟内存,正因为如此才有了网络协议栈中所谓的零拷贝技术。例如,以UDP常用API函数SendTo为例,其发送缓冲区往往使用malloc等运行时函数在用户层2G内的堆空间分配(运行时函数、堆栈区别不知道的请百度),当SendTo最终陷入系统内核时,由于用户态2G空间与内核态2G空间相互独立,所以就需要将用户层的发送缓冲区拷贝至内核空间,这种拷贝在一定程度上导致了性能的降低,因而有人也就提出了所谓的零拷贝技术。

【Win32子系统与内核】

       前戏:在深入内核之前,我们首先需要了解NT内核的基本结构。

       首先纠正Linux环境、实时操作系统或Anti-Windows背景同学的常见误区(作为一名曾经坚定的Anti-Windows者,梅哥真心只能表示谁人年少不2B啊,哎),即可以在相当程度上认为Ntoskrnl.exe为Windows NT架构操作系统的内核。当然,由于Windows为非典型的微内核结构,故Ntoskrnl.exe实际上包括了操作系统内核及执行体组件两部分组成。在Ntoskrnl.exe以外,用户层功能由诸如Win32、OS/2和POSIX等的子系统DLL的形式提供,这些子系统中的APIs最终通过调用Ntoskrnl.exe提供的系统服务实现。

       目前,可以认为Win32子系统为最主要的子系统,其包含的API即应用开发人员常提到的Win32 API,更加具体地讲Win32 API又分为:

       1)GDI函数,对应GDI32.DLL,在物理设备上执行绘图操作;

       2)USER函数,对应USER32.DLL,管理窗口、菜单、对话框及各类控件等;

       3)KERNEL函数,对应KERNEL32.DLL,管理进程、线程、文件、同步等非GUI资源;

在当前的NT内核架构中,上述三个DLL文件均只包含函数导出定义,其实现全部在内核文件Ntdll.dll中,而Ntdll.dll文件中实现的函数即为所谓Native API。Native API一般以Nt开头,例如Win32 API函数OpenProcess在Ntdll.dll中的实现为NtOpenProcess,如图所示(途中是以WriteFile为例的):

 

       可以看出,Native API的出现保证了上层Win32 API的无关性,尽管Windows内核不管变化,但Native API的存在事应用程序开发者只需了解Win32 API,而不用关心Native API的真正变化。而Native API正是从用户态向核心态过渡的关键,Native API最终通过软中断的方式进入内核模式,并调用内核提供的服务。

【Ring3向Ring0的陷入】

       正如前文所述,用户模式的所有调用,如Kernel32.dll、User32.dll等提供的API最终都封装在Ntdll.dll中,然后通过INT 2E或SYSENTER陷入到内核模式,并且将所要调用的服务号(也就是在SSDT数组中的索引值)存放到寄存器 EAX 中、将参数地址放到寄存器EDX中、将参数复制到内核地址空间中,再根据存放在EAX中的索引值来在SSDT数组中调用指定的服务,其大致流程如下图所示(详情可参考后面的反汇编代码):

       我们也可以通过内核函数的反汇编代码看出,如微软为文档化的内核函数ZwQuerySystemInformation函数反汇编如下:

 

其中,在陷入到内核模式后,EAX 中被传入要调用的服务号(也就是在SSDT数组中的索引值)即此处的0x0AD,然后将参数地址放到寄存器EDX中,之后通过pushfd及push 8堆栈传参,并调用KiSystemService最终完成Ring3到Ring0的执行。

       SSDT全称为System Services Dispatch Table,即系统服务分发表,实际是一个存储地址的数组(即ULONG类型数组)。如前文所示,在应用层ntdll.dll中的函数在系统服务描述表中都存在一个与之相对应的系统服务,由于当应用程序调用ntdll.dll中的API时最终会调用内核中与之相对应的系统服务,那么只要向内核传递该调用的服务所在SSDT中的索引然后内核根据这个索引值在SSDT中找到相对应的服务的地址,最后由内核调用相应服务完成应用层API所需要的调用请求即可,如图所示:

Ntoskrnl.exe中导出了一个KeServiceDescriptorTable变量,其实际是一个指向结构体struct ServiceDescriptorTable的指针,是访问SSDT的关键。在WinDbg中可以直接使用dd KeServiceDescriptorTable命令查看:

kd> dd KeServiceDescriptorTable

8055b220  804e36b8 00000000 0000011c 805110c8

8055b230  00000000 00000000 00000000 00000000

8055b240  00000000 00000000 00000000 00000000

8055b250  00000000 00000000 00000000 00000000

8055b260  00002710 bf80c361 00000000 00000000

8055b270  f8a49a80 82378cc0 822090f0 80700f40

8055b280  00000000 00000000 32d80ecc 0005f5b0

8055b290  587f2d8c 01cca04b 00000000 00000000

而KeServiceDescriptorTable定义为(对于该结构网上有大量错误代码):

typedef struct ServiceDescriptorTable_s

{

       PULONG ServiceTableBase;                   // System Service Dispatch Table的基地址

       PULONG ServiceCounterTable(0);

       unsigned int NumberOfServices;          //由ServiceTableBase描述的服务的数目

       PULONG ParamTableBase;                    //SSPT

}ServiceDescriptorTable,*pServiceDescriptorTable;

extern pServiceDescriptorTable KeServiceDescriptorTable;

其中,ServiceTableBase指向SSDT(System Service Dispatch Table)系统服务分发表的基址,其中每个地址4字节长;ServiceCounterTable用于checked builds,包含着SSDT中每个服务被调用次数的计数器,这个计数器由INT 0x2E或SYSENTER处理程序KiSystemService更新,一般为零;NumberOfServices描述了SSDT中包换的系统服务的个数;ParamTableBase指向 SSPT(System Service Parameter Table)。

       通过梅哥以上的介绍,我们已经知道SSDT对应Ntoskrnel.exe为Kernel.dll中包含的API服务;除此以外,还有所谓的Shadow SSDT,对应KeServiceDescriptorTableShadow指针,它与GDI相关API有关,具体对应Win32k.sys。本系列教程以后会对Shadow SSDT做详细说明。

       我们可以直接在WinDbg中查看KeServiceDescriptorTable(符号表中已包含):

kd> dd KeServiceDescriptorTable

8055b220  804e36b8 00000000 0000011c 805110c8

8055b230  00000000 00000000 00000000 00000000

8055b240  00000000 00000000 00000000 00000000

8055b250  00000000 00000000 00000000 00000000

8055b260  00002710 bf80c361 00000000 00000000

8055b270  f8a49a80 823e42f8 8219b0f0 80700f40

8055b280  00000000 00000000 314d818d 00000000

8055b290  3d76644d 01cca1f4 00000000 00000000

显然,第一个0x804e36b8即为SSDT的基址。在老版本的WinDbg中,可以利用poi执行显示某一地址处的内容,例如显示地址0x804e36b8处的内容可使用dd poi[0x804e36b8],而新版的WinDbg已经可以直接支持dd [0x804e36b8]:

kd> dd [804e36b8]

804e36b8  8058a1f1 8057a2d1 8058d5e8 8058b52c

804e36c8  80591aa6 806393f2 8063b583 8063b5cc

804e36d8  8057b8c4 8064a391 80638bad 805910c4

804e36e8  80630cf4 8057bdad 80592876 80627c4d

804e36f8  805de479 80569fca 805da817 805a353d

804e3708  804e3cc4 8062d4ae 805cabb6 804edfbc

804e3718  8056a676 805688cd 80591532 8064fc88

804e3728  8058ca4e 8058af39 8064fef5 8058d63a

上述的内容依次就是用工具观察SSDT时得到的当前地址,也就是说SSDT中存放是相应索引对应函数的所在地址,例如第一个0x8058a1f1中存放的就是索引0对应函数NtAcceptConnectPort的地址,如图所示:

另外,由于每个函数地址为4字节,故索引为Index的函数对应的地址为[[KeServiceDescriptorTable]+Index*4]。

============================我是湿润的昏割线=============================

 在下节课中,将深入讲解SSDT的相关知识,具体涉及SSDT的读取、写不可写页面(SSDT的写入)等。请大家继续期待梅哥的Ring0湿润插入教程吧~

posted @ 2015-06-04 16:07  大重九  阅读(3212)  评论(0编辑  收藏  举报