我翻新了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),流程是:
- 校验令牌结构:长度、size 字段、magic、version、action 字段,任何一项不符直接
STATUS_INVALID_PARAMETER - 对令牌头部(到
signature字段之前)计算 SHA-256(内核里用BCryptHash,BCRYPT_SHA256_ALGORITHM) - 用编译进驱动的 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 面覆盖了极域的全部控制手段:
- 窗口控制类:
SetWindowPos、MoveWindow、DeferWindowPos、SetWindowLongA/W、ShowWindow、SetForegroundWindow、BringWindowToTop——全屏广播转窗口、窗口置顶、黑屏安静的对抗都在这一层 - 输入类:
SendInput、mouse_event、SetWindowsHookExA——防键盘鼠标锁定 - 执行类:
CreateProcessA/W、WinExec、ShellExecuteW/ExW、ExitWindowsEx——拦截教师端远程下发的命令,用户可以逐条选择放行还是拒绝 - 通信类:
DeviceIoControl、CreateFileA/W、FilterConnectCommunicationPort——监控极域自有驱动的通信 - 显示类:
ChangeDisplaySettingsW、DwmEnableComposition、GetDesktopWindow、GetWindowDC、CreateDCW、BitBlt
反监视:三层屏幕捕获对抗
这是整个项目里我花时间最多的部分。极域的"打开看"(教师端实时监视学生屏幕)有好几条捕获路径,得逐条堵:
路径一: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 还加了"替换"能力——教师端看到的不是黑屏,而是你指定的图片或视频:
- 图片路径:RGB24 缓存 + 顶向下 BGR24 DIB 双份像素,
StretchDIBits直接画 - 视频路径(MP4/WMV):独立 Media Foundation 线程解码,捕获热路径只读已发布的当前帧,不做文件 I/O 不做解码——捕获是高频路径,这里卡一下教师端就掉帧了
- 并发安全:临界区 + 引用计数管理共享 DC 和像素缓冲,"正在使用的 DC 不会被删除",旧缓冲进 pending 列表等引用归零再释放
三、发布链路:把"信任"做成流程
代码写完只是一半,另一半是让用户能验证拿到手的二进制没被掉包。
tools\Build-JiYuTrainerRelease.ps1 串起整个发布流程:
- MSBuild 重编 x64 驱动
signtool sign /sha1 <指纹> /fd SHA256 /tr <时间戳服务器>对驱动签名(含 RFC3161 时间戳)Get-AuthenticodeSignature验证签名,提取证书 Subject 和 SHA-1 指纹- 关键一步:把签名者 Subject 和指纹写进
JiYuTrainer\DriverSignerPin.h,编译进主程序——主程序加载驱动前会校验驱动签名者是不是发布者本人,防偷换 - 调
Complete-DriverSigning.ps1:Inf2Cat生成 catalog,再对 catalog 做 SHA-256 签名
也就是说:驱动签名 → 签名者指纹固化进主程序 → 主程序运行时校验 → catalog 二次签名。四道锁,任何一环被替换,整条链路报警。
四、周边工具箱
翻新过程中沉淀下来一批独立小工具,都在仓库里:
- GuardTerminator:对付设了
ProcessBreakOnTermination(进程信息类 29)保护进程的终结工具,测试驱动自保护时用来模拟"敌人" - JiYuAvKernel / JiYuAvCtl / JiYuAvShared:辅助驱动与控制端(
Driver.c/Controller.c/Scanner.c),配套Build-JiYuAv.ps1和独立签名脚本,做模式扫描一类的事 - SlayerAnalyzer / PhoneDialogProbe / ExitInputSimulator:逆向分析和场景模拟用的小工具
- DriverPublisherTests / JiYuAvPatternTests:发布链路和模式匹配的测试工程
五、诚实的支持范围
| 类型 | 状态 |
|---|---|
| 极域电子教室 | 持续维护 |
| 机房管理助手 | 已提供适配基础 |
| Gakataka | 计划中 |
| 红蜘蛛 | 计划中 |
Hook 模块里保留了 jiYuVersions40 / 2016HH 的版本适配逻辑。但我不想虚标——计划中就是计划中,每个版本的内脏(DLL 名、导出序号、窗口类)都不一样,没实测过的平台绝不说"支持"。
写在最后
原版 README 结尾,梦鱼说:"如果您有其他功能需求,可以 fork 项目之后自己研究开发。"
我就是那个 fork 的人。五年后,SSDT Hook 换成了内核回调,裸驱动换成了带 ECDSA 卸载令牌和签名者锁定的发布链路,单一黑屏对抗换成了三层捕获钩加视频流替换。项目以 MIT 协议继续开源:
- GitHub:
- 官网:
- 感谢 JiYuTrainer 原作者快乐的梦鱼;第三方组件 curl、mhook、MemoryModule、XZip/XUnZip、dnlib 各自遵循其许可证
最后照例强调:请仅在你拥有、管理或获得明确授权的设备与网络中使用本项目。先在隔离测试环境验证,再做经授权的部署。工具没有立场,边界由使用者决定。
欢迎 Issue,欢迎 PR。加个星星⭐就更好了。