我最近一直琢磨着那个《盗摄学园》的新版本。不是为了干什么坏事,主要是我之前给社团搞了一套离线学习的备份系统,结果它前阵子突然更新了,把以前能用的老路子全给堵死了。这就很烦,就像你搭好了一栋房子,结果人直接把地基给抽了。
上手开搞:一头撞上铁板
拿到新版本的那天,我立马就跑去试了试我以前用的那个抓取小工具。那玩意儿以前是无往不利,直接在渲染层前面一把就把视频流拽出来了。结果?新版本一打开,工具直接报错,日志里头全是乱码。我心里就咯噔一下,知道这回肯定是要硬啃了。
我的第一反应是它们是不是换了底层框架。我赶紧摸出几个看内存和进程的工具,把学园的那个主程序丢进去跑了一遍。以前它加载资源的时候,路径和文件名都挺规矩的,现在一看,文件名直接被混淆得像一锅粥。更狠的是,视频流数据根本就没在常规的缓存区停留,一出来就被马上塞进了另一个私有的渲染管线里。
- 第一轮尝试:我试着去Hook那个底层的播放器API,想在播放器拿到数据包的瞬间就截胡。失败了。它绕过了所有标准API,走的是自己封装的一套东西。
- 第二轮尝试:我去研究它更新后的网络请求。发现这回请求头里多了个动态生成的校验码,而且这个校验码的算法是嵌死在程序内部的,不是简单的时间戳加盐。想模拟请求?门都没有。
那两天我几乎是废寝忘食,就盯着屏幕上那些跳动的内存地址。我得承认,这回他们是真的下血本了,把以前那些小漏洞全部给补上了。
细节突破:在屏幕渲染前捞数据
既然外部请求和底层API都行不通,我只能把注意力放回到客户端内部。我的思路变了:既然你非得在屏幕上显示出来,那总有一个瞬间,这些数据是处于解密状态的。我要找的就是这个“临界点”。
我抓住了它在调用显卡资源进行渲染前的那个短暂瞬间。这就像是偷拍,你不能在人穿衣服的时候拍,也不能等照片洗出来再拍,你得在模特刚刚脱完衣服,准备摆Pose的那一瞬间按快门。
我用了个土办法,直接去盯着显卡内存里头的纹理缓存区。这个学园程序在播放视频的时候,会把解密后的视频帧数据当成一个纹理(Texture)传给显卡去画。我通过进程注入的方式,在它调用DirectX或者OpenGL相关的绘制指令之前,硬生生插进去了一段代码。
这段代码的目的很简单粗暴:
找到那个视频帧对应的内存地址。
把这个地址的数据,原封不动地复制出来,存成一个临时的图片文件。
这个过程必须快,因为如果我慢了一点,可能下一帧数据就冲进来了,内存地址就被覆盖了。我来来回回调试了好几十次,才把这个时序给卡对。当第一张清晰的、没有水印的画面被我dump(倒出来)到硬盘上的时候,我差点没跳起来。
收尾:拼凑成完整的视频流
现在我有图片了,但那只是一张一张的静态图片,不是完整的视频流。下一步就是把这个“盗摄”过程自动化,并且拼接到一起。
我设置了一个循环,让程序以极高的频率去触发这个抓取动作,比如每秒抓取二十五次,正好对上视频的帧率。为了不让学园程序发现我的操作,我还要控制好我插入的代码的资源占用率,不能让它卡顿,一卡顿程序就可能崩掉,或者被检测出来。
最难处理的是声音。视频流是在显卡内存里,声音流则在声卡驱动那里跑着。我必须同时开动两套抓取机制,一个抓画面,一个抓音频,并且要严格保证两个流的时间戳是对齐的。如果画面和声音差个零点几秒,那看的人能难受死。
我最终弄了个后台脚本,让它默默地跑完整个课程。把抓下来的几万张图片和单独导出的WAV音频文件,用一个简单的编解码工具,咔咔一顿操作,重新合成了一个标准的MP4文件。打开一看,播放流畅,音画同步,完美。
为什么要这么折腾?挺简单的。我那会儿在弄一个线上的知识分享站,里头有个老哥身体不躺病床上好久了,就靠着这个学园的课程自学。但是他那病房里的网络极其不稳定,经常断线,他也没法下课件。为了这个老哥能踏实地学习,我才非得把这个新的保护机制给破了。那几天晚上,我看着成功导出的视频文件,心里舒坦得很,不光是技术上搞定了,更是帮了人一个大忙。