会话空间
“会话空间”(Session Space)是 Windows 内核里一块按用户会话隔离的私有内存区域。
历史
在 Windows XP 之前,Windows 是单用户的。但后来微软要搞“多用户同时登录”(比如远程桌面 RDP,或者快速用户切换)。如果两个用户同时登录,他们各自都会打开桌面、运行 explorer.exe、运行各种 GUI 软件。GUI 相关的内核代码(比如 Win32k.sys,负责画窗口、处理鼠标键盘)如果在内存里只有一份,两个用户的数据就会互相覆盖,直接导致系统崩溃。
为了解决这个问题,微软把内核地址空间切了一块出来,叫做会话空间
- 每个登录的用户(Session)都有自己专属的会话空间。
- 用户 A 的
Win32k.sys和用户 B 的Win32k.sys虽然代码段共享,但数据段(比如窗口句柄表、桌面堆)是物理隔离的。 - 你平时用
Ctrl+Shift+Esc看到的 Session 0、Session 1,就是不同的会话。
里面装了什么
会话空间主要由一个极其核心的结构体来管理:_MM_SESSION_SPACE(内存管理器会话空间)。
这个结构体就像一个“集装箱”,里面装着该会话所有的家当。你可以用 WinDbg 在内核调试下输入 dt nt!_MM_SESSION_SPACE 查看它。
里面最核心的几个部分包括:
- Session (ID):当前是第几号会话。
- ProcessList:(划重点!) 这是一个双向链表,该会话里所有的进程,都会被挂在这个链表上。
- ImageFile:指向的通常是
win32k.sys(负责 Windows 图形和窗口管理的内核驱动) - PagedPool:该会话专属的换页内存池(GUI 对象通常在这里分配)。
我会在这里顺便把windbg的相关操作也一起讲了。
通过windbg查看ProcessList
在windows内核调试中,nt是内核主文件ntoskrnl.exe的简称,整个系统的核心结构体比如_EPROCESS_ 等在这个符号库、.pdb文件内,我们输入 dt nt!_EPROPSS 查结构体的时候windbg实际上就是在nt这个模块的符号库里查询。
kd> .reload /f nt
SYMSRV: BYINDEX: 0x3
C:\ProgramData\Dbg\sym
ntkrnlmp.pdb
C29EBFB06B78B3C020DCA66D99713F9E1
SYMSRV: UNC: C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\C29EBFB06B78B3C020DCA66D99713F9E1\msfz0\ntkrnlmp.pdb - path not found
SYMSRV: PATH: C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\C29EBFB06B78B3C020DCA66D99713F9E1\ntkrnlmp.pdb
SYMSRV: RESULT: 0x00000000
DBGHELP: nt - public symbols
C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\C29EBFB06B78B3C020DCA66D99713F9E1\ntkrnlmp.pdb
在这里我们找到了基础结构:如 _EPROCESS、_UNICODE_STRING 。删除了大量深层内部结构体包括你 _MM_SESSION_SPACE,只保留偏移量。msfz0 代表微软的压缩格式(MSFZ)。第一次下载时它会尝试找压缩包,没找到就直接读了解压后的 ntkrnlmp.pdb。这属于正常现象,不用管。
process是windbg的查看进程的扩展命令,比dt好用很多,他会自动把底层十六进制数据翻译成人能看懂的摘要(比如进程名、PID、父进程等)。 第一个参数是进程ID,windows里 4 是System进程, 第二个参数是详细程度 0 表示只显示最基础的摘要信息,7就是进程的所有线程、堆栈、句柄都显示出来。
我们输出的!process 4 0 :
kd> !process 0 0 explorer.exe
PROCESS ffffa381459b1080
SessionId: none Cid: 071c Peb: 006a0000 ParentCid: 0994
DirBase: 139571002 ObjectTable: ffffdb0cb3e8c0c0 HandleCount: 3127.
Image: explorer.exe
kd> !process 0 0 explorer.exe
PROCESS ffffa381459b1080
SessionId: none Cid: 071c Peb: 006a0000 ParentCid: 0994
DirBase: 139571002 ObjectTable: ffffdb0cb3e8c0c0 HandleCount: 3127.
Image: explorer.exe
!process 0 0 explorer.exe就是在所有进程里把资源管理器拿出来,第一个参数 填 0 就是所有进程,这个很方便,要记住。
接下来的一步,我们需要去拿到会话指针,为了拿到真实的指针,就要去用户桌面环境下的进程,而文件管理器就是最标准的常驻GUI进程。
我们有了explorer的基地址之后,就可以通过dt命令把Session指针拿到手。
如下:
kd> dt nt!_EPROCESS ffffa381459b1080 Session
+0x2e8 Session : 0xffffa381`432fa740 _PSP_SESSION_SPACE
这个就是当前桌面会话空间的内存首地址叫做 _PSP_SESSIOIN_SPACE结构体 ,我们可以强行读取内存,查看内部结构,这里使用dq命令,以8个字节为单位,把指定内存地址里的数据以16进制的格式打印出来,并携带L10参数, L10是十六进制,所以就是16行。
dq 0xffffa381432fa740 L10
kd> dq 0xffffa381432fa740 L10
ffffa381`432fa740 00000005`0000002e 00000030`00000001
ffffa381`432fa750 ffffa381`444d0370 ffffa381`487f52b0
ffffa381`432fa760 ffffa381`43da09f0 ffffffff`80000678
ffffa381`432fa770 00000000`00000409 00000000`00000000
ffffa381`432fa780 00000000`00000000 00000000`00000000
ffffa381`432fa790 fffff802`a6705d40 ffffa381`4420f2b0
ffffa381`432fa7a0 00000000`00000000 00000000`00000000
ffffa381`432fa7b0 ffffa381`4420f2c0 fffff802`377b7bf0
第二行ffffa开头的就是ProcessList的链表头,这是逆向基本功,windbg里面没有标出ProcessList的名字,但是通过分析用户态空间和内核态空间就可以知道:
- 用户态空间:通常在
0x00000000'00000000到0x00007FFF'FFFFFFFF之间。 - 内核态空间:通常在
0xFFFF8000'00000000以上。
dq 输出里看到的 ffffa381444d0370和ffffa381487f52b0,开头全是 0xffffa381...。这毫无疑问是内核地址。而且,因为我们的 Session 基址是 0xffffa381432fa740,这两个指针离它非常近(都在 0xffffa381 这一段),符合 Windows 内核池(Pool)连续分配内存的特征。
我们在0xffffa381432fa750(也就是 Session 基址 + 0x10 的位置)看到了两个连续的指针:
ffffa381`432fa750 ffffa381`444d0370 ffffa381`487f52b0
这就是一个标准的 _LIST_ENTRY(双向链表头) 结构。_LIST_ENTRY 在内存里占 16 个字节(两个 8 字节的指针):
- 第一个指针是
Flink(Forward Link,指向下一个节点)。 - 第二个指针是
Blink(Backward Link,指向上一个节点)。
找到了链表就需要读取并转换为能看懂的字符,windows内核里的进程名是asni编码,刚才我们拿到的 Flink 是 ffffa381444d0370。 由于我们知道它是指向 SessionProcessLinks 的,减去偏移 0x230,得到 _EPROCESS 基址: ffffa381444d0370 - 0x230 = ffffa381444d0140。
现在,我们加上 ImageFileName 的偏移 0x338,得到真实的内存地址: ffffa381444d0140 + 0x338 = ffffa381444d0478
ImageFilepName是windows内核_EPROCESS里的结构体的一个字段,是进程的名字,这是char数组,最大长度15字节,[15]UChar。因为是 15 字节,如果进程名特别长(比如 GoogleUpdateSetup.exe),这里只能存下前 15 个字符(GoogleUpdateSet),后面的会被砍掉。
在内核里,它的首字母有时会被强制转成大写(比如 System),这取决于具体的内核版本。
这里我们使用da命令,一个字节一个字节读取数据,并翻译为ascii码,一直读到00结尾字符为止。
kd> da ffffa381444d0478
ffffa381`444d0478 "csrss.exe"
这是客户端/服务器运行时子系统,使windows系统里的最核心最底层的进程,cmd底层都是它负责渲染和维护的,当任何用户程序要启动的时候,操作系统都不会直接运行,而是通过csrss.exe来负责创建和销毁进程、分配句柄、管理内存基础结构,这里不细讲,自己看详细相关笔记。
接下里我们可以继续向下获取,既然这是一个双向链表,csrss.exe 的 SessionProcessLinks 里一定存着指向下一个进程的指针。 我们需要读取 csrss.exe 的 SessionProcessLinks 结构(地址是 ffffa381444d0370)。
dq ffffa381444d0370 L2
ffffa381`444d0370 ffffa381`4450e2b0 fffa381`432fa750
下一个节点就是e2b0:
减去 0x230,加上 0x338:ffffa3814450e080 + 0x338
kd> da ffffa3814450e080 + 0x338
ffffa381`4450e3b8 "winlogon.exe"
我们的Process大致是这个原理,我们可以用!process /s 1 0 0来查看。 !process /s 1 表示只查看session 1里面的进程。
ImageFile
指向的是内核模块文件(.sys),比如 win32k.sys,它是用来支撑整个桌面的的基础。在 Windows 里,图形界面(GUI)相关的代码都在 win32k.sys 这个内核驱动里。当你的系统有多个用户同时登录时(比如你在本地登录,同时有人用 RDP 远程连进来),为了让这两个用户的桌面互不干扰,系统必须给每个会话建立一个“专属的图形环境”**。
ImageFile 就是用来记录和管理这个图形环境加载的驱动文件的:
- 管理映射:每个会话都有自己独立的
win32k.sys实例(虽然有部分代码段共享,但数据段是物理隔离的)。 - 引用计数:系统通过
ImageFile这个对象,知道这个会话的图形模块被谁在用,什么时候可以卸载。
继续更新...