Windows软件调试初探-进程与线程
Windows软件调试初探-进程与线程
进程资源
每个进程都有这些资源:
- 一个虚拟地址空间。
- 全局唯一Cid,即PID。
- 一个可执行映像,即该进程可执行文件在内存中的表示。
- 一个或多个线程。
- 一个内核空间中的EPROCESS。
- 一个内核空间中的对象句柄表。
- 一个用于描述内存目录表起始位置的基地址,即页目录基地址DirBase。当CPU切换到该进程时,将该地址加载到页表基地址寄存器如CR3或TTBR,再由RVA翻译为正确物理地址。
- 一个用户空间中的PEB。
- 一个访问令牌。
例如列出系统所有进程:
1 | 6: kd> !process 0 0 //第一个参数为EPROCESS地址 0表示所有 第二个0为最少信息 |
DirBase的高20位为该进程页目录的页帧编号PFN,低12位含义因CR4的PCIDE位第17位不同。PCIDE位1时CPU缓存多个进程页表信息,低12位为进程上下文ID。为应对CPU的Meltdown和Spectry漏洞,NT内核引入KVA影子安全补丁,启用PCIDE功能。
例如:启用了KVA影子,使用模式1,该模式要求PCID功能支持。
1 | 6: kd> dd nt!KiKvaShadow L1 |
已知PFN后,用以下命令列出物理地址到RVA间映射:
1 | !ptov 1f350 |
EPROCESS结构
结构定义如下,这玩意儿变得很勤:
1 | 6: kd> dt _EPROCESS |
显示某个进程关键信息:
1 | 6: kd> !process ffff84898fdf1080 |
查Token:
1 | 6: kd> !token ffffe18f306d4060 |
观察令牌对象:
1 | 6: kd> dt nt!_TOKEN ffffe18f306d4060 |
PEB结构
EPROCESS中PEB结构为:
1 | 6: kd> dt _PEB |
也可以用!peb
命令观察某地址处PEB结构。
内核模式和用户模式
例如Win32应用程序中调用Kernel32.dll导出的Kernel32!ReadFile
后堆参数进行检查,再调用Ntdll!NtReadFile
。Ntdll!NtReadFile
将系统服务号,如0xa1等放入RAX,参数指针放入EDX,用int 2e
发出调用。用!idt 2e
看到2e号向量对应服务例程是Nt!KiSystemService
,即内核态中用来分发系统调用的例程,在NtOsKrnl.exe中。Nt!KiSystemService
进行权限检查和准备内核栈,根据系统服务分发表SSDT查找要调用的服务函数NtReadFile
地址和参数描述。KiSystemService
通过iret
将执行权返回NtDll!NtReadFile
。
1 | // attributes: thunk |
从奔腾Ⅱ开始,在Windows XP或Windows Server 2003及以上在启动时检测是否支持快速系统调用命令。IA-32的奔腾Ⅱ引入sysenter
/sysexit
,AMD K7引入syscall
/sysreturn
。当支持快速系统调用时,系统启动时在全局描述表GDT中建立4个段描述符,依次排列为sysenter
进入内核模式时使用的代码段CS和栈段SS,以及sysexit
从内核模式返回用户模式使用的代码段和栈段。然后设置下表所示MSR,SYSENTER_EIP_MSR为sysenter
要跳转到的目标例程Nt!KiFastCallEntry
,SYSENTER_CS_MSR为Nt!KiFastCallEntry
所在代码段KGDT_R0_CODE,SYSENTER_ESP_MSR为新栈指针即SYSENTER_CS_MSR+8。最后将SystemCallStub代码片段复制到SharedUserData内存区,该内存区将被映射到每个Win32进程空间中,每次快速系统调用时NTDLL.DLL中残根stub函数调用SystemCallStub代码片段。
MSR名称 | MSR地址 | 用途 |
---|---|---|
SYSENTER_CS_MSR | 174h | 目标代码段CS选择子 |
SYSENTER_ESP_MSR | 175h | 目标ESP |
SYSENTER_EIP_MSR | 176h | 目标EIP |
每个系统MSR都不同,例如:
1 | rdmsr 174 |
快速系统调用模式切换总结为:
发起系统调用 | 入口内核例程 | 返回 | 返回内核例程 |
---|---|---|---|
int 2e |
KiSystemService |
iret |
KiSystemCallExit |
sysenter |
KiFastCallEntry |
sysexit |
KiSystemCallExit2 |
syscall |
KiFastCallEntry |
sysret |
KiSystemCallExit3 |
IA-32下快速系统调用实例ReadFile
:Win32程序用Kernel32!ReadFile()
调用NtDll!NtReadFile()
,转到SharedUserData!SystemCallStub
用sysenter
进入nt!KiFastCallEntry
,通过Nt!KiSystemService
调用Nt!NtReadFile
。返回时Nt!KiSystemService
通过Nt!KiSystemCallExit2
用sysexit
退出内核模式回到SharedUserData!SystemCallStub
。
内核模式也可主动调用用户模式例程,称为逆向调用。由Nt!KiCallUserMode
发起调用,进入用户模式执行NtDll!KiUserCallbackDispatcher
。用户模式完成后执行int 2b
返回动作,对应Nt!KiCallbackReturn
。
1 | kd> !idt 2b |
线程
类似EPROCESS,在内核模式下也有ETHREAD。
1 | 0: kd> .thread |
第一个成员KTHREAD结构为:
1 | 0: kd> dt _KTHREAD |
上述State状态有:
状态 | 值 | 含义 | 可转换到的状态 |
---|---|---|---|
Initialized | 0 | 正在创建和初始化 | DeferredReady |
Ready | 1 | 就绪,可被分发调度运行 | Running |
Running | 2 | 正在某个CPU上运行 | Waiting、Terminated |
Standby | 3 | 待命,即下一个要执行的线程 | Running、DeferredReady、被抢先Preempt |
Terminated | 4 | 结束执行 | |
Waiting | 5 | 等待,如睡眠函数、取消息函数、等待同步对象等,主动放弃执行机会 | Transition、DeferredReady、Running |
Transition | 6 | 过渡状态,线程已可以运行但内核栈被交换出内存,交换回后进入Standby | DeferredReady |
DeferredReady | 7 | 延迟就绪,为缩短扫描调度数据库加锁时间,内核把就绪线程设置为次状态 | Ready |
Gate Wait | 8 | 门状态,等待门分发器对象时进入 |
上述WaitReason为KWAIT_REASON枚举类型,常见如下:
1 | 1: kd> dt _KWAIT_REASON |
每个CPU有一个处理器控制块PRCB,
1 | 1: kd> dt _KPRCB |
其中DispatcherReadyListHead的32个元素对应32个优先级,每个元素为LIST_ENTRY链表头,挂接对应优先级就绪进程。
ETHREAD一般用更为友好的方式显示:
1 | 0: kd> !thread |
显示就绪状态的线程用!ready
。
线程环境块TEBy用!teb
获取位置,结构为:
1 | 0:007> !teb |
WoW进程
x86与x64运行模式转化(用户层):
1 | ~ //当前进程所有线程 |
在x86模式下查看栈帧时,能看到俩ntdll模块,一个64位一个32位,32位的加上基地址后缀如“ntdll_77700000”。WoW进程中每个进程俩PEB,每个线程俩TEB,俩栈,如:
1 | !wow64exts.info |
其中Guest指x86,Native指x64。在32位NTDLL.DLL中如NtReadFile
跳转WoW64SystemServiceCall
,进入wow64cpu!KiFastSystemCall
,跳转33号段选择子,即32位兼容模式过度64位模式方法。
系统对WoW进程注册表访问实施注册表重定向,如”HKEY_LOCAL_MACHINE\Software”重定向到“HKEY_LOACL_MACHINE\Software\Wow6432Node”等。一些COM组件有关等表键修改时同时修改32位和64位表键,这叫“注册表反射”。WoW进程访问系统文件目录时被自动重定向到SysWOW64或SysArm32目录中,这叫文件系统重定向。
最小进程是一类并列于NT进程的特殊进程,只需创建进程时指定一个特殊标志,但资料有限未完全研究清楚。该进程只创建进程空间,不自动想进程空间中添加内容。目前只有内存压缩技术、基于虚拟化的安全VBS和注册表进程Registry使用了最小进程。当某进程的EPROCESS结构的Flags字段的Flags3为1表示该进程为最小进程。内存压缩技术打开方式如下,有MemCompression进程创建。内存压缩进程在任务管理器中不显示。
1 | Enable-MMAgent-mc |
Registry进程有3个线程,两个线程入口函数为CmpLazyWriteWorker
用于把修改过的注册表数据成批写回硬盘工作线程。另一个线程叫CmpDummyThreadRoutine
,启动后等待CmpDummpyThreadEvent
事件,等待成功则用KeBugCheckEx
触发蓝屏崩溃,其用途为占位,不让内存管理器把它内存页交换出去。Registry进程在任务管理器中有时显示。
Pico进程时最小进程一个子类,但运行时并列于NT进程和最小进程,与NT内核之间交互需要PICO提供器,如WSL子系统核心驱动LXCORE。
在WSL启动早期,LXCORE用nt!PsRegisterPicoProvider
注册Pico提供器,它有俩参数为结构指针,第一个字段表示大小,后面是LXCORE提供给内核的回调函数。如Pico进程执行系统调用时,NT内核逆向调用LXCORE!PicoSystemCallDispatch
转交给LXCORE继续分发处理。当Pico进程内发生错误时,NT内核nt!KiDispatchException
逆向调用LXCORE!PicoDispatchException
。注册成功后NT内核也返回一个类似结构供Pico提供器调用。
WSL中每个Linux进程都是一个Pico进程,EPROCESS显示为System Process,该结构Flags2的第10位PicoCreated标志位为,PicoContext字段指向Pico提供器使用的Pico上下文结构。