如果某个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