过一般保护

更新时间:2023-03-08 08:41:52 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

目 录

1绪论 ................................................ 2

1.1研究背景 ................................................................................................................................ 2

1.2本文的主要研究内容 ............................................................................................................. 3

2 Windows系统基础 ................................... 4

2.1 OD插件知识 ....................................................................................................................... 4

2.1.1 OD插件简介 .............................................................................................................. 4 2.1.2 如何编写OD插件 ..................................................................................................... 5 2.2 Windows系统理论知识 .................................................................................................. 5

2.2.1 Windows进程虚拟地址空间 .................................................................................... 5 2.2.2 Windows系统调用 ................................................................................................ 6 2.2.3 Windows 句柄理解 ............................................................................................... 7 2.2.4 Windows 切换进程空间 ....................................................................................... 8 2.3 Windows调试系统原理 .................................................................................................. 8

2.3.1 Windows调试系统用户模块 ................................................................................ 8 2.3.2 Windows调试系统内核模块 ................................................................................ 9

3系统需求分析 ........................................ 9

3.1 存在的主要问题............................................................................................................... 9 3.2解决方案 ............................................................................................................................. 10 3.3系统需求分析....................................................................................................................... 10 3.4系统流程图 .......................................................................................................................... 12

4系统概要设计 ....................................... 13

4.1 4.2

应用程序模块................................................................................................................. 13 内核驱动模块................................................................................................................. 14

5系统详细设计 ....................................... 15

5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8

HOOK ntdll.dll的KiFastSystemCall()函数 .................................................................. 15 实现HOOK的HxKiFastSystemCall(),改变系统调用的流程 .................................. 16 解析微软提供的PDB文件得到未导出的内核函数地址 ........................................... 18 向Windows系统内核中加入我们自己的系统服务表 ................................................ 24 实现我们自己的系统调用函数 ..................................................................................... 27 移除EPROCESS->DebugPort端口 .............................................................................. 31 HOOK Windows 内核下会发送调试事件的内核函数 ............................................ 32 HOOK Windows内核异常处理函数 ............................................................................ 35

系统开发小结 ........................................ 36 参考文献 ............................................ 36

致 谢 ............................... 错误!未定义书签。

基于OD插件的内核调试器的设计与实现

摘 要:随着计算机互联网技术的飞速发展,网络游戏得到了很好的发展,它的保护也

是日趋完善,几种常见的保护有nProtect GameGuard(NP), hackshield(HS),让OllyDbg调试器(OD)不能调试,Cheat Engine(CE)不能搜索游戏内存,让人们没法开始逆向它们,本文即在这种背景下为了满足人们的工作需求而设计的。系统以VC 6.0 应用程序编译器和WIN DDK 3790.1830 驱动编译器作为开发工具,以OD调试器提供的静态库,Window Research Kernel(WRK)的源码为基础,HOOK OD进程空间ntdll.dll动态库的KiFastSystemCall()函数,通过我们自己在Windows内核下加的系统服务表,使其当OD调试器调用我们感兴趣的系统调用时,跳到我们自己的系统调用,这样使其成功调用,突破游戏的一般保护,本文以C语言为编程语言,本系统是一个具有实际应用意义的典型系统。

关键词:OD插件;NP;HS;游戏保护;

OD plug-in based on the kernel debugger Design and

Implementation

Abstract: With the rapid development of Internet technology, computer, online games have been well developed, and its protection is also maturing, several common protect nProtect GameGuard (NP), hackshield (HS), let OllyDbg debugger (OD) can not debugging, Cheat Engine (CE) can not search for games of memory, so that people can not start reverse them, this is in this context in order to meet the needs of people work designed. Application system to VC 6.0 compiler and WIN DDK 3790.1830 drive the compiler as a development tool to OD debugger provides static libraries, Window Research Kernel (WRK) source-based, HOOK OD process space of KiFastSystemCall dynamic library ntdll.dll () function, through our own in the Windows kernel plus the system service table, so that the debugger is called when the OD system call when we are interested, skip to our own system calls, so call it success, breaking the game in general protection of this paper, C language programming language, the system is a typical system, the practical application of significance.

1

Key words: OD Plugins;NP;HS;Game Protect

1绪论 1.1研究背景

进入二十一世纪以来,随着社会的不断进步和互联网产业技术的飞速发展,网络游戏得到了空前的流行,研究它的人也越来越大众化。

因此,因为种种原因,一些游戏和商业程序就会防止OD调试器和CE数据搜索器搜索游戏的数据防止逆向工作人员逆向这们,一些防调试方法如下: 1, 比如检测系统当前系统运行的进程看有没有Od和Ce,查找窗口看有没有它们,还有就是搜索Od和Ce的特征码看有没有它们,有就退出。 2, 调用IsDebugPresent()函数,检测这个游戏进程是不是被调试。

3, Hook DbgUiRemoteBreakIn()函数防止运行int 3断点中止游戏的程序运行,这样能防止OD调试器附加游戏进程。

4, 游戏故意产生异常,然后自己去处理它,如果有调试的话,异常一般会先给调试器,由调试器处理了,所以游戏就不会进入它自己的异常处理,这样它就检测出了调试器。

5, 还有就是nProtect GameGuard(NP), hackshield(HS),PerfectProtect.sys等等的保护,一般它们都是差不多的,比如系统服务表(SSDT表)HOOK,内核下Inline HOOK,一般HOOK NtOpenProcess()防止打开游戏进程得到句柄,HOOK NtReadVirtualMemory(),NtWriteVirtualMemory()防止读写游戏进程空间的内存,还有就是HOOK NtDebugActiveProcess(),NtCreateDebugObject(),NtWaitForDebugEvent()等等一系列与调试有关的系统调用防止调试游戏,还有就是HOOK 每个CPU的中断描述符表(IDT)表中的INT 1和INT 3断点,防止对游戏进程下断产生单步调试事件和异常调试事件,还有一种就是内核下建立一个内核线程一直对游戏的进程控制块(EPROCESS)结构体的(调试端口)DebugPort一直清0,防止游戏被Od调试收到调试信息。

综上所述,第5种保护方法一般是现在最流行的保护,有很多的人们在研究它们的保护,他们一般采用的方法是跳过这些函数的调用,比如对于SSDT HOOK,它们可以在KiFastCallEntry()函数里进行处理,对于Inline HOOK,我们可以先HOOK SSDT进入我们自己的系统调用,然后才调用真正的函数,对于调试端口

2

DebugPort清0的检测我们可以把Windows内一切操作EPROCESS.DebugPort端口的内核函数,改变这些偏移,使其操作EPROCESS结构中的其它没有多大用的结构成员,比如EPROCESS.time。

这种方法针对一般的保护有效,但是对于特定的保护,我们可以特别的处理,比如Tessafe.sys你可以把自己加入白名单等等处理方法,还有对于HS我们可以调用一个14号功能函数通知HS保护驱动退出等等许多的方法。

1.2本文的主要研究内容

基于OD插件的内核调试器系统主要是在OD调试器的基础上编写OD插件,HOOK OD进程空间的ntdll.dll动态库的KiFastSystemCall()函数,改变OD进程调用一些关键系统函数的调用流程,使其进入我们自己在内核中的实现的系统调用,这样能够绕过一般的保护驱动的钩子。本系统是主要分为四个主要的功能来实现:

1, 编写OD应用程序插件,利用OD调试器提供给我们的OLLYDBG.LIB静态库和Plugin.h头文件来编写OD插件,在OD调试器上加个菜单选项用于控制OD调试器是不是启动我们的这种程序机制。比如当我们点BeginHook菜单时,我们的程序就开始工作,HOOK ntdll.dll动态库的KiFastSystemCall()函数,Windows提供给程序员的一般的API,比如Kernel32.dll里的打开进程OpenProcess(),读进程内存ReadVirtualmemory(),写进程内存WriteVirtualMemory()等等API函数,最终的系统调用都会调用这个函数,这时eax是系统调用ID,堆栈里有调用这个系统调用时的各个参数,这样,我们可以改变这些系统调用ID,使其调用我们自己在Windows系统内核添加的系统调用。当我们点CancelHook时,我们的程序就停止工作。

2, 由于我们内核驱动要调用的很多内核函数是ntkrnelpa.exe内核文件未导出的,所以我们要自己获取内核函数在当前系统的地址,很多同学采用的是暴力搜索内存然后匹配方法处理的,这种方法效率欠缺,并且由于好多硬编码,移值性很差。我是这么做的,在内核驱动下,把这些未导出的函数申明为函数变量指针,然后用户层程序(OD插件)调用DeviceIoControlFile()函数和驱动通信取得这些未导出的函数的名字,然后我们自己解析微软提供给我们的符号文件(PDB文件),得到这个版本系统的未导出的函数在这个系统版本的内核加载地址。然后再次调用DeviceIoControlFile()函数传给内核驱动。

3,把微软提供的Windows Reasarch Kernel(WRK)里的dbgk目录下的dbgkobj.c,dbgkport.c,dbgkproc.c,dbgkp.h文件移值到我们的内核驱动工程里来,这几个.c文件是Windows系统调试系统机制的实现,只要我们集成进我们的

3

驱动里来了,以后游戏保护对这些调试机制的一切HOOK与检测,我们可以不用去管它了,相当于OD插件它们用的是我们自己集成的调试系统机制。还有内核线程对EPROCESS结构体的DebugPort清0,我们也可以跳过它。还有就是增加游戏常HOOK的几个关键函数,比如NtOpenProcess(),NtReadVirtualMemory(),NtWriteVirtualMemory()等等,我们自己在Windows内核中实现它,下次OD调试器调用这些函数对游戏进程操作时,都会进入我们自己的这些系统调用,到时想实现什么功能就实现什么功能。

4, Windows系统内核下有两种系统服务表KeServiceDescriptorTable和KeServiceDescriptorTableShadow,对于图形界面(GUI)线程,它引用的是KeServiceDescriptorTableShadow这个系统服务表,非GUI线程引用的是KeServiceDescriptorTable系统服务表。对于微软提供的大多数API,内核下都有专门对应的系统调用,比如当我们调用OpenProcess() API函数时,最终会调用系统调用NtOpenProcess()。每一个系统调用ID,在Windows内核下都会对应一个系统调用函数,比如NtOpenProcess的系统调用ID,在Windows Xp 3的系统下ID为122。Windows Xp3 系统大概提供了283个系统调用。因此我们自己可以在这两种系统服务表的后面加上我们自己的系统调用函数,对应相应的系统调用ID,这样当OD调试器调用我们感兴趣的系统调用时,我们可以改变系统调用ID,这样就可以进入我们自己实现的系统调用函数。

2 Windows系统基础

2.1 OD插件知识

2.1.1 OD插件简介

OllyDbg 简称(OD) 是一款优秀的用户态调试工具。它不仅拥有强大的反汇编能力和动态分析力,还具有良好的扩展结构,允许用户自行开发插件完成特定的工作。OD插件也可以有自己的窗口逻辑和功能函数。事实上,我们可以将它看成这样一个Windows 程序,它拥有自己的消息循环和窗口过程,但它的启动是由OllyDbg 发起的,具体功能的实现也通过调用OllyDbg 提供的函数来实现的。它的启动过程如下:

在OllyDbg 的启动过程中,有一步是检查插件路径下是否存在DLL 文件。如果存在,逐一进行如下扫描:

1, 加载该DLL 文件,找到其入口点。

2, 通过回调函数,获取插件名称、版本等信息。

3, 通过回调函数,对插件进行初始化,包括申请资源、恢复全局参数等。

4

如果某个DLL 文件无法顺利执行这三步,OllyDbg 的启动将失败、报错并退出。OllyDbg 启动以后,会一直维护插件的队列,在特定时间向该队列发送消息,或者直接调用插件中定义的函数,用户通过插件菜单或快捷键主动执行插件某功能。最后,当OllyDbg 被关闭时,还会调用插件中的回调函数,释放插件申请到的资源,并将需要保存的参数、配置和附加信息分别予以保存。 2.1.2 如何编写OD插件

新建立一个VC 6.0的工程,把Plugin.h头文件和OLLYDBG.LIB静态库文件加进

工程里面来,还有在工程里加入/J选项,使使char 默认为unsigned类型,这是OllyDbg 中的约定。然后定义两个回调函数,这是OD插件必需的两个回调函数,ODBG_Plugindata()和ODBG_Plugininit()函数,之所以说是回调函数,是因为这是由OD调试器调用它们的。比如我们在ODBG_Plugindata()回调函数中传递我们插件的名字进去;在ODBG_Plugininit()中把OD窗口句柄保存起来;我们还实现了几个回调函数,ODBG_Pluginmenu(),ODBG_Pluginaction(),ODBG_Plugindestroy()函数。这些函数用于在OD插件菜单中加入菜单选项和我们的响应函数。我们可以在OD插件里面调用OD提供很多的API函数,比如有Breakpoint functions,Memory functions,Thread functions,Module functions等等许多的函数库。之后编译成功后把它们放在OD调试器目录下的Plugin文件夹下就行了。

2.2 Windows系统理论知识

2.2.1 Windows进程虚拟地址空间

如上图4-1-1,在Windows系统中,每一个进程都有4GB的虚拟地址空间。

5

0—2GB是Windows进程的用户空间,2GB到4GB是Windows的内核空间。物理地址扩展(PAE)除外,这时,每个进程的用户空间为0—3GB。每个进程有4GB的虚拟地址空间,并不是指它真正有4GB的物理内存,而是指每个进程都有其页目录表和页表,通过它会把每个进程映射成4GB虚拟内存空间。

进程用户空间0-2GB的映射,页表一般会把进程的这段虚拟内存空间映射到不同的物理内存中。但是有例外,就是Windows常见的DLL和可执行映象文件,比如kernel32.dll,ntdll.dll和.exe文件运行时的情况。它们有写入时复制(Copy On Write)机制,在没有改写这些代码前,它们在物理内存中只有一份内存,所有的进程都会根据页目录和页表映射到这份物理内存。但是当有一个进程改变这些DLL的内容时,写入时复制就发生了。Windows这时会把这些内容在物理内存中拷贝一份,改变这个进程的页目录和页表,使之指向这份拷贝的物理内存,对其它的进程没有任何影响,所以这就是用户层HOOK 系统DLL时,只对本进程有效,对其它进程无效的原因。

所以当我们HOOK 一个进程的ntdll.dll的KiFastSystemCall()时,它只会对我们HOOK的这个进程的KiFastSystemCall()函数起作用。对别的进程没有影响。 进程内核空间2—4GB空间的映射,通常系统中每个进程的这段虚拟内存空间会根据页目录表和页表映射到相同的物理内存中。比如当我们改写一处内存,此时对每个进程都会有效的,对整个系统也都是有效的。

2.2.2 Windows系统调用

下面以Windows XP3系统分析Windows用户层API是怎么调用系统调用的全过程。图4-1-2是Windows系统进行系统调用的全过程描述:

6

比如当我们用户层进程调用常见的API比如Win32 API 比如OpenProcess(),ReadVirtualMemory(),WriteVirtualMemory(),CreateFile()等等函数,它们接着会依次调用ntdll.dll的Zw********函数,Zw**********函数会传递系统调用ID,然后调用sysenter指令进入ring0,在Windows内核下首先执行的是KiFastCallEntry()函数,它会根据系统调用ID分别从KeServiceDescriptorTable或者KeServiceDescriptorTableShadow这两种系统调用表中取得相应的系统调用函数地址,然后调用之。对于GUI线程是从KeServiceDescriptorTableShadow表中取,非GUI线程是从KeServiceDescriptorTable表中取的。所以我们可以HOOK KiFastCallEntry()这个函数,我们就可以拦截一切的系统调用。360安全卫士就是使用这种方法拦截系统调用的。当这个系统调用成功返回后,KiFastCallEntry()函数会调用sysexit指令退出本次的系统调用,然后返回到用户进程空间,依次返回到调用用户API的地方。

2.2.3 Windows 句柄理解

句柄是Windows系统的特性。你可以简单理解为通过它你就可以访问相应的内核对象。其实Windows是这么设计的,每个进程的EPROCESS结构体中都有个ObjectTable 成员指向进程句柄表(HANDLE_TABLE),里面每一项都是HANDLE_TABLE_ENTRY句柄表项。里面放着相应对象的地址,句柄只是这些表项的索引。还有一点值的注意的是句柄是进程相关的。你在本进程打开的进程句柄,在别的进程当中不能引用它的。它们的结构体定义如下:

lkd> dt _HANDLE_TABLE nt!_HANDLE_TABLE

+0x000 TableCode : Uint4B //指向_HANDLE_TABLE_ENTRY数组

+0x004 QuotaProcess : Ptr32 _EPROCESS //这个句柄表属于哪个进程

+0x008 UniqueProcessId : Ptr32 Void

+0x00c HandleTableLock : [4] _EX_PUSH_LOCK +0x01c HandleTableList : _LIST_ENTRY

+0x024 HandleContentionEvent : _EX_PUSH_LOCK

+0x028 DebugInfo : Ptr32 _HANDLE_TRACE_DEBUG_INFO +0x02c ExtraInfoPages : Int4B +0x030 FirstFree : Uint4B +0x034 LastFree : Uint4B

+0x038 NextHandleNeedingPool : Uint4B

+0x03c HandleCount : Int4B //句柄表的数量 +0x040 Flags : Uint4B +0x040 StrictFIFO : Pos 0, 1 Bit lkd> dt _HANDLE_TABLE_ENTRY nt!_HANDLE_TABLE_ENTRY

7

+0x000 Object : Ptr32 Void //指向内核对象的地址 +0x004 GrantedAccess : Uint4B

比如当我们在用户进程调用kernel32.dll里的OpenProcess() 打开进程时,它先会调ntdll.dll的ZwOpenProcess(),接着会传递NtOpenProcess()的系统调用ID,调用KiFastSystemCall()函数,这函数里面会调用sysenter指令进入Ring0,然后由KiFastCallEntry()根据系统调用ID从KeServiceDescriptorTable或者KeServiceDescriptorTableShadow这两种系统调用表中找到NtOpenProcess()调用之。NtOpenProcess()里面会根据要打开的进程ID还是进程名字调用PsLookupProcessByProcessId()这个函数得到这个进程对象EPROCESS的地址,最后调用ObOpenObjectByPointer()返回进程句柄。ObOpenObjectByPointer()函数大致工作如下:首先会增加这个EPROCESS对象的引用计数,然后构造一个_HANDLE_TABLE_ENTRY结构,填入对象的地址,然后把它加进这个用户进程的句柄表(HANDLE_TBALE)中,最后返回句柄(索引)。

2.2.4 Windows 切换进程空间

一般而言,如果线程T属于进程P,那么当这个线程在内核中运行时的用户空间应该就是进程P的用户空间。它也没有必要访问到别的用户进程空间去,可是Windows允许一些跨进程的操作,特别是跨用户进程空间的操作。所以有时候就需要把当时的用户空间切换到别的进程空间中去。Windows提供的函数是KeStackAttachProcess()和KiAttachProcess()。它的原理其实就是改变CPU的CR3寄存器使之指向要切换进程的页目录表。因为CPU访问进程用户层空间地址都是通过CR3找页目录表,然后通过Windows内存管理器把虚拟地址映射成物理地址才去访问的,所以只要我们改变CR3寄存器就行了。Windows有很多这种跨进程的操作,例如调试DbgkpPostFakeProcessCreateMessages()函数会调用KeStackAttachProcess()这个函数切换进被调试进程的用户层空间中,因为DbgkpPostFakeModuleMessages()这个函数会访问被调试进程的进程环境块(PEB),然后遍历它的用户层模块链表。最后会调用KeUnstackDetachProcess()这个函数回到OD调试器的进程空间来。

2.3 Windows调试系统原理

2.3.1 Windows调试系统用户模块

Windows系统在用户层提供了很多的调试API供用户程序调用,它们分别

在kernel32.dll和ntdll.dll里面。比如Kernel32.dll里面常见的API有DebugActiveProcess(),DebugActiveProcessStop(),DebugSetProcessKillOnExit(),WaitForDebugEvent(),ContinueDebugEvent()。ntdll.dll里面提供的调试函数有DbgUiDebugActiveProcess(),DbgUiIssueRemoteBreakIn(),DbgUiContinue()等等API函数。

OD调试器一般会枚举当系统系统中的所有进程,如果我们要要调试哪个进

8

程,它就会调用kernel32.dll里的DebugActiveProcess()函数,它里面首先会调用ntdll.dll里的DbgUiConnectToDbg(),这个函数里面最终会调用ntdll.dll的ZwCreateDebugObject()建立一个调试对象用来接受调试事件信息,最终依次会进入NtCreateDebugObject()系统调用函数。接着它会调用ProcessIdToHandle(PID)打开进程,它里面会调用ntdll.dll的ZwOpenProcess()函数打开进程,如果打开进程成功,则返回进程句柄。最后它会调用ntdll.dll里的DbgUiDebugActiveProcess()函数进行真正的附加调试。DbgUiDebugActiveProcess()函数首先会调用ZwDebugActiveProcess()函数,这个函数会把这个被调试进程的线程事件和模块事件信息插入在先前建立的调试对象中。然后会调用DbgUiIssueRemoteBreakin()函数在被调试的进程空间中建立一个远程线程用于执行int 3指令断点,目的是为了中断到调试器。之后OD调试器就会循环调用WaitForDebugEvent()和ContinueDebugEvent()等待调试事件,然后处理调试事件信息。 2.3.2 Windows调试系统内核模块

Windows系统在内核层也有很多的系统函数用来支持调试机制,比如有NtCreateDebugObject(),DbgkpPostFakeProcessCreateMessages(),DbgkpSetProcessDebugObject(),NtWaitForDebugEvent(),NtDebugContinue(),DbgkInitialize(),DbgkpSendApiMessage(),DbgkCreateThread(),DbgkMapViewOfSection()NtDebugActiveProcess()等等函数。DbgkInitialize()函数里面会调用ObCreateObjectType()函数建立一个调试对象类型。NtCreateDebugObject()函数里面会调用ObCreateObject()建立一个调试对象,然后再调用ObInsertObject()函数把这个对象插进OD进程的句柄表中。NtDebugActiveProcess()函数里面首先会调用DbgkpPostFakeProcessCreateMessages()函数,这个函数里面会依次调用DbgkpPostFakeThreadMessages()和DbgkpPostFakeModuleMessages()函数向调试对象发送线程和模块事件信息。最后会调用DbgkpSetProcessDebugObject()这个函数把被调试进程的EPROCESS的DebugPort设置成NtCreateDebugObject()函数建立的调试对象。当建立线程时,Windows系统会调用DbgkCreateThread()这个函数向这个调试对象发送建立线程的调试事件;当加载DLL时,Windows系统会调用DbgkMapViewOfSection()这个函数向这个调试对象发送加载模块的调试事件;当被调试进程执行用户层int 3指令时,Windows系统会调用DbgkForwardException()这个函数向这个调试对象发送异常调试事件。

3系统需求分析

3.1 存在的主要问题

1, 一般防调试的程序会对NtOpenProcess(),NtReadVirtualMemory(),NtWriteVirtualMemory(),NtDebugActiveProcess(),NtCreateDebugObject(),NtWaitForDebugEvent()这些函数进行HOOK的,不是SSDT HOOK这些

9

函数,就是Inline HOOK这些函数。当我们用OD调试器还是CE搜索器,要打开它的进程并对其进行读写内存时,它都会禁止操作那进程,从而导致我们操作失败。比如程序对NtOpenProcess()函数HOOK后,防止打开进程,OD调试器就看不到那进程,对NtDebugActiveProcess()函数HOOK后,OD调试器虽然能看到进程,但是不能调试成功的。 2, 他们有可能会在内核下建立系统内核线程不停的检测它们HOOK代码的地方会不会被恢复。还有可能会检测他们HOOK的代码到底有没有被调用。如果没有被调用,则认为是我们在它前面跳过去了,这样它们肯定就检测出了异常,他们还有可能会在内核下建立内核线程不时的对其保护的进程的EPROCESS的DebugPort清0,让我们的OD调试器收不到任何调试事件信息。

3.2解决方案

基于以上问题,我是这么设计的,我们的应用层OD插件HOOK OD进程空间的ntdll.dll动态库的KiFastSystemCall()函数,使其跳到我们自己的HxKiFastSystemCall(),然后对OD进程调用的相应的API函数进行过滤处理,也就是改变系统调用ID。然后在Windows系统内核下加入我们自己的系统调用函数和调试驱动。当OD调用比较关键的函数,就会进入我们自己的系统调用。所以对于别的驱动保护HOOK的关键函数就不起用了,对于一直清除EPROCESS的DebugPort的线程的检测,我们自己在内核下可以不操作要调试进程EPROCESS的Debugport。我们移到另一个结构体中。这样也就可以防止它一直对DebugPort调试端口清0。

3.3系统需求分析

根据以上的分析,为了克服现行OD调试器存在的不足,得到新系统的功能如下:

1, 我们利用OD调试器插件库,HOOK OD进程空间的ntdll.dll的KiFastSystemCall(),它是一切用户层API要进行系统调用的最后入口,然后当OD进程调用API函数时,只要这个API函数里会调用系统函数调用,它都会跳到我们自己的HxKiFastSystemCall()函数。

2, 在我们自己实现的HxKiFastSystemCall()函数中,对OD进程调用的API函数进行过滤处理,比如如果是调用OpenProcess()打开进程,ReadVirtualMemory()读进程内存,WriteVirtualMemory()写进程内存,DebugActiveProcess()附加进程,WaitForDebugEvent()等待调试事件等等关键函数,我们会在这里改变它们的系统调用ID,然后自己调用sysenter指令进入Ring0,sysenter指令是intel CPU公司专门设计的快速系统调用,它会改变EIP寄存器,使其指向KiFastCallEntry()函数,因此在当前系统中,只要发生过系统调用,不管是哪个进程在要求调用系统调用,在Ring0中首先被执行的是KiFastCallEntry()函数,然后它会根据我们传来的系统调用Id从KeServiceDescriptorTable或

10

者KeServiceDescriptorTableShadow表中去取得相应的系统调用函数运行。 3, 由于我们内核驱动里面用到的很多内核函数很底层,MS提供的DDK中并没有导出这些函数。比如这些函数有NtReadVirtualMemory(),NtWriteVirtualMemory(),它们都要调用的MmCopyVirtualMemory()函数,这个函数微软提供的ntoskrnl.lib静态库文件就没有导出,还有很多比如MiProtectVirtualMemory(),PsGetNextProcess(),ObDuplicateObject(),等等没有导出。我们不能在内核驱动中直接调用,我们可以采用暴力搜索内存找特征码然后得到当前系统这些函数的加载地址,然后加以调用。这种方法很费时而且通用性不好。我采用另一种方法,我们应用程序自己解析微软提供给我们的PDB符号文件搜索得到这些未导出函数的地址,然后传进内核驱动这些内核函数的地址,我们内核驱动才可以加以调用这些未导出的函数。

4, 我们要在Windows内核下加入我们自己的系统调用表,Windows内核下有两种系统调用表,

KeServiceDescriptorTable和KeServiceDescriptorTableShadow表。我们自己在非分页内存中分配一个服务表, 先拷贝KeServiceDescriptorTable和KeServiceDescriptorTableShadow两个表,然后把我们要加上的系统调用函数地址加进去,然后写进KeServiceDescriptorTable和KeServiceDescriptorTableShadow表中,下次如果OD进程调用我们感兴趣的系统调用的话,KiFastCallEntry()函数就会根据服务Id从这两个表中找到我们的系统调用,然后调用之。

5, 我们自己的系统调用很多地方可以参考WRK。WRK是Windows Server 2003系统的内核。

跟XP还是有点区别的。对于很重要的内核函数KeStackAttachProcess()和KiAttachProcess (),这两个是Windows系统内核切换到别的进程空间中的关键函数。由于有的游戏也有HOOK,我们要自己实现之用来跳过HOOK,这几个函数我是用IDA把Windows Xp3的NtKernelpa.exe逆向出来的,功能基本已实现。

6, 有的保护驱动会在内核下建立内核线程一直对它要保护的进程的EPROCESS的DebugPort端口一直清0,这样会导致OD调试器收不到任何调试事件信息。我是这么做的,因为我们OD调试器调用的API与调试有关的系统内核函数几乎都是我们自己在内核驱动中实现的函数,所以打算把操作EPROCESS的DebugPort的代码地方都改下。改成操作另一个结构体,里面专门对应要调试的进程的EPROCESS进程环境块和调试对象(DebugObject)。比如NtDebugActiveProcess->DbgkpSetProcessDebugObject()里面会对要调试进程的EPROCESS的DebugPort设置成NtCreateDebugObject()的调试对象。这里不们不把被调试进程的EPROCESS的DebugPort设置成指向那调试对象,而是移到上面说的结构中去。

7, 我们此时还要HOOK 一些常见的Windows内核函数,比如建立线程时,结束线程时,加载DLL时,怎么会把调试事件信息通知到我们建立的这种对应的结构体中。我们要Hook 这些常见的函数像建立线程时的PspUserThreadStartup(),它里面会调用DbgkCreateThread()向被调试进程EPROCESS的DebugPort发消息,我们在这里要处理,还有就是PspExitThread(),NtMapViewOfSection()等等很多函数都要处理的。

8, 我们还要Hook Windows的内核异常处理机制,比如我们用OD调试器经常F2

11

下的断点,都是int 3断点,我们要这CPU 执行Int 3这个指令时向相应的EPROCESS的DebugPort发送调试事件信息,我们就要HOOK Windows的异常处理函数。

3.4系统流程图

如下图 3-3为整个系统整体流程图:

12

4系统概要设计

4.1 应用程序模块

当我们把我们写的OD插件放入OD调试器目录下的Plugin文件夹中后,

OD调试器启动时就会调用ODBG_Plugindata()这个回调函数,我们在这里调用HxLoadKrnl()函数负责加载我们的内核驱动,我们的插件在OD调试器插件菜单中加入了以下两个菜单选项,BeginHook和CacelHook,当用户点击BeginHook后,会调用HxKrnl_Init()这个函数,这个函数里面主要是调用DeviceIoControl()这个函数向我们的内核驱动依次发送相关命令。先是发送HXIOCTL_INIT这个命令,要求我们的内核驱动从Windows内核下得到ntkrnlpa.exe,hal.dll内核模块的加载地址,然后返回给我们的OD应用层插件;接着发送HXIOCTL_GET_FNNUMBERLIST命令,会取得内核国未导出函数的个数,然后发送HXIOCTL_GET_FNLIST命令,会取得这些内核未导出函数的名字,之后我们OD插件解析内核PDB文件得到这些未导出函数的地址,然后再次向我们的驱动发送HXIOCTL_SET_DRIVER命令,通知驱动开启把这些函数地址设置到函数指针变量中。接着OD插件会发送HXIOCTL_BEGIN_ADDSERVICE命令,要求驱动在Windows系统内核下加入我们自己的系统调用表。最后OD插件会

13

调用Hook()函数HOOK OD进程空间ntdll.dll的KiFastSystemCall()函数,之后当OD进程再次调用API时就会进入我们的函数,我们HxKiFastSystemCall()函数里面会对其系统调用ID进行判断,如果是我们感兴趣的系统调用ID,我们在这里把它改变成我们在内核添加的对应的系统调用ID,然后调用sysenter指令一样的进入Ring0,Ring0的响应函数KiFastCallEntry()会根据我们传进来的系统调用ID找到我们对应的系统调用然后调用之。

4.2 内核驱动模块

内核驱动模块有一部分是专门和我们的OD应用层插件通讯用的,当我们的

OD插件调用DeviceIoControl()函数向我们驱动发送相关命令时,我们会在HxCtrl()这个函数中做相关处理,对于HXIOCTL_INIT命令,我们会调用HxGetKenelNameAndLoadAddr()这个函数遍历系统当前所有加载的内核列表中找到ntkrnlpa.exe和hal.dll,然后返回当前的加载地址,返回给我们的应用层OD插件,对于HXIOCTL_GET_FNNUMBERLIST命令,我们返回内核未导出函数的个数,对于HXIOCTL_GET_FNLIST命令,我们返回这些未导出函数的名字,对于HXIOCTL_SET_DRIVER命令,我们把OD插件得到的内核未导出的函数地址设置到函数指针变量中来,对于HXIOCTL_BEGIN_ADDSERVICE命令,我们在Windows内核下加入我们自己的系统调用表,对于HXIOCTL_FREE这个命令,我们做好一些卸载善后操作,因为OD插件通知我们的内核驱动要退出了。内核驱动还有一部分就是实现的这些系统调用函数和调试模块。

14

5系统详细设计

5.1 HOOK ntdll.dll的KiFastSystemCall()函数

下面这是用Windbg看的KiFastSystemCall函数,如下,它只有四个字节,一般的HOOK至少要5个字节以上。所以我们要采用另一种方式HOOK,在ntdll.dll里找到一片没有用的内存,正好KiFastSystemCall上面10个字节就没有用到。所以我们可以在这10个字节里填入我们最终要跳转到的地址,然后从KiFastSystemCall函数开头处改写代码使其跳到KiFastSystemCall-10字节处就行了,具体的HOOK代码和UnHook代码如下: 0:001> u KiFastSystemCall ntdll!KiFastSystemCall:

7c92e4f0 8bd4 mov edx,esp 7c92e4f2 0f34 sysenter

BOOL HOOK() {

PCHAR fun;

UCHAR HookCode[5]={0}; UCHAR Hook[2]={0}; fun = (PCHAR)GetProcAddress(GetModuleHandle(\;//得到KiFastSystemCall函数映射的地址 if (fun!= NULL) {

DWORD lOldProtect; DWORD oldprotect; fun-=10;

if(VirtualProtect((PVOID)((DWORD)fun),14,PAGE_EXECUTE_READWRITE,&lOldProtect))//改写代码段内存为可读写和执行的权限,方便我们能改写代码段 {

fun[0]=0xE9;

*((DWORD*)(fun+1))=((DWORD)HxKiFastSystemCall - (DWORD)fun-5);

fun+=10; fun[0]=0xEB; fun[1]=0xF4;

15

VirtualProtect((PVOID)((DWORD)fun-10),14,lOldProtect,&oldprotect); hookFunctionAddr=fun; return TRUE; } }

return FALSE; }

BOOL UNHOOK() {

if (hookFunctionAddr!= NULL) {

DWORD lOldProtect;

PCHAR fun = hookFunctionAddr;

if(VirtualProtect((PVOID)((DWORD)fun),4,PAGE_EXECUTE_READWRITE,&lOldProtect)) {

fun[0]=0x8b; fun[1]=0xd4;

VirtualProtect((PVOID)((DWORD)fun),4,lOldProtect,NULL); } }

return TRUE; }

5.2 实现HOOK的HxKiFastSystemCall(),改变系统调用的流程

当OD调试器调用一般的API时比如OpenProcess(),ReadVirtualMemory(),WriteVirtualMemory()函数时,最终会进入KiFastSystemCall()这个函数,从而进入我们自己实现的HxKiFastSystemCall()函数,在进入HxKiFastSystemCall函数的环境下,eax指向调用此API对应的系统调用ID,esp指向堆栈,里面放着用户层传来的参数。在这里我们把此函数定义成__declspec( naked ),这么定义是为了防止编译器额外的加上优化和自动保护堆栈代码。在此之前我们先要把esp压栈,保存上下文。然后传进系统调用ID,然后调用FilterServiceFun()函数对我们感兴趣的服务ID进行过滤改变,然后eax返回的是我们改写过的系统调用ID,之后再次调用sysenter真正的进行系统调用,实现Ring3进入Ring0层,这里没用sysenter指令,因为vc 6.0编译器不识别它,我用的是二进制代码,具体实现代码如下:

__declspec( naked ) HxKiFastSystemCall() { __asm {

push esp; //保存堆栈

16

push eax; //服务Id call FilterServiceFun; pop esp; mov edx,esp

__asm _emit 0x0F //sysenter指令进入ring0 __asm _emit 0x34 } }

extern DWORD MyServiceStartID;

int WINAPI FilterServiceFun(DWORD ServiceId) {

DWORD MyServicId; switch(ServiceId) {

case 122: //

MyServicId=MyServiceStartID; //OutputDebugStringA(\层调用了NtOpenProcess()\ break; case 128:

MyServicId=MyServiceStartID+1;

// OutputDebugStringA(\层调用了NtOpenThread()\ break; case 125:

MyServicId=MyServiceStartID+2;

// OutputDebugStringA(\层调用了NtOpenSection()\ break; case 53:

MyServicId=MyServiceStartID+3;

// OutputDebugStringA(\层调用了NtCreateThread()\ break; case 180:

MyServicId=MyServiceStartID+4;

// OutputDebugStringA(\层调用了NtQueueApcThread()\ break; case 186:

MyServicId=MyServiceStartID+5;

// OutputDebugStringA(\层调用了NtReadVirtualMemory()\ break; case 277:

MyServicId=MyServiceStartID+6;

// OutputDebugStringA(\层调用了NtWriteVirtualMemory()\ break; case 137:

17

MyServicId=MyServiceStartID+7;

//OutputDebugStringA(\层调用了NtProtectVirtualMemory()\ break; case 33:

MyServicId=MyServiceStartID+8;

// OutputDebugStringA(\层调用了NtCreateDebugObject()\ break; case 57:

MyServicId=MyServiceStartID+9;

// OutputDebugStringA(\层调用了NtDebugActiveProcess()\ break; case 58:

MyServicId=MyServiceStartID+10;

// OutputDebugStringA(\层调用了NtDebugContinue()\ break; case 269:

MyServicId=MyServiceStartID+11;

// OutputDebugStringA(\层调用了NtWaitForDebugEvent()\ break; case 223:

MyServicId=MyServiceStartID+12; // OutputDebugStringA(\层调用了NtSetInformationDebugObject()\ break; default:

MyServicId = ServiceId; }

return MyServicId; }

5.3 解析微软提供的PDB文件得到未导出的内核函数地址

1,Ring0层内核驱动先通过遍历当前Windows系统的内核模块链表得到hal.dll和ntoskrnl.exe内核模块的装载地址,然后传给Ring3层程序。具体代码如下:

BOOL HxGetKenelNameAndLoadAddr(HX_OSKRNL_INFO *KrnlInfo) {

NTSTATUS Status;

PLDR_DATA_TABLE_ENTRY DataTableEntry; ANSI_STRING AnsiString; PLIST_ENTRY NextEntry;

UNICODE_STRING KernelString; UNICODE_STRING HalString; int count=0;

18

ASSERT (KeGetCurrentIrql() == PASSIVE_LEVEL);

KernelString.Buffer = (const PUSHORT) KERNEL_NAME;

KernelString.Length = sizeof (KERNEL_NAME) - sizeof (WCHAR); KernelString.MaximumLength = sizeof KERNEL_NAME;

HalString.Buffer = (const PUSHORT) HAL_NAME;

HalString.Length = sizeof (HAL_NAME) - sizeof (WCHAR); HalString.MaximumLength = sizeof HAL_NAME;

NextEntry = PsLoadedModuleList->Flink; while (NextEntry != PsLoadedModuleList) {

DataTableEntry = CONTAINING_RECORD(NextEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

//第一次得到内核的名字和装载基址

if (RtlEqualUnicodeString (&KernelString, &DataTableEntry->BaseDllName, TRUE)) {

DbgPrint(%ullDllName);

Status=RtlUnicodeStringToAnsiString(&AnsiString,&DataTableEntry->FullDllName,TRUE);

strcpy(KrnlInfo->KrnlPE,AnsiString.Buffer);

KrnlInfo->ModuleBase[0]=(DWORD)DataTableEntry->DllBase; if ( NT_SUCCESS(Status) ) {

RtlFreeAnsiString(&AnsiString); //释放空间 }

count++; }

//比较得到Hal.dll内核的装载基址

if (RtlEqualUnicodeString (&HalString, &DataTableEntry->BaseDllName, TRUE)) {

Status=RtlUnicodeStringToAnsiString(&AnsiString,&DataTableEntry->FullDllName,TRUE);

19

ArgumentTable成员,指向我们刚刚分配的空间。Windows系统是多线程的系统,线程每时每刻都在Windows内核调度器的管理下协调地切换CPU运行着,对于多核CPU更是如此,因为我们改变的是整下Windows系统的唯一的系统调用表,因此我们在改变的同时,别的线程很有可能会访问这两个系统服务表,从而可能会导致错误。对于单核CPU我们可以紧紧提高中断请求级(IRQL)就行了,比如我们调用KeRaiseIrql(DISPATH_LEVEL)把IRQL提高到这个请求级之后,Windows调度程序就不会得到执行了,别的线程也就没有机会得到运行。但是对于多核CPU ,情况就不是这样了,虽然你在一个CPU上提高了IRQL,但是别的CPU照样可以运行别的线程,所以一定要保证在改变的同时还要保证别的CPU也不能运行别的线程,Windows系统专门提供了自旋锁机制,我们可以利用Windows系统内核自己使用的自旋锁LockQueueDispatcherLock ,它专门锁调度程序的,只要我们锁了这个锁,线程调度程序就一定会得不到执行,所以别的线程也一定会得不到运行的。我们调用KeAcquireQueuedSpinLockRaiseToSynch(LockQueueDispatcherLock);来提高IRQL,同时又锁定了别的CPU的调度,所以此时我们再改写Windows系统内核下这两个共享的系统调用表时会很安全的。改写后再调用KeReleaseQueuedSpinLock(LockQueueDispatcherLock,oldirql);来降低IRQL和释放Windows系统的自旋锁,此时Windows系统就会切换别的线程在其中的CPU上去运行。最后我们返回我们自己的系统调用在Windows内核的开始服务ID,方便应用程序OD插件好改变系统调用的流程,具体代码如下: NTSTATUS AddServices() {

ULONG NumberOfServices; KIRQL oldirql;

DbgPrint(\开始执行\\n\

NumberOfServices=sizeof(ServiceTableBase)/sizeof(ServiceTableBase[0]); // 将要增加的服务函数的个数

//KeServiceDescriptorTableShadow=(PSERVICE_DESCRIPTOR_TABLE)GetAddrssofShadowTable(); //获取 Shadow SSDT 地址

if (KeServiceDescriptorTableShadow==NULL) { DbgPrint(\没有找到TableShadow\\n\ return STATUS_UNSUCCESSFUL; }

DbgPrint(\KeServiceDescriptorTableShadow);

NewNumberOfServices=KeServiceDescriptorTable->TableSize+NumberOfServices; //增加后服务数目

StartingServiceId=KeServiceDescriptorTable->TableSize;

25

//得到后来增加服务中第一个服务的序号

DbgPrint(\增加的第一个服务号是StartingServiceId %x \\n\StartingServiceId); /* Allocate suffcient memory to hold the exising services as well as

the services you want to add */

NewServiceTableBase=(unsigned int *) ExAllocatePool(NonPagedPool, NewNumberOfServices*sizeof(unsigned int)); if (NewServiceTableBase==NULL) {

return STATUS_INSUFFICIENT_RESOURCES; }

NewParamTableBase=(unsigned char *) ExAllocatePool(NonPagedPool, NewNumberOfServices);

if (NewParamTableBase==NULL) {

ExFreePool(NewServiceTableBase);

return STATUS_INSUFFICIENT_RESOURCES; }

/* Backup the exising SSDT and SSPT */

memcpy(NewServiceTableBase, KeServiceDescriptorTable->ServiceTable, //原来的SSDT服务地址表拷贝到分配的NewServiceTableBase内存中去 KeServiceDescriptorTable->TableSize*sizeof(unsigned int));

memcpy(NewParamTableBase, KeServiceDescriptorTable->ArgumentTable, //原来的SSDT参数表拷贝到新分配的NewParamTableBase内存中去 KeServiceDescriptorTable->TableSize);

/*对地址表和参数表追加*/

memcpy(NewServiceTableBase+KeServiceDescriptorTable->TableSize, ServiceTableBase, sizeof(ServiceTableBase));

memcpy(NewParamTableBase+KeServiceDescriptorTable->TableSize, ParamTableBase, sizeof(ParamTableBase));

/*更新KeServiceDescriptorTableEntry的SSDT和 SSPT*/ OldServiceTable[0].oldServiceBase=

(DWORD)KeServiceDescriptorTable->ServiceTable;

OldServiceTable[0].oldServiceParamBase=(DWORD)KeServiceDescriptorTable->ArgumentTable;

OldServiceTable[0].oldServiceSize=

(DWORD)KeServiceDescriptorTable->TableSize;

//这里输出新的SSDT 服务函数的地址

/* 更新KeServiceDescriptorTableShadow的SSDT和 SSPT*/ OldServiceTable[1].oldServiceBase=

26

(DWORD)KeServiceDescriptorTableShadow->ServiceTable;

OldServiceTable[1].oldServiceParamBase=(DWORD)KeServiceDescriptorTableShadow->ArgumentTable;

OldServiceTable[1].oldServiceSize=

(DWORD)KeServiceDescriptorTableShadow->TableSize;

//下面改内核服务表时提IRQL,防止内核调度程序调度,自旋锁锁定CPU,只让这一个CPU访问

oldirql=KeAcquireQueuedSpinLockRaiseToSynch(LockQueueDispatcherLock);

KeServiceDescriptorTable->ServiceTable=(PVOID)NewServiceTableBase; KeServiceDescriptorTable->ArgumentTable=NewParamTableBase; KeServiceDescriptorTable->TableSize=NewNumberOfServices;

KeServiceDescriptorTableShadow->ServiceTable=(PVOID)NewServiceTableBase;

KeServiceDescriptorTableShadow->ArgumentTable=NewParamTableBase;

KeServiceDescriptorTableShadow->TableSize=NewNumberOfServices; KeReleaseQueuedSpinLock(LockQueueDispatcherLock,oldirql);

DbgPrint(\

KeServiceDescriptorTable->ServiceTableBase = %x\\n\KeServiceDescriptorTable->ServiceTable); return StartingServiceId; }

5.5 实现我们自己的系统调用函数

对常见的几个系统调用我们一定要自己实现,因为它们很重要,有几下这些函数,NtOpenProcess(),NtReadVirtualMemory(),NtWriteVirtualMemory() NtProtectVirtualMemory(),KeStackAttachProcess(),KiAttachProcess(),等等这几个函数是操作游戏进程的关键函数。还有NtCreateDebugObject(),NtDebugActiveProcess(),NtDebugContinue(),NtWaitForDebugEvent(),NtSetInformationDebugObject()等等这些函数是与调试有关的关键函数。

这些函数代码大多是参考WRK写的,因为WRK是Windows Server 2003的内核,对于Windows XP变化不是很大,有些函数是用IDA逆向ntkrnlpa.exe内核文件得到的,比如KeStackAttachProcess和KiAttachProcess 2003的变化和XP的变化很大。贴个关键函数的代码。 NTSTATUS WINAPI HxNtOpenProcess (

__out PHANDLE ProcessHandle, __in ACCESS_MASK DesiredAccess,

27

__in POBJECT_ATTRIBUTES ObjectAttributes, __in_opt PCLIENT_ID ClientId ) {

HANDLE Handle;

KPROCESSOR_MODE PreviousMode; NTSTATUS Status; PEPROCESS Process; PETHREAD Thread;

CLIENT_ID CapturedCid={0}; BOOLEAN ObjectNamePresent; BOOLEAN ClientIdPresent; ACCESS_STATE AccessState; AUX_ACCESS_DATA AuxData; ULONG Attributes; KIRQL CurrentIrql; PAGED_CODE();

CurrentIrql = KeGetCurrentIrql(); if(CurrentIrql>PASSIVE_LEVEL) {

KeLowerIrql(PASSIVE_LEVEL); }

//DbgPrint(\ DbgPrint(\已经调用过我们的HxNtOpenProcess ()函数 \\n\ PreviousMode = KeGetPreviousMode(); if (PreviousMode != KernelMode) { try {

ProbeForWriteHandle (ProcessHandle);

ProbeForReadSmallStructure (ObjectAttributes,

sizeof(OBJECT_ATTRIBUTES), sizeof(ULONG)); ObjectNamePresent = (BOOLEAN)ARGUMENT_PRESENT (ObjectAttributes->ObjectName); //Attributes = ObSanitizeHandleAttributes (ObjectAttributes->Attributes, UserMode);

Attributes=ObjectAttributes->Attributes; if (ARGUMENT_PRESENT (ClientId)) {

ProbeForReadSmallStructure (ClientId, sizeof (CLIENT_ID), sizeof (ULONG));

CapturedCid = *ClientId; ClientIdPresent = TRUE; } else {

ClientIdPresent = FALSE; }

28

} except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } else {

ObjectNamePresent = (BOOLEAN)ARGUMENT_PRESENT (ObjectAttributes->ObjectName); //Attributes = ObSanitizeHandleAttributes (ObjectAttributes->Attributes, KernelMode); Attributes=ObjectAttributes->Attributes; if (ARGUMENT_PRESENT (ClientId)) { CapturedCid = *ClientId; ClientIdPresent = TRUE; } else {

ClientIdPresent = FALSE; } }

if (ObjectNamePresent && ClientIdPresent) { return STATUS_INVALID_PARAMETER_MIX; }

Status = SeCreateAccessState( &AccessState, &AuxData,

DesiredAccess,

&(*PsProcessType)->TypeInfo.GenericMapping );

if ( !NT_SUCCESS(Status) ) { return Status; }

if (SeSinglePrivilegeCheck( SeDebugPrivilege, PreviousMode )) { if ( AccessState.RemainingDesiredAccess & MAXIMUM_ALLOWED ) { AccessState.PreviouslyGrantedAccess |= PROCESS_ALL_ACCESS; } else {

AccessState.PreviouslyGrantedAccess |= ( AccessState.RemainingDesiredAccess ); }

AccessState.RemainingDesiredAccess = 0; }

if (ObjectNamePresent) {

Status = ObOpenObjectByName( ObjectAttributes, *PsProcessType, PreviousMode, &AccessState, 0,

29

NULL, &Handle );

SeDeleteAccessState( &AccessState ); if ( NT_SUCCESS(Status) ) { try {

*ProcessHandle = Handle;

} except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode (); } }

return Status; }

if ( ClientIdPresent ) { Thread = NULL;

if (CapturedCid.UniqueThread) {

Status = PsLookupProcessThreadByCid( &CapturedCid, &Process, &Thread );

if (!NT_SUCCESS(Status)) {

SeDeleteAccessState( &AccessState ); return Status; } } else {

Status = PsLookupProcessByProcessId( CapturedCid.UniqueProcess, &Process );

if ( !NT_SUCCESS(Status) ) {

SeDeleteAccessState( &AccessState ); return Status; } }

Status = ObOpenObjectByPointer( Process, Attributes, &AccessState, 0,

*PsProcessType, PreviousMode, &Handle

); //得到进程句柄

30

SeDeleteAccessState( &AccessState ); if (Thread) {

ObDereferenceObject(Thread); }

ObDereferenceObject(Process); if (NT_SUCCESS (Status)) { try {

*ProcessHandle = Handle;

} except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode (); } }

return Status; }

return STATUS_INVALID_PARAMETER_MIX; } }

5.6 移除EPROCESS->DebugPort端口

现在一般游戏的保护都会对游戏进程的EPROCESS->DebugPort调试端口清0,这样OD调试器就会收不到调试事件信息。有种方法是把Windows系统内核下所有操作EPROCESS->DebugProt的内核函数,改写成操作EPROCESS结构中不重要的地方,比如操作EPROCESS结构的CreateTime成员,这样就算它对ERPCESS->DebugPort调试端口每时每刻清0,但是并不会影响我们调试的。

由于我们用的所有调试模块都是我们自己实现的,我们可以不用这么做,当OD调用NtDebugActiveProcess()时,进入我们的函数,之后会调用DbgkpPostFakeProcessCreateMessages()函数把这个要调试的游戏进程的所有模块,线程调试事件插入在DebugObject的事件链表中。之后会调用DbgkpSetProcessDebugObject()这个函数设置被调试进程EPROCESS的Debugport成员为先前建立的调试对象,下次有调试事件了就会插入在这个调试对象的事件链表中。

设置代码如下:

if (Process->DebugPort != NULL) {

Status = STATUS_PORT_ALREADY_SET; break; } //

// Assign the debug port to the process to pick up any new threads

//

Process->DebugPort = DebugObject;

我们其实可以自己定义一个结构体来对应要调试的进程的EPROCESS和DEBUGPORT

31

typedef struct __EPROCESSDEBUGPORT {

PEPROCESS Process;

PDEBUG_OBJECT DebugPort;

} EPROCESSDEBUGPORT,*LP EPROCESSDEBUGPORT;

5.7 HOOK Windows 内核下会发送调试事件的内核函数

上面已经提到怎么定义一个结构体,但是怎么改变Windows内核下调试的处理机制,也就是怎么会把调试事件发送到我们自己定义的这个结构体中的DebugPort,而不是原来的EPROCESS的DebugPort成员。这就需要改变Windows内核下一切的这些内核函数。比如常见的DbgkCreateThread,DbgkExitThread,DbgkMapViewOfSection,DbgkUnMapViewOfSection,DbgkExitProcess等等这些函数。

比如用户层建立一个线程,会调用CreateThread(),CreateRemoteThread()。这两个函数。这两个函数最终都会通过ntdll.dll的sysenter进入Ring 0,然后调用系统调用NtCreateThread()。一层一层的调用PspCreateThread()。最后这个线程被Windows系统调度程序调度运行时,首先会调用PspUserThreadStartup()这个函数,它里面会判断调用DbgkCreateThread()函数通知调试器接受建立线程的调试事件信息。部分代码如下:

// If the create worked then notify the debugger. //

if ((Thread->CrossThreadFlags&

(PS_CROSS_THREAD_FLAGS_DEADTHREAD|PS_CROSS_THREAD_FLAGS_HIDEFROMDBG)) == 0) {

DbgkCreateThread (Thread, StartContext);

}

最终DbgkCreateThread会调用DbgkpSendApiMessage()-> DbgkpQueueMessage(),这个函数就会向Process->DebugPort发送调试事件信息。

用户层线程加载DLL时会调用LoadLibrary,LoadLibraryEx,函数,它们同样会通过sysenter进入Ring 0调用系统调用NtMapViewOfSection(),它里面同样会调用DbgkMapViewOfSection()向Process->DebugPort发送调试事件信息。

所以我们做的是HOOK调用这个函数的地方,向它进入我们自己的函数里面处理,然后我们自己再把调试事件信息发向我们想要的地方去。

具体HOOK代码如下:

typedef struct __hook_find {

DWORD* dwFindSrc; //查找的源地址 DWORD* dwFindDst; //查找的函数地址 DWORD dwCallDst; //重定向

32

DWORD dwFindPos; //位置找旱后旱位置 }__hook_find;

__hook_find HookData[]={

{&Addr_PspUserThreadStartup,&Addr_DbgkCreateThread,(DWORD)HxDbgkCreateThread,0},

{&Addr_PspExitThread,&Addr_DbgkExitProcess,(DWORD)HxDbgkExitProcess,0},

{&Addr_PspExitThread,&Addr_DbgkExitThread,(DWORD)HxDbgkExitThread,0},

{&Addr_NtMapViewOfSection,&Addr_DbgkMapViewOfSection,(DWORD)HxDbgkMapViewOfSection,0},

{&Addr_MiUnmapViewOfSection,&Addr_DbgkUnMapViewOfSection,(DWORD)HxDbgkUnMapViewOfSection,0},

{&Addr_CommonDispatchException,&Addr_KiDispatchException,(DWORD)HxKiDispatchException,0},

{&Addr_KiRaiseException,&Addr_KiRaiseException,(DWORD)HxKiDispatchException,0}

};

PUCHAR Search_CallFunction(PUCHAR pbCode,PUCHAR pbCallfunc,IN OUT UINT* pnPos)

{

//e8baf1feff call nt!ObOpenObjectByPointer (80933d4c)

ULONG nSearchAddr=(ULONG)pbCallfunc; ULONG nLen=0,nTmpLen=0;

PUCHAR pPos=pbCode+(*pnPos),pFind=NULL; UCHAR uTemp[20]={0};

UCHAR btCode[5]={0xE8,0,0,0,0}; while(*pPos!=0xCC){

nTmpLen = getNextInstruction(pPos,1,uTemp,20); nLen+=nTmpLen; pPos+=nTmpLen;

if(*pPos==0xE8){

*((ULONG*)(btCode+1)) = ((ULONG)nSearchAddr-(ULONG)pPos-5);

//

DbgPrint(\%2X%2X%2X%2X%2X==%2X%2X%2X%2X%2X\],pPos[3],pPos[4],btCode[0],btCode[1],btCode[2],btCode[3],btCode[4]);

33

if(pPos[0]==btCode[0] && pPos[1]==btCode[1] && pPos[2]==btCode[2] && pPos[3]==btCode[3] && pPos[4]==btCode[4] ) {

pFind=pPos; *pnPos = nLen; break; } } }

return pFind; }

VOID StartHook() {

UINT ln=sizeof(HookData)/sizeof(__hook_find); UINT i,pos; PUCHAR pAddr; DWORD dwOldAddr; KIRQL oldirql; for (i=0;i

pos=0;

pAddr=Search_CallFunction((PUCHAR)(*HookData[i].dwFindSrc), (PUCHAR)(*HookData[i].dwFindDst),

&pos); if(pos) {

HookData[i].dwFindPos=(DWORD)pAddr; dwOldAddr = *((ULONG*)(pAddr+1)); CLR_WP();

oldirql=KeAcquireQueuedSpinLockRaiseToSynch(LockQueueDispatcherLock);

*((ULONG*)(pAddr+1)) = (HookData[i].dwCallDst-HookData[i].dwFindPos-5);

KeReleaseQueuedSpinLock(LockQueueDispatcherLock,oldirql);

SET_WP();

HookData[i].dwCallDst=dwOldAddr; }

34

} }

VOID RestoreHook() {

UINT ln=sizeof(HookData)/sizeof(__hook_find); UINT i;

PUCHAR pAddr; DWORD dwOldAddr; for (i=0;i

if(HookData[i].dwFindPos) {

pAddr=(PUCHAR)HookData[i].dwFindPos; dwOldAddr = *((ULONG*)(pAddr+1)); CLR_WP();

*((ULONG*)(pAddr+1))

(HookData[i].dwCallDst-HookData[i].dwFindPos-5);

SET_WP();

HookData[i].dwCallDst=dwOldAddr; HookData[i].dwFindPos=0; } } }

=

5.8 HOOK Windows内核异常处理函数

上面功能都还容易实现,但是异常处理就有点困难,因为它属于Windows系统内核很底层了。在Windows系统中,中断和异常是其重要的机制。Windows系统采用中断描述述表(IDT),对于每个CPU,Windows系统都定义一个IDT表。IDT表中比较常见的异常有_KiTrap00(除0异常), _KiTrap03(断点异常)是我们的关键。_KiTrap0E(缺页异常),后面一般是中断的,比如键盘,鼠标驱动的中断响应程序等等。

Windows内核采用分页文件虚拟内存技术,当我们进程要操作的内存不在物理内存时或者因为我们的权限不够不能访问相应内存时,CPU就会产生0xE号异常,CPU会自动保存当前上下文。然后调用_KiTrap0E异常响应函数,它里面就会判断是不是因为要访问的内存在分页文件中,如果在分页文件,则Windows 内存管理器会把它调进物理内存,然后继续访问。如果是因为访问权限不够的话就会抛出异常。一层一层的返回到用户层。如果用户层有处理的话就处理了,没有处理的话,这个程序就会弹出大家所熟悉的(访问内存错误)ACCESS_VIALATION对话框,然后程序崩溃。

Int 3异常才是我们真正感兴趣的异常。比如我们用OD调试一个进程时,F2下断点,真正做的是把这个指令改成0xCC也就是Int 3。为什么程序一运行这个int 3,OD就会中断理解如下:

当用户层进程执行INT 3指令时,CPU会产生异常,从而执行_KiTrap03异

35

常处理机制,_KiTrap03会先构造此次异常结构,然后调用CommonDispatchException()这个函数,这个函数最终会调用Windows内核真正通用的异常处理函数KiDispatchException()。一般产生的异常都会进入这个函数进行处理,这个函数会对产生此次异常时是处于用户层模式还是内核模式,如果产生此次异常时是内核下,刚这个函数会几次交给内核调试器处理,如果没有找到内核调试器的话,会再次调用RtlDispatchException()这个函数交给内核我们自己的异常处理,如果这也没有找到异常处理函数,则调用KeBugCheckEx()使之蓝屏。如果产生些次异常是在用户模式下,则会在内核下构造堆栈,使之返回到用户层,然后用户层就会一次一次的处理异常。找到处理这次异常的处理函数,如果这个进程当前处于被调试状态,刚会向调试这个进程的调试器发送调试事件。然后会给异常处理机制,如果都没有处理,刚这个进程就会强制结束。所以我们要HOOK的函数就是KiDispatchException(),然后进入我们自己的处理函数。

系统开发小结

本系统基本实现了能改变OD调试器的关键函数的系统调用,使其调用我们自己在内核中实现的系统调用,还有调试时常用的调试模块也是用的我们自己实现的。但是DebugPort的移值还没有测试,异常处理机制的HOOK也没有完善,因为Windows异常处理机制是Windows内核的处理核心,我调试了很久,仍然存有问题。仍需以后慢慢完善。通过此次的论文,我学到了很多,比如弄懂了Windows内核的调试机制是怎么实现的。Windows系统调用是怎么实现的。还有就是学会了怎么在内核下调用微软未导出的内核函数,弄懂了很多Windosw XP内核的系统知识。

参考文献

[1] Mark E.Russinovich and David A.Solomon < Windows Internals>(第5版) [2] Jeff Prosise (第2版). 北京:清华大学出版社 [3] 张帆 史彩成 < Windows驱动开发技术详解> 电子工业出版社

[4] David j.Kruglinski (第4版) 北京:清华大学出版社 [5] Charles Petzold (第5版). 北京大学出版社 [6] Jeffrey Richter (第4版) 机械工业出版社 [7] 侯俊杰 <深入浅出MFC> 华中科技大学出版社 [8] 毛德操 电子工业出版社 [9] 毛德操 [10]

36

本文来源:https://www.bwwdw.com/article/kvf3.html

Top