作者:爱飞的猫@52pojie.cn
前言
想着将论坛 2015 年录制的入门教程翻录到更方便、通用的 MP4 格式,方便观赏。
在网上晃了一圈发现也有其它人翻录,或做了音频降噪处理;不过没变的是右上角的文字水印和时间戳。
因为设置了“编辑加密”锁定,因此无法直接通过屏幕录像专家主程序来去除这些文字再导出,就试着解除这个锁定。
解锁前后的状态:
(当然也可以直接去掉水印然后翻录,最终的效果和文件大小都大差不差)
题外话
为什么要解除“编辑加密”锁定
为了生成便携影音格式。EXE 播放器已经不是主流,从现在来看也有很多缺点:
- 不能跨平台播放(虽然看他官网有个适配安卓的播放器)
- 无法倍速播放(只能 2 倍速,且倍速播放无音频)
- 无法透过浏览器播放 / 上传到在线流媒体平台
- 只能进行简单的后期编辑。
《屏幕录像专家》免费平替
目前更好用的免费、开源替代是 OBS。
除非有特殊需求,OBS 应为录制教程的首选。
准备工作
逆向的时候用到了这些工具,不过文章并没有详细解释工具的用法,需要读者自行摸索:
※ 读者应当有一定的逆向工具使用经验。部分分析过程没有详细解释操作,而是思路。
第六课课件:
同时也借助了 Hmily 提供的“编辑加密”锁定之前的原始文件用于对比参考。
开始逆向
准备好了吗?开始了哦!
内幕消息
H 大在我之前做了一点初步的分析,做了个 bindiff 列出二者的区别,以及关键部分的算法。
bindiff 节选:
地址 |
大小 |
未加密 |
“编辑加密”锁定后 |
B9C37h |
14h |
96 4A FE 49 F4 3D 70 91 FE 75 E7 A3 D6 8F F1 9B 73 59 8F 80 |
EE D6 32 34 FD 45 24 D4 48 0A 12 31 72 B4 9A EC 50 2F 04 3C |
CC2D0h |
14h |
9F C2 32 79 02 66 A0 A0 25 F7 62 FB AD 0D 51 DF 64 79 48 A1 |
E7 5E DF 04 6F D6 FD 65 54 17 8A 85 72 7A 8E A4 2F 5F 3B 8B |
... |
|
都是 14h 或 13h 字节更改的情况 |
|
37A65BBh |
2h |
00 00 |
25 3B |
以及关键的解密循环:
lb_00402B37:
xor esi,esi ; 计数器
lea edx,dword ptr ss:[ebp-D4] ; 密钥 1
lea eax,dword ptr ss:[ebp-E8] ; 解密后的内容
lb_00402B45:
mov ecx,14
sub ecx,esi ; ecx = 0x14 - 计数器
inc esi ; 计数器自增
mov cl,byte ptr ss:[ebp+ecx-C0] ; 密钥 2 (偏移 = ecx)
xor cl,byte ptr ds:[eax] ; xor 原文
xor cl,byte ptr ds:[edx] ; xor 密钥 1 内容
inc edx
mov byte ptr ds:[eax],cl ; 储存结果到原文位置
inc eax
cmp esi,14 ; 一共处理 0x14 字节
jl lb_00402B45
(关键点让我自己来找的话,估计又要花几个小时了)
看着很复杂,但整理到高级语言后其实还行:
char* edx = ptr_d4; // ebp-D4
char* eax = ptr_e8; // ebp-E8
char* var_c0 = ptr_c0; // ebp-C0
for (int i = 0; i < 0x14; i++) {
eax[i] = eax[i] ^ edx[i] ^ var_c0[0x14 - i];
}
(注意代码会跳过 C0[0]
的值,这不是文章的错误,而是播放器代码如此)
解密算法得到了,现在还差两项内容:
密钥来源
首先想办法搞明白密钥从哪来。
稍微看看之前的代码,可以发现它会检查大小是否超过 10240
。如果未超过则不进行解密处理。
在检查长度之后的地方,用 x64dbg 设置条件断点,使其在即将解密的时候打印各项目的值:
断点地址 00402B37
暂停条件 0
日志文本 d4: {mem;14@ebp-D4} / e8: {mem;14@ebp-E8} / c0: {mem;14@ebp-C0}
日志条件 1
然后跑起来,看看日志:
第一段:
d4: 789CCC7D09785445B67FF592A43B6B771242BA89 <- 密钥 1
e8: EED63234FD4524D4480A123172B49AEC502F043C <- 密文 (解密后覆盖为明文)
c0: 3135313431000000000000000000000000000000 <- 密钥 2
第二段:
d4: 789CED7D6DB05DC571E0E87EDF77DF7B7A12421F
e8: E75EDF046FD6FD6554178A85727A8EA42F5F3B8B
c0: 3135313431000000000000000000000000000000
第三段:
d4: 789CED7D59CC65D955DEAE3BFF53D5DF735563D3
e8: 605CF1EF4359370F9196881782A75BD2A6634043
c0: 3135313431000000000000000000000000000000
观察上述数据:
ebp-D4
的内容每次都不一样,但是开头都是 78 9C
,可以在原文件找到。
ebp-E8
就是加密后改变的 0x14
长度内容,可以在原文件找到。
ebp-C0
是固定值,文件内找不到(注意解密时第一个字节 31
不参与运算)。
此外,对 DecompressImage_4027BC
进行交叉检索可以得到下述可能的调用路径:
- (初始化)
_TPlayForm_FormShow
→ _TPlayForm_repareplay2(...)
→ DecompressImage_4027BC(...)
- (播放时)
sub_403ACC
→ RenderFrame_405B80(PlayForm, ...)
→ DecompressImage_4027BC(...)
文件密钥 (ebp-C0
)
ebp-C0
处的文件密钥每个文件都不一致,储存在 [_PlayForm]+338
处。
// nonce = [[_PlayForm]+338]
memset(nonce_key, 0, 21u);
sprintf(nonce_key, "%d", nonce); // nonce = 15141, 0x3b25
另两个密钥
在文件进行查找,可以发现 ebp-D4
处的内容是个“长度前缀编码”的数据 (Length-Prefixed Encoding, 简称 LPE):
| 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 说明 |
---------+---------------------------------------------------+------------------|
00A:B1B0 | F6 D4 01 00 84 A9 05 00 | 帧头 |
00A:B1C0 | 78 9C CC 7D 09 78 54 45 B6 7F F5 92 A4 3B 6B 77 | 起始数据 |
00A:B1D0 | 12 42 BA 89 | |
| | |
---------+---------------------------------------------------+------------------|
| | |
00B:9C30 | EE D6 32 34 FD 45 24 D4 48 | 修改点 |
00B:9C40 | 0A 12 31 72 B4 9A EC 50 2F 04 3C | |
| | |
---------+---------------------------------------------------+------------------|
| | |
00C:86B0 | 00 00 00 00 61 03 00 00 CC 01 00 00 00 00 | 帧后面的内容 |
00C:86C0 | 00 00 FF FF FF FF 5B 03 00 00 C5 01 00 00 00 00 | |
00C:86D0 | 00 00 02 00 00 00 39 02 00 00 9D 00 00 00 3A 02 | |
---------+---------------------------------------------------+------------------|
其中:
F6 D4 01 00
: 表示后续数据的长度为 0x0001d4f6
。
- 下一节数据在
0x00AB1B8 + 0x0001d4f6
,也就是偏移 0x00C86B2
处;
84 A9 05 00
: 表示解压后的数据长度(猜测)。
ebp-E8
指向的内容刚好在数据中间
- 算法为
0xAB1C0 - 4 + (0x1d4f6 / 2)
,也就是 0xb9c37
处。
不过这个帧与帧之间又有一些“意义不明的数据”,需要结合代码分析。
帧储存格式
帧与帧之间有很多意义不明的数据,需要想办法知道它的计算规则(或如何正确的跳过这些内容)。
回溯调用方,找到播放时执行的 RenderFrame_405B80
方法,再想办法找点“小”的帧数据来看:
--- 第一帧的数据
decompress_enter: pos=CFAF4; exp_len=(49)
decompress_done: pos=CFB3D
49 00 00 00 A2 00 00 00 // 帧头
78 9C 73 ... 66 0F 8D // 数据
// 偏移: CFB3D
D3 FF FF FF // 负数,帧结束标志。
F5 01 00 00 | D0 00 00 00 // 意义不明,两个 f32 浮点数
// 新的“帧”开始:
00 00 00 00 // 另一个 LPE,空的
2E 00 00 00 // 帧开始标识符(负数表示没有更新或结束)
25 05 00 00|7D 02 00 00|31 05 00 00|89 02 00 00 // 坐标
--- 第二帧的数据
// 偏移: CFB61
98 00 00 00 // compressed size
decompress_enter: pos=CFB65; exp_len=(98)
decompress_done: pos=CFBFD
整理下逻辑,这就是跨越了 2 “帧”(实际上是帧引用的图像)的数据。
结合实际猜测,大概是这样的结构:
struct frame_data {
uint32_t compressed_size;
uint32_t decompressed_size;
uint8_t compressed_data[compressed_size - 4];
};
// 连续的 `other_frame` 结构体。
struct other_frame {
// 未知数据流
uint32_t stream2_len;
uint8_t stream2[stream2_len];
uint32_t frame_id; // 帧序号
if (frame_id > 0) { // 负数表示无数据,例如屏幕无更新内容
RECT patch_cord; // 应该是坐标
while (frame_id > 0) {
struct frame_data frame; // 帧信息
uint32_t frame_id; // 帧序号
}
}
// field_24 == 1 的情况,会有两个额外的 f32 数据。
if (field_24 == 1) {
float unknown_1;
float unknown_2;
}
};
毕竟是个播放器,合理怀疑 stream2
储存的数据其实是用于辅助定位上一个“完整帧”的信息或鼠标指针数据。
不过看起来和“编辑加密”的锁定无关,就没继续跟进了。
大概分析清楚“帧”储存的格式后,就可以尝试定位数据的起始位置。
帧开始的地方
首先对 _PlayForm
地址下写入断点,看它如何赋值(EXE 自带符号,x64dbg 直接输入即可)。
断在 004965A4
,发现是实例化新类的代码中间:
00439944 | 8918 | mov dword ptr ds:[eax],ebx |
回溯一下:
lb_00401341:
mov ecx,dword ptr ds:[4961C8]
mov eax,dword ptr ds:[ecx]
mov ecx,dword ptr ds:[<&_PlayForm>]
mov edx,dword ptr ds:[48E980]
call 0x0043992C ; 调用实例化方法
lb_0040135A:
mov eax,dword ptr ds:[4961C8] ; 执行到此处时,[_PlayForm]+338 的初始化已完成
等 CALL 结束后已经太迟了,因此在 004965A4
赋值之后对 [_PlayForm]+338
下硬件写入断点。
继续运行,断在此处:
lb_004089E5:
mov dword ptr ds:[ebx+338],edx
jmp 0x00408A51
放到 IDA 里一看,发现在 _TPlayForm_FormCreate
函数内。
然后快速过一遍这个函数,发现该函数进行了基本的初始化。例如从文件末尾读入 0x2c
字节:
| 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | 预览区 |
---------+---------------------------------------------------+------------------|
37A:65BB | 25 3B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | %;.............. |
37A:65CB | 00 00 00 00 08 00 00 00 08 00 00 00 0A AE 0A 00 | ................ |
37A:65DB | 70 6D 6C 78 7A 6A 74 6C 78 00 00 00 | pmlxzjtlx... |
---------+---------------------------------------------------+------------------|
简单分析下这段数据:
0x00
处就是“文件密钥”了,我认为它是“每个文件都有的一个随机值”,称之为 nonce
;
0x1c
处为 0A AE 0A 00
,小端序读取是 0xaae0a
。这是数据起始的位置;
0x20
处是用于判定文件是否为“屏幕录像专家生成的录像文件”特征码 (magic number)。
- 其他内容不明。
部分窗体的数据稍后在 _TPlayForm_repareplay1
读取,从文件的 -0xE0
到 -0xE0 + 0xB4
:
37A:6507 00 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00 ................
37A:6517 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 ................
37A:6527 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 ................
37A:6537 01 00 00 00 01 00 00 00 CE E1 B0 AE C6 C6 BD E2 ........吾爱破解 <-- GBK 编码
37A:6547 C5 E0 D1 B5 B5 DA C1 F9 BF CE 00 00 00 00 00 00 培训第六课...... 窗体标题
37A:6557 03 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 ................
37A:6567 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
37A:6577 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
37A:6587 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
37A:6597 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
37A:65A7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
37A:65B7 00 00 00 00 ....
是窗体的一些配置信息,和我们研究的内容没啥关系。
到这里就没有明显的线索继续了,_TPlayForm_FormCreate
函数就此结束。
不过别急,还记得之前查找的 DecompressImage_4027BC
调用链吗?
_TPlayForm_FormShow
→ _TPlayForm_repareplay2(...)
→ DecompressImage_4027BC(...)
我们现在位于 _TPlayForm_repareplay1
函数,进行了第一段初始化;
那么合理猜测,_TPlayForm_repareplay2
函数有第二部分的初始化代码。
猜测下初始化的流程:
- 初始化播放器主窗口
- 触发事件
FormCreate
,调用 _TPlayForm_repareplay1
进行第一阶段的初始化。
- 触发事件
FormShow
,调用 _TPlayForm_repareplay2
进行后续部分的初始化。
抵达 _TPlayForm_repareplay2
后,记得尝试补充下结构体来优化阅读体验,大概补充出“文件流”和它 VTable
里的部分方法名就好,不需要处理得很完美:
// 像这样:
this->file_stream->vt->read(this->file_stream, &temp_u32, 4);
DecompressImage(this, this->bitmap2, this->file_stream, temp_u32, ...);
将整个函数代码复制下来,重点观察 file_stream
是如何读取数据的:
// 在屏幕录像专家 V2014 和 V2023 版本生成的文件中,off_reader 的值等价于之前找到的 “0xaae0a + 4” 的结果。
this->file_stream->vt->seek(this->file_stream, off_reader, SEEK_SET);
// 读入 40 字节数据。这里面有重要的东西,需要记录下来备用。
this->file_stream->vt->read(this->file_stream, &this->field_14D8, 40);
// 此处跳过一堆意义不明的数据读取过程,都是固定长度的数据,因此不需要花费太多时间分析。
// 不清楚这个 flag 的含义,解密的时候照抄就好。
if (this->field_14D8.field_24 == 1) {
// 读入两个 f32 浮点数
this->file_stream->vt->read(this->file_stream, &temp_u32, 4);
this->file_stream->vt->read(this->file_stream, &temp_u32, 4);
// LPE 数据,意义不明
this->file_stream->vt->read(this->file_stream, &temp_u32, 4);
TStream_CopyFrom((TStream *)this->stream2, this->file_stream, temp_u32);
}
// 开始解密第一帧数据了
this->file_stream->vt->read(this->file_stream, &temp_u32, 4);
DecompressImage(this, this->bitmap2, this->file_stream, temp_u32, ...);
其中 field_14D8
有很多成员意义不明,有用的就这个 field_24
和代表帧数量的 field_C
。
中间这些意义不明的数据不需要管它,只要知道如何抵达第一帧的位置即可:
0xaae0a /* 初始偏移 */ + 4 /* audio_offset */
+ 40 /* field_14D8 */
+ 20 + 20 + 40 + 4 + 4 + 4 + 20 + 4 + 1 + 1 + 1 + 1 /* 无关数据 */
+ /* field_14D8.field_24 == 1 */(4 + 4 + 4 /* stream2 长度 */ + 0x2FE /* stream2 */)
= 0xab1b8
在编辑器验证一下找到的地址:
00A:B1B0 F6 D4 01 00|84 A9 05 00
00A:B1C0 78 9C CC 7D 09 78 54 45 B6 7F F5 92 A4 3B 6B 77
成功抵达第一帧的位置。
修复
算法和定位帧数据的逻辑都找到了,简单写个工具枚举全部帧然后打补丁就好。
补丁完后记得将结尾的 nonce
清零,去除锁定状态。
当然,我也做了个简单的解锁工具。源码和编译好的二进制文件都可以在 GitHub 或百度网盘找到。
使用方法很简单,起一个终端,然后执行:
.\pmlxzj_unlocker.exe unlock "吾爱破解培训第六课:潜伏在程序身边的黑影--实战给程序补丁.exe" "666.exe"
(XP 或 32 位系统应使用 pmlxzj_unlocker_i686.exe
;XP 下中文显示会乱码)
把解锁后的文件放到屏幕录像专家里看看:
非常完美!可以修改设定然后转换格式。
※ 工具使用说明可以参考压缩包内的 README.MD
说明文件(使用记事本打开)。
结语/碎碎念
算法不是特别复杂,主要是“面向对象”的各种虚表调用看起来头大。
研究这算法比我用 OBS 连续翻录两次完整教程的时间还长。
OBS 永远的神,屏幕录像专家可以说是时代的眼泪了…
一开始也想过所谓的“高度无损压缩”会是什么黑科技,结果却发现就是简单的 zlib 压缩有点失望。
解锁工具顺便做了解锁“播放加密”文件的支持(需要提供原始密码)。不过,如果原始密码只有一位,可以添加 -r
来绕过(偷偷说一声,第一位密码不参与数据解密的)。
更新记录
- v0.1.6 - 2024.08.15
- 支持 “不压缩 (WAV)” 音频提取
- 支持 “有损压缩 (MP3)” 音频提取
- v0.1.5 - 2024.08.08
- 支持解锁播放密码保护的文件
- 支持解锁更古老版本(<= 7.0)录制的 EXE 播放器数据。
回复一下贴子里的疑问/内容。如果有问题也可以回帖询问,不定期查看。
#20:
这个软件的算法应该是比较简单的一个,我试过好几个版本,一个注册机全搞定!
就我所知,网上流传的注册机都没有完美解决暗桩。
#55:
大神可否把成品也发了 这个要自己动手 太难
完美避开了文章的内容。
#63:
请问有什么办法直接把屏幕录像专家的exe视频文件转换成mp4吗?
解锁 exe 视频(如果有),然后用屏幕录像专家转换。
屏幕录像专家本体不需要注册也可以转换。
有兴趣可以分析帧数据,提取图像然后自动生成。
#67:
天狼星屏幕录像专家,哎,现在官网都没了
官网还在啊?我看记录,网站 02 年到现在一直都在。虽然网站设计看起来还是上个世纪的风格。
#73:
这个软件以前很流行,现在还在更新吗
看官网应该快一年没更新了。
#79:
开源录像如果能录游戏高帧率就好点了。
OBS 的话可以在编码设定启用显卡加速,或更改设定让 OBS 生成更大的文件(系统占用低模式),或降低录制目标的分辨率。
#131
加密注册播放器能使用这种方式吗?
不能。如果他官网的信息没错的话,用的是不同的加密方案。
#135
楼主有没有研究过屏幕录像专家的压缩问题。
说实话,无损高压不怎么样,
但无压缩+mp3的exe虽然自身占用空间大,但是经过winrar或7z压缩,压缩率10%以内,最终存储文件绝对是最小的。这种用来存储再合适不过了。是高压缩的1/3不到。
你描述的操作和固始压缩的原理一致。屏幕录像专家自己的“无损高压”本质上就是每帧数据进行压缩。
缺点就是解压复杂度(耗时)会相对更久。
#207
exe转视频的时候会弹出:声音是有损压缩的EXE/LXE不支持此操作
更新工具到 v0.1.6,进行下述操作:
- (解锁工具)提取音频
- (解锁工具)禁用音频
- (屏幕录像专家)EXE 转视频 - 注意此时产出的视频不包含音轨
- (第三方工具)合并视频+音频,例如
ffmpeg -i 视频.mp4 -i 音频.mp3 -c copy 合并.mkv