
Janus框架实现Win9x加载NT内核驱动
来源标题:Universal bidirectional NT/9x driver translation
Janus驱动翻译框架能让Windows 9x系统加载未经修改的NT内核模式驱动(.sys PE文件),跨越i386、AMD64和Itanium三种架构。i386驱动可直接执行硬件I/O,64位驱动能在32位宿主上完成加载和导入解析。该框架旨在为仍运行Windows 9x的工业控制、医疗影像等遗留系统提供支持。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
任何 NT 版本、任何架构的 Windows NT 内核模式驱动,都能在 Windows 9x 上加载。Janus 是一个驱动翻译框架,用来弥合 Windows NT 内核模式驱动(.sys PE 文件)与 Windows 9x VxD 子系统之间的根本性不兼容。它能在 Win98 VxD 包装层内加载未修改的 NT 驱动、完成重定位并解析导入,覆盖 Windows 十二年的历史、三种 CPU 架构。i386 驱动可一路执行到真实的硬件 I/O;64 位 AMD64 和 Itanium 驱动能加载并完成全部导入解析——这是 32 位宿主所能达到的上限。
Janus 得名于罗马神话中掌管门道、过渡与通道的神,其形象是两副面孔、望向相反方向。Janus 主掌的是互不兼容的内核架构之间的通道,把 NT 的开端(3.1,1993)与 9x 的终点(ME,2000)连接起来。
这个世界跑在搁浅的遗留系统上。工业控制器、医学影像设备、实验室仪器、公用事业 SCADA 站、轨道交通信号、制造产线:仍有数量庞大且静默关键的一群系统在运行 Windows 9x,同样的困境也波及嵌入式 XP、NT4 及更早版本。它们留在那里,只因为一个顽固的原因:那些昂贵、不可替代的设备随附的内核驱动从未被移植到新系统,于是要升级操作系统就得放弃整台机器。结果就是一大批无法打补丁、已服役数十年的主机,既没有现代内存保护,网络栈也老旧不堪,偏偏还处在一旦被攻破危害最大的环境里。
Janus 有两副面孔,各自应对这个问题的一半。
有两个坦诚的前提,使它不至于沦为空洞的承诺。翻译层必须隔离它所导入的东西:1990 年代的 ring-0 驱动不带任何现代威胁模型,所以它应当被置于硬件隔离之后(IOMMU、基于虚拟化的驱动隔离),而不能只是让它能加载。而且加载是必要条件而非充分条件,因为真实的迁移还取决于时序、DMA 和总线枚举方面的假设,Janus 把这些当作一等公民的问题来处理。
用得得当,Janus 就是一条退役通道,适用于世界上最重要也最脆弱的遗留系统;同时也是一台带监控、带沙箱的分析台,用于研究那个年代的驱动和恶意软件。
同一套机制也服务于保存,而且覆盖面很广:
Janus 在带监控的沙箱环境中加载未修改的厂商驱动,而这正是上述每个领域都想要的那张工作台。
安全、迁移与保存是同一件事,只是从同一道门的两副面孔看过去。
两把梯子,刻意分开。功能级意味着被托管的驱动自身代码对忠实的原版 QEMU 搬运了真实数据,没有打任何设备模型补丁:网络上的一帧,或者由宿主预置、经由驱动自己的 busmaster DMA 返回的磁盘扇区,每一项都由不可伪造的哨兵验证,并经对抗性审查确认。加载验证意味着完整 PE 加载、重定位与导入解析,这是 32 位宿主对 64 位二进制所能达到的上限。
每一项都是外来驱动自己的代码在执行真实的硬件 I/O。哨兵不可伪造:一个捕获的 ARP 帧,由自身 MAC + opcode + IP 三元组认定;或者一个仅存在于宿主的扇区签名,被写入清零的缓冲区并经驱动自己的 DMA 返回。
完整 PE 加载、重定位、全部导入解析完成。这些 64 位二进制达到了 32 位宿主的上限;它们能解析和重定位,但不会在 32 位客户机的长模式下执行。
这些只有在打了 QEMU 设备模型补丁后才能通过,因此被压在忠实的原版 QEMU 门槛之下——上面那 18 项则越过了这条线。矩阵把它们记为在当前门槛下受阻,可在真实硅片(vfio-pci)上或通过上游 QEMU 修复恢复,而不是通过 shim。
Win98 客户机 ───────────────────────────────────────── NTMINI.VXD(单一 LE 对象,由 SYSTEM.INI 加载) ├── PE Loader(PELOAD.C,1809 行) │ ├── PE32(i386,0x014C)—— 完整加载 + 执行 │ ├── PE32+(AMD64,0x8664)—— 完整加载 + 导入解析 │ ├── PE32+(IA-64,0x0200)—— 完整加载 + 导入解析 │ └── 通过 shim 注册表进行多 DLL 导入解析 │ ├── NTOS_SHIM.C(2077 行,130+ 个 stub) │ ├── ntoskrnl.exe:内存、同步、定时器/DPC、IRP、电源、 │ │ 字符串、注册表、对象、线程、CRT 函数 │ └── HAL.dll:端口 I/O、MMIO、自旋锁、IRQL、计时 │ ├── 类别 Shim: │ ├── ScsiPort → IOS VxD 桥接(HDD/CD-ROM 读写) │ ├── NDIS → 直接数据包 TX/RX(ARP 发送+接收) │ ├── VideoPort → PCI GPU、VGA 寄存器、Int10 V86、PCI BAR 扫描 │ ├── HID → HidRegisterMinidriver │ ├── PCI IDE → PciIdeXInitialize/GetBusData/SetBusData │ ├── PortCls → COM vtable stub(WDM 音频) │ └── 另有 6 个已编写(USB、DirectDraw、WiFi、AGP、Joystick) │ └── VxD 基础设施(VXDWRAP_V4.ASM,2700 行) ├── VMM 服务包装、DDB、控制分发 ├── Safe_HwFindAdapter(IDT 故障捕获) ├── VxD_PageAllocateDMA(连续物理内存) ├── Win9x 页表自映射(PDE 0x3FE),用于 DMA 的 VA→PA ├── V86 嵌套执行(INT 10h BIOS 调用) └── PCI 配置空间(0xCF8/0xCFC) 关键发现:Win9x 上的 DMA Win9x 把递归页表映射在 PDE 索引 0x3FE,而不是像 NT 那样在 0x3FF: // 页目录位于 VA 0xFFBFE000(而非 0xFFFFF000) // 页表位于 VA 0xFF800000(而非 0xFFC00000) volatile ULONG *pde = (volatile ULONG *)(0xFFBFE000 + (va >> 22) * 4); if (*pde & 1) { volatile ULONG *pte = (volatile ULONG *)(0xFF800000 + (va >> 12) * 4); if (*pte & 1) return (*pte & 0xFFFFF000) | (va & 0xFFF); Win98 上所有用于 VA 到 PA 转换的 VMM 服务都失效。自映射是唯一可行的办法。 前置条件 Docker(用于带 Open Watcom 2.0 + NASM 的 ntmini-builder 镜像) Python 3 QEMU(原版用于 18 项功能穿越,包括 SCSI;打过补丁的构建仅用于旧版 LSI 53C8xx SCRIPTS 路径) Windows 98 SE 磁盘镜像(FAT32) 构建 cd source/
SCSI(sym_hi.sys,LSI 53C810):
docker run --rm -v "$PWD:/src" -v "$PWD/../builds:/out"
ntmini-builder:latest sh -c "cd /src && \
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。