[lbk]古早文章翻译分享[rbk]想当个破解者?
galgame吧
全部回复
仅看楼主
level 13
huohua_离民 楼主
前言:本人在网络冲浪时,偶然发现了澄空学园的一个老贴子,是对一篇游戏破解相关教程的翻译/读书笔记内容[lbk]1[rbk]。然而该内容仅更新至Part III,而原文总共有四个部分[lbk]2[rbk]。因此我计划借用DeepSeek这一工具将全文翻译,并对部分内容加以人工校正,以供阅读参考。由于本人对逆向工程完全不熟悉,因此在“判断DeepSeek翻译的一些专业术语是否正确”这一点上难有保证,还望各位见谅,也希望有专业人士在评论区补全。正文部分将会以原文与译文相间的方式放出,便于各位参考原文与译文。若无特别说明,以下所有带全角中括号的“译者注”都是我添加的部分,与AI无关;其余部分则尽可能忠实原文。另,最开始我计划在正文结束后加上一处实操截图,不过考虑到原文已经附带了图片,新截图的意义不大,故删去了这一段。
2025年02月24日 15点02分 1
level 13
huohua_离民 楼主
第一部分:能力与责任
“我想翻译游戏,但我对日语一窍不通。”要是我每听到这句话都要花一个字节去记录,我早就要换硬盘了……
好在要当翻译家还能走另一条路——这是一套与“摆弄汉字并试图弄清代词都跑哪儿去了”全然不同的技能组。然而这项工作也同样重要。这是因为,倘若没有你提供的脚本,翻译者如何进行翻译工作?倘若没有你提取的图片,修图者如何进行图片编辑?倘若没有你重新组装好的游戏,玩家如何进行游玩?
那么,问题就变成了这样——“你想当个破解者吗?”
或者,更准确地说,你是否具备这样的能力?破解者(或者叫程序员,或者叫技术指导,随便怎么叫都好)在项目早期有一些重要的责任。首先,第一事项是要设法提取出游戏资源,尤其是游戏的脚本和图像,以便组内其他成员开展工作。第二,破解者必须找到能将修改后的数据重新封装回游戏而不致报错的方法。最后一点,也是最麻烦的一点,他必须常常想办法修改游戏本身,使之正确使用修改后的资源(譬如说,举个例子,让游戏的文本引擎使用半角英文字符或是支持日文文本不需要的词语换行功能)。
有时候游戏解包不难,因为可能已经有其他破解者在对HCG(成人内容图像)的不懈追求中写好了提取图片的工具。而封包工具就罕见得多,大多数情况下你得自己去写一个,除非你要处理的游戏的引擎已经有别的翻译组处理过。并且在你把脚本交给翻译者之前,你要想一想对方到底会吐什么出来……你可以自动把脚本再插入呢,还是说你得自己逐行逐句地手动复制粘贴?
为明确起见,我必须得说,我不会谈及仅为盗版而破解游戏防护程序的技术。我在此处提到的破解仅仅是为翻译免费发行的游戏,或者是为那些已经购入日文版的人创建英文版补丁。
那么你现在应该已经掌握了什么呢?如果你确实这么想过,我希望你会编程,毕竟我教不了你。好消息是你通常不需要太多的编程……只要最基本的工具就行了,越基本越好:你不需要在Windows系统里做出哪怕一个单选按钮控件,但你需要能区分大端序(big-endian)和小端序(little-endian)。这一点我倒是能教你。
那么,想一想你知道的所有编程语言。抛掉一切不能让你轻松读写文件原始字节的东西,用你最熟悉的语言,或者你也可以尝试一些新的内容。就我个人而言,我主要用C语言,不过要是我必须从头再做一次的话,我应该会考虑别的语言,比如Python、Java、C++或别的常见好用的语言。如果你最喜欢的语言是Visual Basic的话……呃,我觉得你是该扩展一下技能树了。
除了这些以外,你还需要解决问题的意识。你要面对的任务会是难题,它们可不是由你的计算机课教授为了巩固这周的学习内容精心设计的、可以用一页甚至更少的代码完成的作业。正相反,你实际上是在隔空和原作的日本程序员较量,他们可能并不在乎自己的数据能不能被逆向工程破解——他们要是真关心这个,兴许还会刻意加密,增大你的工作难度呢!
好,撸起袖子,启动你的编译器,开动脑筋。通过这个系列,我们会经历破解一些示例游戏的过程,希望你能看出破解者这个角色是否适合自己……同时学到一些技巧!
2025年02月24日 15点02分 2
level 14
[呵呵]
2025年02月24日 15点02分 3
level 13
huohua_离民 楼主
第二部分:十六进制编辑器
那么我们开始吧。今天我们会着眼于一个简单的样例档案格式,把它作为一个探讨游戏数据文件“标准结构”的切入点。当你面对未知的格式并试图解析遭遇的看似随机的字节时,牢记此类模板将大有裨益。
我们接下来要关注的游戏是CROSS†CHANNEL(在这一页末端会有一个试用版的链接),现在已经有进行中的翻译项目。那么,如果乐意的话就拿上这份拷贝,然后看一看吧。
(历史注记:CROSS†CHANNEL的开发兼发行会社FlyingShine很不幸在2019年的时候破产,这一处的链接也就用不了了。同样地,此处提及的爱好者翻译项目也已经不复存在。目前CROSS†CHANNEL本身由代理商MoeNovel代理发行,并在Steam平台上提供正式的官方翻译版。至于文章提到的试用版,我们很乐意在这里提供一份副本,以供学习者实践参考。)【链接:insani点org杠hosted杠C+C_trial点exe】
游戏目录中除可执行文件外,仅有少数几个文件:bgm.pd、cg.pd、script.pd、se.pd和voice.pd。这是典型的设计——大多数游戏不会将每个音效和图像单独存为独立文件,而是将其整合到若干数据包中供引擎随机读取。破解游戏的第一步,便是从这些数据包中提取独立文件。
现在该启动破解者的利器——十六进制编辑器了。这款工具的基础功能是显示文件的原始字节及其偏移地址,其高阶功能通常包括解析常见数据格式(整型、浮点数等)、文件比对,尤其是模式搜索。笔者个人使用Mac平台的HexEdit(历史注记:该软件自2012年后未再更新,已无法兼容新版macOS系统;Mac用户可改用App Store中的HexFiend)。若您有推荐的Windows或Linux十六进制编辑器,欢迎在评论区分享以供其他读者参考。
(更新:目前读者推荐的Windows工具有WinHex、Hiew、XVI32、Hex Workshop,以及十六进制与文本双模式编辑器UltraEdit;Unix系系统则提及hexcurse。感谢补充!)
【译者注:本人推荐的十六进制编辑器就是我目前在使用的wxMEdit,可在官方网站上免费下载使用,直接搜索即可。】
这些文件的结构如何?以下是cg.pd文件的起始部分,我们可以确定其中包含游戏图像资源:
太棒了,看这些可识别的文件名!(比如bgcc0000e.png。)当你看到这类可读字符而非随机字节时,应该感到欣喜——即使尚未完全理解其含义,至少已找到明确的解析方向。
为了更好地解析当前数据,我们需要先了解典型游戏归档的标准结构。
(未完待续)
2025年02月24日 15点02分 4
level 13
huohua_离民 楼主
1.The Header
1.文件头(File Header)
【译者注:文件头是文件的开头部分,通常包含文件的类型、版本、编码等信息。】
1.Signature
1.1文件签名(File Signature)
【译者注:文件签名就是指各种文件格式中一段独特的字节。每一种格式都有和其他格式不同的字节段,这一段字节即“文件签名”,一般来说它的存在可以告诉我们某个文件是什么格式的[3]。譬如.png图片文件的数据开头就有“塒NG”,即十六进制的89 50 4E 47。更多信息可参考https://www.filesignatures.net/
Usually an archive will start with some sort of identifying string giving a signature for the archive format and version. You can use this as a way to make sure your utility is being run on the right type of file.
归档文件通常以特定标识字符串开头,用于标明格式类型及版本号。这可用于验证工具是否处理了
正确的
文件类型。
2.Index position
1.2索引位置
Most of the time, the archive index will start immediately, but sometimes the index is actually stored at the end of the file instead, since the archive packer doesn’t know how big it will be until afterwards (if the index is itself compressed, for instance). In that case, there will be a pointer to where the index is.
通常情况下索引会紧随开头的文件签名,但有时候索引实际上是储存在文件末尾处的,毕竟归档打包工具直到文件末尾处才知道索引的大小(例如,索引本身被压缩过的情况)。此时会有一个指向索引位置的指针。
2.The Index
2.内容索引
The key structure you will want to understand is the index of the archive contents, since it tells you how to get at the files contained inside.
内容索引是归档文件的核心结构,因为它会告诉你如何获取归档内包含的文件。
1.Index size
2.1 索引大小(Index size)
Usually the index will start with a size value, often simply the number of files contained in the index. This isn’t always the case, though, as sometimes instead the index will just continue until it hits a special ending entry (with, say, a negative file size or a null filename, etc.).
索引开头通常是一个表示大小的值,大部分情况下这个值表示包含的文件数。不过也会有例外,索引也可能一直延伸到特殊结束符(如负值文件大小或空文件名)为止。
2List of file entries
2.2文件条目列表(List of file entries)
Next there will be an list of the individual files contained in the archive. This can be either a constant- or variable-length data structure depending on how the filenames are handled. Sometimes there will be a hierarchical structure of filepaths to represent a whole directory tree inside, too. Each entry contains a number of standard bits of information:
接下来是记录归档内包含的独立文件的列表。条目长度可能是固定或可变的,这取决于文件名的存储方式。有时也会有一个文件路径的层次结构来表示整个目录树。每一项都记录着一定的标准信息:
1.Filename/filepath: can be a zero-terminated string, or sometimes the length will be explicitly given. Believe it or not, filenames are optional, as I’ve run across at least one case of a filename hash being stored instead.
2.2.1文件名/路径(Filename/filepath):可能以零终止符(\0)结尾或显式标注长度。需注意:文件名并非必需字段,笔者曾遇到以哈希值替代文件名的情况。
2.Position: an offet to the start of the file in the archive. Offsets can be from the start of the archive, the start of the index, or sometimes the start of the “file area” in the archive (i.e. an offset from the start of the contents of the first file, or equivalently from the end of the index).
2.2.2起始位置(Position):这是一个指向文件数据起始位置的偏移量,基准点可能是:归档文件头、索引起始位置,或首个文件内容起始位置(即归档内第一个文件的位置,或者说索引结束位置)。
3.Size: how large the file is. This is sometimes left out, since it can often be inferred from the offset to the next file. Other times, there are two different sizes: an original size for the file, and the compressed size as it is stored in the archive.
2.2.3文件大小(Size):记录了文件占用空间的大小。这个信息有时可能被省略,因为它可通过相邻文件偏移推算。有时候会有两个记录:原始大小与在归档中压缩后的大小。
4.Flags: is the file compressed or not, and if so, with what algorithm? Is it encrypted, and if so, is there a key or initialization value to use?
2.2.4标志位(Flags):标识文件是否被压缩过以及是否被加密过。若被压缩过,具体的压缩算法是什么呢?若被加密过,会否包含可供利用的密钥或者初始值?
5.Checksum: to ensure data integrity, sometimes a checksum for the file will be given. This can be highly annoying for hackers, since it means that to modify the archive we will need to reverse-engineer the checksum algorithm to be able to compute appropriate values for our new data (or else disable the check in the executable).
2.2.5校验和(Checksum):为确保数据的完整性,有时校验和会被给出。对破解者而言这可能比较麻烦,因为它意味着要修改归档就必须对校验和的算法进行逆向工程,以便计算出新数据对应的值(当然也可以禁用可执行文件中的检测)
Note that sometimes this information will be distributed, such as, say, the file position being given with the filename in an index structure, but the file size and compression flags given at that offset, right before the file data itself.
注意,某些归档会将元数据分散存储,例如索引中仅记录文件名和偏移量,文件大小和压缩标志存储在数据区起始位置。
3.The Files The files themselves are then just concatenated together in the archive, possibly compressed and possibly encrypted. The game engine knows where to find them from their index entries, so it can jump immediately to the ones it wants.
3. 文件数据(The Files)
文件数据按顺序连续存储,它们可能被压缩或加密。游戏引擎通过索引条目快速定位目标,以便能第一时间找到要调用的文件。
【由于涉及到专有名词,此处将原文一并附上,若有错误之处欢迎指出】
(未完待续)
2025年02月24日 15点02分 5
level 13
huohua_离民 楼主
好,我们现在以cg.pd为例开始分析。文件开头的"PackOnly"看着像文件签名。其后填充零字节直至位置0x40处,出现十六进制序列21 02 00 00 00 00 00 00,随后紧跟可识别的ASCII文件名。
这会是索引的大小吗?话说回来,我们应该怎么处理这几个字节?有以下几种选择:
·作随机标志位解释。首字节0x21包含第0位和第5位标记,次字节0x02仅有第1位标记。并非全无道理,但没什么用
·作小端序整数解释。那么首字节0x21在最低位,次字节0x02在倒数第二位,以此类推。最后解析为0x0000000000000221,即十进制545。是个合理的文件数量范围。
·作大端序整数解释。那么0x21为第一位,0x02为第二位,解析为0x21020000也就是十进制553,779,200(我们还略过了四个零呢)。考虑到归档文件本身不到500MB,这个结果是不实际的。又或者这只是十六进制的0x2102=8450,那么还有可能是索引本身的字节数。
那么我们检查一下吧:从文件开头一路扫描,文件名似乎在0x48、0xD8、0x168、0x1F8、0x288等位置,也就是说每一个文件名占用144字节。最后一个文件(TCYM0005c.png)在0x13248位置,也就是说大概有(0x13248-0x00048)/144+1=545个文件。
对,正是545!因此将21 02 00 00 00 00 00 00解析为小端序64位整数是合理的。更进一步,我们现在可以只关注每个144字节区域,确信每个区域都对应一个文件条目。此外,我们还知道这个归档是采用小端序8字节整型的。
好,我们检查一下这些文件吧。不过(这里有个小技巧)不要管第一个条目。这个条目是无效零值的 可能性不小,若是如此我们就看不出其意义。那么我们先看第二个条目,也就是从0xD8到0x167的位置:
我们看到了文件名、大量零字节,以及两个疑似8字节小端序整数。回想典型归档文件的结构模板……我们需要从中提取文件大小和位置信息,但部分零字节可能是标志位——目前尚无法确定。
现在暂且假设所有零字节属于文件名数据结构:这为文件名保留了128字节空间,很像是一个(怕麻烦的?)开发者有意为之。剩余数据为整数0x002420FA和0x0008370E。现在暂时无法解析其含义……不过再找找更多的例子吧。前几个文件的这些数字看起来如何?
File 1 0×00240048 0×000020B2
File 2 0×002420FA 0×0008370E
File 3 0×002C5808 0×00002FA6
File 4 0×002C870E 0×00063B8A
File 5 0×0032C338 0×0006A7CB
现在开始有些规律了:第一列的数值持续递增,且每次增加的数值恰好等于前一行的第二列数值!这正是“偏移量-文件大小”连续存储的典型特征。
(未完待续)
2025年02月24日 15点02分 6
level 13
huohua_离民 楼主
如果我们的假设成立,第一个文件bgcc0000e.png的大小应为0x000020B2=8370字节,起始偏移应是0x00240048。不过我们仍不确定该偏移量是相对文件开头还是文件签名,毕竟我们知道索引结束于0x000132D7。尽管如此,我们仍可验证——就算文件存储顺序与索引顺序不一致:
尤里卡!我们猜对了,偏移量0x00240048处的确是PNG文件的起始位置。该文件未压缩且未加密。将后续8370字节复制为新文件并用图像编辑器打开后,可见……一张640×480的纯白空白画面。
呃……游戏确实需要空白画面,至少分辨率对得上。为稳妥起见,继续提取下一张图像。从偏移0x002420FA处截取0x0008370E也就是847,118字节,得到以下结果:
好,旗开得胜!
那么,总结一下。我们可以认为此处的.pd文件包括:
1.文件签名“PackOnly”
2.56字节的0x00
3.8字节的小序端文件数
4.144字节的文件条目记录,包括:
4.1.128字节的文件名,以0x00表示终止
4.2.8字节的小序端文件偏移(相对于归档文件开头处)
4.3.88字节的小序端文件大小
5.文件未压缩或加密,正好在文件偏移处开始。
我们该如何处理索引结束位置与文件数据起始位置之间的空白区域?嘛,偏移量0x00240048值得注意……首个文件条目位于0x48处,这意味着从0x48到0x240000的区间共预留了0x240000字节用于存储文件条目。已知每个文件条目占144字节,则该区域最多可容纳16384个文件条目。16384即2的14次方,这显然是(怕麻烦的?)程序员的典型做法:直接为大量文件预留固定空间。
这种设计是否必要?若重建资源包时删除空白区域,或许能节省数兆字节空间。但若调整数据位置时偏移超过一字节,游戏可能会崩溃……具体影响还有待实验测试。
那么,下一回我们将基于现有知识编写解析工具。当然,过程中难免会遇到障碍,嘿嘿。
(第二部分到此结束)
2025年02月24日 15点02分 7
这里4.3笔误多打了一个8
2025年02月24日 17点02分
@Bes21Q🐭 感谢指正
2025年02月24日 22点02分
level 13
huohua_离民 楼主
第三部分:代码原型设计
(历史注记:本教程撰写时Python 3尚未发布,Python 2为主流版本。相关脚本可能在Python 3中无法运行,建议使用Python 2以获得最佳效果。)
在上一章节中,我们通过十六进制编辑器分析了《CROSS†CHANNEL》的资源包格式,并通过手动提取图像验证了猜想。但唯一能完全确认的方法,是构建工具并尝试修改游戏!
本章将通过快速代码原型设计,开发处理资源包的工具。目标是保持代码简洁直观——若你在本章结束时感叹"破解游戏只需写这些代码?",那……就说明目标达成了。
选择何种编程语言?我通常使用C语言,但为了兼顾教学效果,我们将使用Python——这也将是我的一次学习体验。Python的主要优势包括:
·Cross-platform. It’s available for Windows, Linux, and Mac OS, and it unifies some inherent differences like directory-path separators.
·跨平台性:支持Windows、Linux和Mac OS,并统一处理路径分隔符等系统差异。
·Good mix of levels. There are flexible high-level data structures, like hash-table dictionaries and function continuations, as well as decent low-level bit manipulations.
·多层级支持:既有哈希表、函数闭包等高级数据结构,也支持底层位操作。
·Intuitive syntax. Python is almost like executable pseudocode, so if you know virtually any other language, you should be able to read it without trouble.
·直观语法:类似可执行的伪代码,熟悉其他语言的读者可轻松理解。
·Interactive mode. If you want to, you can just type in commands one by one and have them execute immediately. This is great for avoiding a slow edit-recompile-test step during prototyping.
·交互模式:支持逐行执行命令,避免原型设计时的"编辑-编译-测试"循环。
【译者注:原文中Good mix of levels一段并未单独分点。考虑到具体内容,应该与其它三点优势并列。这里同理将原文与译文一起列出,以供大佬检查是否有专业名词出错】
不幸的是,Python的主要劣势是运行速度。Python无法与C等底层编译语言在密集计算上匹敌,但当前需求尚不涉及性能瓶颈。若未来需要优化,可通过Python/C混合编程实现。
如果你用的是Mac OS X系统,大概系统上已经预装了Python;如果你是Linux用户,可通过包管理器轻松安装;至于Windows用户,则需下载安装程序。请准备好环境并跟随操作。
我们需要实现哪些基础功能?嗯,先回顾上一章解析的归档文件结构吧:
【请读者自行查看第六楼】
(未完待续)
2025年02月24日 15点02分 8
level 13
[呵呵]
2025年02月24日 15点02分 9
level 13
huohua_离民 楼主
看起来我们需要实现以下功能模块:
·Read/write integers of specific lengths and endianness
·Read/write zero-terminated strings
·Check signatures and known runs of zeroes
·读取/写入特定长度和字节序的整型
·读取/写入以零结尾的字符串
·验证文件签名与空白填充区
无需紧张!这些工具函数将贯穿后续工作,编写后可在不同项目中复用。以整型处理为例:
【译者注:如本部分开头,本文原文的Python代码是Python2版本。此处我们让DeepSeek写一个Python3版本的同义内容,下文代码后方若有全角中括号内容,即为Python3版本的同义代码,不再赘述。若需要查看原版代码,可以到文章所在网页查看,或者翻一翻我之后可能会分享的word文档(或者pdf文件)。此外请各位注意缩进:
def read_unsigned(infile, size=4, endian='little'):
result = 0
for i in range(size):
byte = infile.read(1) # 读取字节数据
if not byte:
raise EOFError("Unexpected end of file")
temp = byte[0] # 直接获取字节值(0-255)
if endian == 'little':
result |= temp << (8 * i)
elif endian == 'big':
result = (result << 8) | temp
return result

这段代码或简单易懂或令人困惑,取决于你的编程背景。其核心逻辑是:按字节顺序(小端序低位在前/大端序高位在前)逐字节读取文件,并通过位移运算组合成多字节整型。
其他功能模块实现逻辑相似,实在不值得再行赘述。笔者已将工具函数打包为insani.py,可供下载使用。
【同理,需要下载的请找原网页,这里我们还是让DeepSeek帮忙写一写Python3的等效内容】
现在进入核心环节——归档文件的读写实现!
(未完待续)
2025年02月24日 15点02分 10
level 13
huohua_离民 楼主
基于前述归档文件结构分析,我们可直接将其转化为代码。首先是文件签名与文件头的验证:

from insani import *
arcfile = open('cg.pd', 'rb')
assert_string(arcfile, b'PackOnly', ERROR_ABORT) # 字节串需加b前缀
assert_zeroes(arcfile, 56, ERROR_WARNING)
numfiles = read_unsigned(arcfile, size=LONG_LENGTH)
print(f'正在提取 {numfiles} 个文件...')

此处使用insani.py中的assert_string()和assert_zeroes()函数验证文件签名与预期的零填充。常量LONG_LENGTH值为8(即8字节整型长度),默认采用小端序。
需重点关注的是错误检查。我所谓的错误检查并非“通常”的错误检查,如文件是否成功打开、内存是否足够读取8字节等。对原型工具而言这有些蠢了:我们不关心文件无法读取导致的崩溃,倒不如说这反而符合我们的预期。
不不,我是说,真正的重点是验证我们对资源包的假设。你看,理论上我们可以跳过前64字节,直接读取numfiles。那些才是我们的需要的信息不是吗?但为何要我们还要检查签名和零填充?
需重点关注的是错误检查。我所谓的错误检查并非“通常”的错误检查,如文件是否成功打开、内存是否足够读取8字节等。对原型工具而言这有些蠢了:我们不关心文件无法读取导致的崩溃,倒不如说这反而符合我们的预期。
不不,我是说,真正的重点是验证我们对资源包的假设。你看,理论上我们可以跳过前64字节,直接读取numfiles。那些才是我们的需要的信息不是吗?但为何要我们还要检查签名和零填充?
因为我们仍在验证对自己文件格式的理解……我们仅仅只分析了一个游戏的单个资源包样本。那56字节是否无用?永远如此?尚未可知。所以当工具遇到不符合预期的内容时——或者说我们没预测到的内容时——就可选择在“我完全懵了”的时候终止处理,或者至少在不那么关键的不符的情况下输出一些警告信息,以便我们进一步排查并修正工具。
关于错误检查的讨论到此为止。请相信我:与其直接跳转到关键数据,不如编写额外逻辑验证假设。现在开始读取索引吧:

print('正在读取资源包索引...')
entries = []
for i in range(numfiles):
filename = read_string(arcfile) # 返回str类型(假设已处理编码)
assert_zeroes(arcfile, 128 - len(filename.encode('utf-8')) - 1) # 按字节长度计算
position = read_unsigned(arcfile, size=LONG_LENGTH)
size = read_unsigned(arcfile, size=LONG_LENGTH)
entries.append((filename, position, size))
assert_zeroes(arcfile, 144 * (16384 - numfiles), ERROR_WARNING)

此处我们利用Python的特性简化操作。读取文件名、偏移量和大小后,我们可以将三者组合为一个“元组(tuple)”并存入列表。若用C语言复现该操作会更复杂,但在Python中只要近乎伪代码的部分即可。此外,还要注意断言语句:文件每个字节均被检查,或被使用,或被确认为无效数据。
现在完成整个流程,实际提取文件并进行一些清理工作:

for filename, position, size in entries:
print(f'正在提取 {filename}({size} 字节),偏移量 0x{position:X}')
with open(filename, 'wb') as outfile:
current_pos = arcfile.tell()
assert current_pos == position, \
f"文件指针位置不符(当前 0x{current_pos:X},预期 0x{position:X})"
outfile.write(arcfile.read(size))
# 验证是否已读取整个文件
remaining_data = arcfile.read(1)
assert not remaining_data, f"文件末尾残留数据:{remaining_data.hex()}"
arcfile.close()

循环结构再次体现Python的简洁性:我们可直接遍历条目列表,逐项解包元组并复用变量。本可用arcfile.seek(position)跳转至目标偏移,但此处仍通过断言验证对资源包格式的理解:文件是否按顺序连续存储?提取完成后是否真的到达文件末尾?
如果我们在做的过程中没有犯错,执行过程应无意外。那么动手试一试吧。你可以下载这个文件crosschannel-extract1.py,但我还是建议你自行实现代码,至少自己输入,以熟悉Python语言特性。
(未完待续)
2025年02月24日 15点02分 11
level 12
好家伙,先进生产力了这下🥰
2025年02月24日 15点02分 12
level 13
huohua_离民 楼主
% python crosschannel-extract1.py
Extracting 545 files...
Reading archive index...
Extracting bgcc0000e.png (8370 bytes) from offset 0x240048
Extracting bgcc0023.png (538382 bytes) from offset 0x2420FA
...
Extracting TCYM0005c.png (156881 bytes) from offset 0x566E1B4
【译者注:这里是模拟计算机上运行上面的py文件的显示结果,在Windows系统,首先在powershell或者cmd框内输入“python crosschannel-extract1.py”,然后就能看到类似的显示结果(得保证py文件和cg.pd放在同一个文件夹)。当然,如果你使用了DeepSeek提供的Python3的代码,你会看到中文内容】
看来成功了,所有的png文件都能在你最喜欢的图片编辑器里查看了。呃,虽然有部分图片稍微NSFW(Not Safe For Work)了一些。^^;
【译者注:所谓不宜在办公室浏览(Not Safe For Work)的内容,自然就是里面的一些HCG了,不过体验版里面有HCG这点倒是有些奇妙呢(笑)】
基础提取功能已验证可用。现在稍微改进一下工具:原版本硬编码资源包路径的设计显然不够优雅。这里有升级版crosschannel-extract2.py,通过Python操作系统接口库读取命令行参数,支持提取文件到子目录,并优化错误检查逻辑——允许处理乱序文件,且在遇到明显异常的偏移或文件大小时提前终止。完整代码可参考示例,但核心逻辑与本文主旨无关。
我们接下来真正要做的是重建归档文件!别紧张,只需要把刚才的代码拿过来稍微编辑一下。首先我们收集需要打包的文件的信息,从命令行传入的子目录名获得:

import sys
import os
from insani import *
# 读取命令行参数:输入目录与输出文件路径
input_dir = sys.argv[1]
output_archive = sys.argv[2]
# 计算初始偏移(根据资源包头部结构)
header_size = 64 # 'PackOnly'签名 + 56字节零填充 + 8字节文件数
index_entry_size = 144 # 每个索引条目占144字节
initial_offset = header_size + index_entry_size * 16384 # 0x240000 + 72
# 收集待封包文件信息
entries = []
current_offset = initial_offset
for filename in os.listdir(input_dir):
filepath = os.path.join(input_dir, filename)
if os.path.isfile(filepath):
file_size = os.path.getsize(filepath)
entries.append((
filename, # 原始文件名
filepath, # 完整路径
current_offset, # 文件数据起始偏移
file_size # 文件大小
))
current_offset += file_size # 连续存储,无间隙

我们可以基于文件大小和已知的文件头及索引结构,计算各个文件的最终偏移。接下来开始写入:

# 写入资源包头与索引
with open(output_archive, 'wb') as arcfile:
# 1. 写入签名与头部
arcfile.write(b'PackOnly') # 文件签名(7字节)
write_zeroes(arcfile, 56) # 填充56字节零
write_unsigned(arcfile, len(entries), size=8) # 文件数(8字节小端序)
# 2. 写入索引
print(f'正在写入索引(共{len(entries)}个条目)...')
for filename, _, position, size in entries:
# 文件名处理(Shift-JIS编码,128字节定长)
encoded_name = filename.encode('shift_jis')
write_string(arcfile, encoded_name) # 写入字节串(自动补零)
write_zeroes(arcfile, 128 - len(encoded_name) - 1) # 计算剩余零填充
# 写入偏移与大小(各8字节)
write_unsigned(arcfile, position, size=8)
write_unsigned(arcfile, size, size=8)
# 填充未使用的索引空间(16384总条目数)
remaining_entries = 16384 - len(entries)
write_zeroes(arcfile, index_entry_size * remaining_entries)

封包过程比解包更简单:因为我们无需验证任何内容,只要写入就行了……没有不确定因素。你也可以注意到该结构与最开始的归档结构非常相似:可见实用程序例程的价值,你可以用一两行代码处理结构的每一个项目。最后我们写入文件数据:

for filename, fullpath, position, size in entries:
print(f'正在封包: {filename} ({size} 字节) @ 偏移量 0x{position:X}')
current_pos = arcfile.tell()
assert current_pos == position, \
f"文件指针位置异常(当前 0x{current_pos:X},预期 0x{position:X})"
with open(fullpath, 'rb') as infile:
arcfile.write(infile.read())
arcfile.close()

此处我们使用断言检查(assert statement)用于基础验证,然而这实际上属于"愚蠢程序员"检查机制:若代码编写完全正确,无论资源包格式细节如何,该断言条件在程序正常运行时始终成立。这正是中止型断言(abort-if-false)的经典用途:快速验证理论上不可能出错的条件。
(未完待续)
2025年02月24日 15点02分 13
level 13
huohua_离民 楼主
您可下载crosschannel-repack1.py直接使用,但我仍建议您动手编写代码以加深理解。现在进行测试:
% python crosschannel-repack1.py temp temp.pd
Packing 545 files...
Writing archive index...
Packing bgcc0000a.png (408458 bytes) at offset 0x240048
Packing bgcc0000b.png (436171 bytes) at offset 0x2A3BD2
...
Packing xiconp.png (2399 bytes) at offset 0x5693D26
【还是模拟运行的过程】
一个成功工具的金牌标准是能对归档文件进行封解包处理,所得的新归档文件是原文件的完美复制品。那么我们成功了吗?
% diff cg.pd temp.pd
Binary files cg.pd and temp.pd differ
【译者注:此处并非Python或Windows指令,而是Unix/Linux/macOS终端命令。Windows系统可以使用comp [file1] [file2]进行,假设新的封包归档是test.pd,原始归档是cg.pd,那么这里就打开cmd输入comp cg.pd test.pd。此外还可以用fc /b [file1] [file2]】
唉,果然失败了。但这在预料之中——我们重新封装的资源包按字母顺序排列文件,而原始包的顺序是随机的。这种顺序差异会影响结果吗?目前尚不明确。
成功工具的银牌标准是:确保它能正确解包自己封装的归档文件。因此我们执行“解包→重新封包→再次解包”的流程,并验证两次提取结果是否完全一致。
% python crosschannel-extract2.py temp.pd temp2
Extracting 545 files...
...
% diff -r temp temp2
这回成功了。至此,我们的提取与封包工具实现了内部兼容性。不过,它们能否与游戏本身兼容呢?
这里分享一条行业技巧:在首次测试重构的归档文件时,切勿修改任何内容。仅需解包后再次封包并验证游戏能否正常运行。通过此方法可检测游戏是否在可执行文件中存储了资源包的MD5校验和,或是否存在未被解析的格式细节。只有通过此验证之后,你才能开始修改文件并插入翻译内容。
简而言之,测试成功了。幸运的是,游戏中未发现校验和验证机制,且资源包格式已完整复现(尽管文件顺序本可能影响结果)。
下一步就是试着改一些东西。游戏的主界面图片如何?这一项的文件名是x0000.png,那么我们不妨插入我们自己的分辨率为640×480的文件,然后重新做好一个归档,看看会发生什么……
这个额外的彩色矩形是主菜单高亮系统的一部分。看来进展顺利……大部分用户界面资源仅以PNG格式存储,因此我们的翻译人员和修图人员完全可以通过现有工具重制游戏界面,毫无问题。
但且慢……游戏脚本怎么办!
% python crosschannel_extract2.py script.pd script
Expected "PackOnly" at position 0x0 but saw "PackPlus".
Aborting!
Traceback (most recent call last):
...
【译者注:此处是使用原来的解包工具试图解析脚本的封包,但脚本封包的文件签名并不是PackOnly,而是PackPlus,所以解析失败。】
(未完待续)
2025年02月24日 15点02分 14
level 13
huohua_离民 楼主
PackPlus?!糟糕。但此时错误检查机制的价值显现了——我们立刻意识到脚本资源包存在异常,而非在后续步骤中提取出乱码后被迫逆向分析。让我们回归十六进制编辑器一探究竟。
嗯,实话说看起来很正常。零字节都在,而且也有看起来像是文件数、文件名、偏移量、大小的数据在。把界面往下拉一拉,你还能看到直到0x240048之前都是清一色的零字节,和之前一样。于是整个文件头和索引都一样,只有文件签名不同。我们做的这一切都是无用功吗?
【译者注:no-op是计算机科学中的无操作指令No Operation,此处是比喻义,表示无效行动或者说无用功之类的意思】
啊,看看原始文件本身吧:
这现象有些诡异。字节数据并非随机分布——存在大量重复值——说明可能未真正压缩或加密。同时高频出现大于0x80的数值,这也非同寻常。
我们很可能遭遇了某种基础"加密"——即经典的异或/加法常量加密算法,这种机制能防住六岁妹妹偷看文件……前提是她不会编程。
【译者注:异或常量加密算法是,使用一个固定的常量作为密钥,比较待加密字符与常量的二进制数,对每一位数,相同的则输出为0,不同的则输出为1,最后得到新的二进制数;两次异或可以还原数据。加法常量加密算法就是将密钥的二进制数加到待加密字符上并对256取模,类似数字版凯撒加密】
【当然我不敢保证自己的理解是对的,还望各位大佬纠错了】
我们当然可猜测密钥……实际上若运气好也能命中。但我这人比较懒,所以我选择用快速编写的代码暴力破解。

with open('script.pd', 'rb') as arcfile:
arcfile.seek(0x240076) # 定位可疑偏移量
data = arcfile.read(16) # 读取16字节样本
with open('bruteforce.dat', 'wb') as outfile:
for i in range(256): # 遍历所有可能的单字节密钥(0-255)
# 异或(XOR)运算
for byte in data:
outfile.write(bytes([byte ^ i]))
# 加法取模运算
for byte in data:
outfile.write(bytes([(byte + i) % 256]))

(未完待续)
2025年02月24日 15点02分 15
1 2 3 尾页