xbc66 发表于 2026-9-5 16:08

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

# 我翻新了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 面覆盖了极域的全部控制手段:

- **窗口控制类**:`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` 串起整个发布流程:

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 二次签名。四道锁,任何一环被替换,整条链路报警。

## 四、周边工具箱

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

- **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:https://github.com/yunsjxh/Dzjs-Trainer
- 官网:https://dzjstrainer.xn--9kq396ceqaq4si9m.cn/
- 感谢 JiYuTrainer 原作者快乐的梦鱼;第三方组件 curl、mhook、MemoryModule、XZip/XUnZip、dnlib 各自遵循其许可证

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

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

xbc66 发表于 2026-9-5 16:32

平时要上学,寄宿的高二,可能更新不及时,还见谅

bachelor66 发表于 2026-9-8 08:35

楼主厉害啊,感谢分享                        

ParllelShifterX 发表于 2026-9-7 12:08

我的天哪,居然后继有人,支持!

shaunkelly 发表于 2026-9-7 12:43

不错哦,逃离塔科夫

dork 发表于 2026-9-7 13:48

@极域电子教室,看来你要加把劲了,道高一尺,魔高一丈呀

dushixiaoge 发表于 2026-9-7 14:36

支持一下楼主辛苦

dwcj 发表于 2026-9-7 15:52

反电子教室软件确实很需要

superlnts 发表于 2026-9-7 15:57

高中生怎么会弄这个啊,太超前了

eata2017 发表于 2026-9-7 16:06

不支持这种行为

QT2008 发表于 2026-9-7 19:14

吾辈&#128002;!!!!!
页: [1] 2 3
查看完整版本: 我翻新了JiYu Trainer!!!部分重写一个停更五年的反电子教室软件