我翻新了JiYu Trainer!!!部分重写一个停更五年的反电子教室软件 —— Dzjs Trainer v1.0.0

我是云散皆星河。这篇文章讲讲我怎么把快乐的梦鱼的 JiYuTrainer,从 SSDT Hook 时代搬进内核回调时代。

为什么接手这个项目

2019 年,快乐的梦鱼(imengyu)写了 JiYuTrainer——一个让极域电子教室全屏广播失效的工具。被老师全屏广播时自动转成窗口模式:自己能操作,老师演示也照看。项目开源在 GitHub 上,MIT 协议,从 2019 年 5 月维护到 2021 年。

然后他毕业了。2021 年 12 月 3 日,仓库归档,顶部一行字:"已废弃。本项目将永远不再更新。"

我 fork 它的时候面对的现实是:原版驱动的核心是 SSDT Hook——直接改写系统服务描述符表。这在 x86 时代是经典操作,但 x64 系统的 PatchGuard(内核补丁保护) 会周期性校验内核关键结构,发现被篡改直接蓝屏。也就是说,原版的驱动方案在今天的 Win10/11 x64 上本身就是颗不定时炸弹。

所以这不是"fork 接着用"的事。要让它活在 2026 年的机房里,驱动层必须重写。

我联系了原作者,拿到了口头许可,在保留原项目贡献说明的基础上继续演进。这就是 Dzjs Trainer。

一、驱动:从 SSDT Hook 到内核回调

新驱动(JiYuTrainerDriver,x64 WDM)完全重写了。拦截能力不变,但实现全部换成操作系统官方提供的注册机制:

进程创建/退出通知——PsSetCreateProcessNotifyRoutineEx(Monitor.c)。驱动随时知道哪个进程起了、哪个进程退了。受保护的 PID 对应的进程退出时,自动调 KxUnProtectProcessWithPid 解除保护,不留悬空状态。

句柄拦截——ObRegisterCallbacks(Protection64.c)。注册进程/线程对象的 pre-operation 回调,在外部进程试图打开或复制受保护进程/线程句柄时,直接剥掉危险的访问权限。这是替代 SSDT Hook 的核心:不改系统表,只订阅系统事件,PatchGuard 无从下手。

保护目标绑定——JdrvProtectionSetTarget 通过 PsLookupProcessByProcessId 按 PID 绑定极域主进程,是用户态告诉驱动"保护谁"的入口。

IOCTL 通信协议

用户态和驱动的通道是经典的 DeviceIoControl,设备类型 0x8338,全部 METHOD_BUFFERED。IoCtl.h 里定义了 14 个控制码:

功能码 IOCTL 用途
0x800 CTL_QUERY_VERSION 查询驱动协议版本
0x801 CTL_INITPARAM 初始化参数
0x802 CTL_INITSELFPROTECT 开启驱动自保护
0x803 CTL_KILL_PROCESS 内核级杀进程
0x804/0x805 CTL_SUSPEND_PROCESS / CTL_RESUME_PROCESS 挂起/恢复进程
0x806/0x807 CTL_SHUTDOWN / CTL_REBOOT 关机/重启
0x808 CTL_CLIENT_QUIT 客户端退出通知
0x809 CTL_UNINIT 反初始化
0x80A CTL_READ_EVENTS 读取内核事件队列
0x80B CTL_HEARTBEAT 心跳保活
0x80C/0x80D CTL_DRIVER_GUARD_CONFIG / CTL_DRIVER_GUARD_QUERY 驱动守卫配置/查询

主程序(DriverLoader.cpp)启动一个事件监控线程循环读 CTL_READ_EVENTS,把内核上报的事件搬回 UI;同时定时发 CTL_HEARTBEAT,校验协议版本——驱动和主程序任何一边失联,另一边都能立刻察觉,不会出现"驱动还在拦、程序已经死了"的半死状态。

卸载令牌:ECDSA P-256 签名

原版驱动装上去就下不来,这不是好设计。新版驱动卸载需要一枚签名的卸载令牌(UnloadToken64.c),流程是:

  1. 校验令牌结构:长度、size 字段、magic、version、action 字段,任何一项不符直接 STATUS_INVALID_PARAMETER
  2. 对令牌头部(到 signature 字段之前)计算 SHA-256(内核里用 BCryptHash,BCRYPT_SHA256_ALGORITHM)
  3. 用编译进驱动的 ECDSA P-256 公钥(UnlockPublicKey.h)做 BCryptVerifySignature 验签

密钥对由 tools\New-DriverUnlockKeys.ps1 生成,令牌由 tools\New-DriverUnlockToken.ps1 构造 payload、算 SHA-256、用私钥签名后输出 .unlock 文件。私钥永不入库——仓库里只有一个 JiYuTrainerDriver.unlock.enc.sample 占位。也就是说,公开构建的驱动能装能跑,但只有持有私钥的发布者能生成合法的卸载令牌。这是"谁发布、谁负责"的信任链。

二、Hook 模块:战场在 StudentMain 进程内

JiYuTrainerHooks.dll 是注入进极域学生端(StudentMain.exe,也支持 MasterHelper.exe)的 DLL,用 mhook 做 inline hook。主程序(TrainerWorker.cpp)定时刷新进程列表,定位到目标进程后注入,然后走心跳/控制握手。DLL 里还挂了一个 WH_CBT 钩子盯着窗口事件。

hook 的 API 面覆盖了极域的全部控制手段:

反监视:三层屏幕捕获对抗

这是整个项目里我花时间最多的部分。极域的"打开看"(教师端实时监视学生屏幕)有好几条捕获路径,得逐条堵:

路径一:DispFilter.dll 的 DXGI 捕获。 用 IDA 逆向确认它只按序号导出:ordinal 1 是 DispDXGIBitBlt(真正读桌面像素),ordinal 4 是 DispDXGIGetScreenUpdate(只取脏矩形,结构体大小 0xC84、200 个 RECT)。DispDXGIBitBlt 把 DXGI 桌面帧按 RGBA 逐行 memcpy 进调用方缓冲——我的钩子在原始函数返回后,把整块像素填成不透明黑(B=G=R=0, A=0xFF),教师端看到的就是纯黑一片。

还有一个首帧空窗问题:StudentMain 是通过 LibDeskMonitor::TDDeskCreateInstance → libTDDesk2 在"打开看"开始时才 LoadLibraryW("DispFilter.dll") 动态加载它的。所以我同时钩了 LoadLibraryW/LoadLibraryExW——DispFilter 一被加载,捕获钩立即装上,不留空窗。

路径二:LibDeskMonitor.dll 的 GDI 抓屏。 它静态导入 BitBlt/StretchBlt,把源窗口 DC 拷进内存位图。之前试过钩 GetDesktopWindow/GetWindowDC 拦不住——它的源 DC 是 MFC CWnd::GetDC 直接拿的,不走这几个 API。解法是直接改写 LibDeskMonitor 自身的 IAT:钩住它内部的 BitBlt/StretchBlt,捕获完成后用 PatBlt(BLACKNESS) 把目标内存位图刷黑。还有一个细节:libTDDesk2 要等最终 GdiFlush 之后才提交捕获位图,所以我用 thread_local 记录同线程最近一次捕获的目标 DC,等原始 flush 返回后再覆盖整张持久位图——刷早了会被真实画面盖回去。

路径三:JPEG 编码流。 极域用 LibJPEG20 的 EncodeToJPEGBuffer(及 I422 变体)把帧编码后发走。这里我做了三层钩:函数实现体 inline hook + 多模块 IAT patch + GetProcAddress 拦截——因为它按序号导出、且被多个模块以不同方式引用,单层钩总会漏。卸载时逐个还原 IAT 槽位。

信息流保护:不只是涂黑

反监视之外,v1.0.0 还加了"替换"能力——教师端看到的不是黑屏,而是你指定的图片或视频:

三、发布链路:把"信任"做成流程

代码写完只是一半,另一半是让用户能验证拿到手的二进制没被掉包。

tools\Build-JiYuTrainerRelease.ps1 串起整个发布流程:

  1. MSBuild 重编 x64 驱动
  2. signtool sign /sha1 <指纹> /fd SHA256 /tr <时间戳服务器> 对驱动签名(含 RFC3161 时间戳)
  3. Get-AuthenticodeSignature 验证签名,提取证书 Subject 和 SHA-1 指纹
  4. 关键一步:把签名者 Subject 和指纹写进 JiYuTrainer\DriverSignerPin.h,编译进主程序——主程序加载驱动前会校验驱动签名者是不是发布者本人,防偷换
  5. 调 Complete-DriverSigning.ps1:Inf2Cat 生成 catalog,再对 catalog 做 SHA-256 签名

也就是说:驱动签名 → 签名者指纹固化进主程序 → 主程序运行时校验 → catalog 二次签名。四道锁,任何一环被替换,整条链路报警。

四、周边工具箱

翻新过程中沉淀下来一批独立小工具,都在仓库里:

五、诚实的支持范围

类型 状态
极域电子教室 持续维护
机房管理助手 已提供适配基础
Gakataka 计划中
红蜘蛛 计划中

Hook 模块里保留了 jiYuVersions40 / 2016HH 的版本适配逻辑。但我不想虚标——计划中就是计划中,每个版本的内脏(DLL 名、导出序号、窗口类)都不一样,没实测过的平台绝不说"支持"。

写在最后

原版 README 结尾,梦鱼说:"如果您有其他功能需求,可以 fork 项目之后自己研究开发。"

我就是那个 fork 的人。五年后,SSDT Hook 换成了内核回调,裸驱动换成了带 ECDSA 卸载令牌和签名者锁定的发布链路,单一黑屏对抗换成了三层捕获钩加视频流替换。项目以 MIT 协议继续开源:

最后照例强调:请仅在你拥有、管理或获得明确授权的设备与网络中使用本项目。先在隔离测试环境验证,再做经授权的部署。工具没有立场,边界由使用者决定。

欢迎 Issue,欢迎 PR。加个星星⭐就更好了。