停更了一断时间了,一个原因是最近工作很忙,用来研究的时间没有以前多, 还有一个原因是我掉坑里了,研究一直没有进展,还好目前看到一丝曙光, 那就记录一下现在的困境,准备开启新的一章。
本系列已完结,以下是各章节说明,17 之前是 dos 版相关,之后是 2001 版:
其实我最近研究的内容都并不在我的计划内,只是因为想看看修订版的游戏脚本, 就顺带分析了修订版的游戏脚本,于是乎,觉得那顺带看看修订版的事件图是如何解析的吧, 我也不想再开一个系列,索性都放一起。
于是我就掉坑里了。
从 dos 切换到 windows,调试程序不再有 doabox 这么丝滑顺利,几经比较, 我选择了 ollydbg 1.10,其实 ollydbg 有 2.0,但是我在 win98 上跑一直会有问题, 比如 trace into 很快就会报错
尝试自己解决未果,于是退守回了 1.10。
由于 ollydbg 也是运行在 win98 上,所以本身就会有一些限制, 比如日志没办法以 gb 为单位记录,调试和游戏在一起,有时卡住没办法切回去。
熟悉我的解析系列的朋友都知道,我主要靠 ghidra 和大日志这两条腿来分析资源, 外带一些推测。现在日志了不起记个几十兆,于是我就像少了一条腿。
读取其实很容易去推测位置,在 ghidra 中搜索文件名就好。
于是我们很容易就找到了读取文件的位置:
iVar1 = read_paf_0041b6d0((void *)((int)param_1 + 0x188c),(undefined4 *)s_AREA.PAF_00554ea4,DAT_0056c8d0,0,0);
if (iVar1 == 0) {
local_4 = 0xffffffff;
assert_wrapper_00496fdc((int *)&local_10);
ExceptionList = local_c;
return 0;
}
iVar1 = read_paf_0041b6d0((void *)((int)param_1 + 0x1a14),(undefined4 *)s_REDSYS.PAF_00554e98,DAT_0056c8d0,0,0);
if (iVar1 == 0) {
local_4 = 0xffffffff;
assert_wrapper_00496fdc((int *)&local_10);
ExceptionList = local_c;
return 0;
}
...
与文件名相关的入口只有这么一个,那么看上去只要我们吃透 read_paf_0041b6d0
这一个函数就好了,不过事实远不止这么简单,不然我也不会掉坑里。
根据读取的日志,AREA.PAF 大约只读了 10 万个字节,而一张背景图的数据量大概是 23 万, 这个数据量明显是不足以承载一百多张图,所以我们推测还是在游戏中用到哪张读哪张。
有这个思路,我的想法是既然现在我可以控制脚本,那么我在脚本中运行 ShowEvent
,
那只要找出 ShowEvent
执行的位置,那我们也就能找到程序是如何读取具体图片了,这个位置也不难找:
所以我们只要断点放在 405cb0
,进断点后 trace into,然后就等日志就好了。
现实很残酷,日志并没有很长,而且没有读文件的记录。很显然,我掉坑里了。
启动时没有读取完整的数据,调用脚本时 cpu log 又没有看到文件读取,这是一对矛盾, 而我又不知道哪里错了,于是我开始不断试错,这是个很沮丧的过程,我简单列一下我形成的一些结论:
440h
处有 PAT2000 标记,对应以前的 Pattern多次确认启动时的读取内容不足以覆盖图像数据后,我觉得问题出在 ollydbg, 它可能没有抓到读取文件的点,或者说,trace 的记录并不完整,于是我去试了试 ollydbg 2, 但是 ollydbg 2 trace into 和下断点一直都会有问题,我甚至重新准备了 Windows ME,无奈还是切回了 110 的版本。
直到一个偶然的机会我发现了 ollydbg 的一个功能:
这个功能可以把程序中所有外界调用都列出来,更神奇的是,我们还可以给所有的 ReadFile
设断点:
于是断点多出来一大堆:
立即我就有了思路:进入 ShowEvent
后,立即给所有 ReadFile
下断点。
终于,看上去我从坑里出来了,我们可以根据中断的位置,从 ghidra 找到一个合适的函数入口开始 trace into 就可以了。
找到文件读取,我们就可以迈开步子往前走了:
加载事件时,只读了两次文件,第一次读取了 38c8ch
(232588)个字节,
第二次读取了 1d69h
(7273)个字节,第一次读取的内容超过了 640x360 的大小,
所以我们不难推测,这个就是底图,而另一个,我想就是以前的眼部动画,
因为这个数据量对于调色板来说也过大。基于这一点我们就可以进一步找到底图的处理:
这里的 EBX
保存的值,或者说 ESI
指向的值就是文件内容,程序会计算一个偏移量,
从 EDX
那边取一个值,然后写到 EDI
中去。ECX
看上去就是循环次数,
从 280h
开始,如果我们去搜 412A62
行,并且指定 ECX = 27F
,日志会告诉我们,
有 360 条:
280h = 640
,外层又循环了 360 次,看来我们找到了第一张图。
细心的读者可能会注意到另一个问题,每次写到 EDI
的值,其实都是 7FFFh
,
因为 EAX
的值一直没有变化,这个其实很简单,我们很容易猜到 EDX
里面存的是调色板,
因为游戏里显示事件有一个先白屏,然后再显示图片的效果:
所以这个特效是通过修改调色板实现的。不过今天我们不去讨论调色板了,文章内容已经太长了, 有了这些内容,我们应该可以画出来第一张图了,不过这里面还有一个小坑,那就是图像数据并不连续,实际是这样读取的:
let content = from_bin("data/Redevent.paf");
let start = 0x8a2;
let width = 640;
let height = 360;
let mut image_buffer = image::ImageBuffer::new(width, height);
for y in 0..height {
for x in 0..width {
let offset = start + y * (width + 6) + x;
let pix_val = content[offset as usize];
let pixel = image_buffer.get_pixel_mut(x, y);
*pixel = Rgb([pix_val, pix_val, pix_val]);
}
}
image_buffer.save("event-1.png").unwrap();
注意 start + y * (width + 6) + x
,每行都有 6 个字节的偏移量。这下我们可以看一下画出的结果了,还记得这个灰姑娘么:
为期一个月的摸索终于有了个还算像样的结果,终于我们也有了点结论:Red2001 并没有压缩图像数据, 这也不难理解,2001 年已经不再是磁盘的天下了,大家都在想破脑筋如何塞满六百多兆的光盘, 明显修订版内容和原版高度一致,一定塞不满光盘,所以为啥还要再去压缩数据呢。