
很多做 Windows 开发的工程师,第一次听说 Session 0 隔离(Session 0 Isolation),大概率是因为遇到了 Bug。
比如,写了一个系统服务(Windows Service),想在后台弹出对话框通知用户,结果发现对话框压根弹不出来;或者在服务里调用 MessageBox,程序直接挂起;甚至有时候想通过服务启动一个有界面的客户端,发现进程虽然跑起来了,但屏幕上空空如也,连个 UI 影子都看不到。
翻官方文档,微软会用非常标准、非常严肃的语气告诉你:
从 Windows Vista 开始,出于安全考虑,系统服务运行在 Session 0 中,而用户登录运行在 Session 1 及后续 Session 中。Session 0 与用户会话完全隔离,不允许任何交互式 UI。
官方解释非常合理。但每次读到这里,我其实都有个疑问:
如果这个设计这么好、这么安全,为什么微软到了 2006 年(Vista 发布时)才想起来做?
Windows NT 架构过了整整 13 年。难道 Dave Cutler(Windows NT 之父)和他的顶尖团队,当年就没意识到后台服务和用户界面混在一起有安全风险吗?
后来查了一些早期的 NT 技术文档和旧邮件,我才发现,事情根本不是 " 微软突然醒悟了 ",而是他们终于买单了早期一个看似极其高明、实则后患无穷的架构设计。
现在的年轻程序员可能很难想象,在 90 年代初,Windows NT 的目标不是去替代 Windows 3.1,而是要跟 UNIX 抢服务器市场,跟 VMS 抢企业市场。
那时候的 Windows NT 架构其实非常优雅。为了追求极致的安全和模块化,微软采用了微内核思想(虽然不是纯粹的微内核):
图形子系统(GDI、USER)是运行在用户态的一个独立进程,叫 csrss.exe。
如果你在系统里启动一个后台服务,或者运行一个桌面程序,大家要画界面,都得通过 LPC(Local Procedure Call,本地过程调用)跨进程发消息给 csrss.exe,让它帮忙把窗口画出来。
在那个时代,不管你是后台 Service,还是前台用户程序,大家都生活在同一个巨大的空间里。这个空间,就是后来的 Session 0。
这个设计在架构上非常漂亮,隔离性也很好。但它带来了一个致命的问题:慢。
在硬件条件下,频繁的跨进程通信(Context Switch)开销大得吓人。用户拖动一个窗口,系统就要在用户进程、csrss.exe 和驱动之间来回切上下文几百次。当时的电脑跑 Windows NT 3.1,用户体验简直是一场灾难,窗口刷新像幻灯片一样。
微软面临一个选择:是坚持优雅但极其缓慢的架构,还是为了性能向现实妥协?
1996 年,Windows NT 4.0 发布。
为了彻底解决图形性能问题,微软做了一个在当时争议极大的决定:把图形子系统(GDI 和 USER)直接搬进了内核态(Kernel Mode)。
这就是 win32k.sys 的由来。
这招极其管用。图形渲染不再需要繁琐的跨进程通信,系统性能瞬间飞升。Windows 桌面变得顺滑无比,这也是 Windows 后来能统治 PC 桌面的关键一步。
但天下没有免费的午餐。
把图形子系统放进内核后,为了让后台服务(比如 SQL Server、IIS、或者各种打印服务)也能方便地给管理员弹窗、显示状态,微软保留了所有的便利性:
所有服务和第一个登录的用户,依然共享同一个 Session,也就是 Session 0。
不仅共享 Session,它们还共享同一个桌面对象(WinStation0Default)、同一个消息队列,甚至可以互相发 Windows 消息(WM_SYSCOMMAND、WM_DROPFILES 等)。
在 90 年代末,这被认为是一种 " 特性 "(Feature)。
你想想,一个后台运行的备份服务,发现磁盘满了,直接在屏幕上弹个框提示管理员:" 请插入光盘 B",管理员点个确定,服务继续跑。多方便!
这种便利性,直接导致了接下来的十年里,全世界的 Windows 程序员都在这么写代码。无数的企业级软件、杀毒软件、硬件驱动服务,都深深地依赖 "Session 0 可以直接弹窗和交互 " 这个假设。
但灾难的种子,就在这时候种下了。
安全专家 Chris Paget 发表了一篇著名的论文,揭露了一种被称为 Shatter Attack(粉碎攻击)的漏洞利用方式。
这种攻击的原理简单得令人发指,但后果极其严重。
在 Windows 的消息机制里,你可以用 SendMessage 或者 PostMessage 给任意一个窗口发消息。如果这个窗口属于一个高权限的系统服务(运行在 System 权限下),事情就变得有意思了。
比如,有些 Windows 控件(如 RichEdit)支持一些特殊的消息,比如 EM_SETWORDBREAKPROC。这个消息的作用是:" 请把我提供的这段代码地址,作为换行回调函数来执行。"
如果一个运行在低权限的用户程序,给 Session 0 里高权限服务的窗口发了这条消息,然后再触发一次换行……
高权限服务就会无条件地去执行那个地址里的代码。
低权限用户直接拿到了系统最高权限(SYSTEM)。
更要命的是,因为大家都在 Session 0 里,所有服务的窗口对低权限程序都是完全可见、可发消息的。这根本不是某个具体软件的 Bug,而是整个 Windows 交互模型的底层设计缺陷。
只要 Session 0 里还允许服务弹窗口,只要服务和用户还待在同一个 Session 里,这种攻击就永远禁不绝。
当时正值微软推出 " 可信计算 " 倡议,盖茨亲自发内部信要求把安全放在第一位。面对 Shatter Attack 这种底层的系统级漏洞,微软必须做决定了。
修补这个漏洞有两个选项:
选项 A:挨个修补 API,禁止跨权限发某些危险消息(UIPI 机制)。
选项 B:彻底切割。把服务和用户强行拆开,服务留在 Session 0,用户去 Session 1、2、3 ……
微软很快发现,只搞选项 A 是治标不治本的。因为只要有图形交互,就有无数种奇奇怪怪的方法可以通过窗口句柄搞事情。
必须选 B。
于是,在 Windows Vista 中,Session 0 隔离正式上线。
微软划出了一道无比坚固的红线:
Session 0 专门给系统服务用,里面完全没有图形界面(No UI)。
第一个登录的用户,不再分配到 Session 0,而是从 Session 1 开始。
Session 0 和 Session 1 之间的所有 GUI 消息通道、剪贴板、共享内存全部切断。
从技术角度看,这个方案极为干净利落,彻底封死了 Shatter Attack。
但对整个 Windows 生态来说,这是一场巨大的地震。
Vista 发布后的好几年里,无数软件被骂 " 不兼容 Vista",其中相当一部分就是因为 Session 0 隔离。
那些习惯了在服务里弹窗、在服务里截屏、在服务里启动 UI 进程的软件,全部瘫痪。
微软为了安抚开发者,甚至在 Vista 和 Win7 里做了一个非常妥协的补丁服务—— Interactive Services Detection(UI0Detect)。
当 Session 0 里的服务试图弹窗时,系统会在右下角冒出一个提示:" 有一个程序试图在屏幕上显示消息 "。用户点进去后,整个桌面会被切到一个黑乎乎的、极其丑陋的临时的 Session 0 桌面里,让你点那个对话框。
这个过渡方案丑陋至极,但它折射出架构变革时的无奈:为了兼容性,你总得给旧世界的遗产留一口气。
直到 Windows 10,微软才彻底废弃了 UI0Detect 服务。至此,Session 0 的交互彻底成为了历史。
如果重新审视这段历史,你会发现技术演进中一个非常有意思的轮回:
1993 年(NT 3.1):想要优雅的隔离,但因为性能代价太高,放弃了。
1996 年(NT 4.0):为了极致的性能和便利,把安全和隔离塞进了同一个桶里(Session 0)。
2006 年(Vista):享受了十年的性能红利后,安全欠债爆仓,不得不花极大的生态代价,重新把隔离做回来。
这种 " 先为了性能 / 便利合并,再为了安全 / 可维护性拆分 " 的故事,在计算机历史上一次次重演。
今天的 Linux 容器、浏览器沙盒机制、甚至 WebAssembly 的设计,其实多多少少都能看到当年 Windows 处理 Session 0 时面临的相同困境:我们到底该在安全边界和运行效率之间划哪一条线?