手游反外挂-反内核物理访问内存方案思考
准备工作
逆向的时候用到了这些工具,不过文章并没有详细解释工具的用法,需要读者自行摸索:
- 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
储存的数据其实是用于辅助定位上一个“完整帧”的信息或鼠标指针数据。
不过看起来和“编辑加密”的锁定无关,就没继续跟进了。
大概分析清楚“帧”储存的格式后,就可以尝试定位数据的起始位置。
一旦您浏览本站,即表示您已接受以下条约:
1.使用辅助可能会违反游戏协议,甚至违法,用户有权决定使用,并自行承担风险;
2.本站辅助严禁用于任何形式的商业用途,若被恶意贩卖,利益与本站无关;
3.本站为非营利性网站,但为了分担服务器等运营费用,收费均为赞助,没有任何利益收益。
死神科技 » 手游反外挂-反内核物理访问内存方案思考