作者论坛账号:爱飞的猫
作者:爱飞的猫@52pojie.cn
前言
想着将论坛 2015 年录制的入门教程翻录到更方便、通用的 MP4 格式,方便观赏。
在网上晃了一圈发现也有其它人翻录,或做了音频降噪处理;不过没变的是右上角的文字水印和时间戳。
因为设置了“编辑加密”锁定,因此无法直接通过屏幕录像专家主程序来去除这些文字再导出,就试着解除这个锁定。
解锁前后的状态:
(当然也可以直接去掉水印然后翻录,最终的效果和文件大小都大差不差)
题外话
为什么要解除“编辑加密”锁定
为了生成便携影音格式。EXE 播放器已经不是主流,从现在来看也有很多缺点:
不能跨平台播放(虽然看他官网有个适配安卓的播放器)
无法倍速播放(只能 2 倍速,且倍速播放无音频)
无法透过浏览器播放 / 上传到在线流媒体平台
只能进行简单的后期编辑。
《屏幕录像专家》免费平替
目前更好用的免费、开源替代是 OBS。
除非有特殊需求,OBS 应为录制教程的首选。
准备工作
逆向的时候用到了这些工具,不过文章并没有详细解释工具的用法,需要读者自行摸索:
x64dbg 调试器 - 爱盘 | 官方快照构建
IDA - 8.3 绿色版 - 静态逆向分析
IDR (Interactive Delphi Reconstructor) - Delphi 逆向分析辅助程序
十六进制编辑器 - 010Editor | HxD
文件对比工具 - WinMerge | Beyond Compare
※ 读者应当有一定的逆向工具使用经验。部分分析过程没有详细解释操作,而是思路。
第六课课件:
《吾爱破解培训第六课:潜伏在程序身边的黑影--实战给程序补丁》 讲师:我是用户
百度网盘
包含解锁前后的第六课录像、通用解锁工具 v0.1.5 和对应源码。如果读者已通过其它方式下载到第六课的原件,也可以使用解锁工具进行转换来得到解锁后的文件。
同时也借助了 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
;未锁定的时候这里是
0
。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 下中文显示会乱码)
把解锁后的文件放到屏幕录像专家里看看:
非常完美!可以修改设定然后转换格式。
结语/碎碎念
算法不是特别复杂,主要是“面向对象”的各种虚表调用看起来头大。
研究这算法比我用 OBS 连续翻录两次完整教程的时间还长。
OBS 永远的神,屏幕录像专家可以说是时代的眼泪了…
一开始也想过所谓的“高度无损压缩”会是什么黑科技,结果却发现就是简单的 gzip 压缩有点失望。
解锁工具顺便做了解锁“播放加密”文件的支持(需要提供原始密码)。不过,如果原始密码只有一位,可以添加 -r
来绕过(偷偷说一声,第一位密码不参与数据解密的)。
-官方论坛
www.52pojie.cn
👆👆👆