《加密与解密》-系统篇-Windows内核基础

目录:

内核理论基础

内核数据结构

内核调试基础

1.内核理论基础:

1.1权限级别:

R0和R3,内核和驱动运行在R0。

用户的应用程序(就是用VisualC++等工具开发的应用程序)也是运行在R3级上的,也就是
说,它们享有的权限是最低的。该类应用程序没有权限去破坏操作系统,只能规矩地使用Win32API
接口函数与系统打交道。如控制系统,就必须取得RO特权级,例如大多数驱动程序就是工作
在RO特权级上的。

 

 

 

1.2内存空间布局:

x86 32位寻址

在4CB的虚拟地址空间中,Windows系统的内存主要分为内核空间和应用层空间两部分,每部分各占约2GB,其中还包括一个64KB的NULL空间及非法区域。

Windows内存的逻辑地址分为两部分,即段选择符和偏移地址。
CPU在进行地址翻译的时候,先通过分段机制计算出一个线性地址,再通过页表机制将线性地址映射到物理地址,从而存取物理内存中的数据和指令。

 

 

 

x64(AMD64)的内存空间布局与x64内存布局相似,但空间的范围和大小不同,同时存在一些空洞。

Windows支持44位最大寻址空间,Linux支持48位。

但是无论是在内核还是应用层,有些TB空间并不是可用的,二十存在所谓的空洞。

 

1.3WIndows与内核启动过程:

1.启动自检2.初始化启动3.Boot加载4.检测和配置硬件5.内核加载6.WIndow的会话管理7.登录阶段

1.4Windows R3与R0通信:

当应用程序调用一个有关I/O的API(例如WriteFiIe)时实际上这个API被封装在应用层的某个DLL库(例如keme132.dll和)文件中。而DLL动态库中的函数的更底层的函数包含在ntdll.dll文件中,也就是会调用在ntdll.dll中的NativeAPI函数。ntdll.dll中的NatvieAPI函数是成对出现的,分别以"Nt”和"Zw"开头(例如ZwCreateFiIe、NtCmateFiIe)。在ntdll.dll中,它们本质上是一样的只是名字不同。

(1)从用户模式调用Nt*和Zw*API(例如NtReadFiIe和ZwReadFiIe),连接ntdll.lib
  二者没有区别,都是通过设置系统服务表中的索引和在栈中设置参数,经由SYSENTER〈或
syscall)指令进人内核态,并最终由KiSystemSewice跳转到KiServiceTable对应的系统服务例程中的
。由于是从用户模式进人内核模式的,代码会严格检查用户空间传人的参数。


(2)从内核模式调用Nt*和Zw*API,连接ntoskrnl.lib
  Nt*系列API将直接调用对应的函数代码,而Zw*系列API则通过KiSystemService最终跳转到
对应的函数代码处。重要的是两种调用对内核中PreviousMode的改变:如果从用户模式调用Native
API,则PreviousMode是用户态:如果从内核模式调用NativeAPI,则previousMode是内核态。当
Previous为用户态时,NativeAPI将对传递的参数进行严格的检查,而Previ。us为内核态时则不会。
  在调用用户模式Nt*API时,不会改变PreviousMode的状态;在调用Zw*API时,会将Previous
Mode改为内核态。因此,在进行Kernel Model Driver开发时,使用Zw*系列API可以避免额外的参
数列表检查,从而提高效率,也就是当通过int2EH(WindowsXP以前)或者SYSENTER(Windows
XP及以后版本;在AMD中为syscall)的KiFastCallEntry()例程时,将要调用的函数所对应的服务号

(也就是在SSDT数组中的索引值)存放到寄存器EAX中,再根据存放在EAX中的索引值在SSDT
数组中调用指定的服务(Nt*系列数)。
在这个过程中,应用层的命令和数据会被系统的I/O管理器封装在一个叫作IRP的结构中。之
后,IRP会将R3发下来的数据和命令逐层发送给下层的驱动创建的设备对象进行处理,完成对应的
功能。

内核主要由各种驱动(在磁盘上是文件)组成,这些驱动有的是Windows系统自带的(例
如ntfs•sys、tcpip•sys、win32ksys),有的是由第三方软件厂商提供的。驱动加载之后,会生成对应的设
备对象,并可以选择向R3提供一个可供访问和打开的符号链接。常见的盘符C、D、E等其实都是文
件系统驱动创建的设备对象的符号链接,对应的符号链接名分别是“\??\c:\”“\??\D:\”“\??\E:\”等。
应用层程序可以根据内核驱动的符号链接名调用CreateFile()函数打开。在获得一个句柄
之后,程序就可以调用应用层函数与内核驱动进行通信了,例如ReadFile()、WriteFile()及
DeviceIoControI()等。
内核驱动一旦执行了DriverEntry()人囗函数,就可以接收R3层的通信请求了。在内核驱动中专
门有一组分发派遣函数用来分别响应应用层的调用请求,。每一个应用层负责I/O的

API都对应于一个内核中的分发派遣函数,例如CreateFile()对应于DispatchCreate()API被调用之
后,传递给API的数据和命令就会通过IRP直接传递给对应的驱动分发派遣函数来处理。当驱动的
分发派遣函数处理完这个IRP请求之后,驱动可以结束〈或允许,或阻止)这个IRP,或者把这个
IRP发给下层驱动继续处理。

1.5内核函数:

Ex:管理层。"Ex”是"Executive”的开头两个字母。
Ke:核心层。"Ke”是“'Kemel"的开头两个字母。
HAL:硕件抽象层。"HAL"是"HardwareAbstractionLayer”的缩写。
Ob:对象管理。“Ob”是"Object”的开头两个字母。
MM:内存管理。"MM”是Memory Manager”的缩写。
Ps:进程(线程)管理。"Ps”表示"Process”。
Se:安全管理。、"Se”是"Security”的开头两个字母。
lo:I/O管理。
Fs:文件系统。"Fs”是"FileSystem"的缩写。
Cc:文件缓存管理。"Cc”表示"Cache'。
Cm:系统配置管理。"Cm”是"ConfigurationManager”的缩写。
Pp:即插即用管理。"Pp" 表示"PnP”
RtI:运行时程序库。"Rtl”是"RuntimeLibrary”的缩写。
Zw/Nt:对应于SSDT中的服务函数,例如与文件或者注册表相关的操作函数。
Flt:Minifilter文件过滤驱动中调用的函数。
Ndis:Ndis网络框架中调用的函数。

在调用内核函数的时候需要注意它的IRQL(lnterruptRequestLevel,
中断请求级别)要求。内核在不同的情况下会运行在不同的IRQL级别上,因此在不同的IRQL级别
上,必须调用符合该IRQL级别要求的内核函数.

PASSIVE-LEVEL:IRQL的最低级别,没有被屏蔽的中断。在这个级别上,线程执行用户模
式,可以访问分页内存。线程运行在该中断级别上,对所有中断都作出响应。用户模式代
码都是运行在该中断级别上的。

APC-LEVEL:在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当有APC
发生时,将处理器提升到APC级别,就能屏蔽其他APC了。为了与APC同步,驱动程序
可以手动提升到这个级别。分页调度管理就运行在该级别上。
DISPATCH-LEVEL:在这个级别上,DPC(延迟过程)和更低的中断被屏蔽、不能访问分页
内存,所有被访问的内存不能协页。因为只能处理不可分页的内存,所以在这个级别上能
够访问的API大大减少。线程调度和DPC例程运行在该级别上。为了执行多任务,系统必
须允许线程调度,而线程调度是由时钟中断来保证的,因此该级别的中断就是调度中断。
代码运行的IRQL被提升为DISPATH-LEVEL时,就意味着代码不再受线程中断的影响。代
码会一直运行到将IRQL设置为低于DISPATH-LEVEL时为止。其间如果发生缺页错误之类
的IRQL级别在DISPATH-LEVEL之下的严重中断,这些中断均不会被处理,这时代码将无
法正常运行。所以,DISPATH-LEVEL的使用绝对要慎之又慎,只有在使用自旋锁时才考虑
选择该IRQLO
DIRQL(DeviceIRQL):处于高层的驱动程序通常不会使用该IRQL级别,在该级别上所有的中断都会被忽略。这是IRQL的最高级别,通常使用它来判断设备的优先级。

1.6内核驱动模块:

Windows内核驱动模块是内核的重要组成部分,既有微软自己开发的内核驱动,也有第三方开
发的内核驱动;既有硬件的驱动,也有软件的驱动。内核驱动在磁盘上是一个扩展名为.sys的文件,
遵守PE格式规范,能被系统加载运行。

那么,编译好的驱动是如何在系统中被加载并执行的呢?
1.创建一个服务(注册表)。在注册表的Services键下建立一个与驱动名称相关的服务键(例
如SrvName),即HKEY_LOCAL_MACHINE\SYSTEM\CurrentControISet\Services\SrvNameg这个服务键
规定了驱动的一些属性,例如启动GROUP与决定了驱动加载的先后,StartType为0的比
StartType为1的先启动。
2.对象管理器生成驱动对象(DriverObject)并传递给DriverEntry()函数。执行DriverEntty()函
数(它是驱动执行的人口函数,也就是驱动执行的第1个函数,类似于R3程序中的main()函数)。
3.创建控制设备对象。
4.创建控制设备符号链接(R3级可见)。
5.如果是过滤驱动,则创建过滤设备对象并绑定。
6.注册特定的分发派造函数。
7.其他初始化动作,例如H00k、过滤〔文件系统过滤、网络防火墙过滤等)、回调框架(注册
表回调等)等的注册和初始化。

2.内核重要数据结构:

2.1内核对象:

在Windows内核中有一种很重要的数据结构管理机制,那就是内核对象。应用层的进程、线程、文件、驱动模块、事件、信号量等对象或者打开的句柄在内核中都有与之对应的内核对象。

一个Windows内核对象可以分为对象头和对象体两部分。在对象头中至少有1个OBJECT-HEADER和对象额外信息。对象体紧接着对象头中的OBJECT-HEADER0一个对象指针总是指向对象体而不是对象头。如果要访问对象头,需要将对象体指针减去一个特定的偏移值,以获取OBJECT—HEADER结构,通过OBJECT—HEADER结构定位从而访问其他对象结构辅助信息。对象体内部一般会有1个type个和1个size成员,用来表示对象的类型和大小。
Windows内核对象可以分为如下3种类型:
1.DIspatcher对象:

这种象在对象体开始位置放置了一个共享的公共数据结构DISPATCHER-HEADER,其结构
代码如下。包含DISPATCHER-HEADER结构的内核对象的名字都以字母"K"开头,表明这是一个
内核对象,例如KPROCESS、KTHREAD、KEVENT、KSEMAPHORE、KTIMER、KQUEUE、KMUTANT、
KMUTEX,但以字母"K”开头的内核对象不一定是Dispatcher对象。包含DISPATCHER-HEADER
结构的内核对象都是可以等待的(waitable),也就是说,这些内核对象可以作为参数传给内核的
KeWaitForSingleObjectO和KeWaitForMultipleObjects0函数,以及应用层的WaitForSingleObject0和
WaitForMuItipIeObjectsO函数。

2.I/O对象
阳对象在对象体开始位置并未放置DISPATCHER-HEADER结构,但通常会放置一个与type
和size有关的整型成员,以表示该内核对象的类型(例如文件内核对象的类型为26)和大小。常见
的对象包括DEVICE_OBJECT、DRIVER_OBJECT、FILE_OBJECf、IRP、VPB、KPROFILE等。

3.其他对象
除了Dispatcher对象和]/0对象,剩下的都属于其他内核对象。其中有两个常用的内核对象,
分别是进程对象(EPROCESS)与线程对象(ETHREAD)
EPROCESS用于在内核中管理进程的各种信息,每个进程都对应于一个EPROCESS结构,用于
记录进程执行期间的各种数据。尽管EPROCESS结构非常大,但它是一个不透明的结构(Opaque
Structure),具体成员并未导出,并随着操作系统版本的变化而变化。因此,要想查看EPROCFSS结
构中的成员,只能查阅网上资料或者在使用WinDbg调试器加载内核符号后进行。
所有进程的EPROCFSS内核结构都被放人一个双向链表,R3在枚举系统进程的时候,通过遍
历这个链表获得了进程的列表。因此,有的Rootkit会试图将自己进程的EPROCESS结构从这个链
表中摘掉,从而达到隐藏自己的目的。


调用下面两个内核函数可以获得进程的EPROCESS结构:
PsLookupProcessByProcessId(IN HANDLE ProcessID,OuT PEPROCESS * process) 
EPROCESS PSGetCurrentProcess(VOID);

ETHREAD结构是线程的内核管理对象。每个线程都有一个对应的ETHREAD结构。ETHREAD
结构也是一个不透明的结构,具体成员并未导出,而且会随着操作系统版本的变化而变化。在
ETHREAD结构中,第1个成员就是线程对象KTHREAD成员,所有的ETHREAD结构也被放在一
个双向链表里进行管理。
ETHREAD结构中的一些重要成员:KTHREAD Tcb //线程内核对象  ;CLIENT_TD Cid //线程PID

 2.2 SSDT

"SSDT"的全称是"SystemServicesDescriptorTable”(系统服务描述符表),在内核中的实际名

称是"KeServiceDescriptorTable"。这个表已通过内核ntoskrnl.exe导出(在x64里不导出)。
SSDT用于处理应用层通过kerne132.dll下发的各个API操作请求。ntdll-dll中的API是一个简单
的包装函数,当keme132.dll中的ÅPI通过ntdll.dll时,会先完成对参数的检查,再调用一个中断(int
2Eh或者SysEnter指令),从而实现从R3层进人RO层,并将要调用的服务号(也就是SSDT数组中
的索引号index值)存放到寄存器EAX中,最后根据存放在EAX中的索引值在SSDT数组中调用指
定的服务(Nt*系列函数)。

 

 SSDT其中最重要的两个成员为ServiceTableBase(SSDT表的基地址)和NumberOfServices(表示系统

中SSDT服务函数的个数)。SSDT表其实就是一个连续存放这个函数指针的数组。

由此可以知道SSDT表的基地址(数组的首地址)和SSDT函数的索引号(index),从而求出对
应的服务函数的地址。在x86平台上,它们之间满足如下规则:
FuncAddr=KeServiceDescriptortable+4×index
与x86平上直接在SSDT中存放SSDT函数地址不同,在x64平台上,SSDT中存放的是索引号所
对应SSDT函数地址和SSDT表基地址的偏移量×16(即左移4位)的值,因此计算公式变为:
FuncAddr=(KeServiceDescriptortabIe+index*4]>>4+KeServiceDescriptortable)
通过这个公式,只要知道SSDT表的首地址和对应函数的索引号,就可以将对应位置的服务函数替
换为自己的函数,从而完成SSDTHook过程了。
ShadowSSDT的原理与此SSDT类似,它对应的表名为KeServiceDescriptorTableShadowv是内核
未导出的另一张表,包含Ntoskrnel.exe和win32k.sys服务函数,主要处理来自User32.dlI和GDI32.d11

的系统调用。与SSDT不同,ShadowSSDT是未导出的,因此不能在自己的模块中导人和直接引用。
挂钩该表中的NtGdiBitBIt、NtCdiStretchBlt可以实现截屏保护。挂钩NtUserSetWindowsHookEx
函数可以防止或保护键盘钩子,挂钩与按键相关的函数NtUserSendInput可以防止模拟按键,挂
钩NtUserFindWindowEx函数可以防止搜索窗口,挂钩与窗口相关的函数NtUserPostMessage、
NtUserQueryWindow可以防止窗口被关闭。
ShadowSSDT的挂钩原理与SSDT的挂钩原理一样,只不过由于未导出,需要使用不同的方法来
获取该表的地址及服务函数的索引号。例如,硬编码与KeServiceDescriptorTabIe在不同系统中的位
置偏移、搜索KeAddSystemServiceTable、KTHREAD.ServiceTable,以及有效内存搜索等。
KeServiceDescriptorTableShadow实际上也是一个SSDT结构数组,也就是说,KeServiceDesc1iptor
TableShadow是一组系统描述表。在WindowsXP中,KeSewiceDescriptorTableShadow表位于
DescriptorTable表上方偏移0x40处。
KeServiceDescriptorTableShadow包含4个子结构,示例如下。第1个子结构是"ntoskrnl.exe
(nativeapi)",与KeServiceDescriptorTable的指向相同。真正需要获得的是第2个子结构,即
"win32k.sys(gdi/usersupnqpt)"。第3个和第4个子结构一般不使用

2.3 TEB 

TEB与PEB一样,不在系统内核空间中,而是应用层中的结构。
TEB(Threadbloc.k,线程环境块)结构中包含了系统频繁使用的一些与线程相关的
数据。进程中的每个线程(系统线程除外)都有一个自己的TEB。一个进程的所有TEB都存放在从
0x7FFDE000开始的线性内存中每4KB为一个完整的TEB

1.TEB结构体
与EPROCESS结构类似,在不同版本的Windows中,TEB结构略有差异。例如,在R3级的应
用程序中fs:[0]的地址指向TEB结构,这个结构的开头是一个NT_TIB结构,具体如下。

NT_TIB结构的18偏移处是一个self指针,指向这个结构自身,也就是TEB结构的开头。TEB
结构的偏移处是一个指向PEB的指针。

利用WinDbg的本地调试功能可以查看系统中的TEB结构。启动WinDhg,选择"File”乛
"KernelDebug"乛“a广选项,然后在弹出的对话框中单击“cal”标签,就可以打开WinDbg
的本机调试功能。在Windows及以后的版本中会弹出信息,提示系统不支持本地内核调试,这
时可以以管理员模式打开cmd.exe,输人命令"bcdedit-debugon",重新启动计算机,再以管理员身
份打开WinDbg.

2.TEB访问
可以通过NtCurrentTeb函数调用和FS段寄存器访问这两种方法访问TEB结构。
(1)NtCurrentTeb函数调用
从ntdll.dll中导出了一个NtCunentTeb函数,该函数可以返回当前线程的TEB结构体的地址。通
过下面的代码,就可以从ntdll.dll中找到对应的NtCunentTeb函数地址并调用它,返回TEB结构的
地址。

(2)FS段寄存器访问
FS为段寄存器,当代码运行在R3级时,基地址即为当前线程的线程环境块(TEB),所以该段
也称为"TEB段”。运行如下代码可获得TEB的指针。

mov eax,dword ptr fs:[18h]  ;此时eax里为TEB指针

2.4 PEB

PEB(ProcessEnvironmentBlock.进程环境块)存在于用户地址空间中,记录了进程的相关信息。
1.PEB访问

TEB中的ProcessEnvimmentBlock就是PEB结构的地址,其结构的0x30偏移处是一个指向
PEB(进程环境块)的指针。PEB的0x2偏移处是一个UChar成员,名叫"BeingDebugged”,进程被
调试时值为1,未被调试时值为0。

访问PEB的地址有如下两种方法:
*直接获取,代码如下。
mov eax ,dword ptr fs:[30〕 fs〔30〕里存放的即为PEB地址
*通过TEB获取,代码如下。
mov eax,dword ptr fs:[18h],此时eax里为TEB的指针
mov eax  dword ptr [eax+30h] ;此时eax里为PEB的指针

此外,在内核结构对象EPROCESS结构中,同样记录了PEB结构的地址。因此,可以通过查
看EPROCESS找到进程的PEB信息。

2.PEB结构

与TEB一样,PEB结构也是一个随着Windows系统版本的变化而略有差异的结构。通过查阅

MSDN或winteml.h,可以知道PEB的结构定义如下。

MSDN中定义的PEB是不完整的,微软隐藏了很多细节(其他结构也是这样的),需要读者在
深人了解Windows的基础上自己去逆向和挖掘。在前面用WinDbg查看TEB结构的基础上查看完整
的PEB结构,即可得到进程PEB成员的完整信息,具体如下。

其中,BeingDebugged成员用于指定该进程是否处于被调试状态CheckRemoteDebuggerPresent()函数用于判断进程是否处于调试状态。ProcessParameters是一个RTL_USER_PROCESSPARAMETERS,即用于记录进程的参数信息(例如命令行参数等).。


3.内核调试基础

3.1 使用WinDbg搭建双机调试环境
3.2 加载内核驱动并设置符号表
3.3 SSDT与ShadowSSDT的查看

 

posted @ 2021-03-06 15:05  KnowledgePorter  阅读(711)  评论(0)    收藏  举报