会话空间

“会话空间”(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 查看它。

里面最核心的几个部分包括:

  1. Session (ID):当前是第几号会话。
  2. ProcessList:(划重点!) 这是一个双向链表,该会话里所有的进程,都会被挂在这个链表上。
  3. ImageFile:指向的通常是 win32k.sys(负责 Windows 图形和窗口管理的内核驱动)
  4. 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 就是用来记录和管理这个图形环境加载的驱动文件的:

  1. 管理映射:每个会话都有自己独立的 win32k.sys 实例(虽然有部分代码段共享,但数据段是物理隔离的)。
  2. 引用计数:系统通过 ImageFile 这个对象,知道这个会话的图形模块被谁在用,什么时候可以卸载。

继续更新...