转自http://bbs.sumisora.com/read.php?tid=209302
% E3 u3 U1 m0 z2 i2 f: Y" F+ c$ ~: \8 s' ]: C6 U l7 J x
想成为一名破解者吗?; r2 B6 a4 U3 C8 M1 Y; t" a9 _" @
本系列文章由RednaxelaFX翻译自insani在他们的网站上提供的破解相关教程,面向的读者群是有志于在中文化GALGAME中程序方面的工作贡献力量,但是尚未入门的人.文章是以原作者的视角,以第一人称展开的,与读者交谈式的破解教程.到目前(2006-12-03)为止已发表的有四篇,主要都是描述资源文件的处理相关的技术的,有些部分或许会与译者之前发过的帖有所重叠.今后或许也会涉及到游戏原程序(不是源程序=_=)的修改相关,若出现的话或许也会翻译出来与大家一同学习.希望该系列的文章能对中文化工作者在程序技术上有所帮助.对程序处理方面已经具备相当经验的人来说本系列文章多是十分基础的,请快速跳过已经熟知的部分.当然其中要是有翻译错误或者原文错误的话,敬请指正 ^ ^
1 l/ ?9 G) ?2 K; m3 M+ U% m: q% Q' P% w. Y, j% w. B
原文已发表的篇章:' z9 F) \3 X& u& M/ Q2 V
So You Want To Be a Hacker?
/ d2 b5 t6 m, m: O% \$ O4 gPart I: Abilities and Responsibilities
8 `- |- V. \# D jPart II: The Hex Editor
' X$ M2 a* [2 ]: l, I; [ Q: l; iPart III: Code Prototyping
& c4 i$ E- F8 c% oPart IV: Compression Formats! x: w; F6 R- I+ i
0 `6 ?/ ?7 m" v9 z
原文可以在 http://sekai.insani.org/ 找到,原作者Edward Keyes.同一站上也有关注翻译者的相关技能的系列文章《Letters to a Young Translator》,作者Seung Park.' t/ ~+ g# d4 C6 a; S) [1 D
关于insani及其相关作品,请到 http://www.insani.org/查看.他们是一个二人组合,虽然两人都有英化作品所需的全方位能力,却还是分工为各自分别专门负责程序和翻译的进行.值得一提的是,较流行的英文版ONScripter就是insani改造版.原作者的工作环境并不总在Windows下,因而所用工具也并非仅限于Windows平台上.
" F# ]! Q& h+ Q: S0 r7 O( @7 ]7 E本文的翻译保留了原文的第一人称形式;而对原文稍微调整的部分,纯属个人见解,无意歪曲原作者的意思.特此说明.
5 @. Q& a& @8 u' O& O( z% F其实是想说...与其说是翻译,还不如说做成"读书笔记"了...总之蓝色的部分是翻译自原文的就是了 ^ ^+ }0 {% s6 h/ e0 D2 y* _
3 E' o2 E% O" o0 y# G想成为一名破解者吗? Part I: 所需技能与相应责任
6 _0 M/ P! P2 I" F
3 J; k( x5 p2 }(译者按: PartI的开始前想先提一下insani文章的风格.他们在系列文章的开篇总会提到"责任"问题.这部分打算按照原文翻译,但并不代表译者完全认同其观点.这部分的翻译之后,也会附上一段Susie的简单使用介绍.另外对"hacker"一词翻译为了"破解者",因为译者认为实际上文章所描述的工作与其说是hack还不如说是crack.)
2 O1 ^, r5 Q. T, _. o% D3 i" P* U5 H! b7 V1 h
"我想翻译游戏,但是我一点日语都不懂." 如果每次听到这种话我都用一个字节来记录的话,我可以换掉我的硬盘了...% ]/ @1 |: W- }( W
( H) t* Y* c2 [( i所幸的是,在做翻译之外还有一条路——这与[捣弄假名然后为代词都跑哪去了而疑惑]需要的是完全不同的一套技能.然而这项工作与翻译同等重要.试想没有你提供的脚本的话,一个翻译能对着什么翻译? 没有你提取出的图像,一个改图者能改什么图? 没有你重新组装好的游戏,一个玩家能玩什么游戏?" ^0 M9 i- A1 [% J) K2 a: v d
( @, a5 v+ a5 R
所以,问题就变成了,"想成为一名破解者吗?"
8 {2 V, ?" }8 Z, U* t4 P5 U+ ^% a4 z) n5 r0 T
或者,抓住要点来说,你拥有破解游戏所需要的技能吗?破解者(或者程序员,或者技术指导,或者随便怎么称呼)在翻译过程中的早期阶段有着重要的职责.最优先的就是找出提取游戏数据的方法,具体来说就是脚本和图像.这样小组的其他成员才能开始工作.其次,他必须找到把修改过的数据重新放入游戏中而不出错的方法.最后也是最麻烦的,他经常须要修改游戏本身的方法来让游戏正确使用修改过的资源(例如,将游戏的文本引擎改为使用半角英语字符,或者实现日语游戏所不需要的自动换行功能)" O. P9 q3 L# ^4 j/ J5 G
/ m3 K; L' N/ f. T- ?) h
(译者按: 对于中文化工作者来说,这些例子的对应版本就会是修改字体的参数来让汉字编码得以正确显示,或者修改边界值来逃过字符集的边界检查等.上面提到的自动换行原文是word-wrapping,关键在于自动换行时要考虑到单词的完整性.)
, y7 ^2 L" v+ @0 X& W8 O h; h" q% y& g' Z n/ i
提取过程有时候很简单,因为在对HCG的不懈追求中,可能已经有其他破解者写好了从游戏提取图像的工具.相比之下,再插入工具则甚为罕见;你几乎总是得自己来写,除非你处理的游戏与其他的翻译小组以前处理过的使用了相同的游戏引擎.就算顺利提取出了脚本,在把脚本交给翻译前,你得先想清楚他会返回些什么东西...你能把脚本自动再插入,还是得自己手动复制粘贴 一·行·行·的 脚本呢?
1 E- p- G! L6 W& e" l
4 w. c. P8 e9 R3 n9 @, o有一点我要明确一下,我不会单纯为了盗版而讨论破解防拷贝技术.这里所讨论的破解纯粹是为了翻译自由发行游戏,或者是为已经购买了完整的日语版游戏的人制作英语补丁., d" h) q$ C7 V0 t8 [, }
$ Q+ i1 ^9 y4 u
(译者按:这点翻译得相当无奈.在一些客观条件的限制下,玩家的需求与条件的限制之间的矛盾,促使越来越多的中文化工作组出现和成长.相信多数的非官方中文化工作者,在中文化一部游戏作品的时候,并不是站在侵犯任何组织或个人的权益的出发点上的.相反,这是在得不到有效的官方代理的条件下,为了推广自己喜欢作品而尽的努力.在可能的范围内,中文化工作者都应该尽量少涉及与作品原作者权益相背的范围.但是许多时候实在是事出无奈...嗯,这个问题就此打住了.Ed的观点译者在此只能理解却无法认同了.)4 `3 o8 U0 [7 l/ v) M- S
. A& |$ s3 g* n* w! a! r% o
你需要已经具备哪些知识呢?如果你在考虑这个问题,我希望你懂得一定的编程,因为那个我教不了你.好在,你并不会经常需要[大量]编程...只要些基本工具就够用了,越底层越好:你连如何在Windows中制作一个单选按钮都不需要知道,但是你得知道区分big与little-endian.这个,我能教会你.
. s( s$ b7 v3 l* c; }8 U& ]8 \, y" j+ e- ]6 U0 `
来想想所以你懂得的程序设计语言.把不能轻松完成如在文件与未处理的字节数组间读写之类的一些底层操作的语言扔掉.用你觉得最顺手的语言,或者也可以尝试点新东西.个人而言,我主要直接用C,不过如果我得从头再做一次的话,我可能会仔细考虑一下,比如说,Python,Java,C++,或者其它好用常用的语言.不过如果你最喜欢的语言是Visual Basic的话...我觉得你是时候扩展一下你的技能了.& ^0 }) T6 p! [
# Z5 j7 i$ ]; Y4 \5 ~. X(译者按:嗯,这部分译这也想说点.在中文化游戏的过程中,破解者自己所写的程序多数只是自己临时使用的,所以就算慢一点或者代码乱一点问题也不大.在语言的选择范围上也就有相当大的余地.如Ed所说,用什么语言其实是自己的习惯,并不一定要专门另外去学一门困难的语言的.然而如果完全不懂得编程的话而又想加入到中文化工作的程序破解方面中,如果不学习哪怕一点点的编程知识,会带来比较大的限制.
4 _4 M* S, ~/ m2 o6 M& N4 q, Y, y6 M. l! t9 Y! d
对于有C或者语法与C类似的(例如C++,Java,C#)语言基础的人而言,安心继续使用你觉得顺手的就可以了.熟悉JavaScript或者FlashActionScript的,在破解时也可以考虑转向Java或者C#,语法比较相似.haeleth喜欢用CAML,ML的一种变体,那个也是相当方便的语言.
$ Z& d- b; a3 }0 C J0 n: ?' ~& c. k9 t# h6 _* @
如果是完全没接触过编程的,译者推荐Python或者Perl,或者其他功能强大的脚本语言.相对于较底层的C/C++来说,这两种脚本语言都既有足够能力完成工作,又不会让你为底层操太多心.% u' `, _, C6 }# F1 c
2 m, n& J% j( ]3 X请不必在"比较编程语言"这个环节上浪费太多时间.不考虑实际使用背景就比较语言毫无意义.在破解工作中能用得顺手,时刻让自己保持清醒的头脑才是最重要的.速度之类的重要性并不优先.
+ D* {, \# v) m4 B+ F R* d* j F3 p3 J* @4 z8 ^
汇编的话,并不一定需要掌握.只是,掌握多少知识在破解上就能做到多少程度.许多时候并不需要做到很深的程度就能完成破解任务了...至少,一部分吧 XD
4 ^4 p8 J6 Y4 Z$ k0 n0 K1 U
$ P- O( t, p3 n: N9 U. H( [学习程序设计语言时,一开始要注意的是一些基本概念,如执行流的控制等;又例如面向对象语言里的"对象"等.只要在一门语言上清楚掌握了这些概念,就会发现在其他语言里其实都是一样的,只是具体写法可能不同./ |" e+ \" p# \# ]
5 W. q6 _( ?7 @% ^过程化概念:5 `+ G1 I4 ?, |) F4 [
变量,以及变量的声明
; K2 V2 b: x: x2 z语句,以及语句块 a0 j4 U, ]2 a& a4 R* n$ j9 k
表达式与操作符- Z9 Z! V1 u6 |! z7 b# I5 z
执行流控制,例如循环,分支,跳转等
3 l5 s5 T( s% o( B3 ^' P函数的声明和调用4 o$ w- m; k; q& y- k
函数的重载
F% m# S s% U
: O2 K' i& W J! y) K9 A, s面向对象概念:0 S5 G. t' g; w% [: i J2 E
对象的概念
% ]! |2 d; m( M6 |成员变量/方法与类变量/方法的区别
1 O, k( x3 ?. _# f4 B7 [/ ^& Y类的继承, h, I) x( f" y, F# E6 x
...$ {1 k' g$ [& H& L |8 y
0 h2 S# h/ C: W5 S6 Z2 F
基本上就是这么些概念了.
/ v6 Y: @$ x4 r3 f' {3 ]: ~" Y* Y- a% U+ i7 y8 |- { }
推荐书籍:
" L) A8 {5 t4 J3 ]C++: C++ Primer, Effective C++
5 t0 s" ~0 [$ X' uJava: Core Java 7th Edition/ W8 e [. M. Q8 @# a
C#: C# Bible: f% w# ]3 `# R3 q
Perl: Programming Perl(编程珠玑)
+ M4 Y" E0 p- nPython: 完了我不记得Python我看的是什么了...=_=% B8 d* g" N& ]/ l) s0 j6 M5 ]6 j
)1 e& \* \+ F) |1 T
* f, X$ k7 m/ B- K. C' q7 r# y最重要的是,你得有解决问题的意识.你将着手的任务是谜题,而且它们并不会像你的计算机科学教授特意设计的题目一样,能正好用上这个星期的课教的内容以一页纸或不到的代码就解决.正好相反,你在与原作的日本程序员作战,而他们或许根本不在乎他们数据格式是否能被逆向工程所破解——如果他们在乎的话,他们大概更是会特地把数据加密来让你的工作更难进行吧!
+ A$ O/ T7 l. c
% l7 g C, f& `1 A, r9 E好了,卷起你的袖子,启动你的编译器,戴上你的"思维帽".在这个系列的文章中,我们会把一些范例游戏的攻破过程完整走一遍.希望你能看出自己是否适合破解者的角色...同时也学会几招道中小技巧吧!
3 m& u0 }# R3 E9 g0 S9 r- D2 b9 ^; V0 u2 L
(译者按: PartI结束.上面提到了从游戏中提取图像资源的问题.嗯,也不排除有就是为了CG或者BGM而想加入到破解行列的读者.如果目的只是到这一步而已,那系列的后续文章基本上不看也可以,只要掌握一个基础工具就能够应付非常多的游戏了.这个工具就是Susie.
4 U+ }3 T: R5 }
4 Q( D8 _) n2 m* U( y0 k. ^6 P3 vSusie简介:
( r& b% g# H/ A) A) NSusie是一款在Windows上用于图像浏览的免费软件.凭借其良好的可扩展性,它不但可以用于浏览图片,在加上相应插件后也可以用于归档文件的资源提取,或者文件格式转换之类,因此在日本十分流行.所谓"有其他破解者写好了提取工具"很多时候就是以Susie插件(后缀为spi, spi=Susieplug-in)形式发布的.
4 S1 Y" P7 f+ v1 P
7 G, [ I* C5 t! e+ i% w. q官网: 「Susieの部屋」
' X- F! m, n3 n! a; e4 V( shttp://www.digitalpad.co.jp/~takechin/
. y1 l( w- B; T+ [+ j. U在此可以找到Susie的本体.$ u! ~+ o+ o; @3 o) @; D
" i8 W# f% E' @" l澄空学园CK-GAL区里的工具交流帖里有Susie本体以及相关插件包可以下载:3 Y6 m/ ?' G& J7 ]- o7 ^$ e
http://bbs.sumisora.com/read.php?tid=207722
4 y8 J- K: [5 x0 A5 b' H- z& u. V; o) S0 a6 ]5 M: Q
此外如果需要给Susie寻找新插件,直接在搜索引擎上输入你希望提取资源的游戏名,加上"Susie""plug-in"等关键字就可以了.非常的多...% U( v/ s3 ?6 F: j; ?0 I# c; t
, j$ @1 e$ y% @! n; pSusie简单使用说明:$ R' S. \. P/ }9 A3 j
a! I# X" C. i* }( A& R, l8 ?' |
首先,Susie是绿色软件,安装简单.使用归档管理软件(WinRAR,WinZIP,7-zip等都可以)将susie347b.lzh解压到任意目录即可.1 [6 B- ?8 Z. u/ ^8 j) H1 m0 w
本体解压完成后,将插件也解压到Susie的安装目录下.Susie在不安装任何插件的时候能做的事情并不多,所以在试图提取什么资源前务必确认是否已经正确安装好相应的插件.0 o( g6 Q) J; a2 c* H1 ~0 z9 d
( E3 V$ w I1 s要运行,如果不是在J-Windows或者当前语言区域在日语下的话,请使用AppLocale来引导Susie以日语启动;否则可以直接启动Susie.exe.
# z$ |5 P; H6 Y+ i6 F1 j
) ~" g- M* G) |2 T9 L* R, z启动后会见到一个工具条:3 z% [! s& k/ I' V
) P# P% y/ H' b6 N: q8 F4 @* D. U# v
点击「開」.会看到下面的文件选择界面:1 t J+ ~! B) Y# I! z& ]* e1 n9 p
& a" e% ?8 R6 Q( v; p: Y& i选择需要打开的文件即可.如果打开的文件是归档文件,那么可以直接从打开后的文件窗口里把需要的文件拖放到目标目录.
* k2 p; f& n' s4 [5 R上面的文件选择界面里有"Catalog"选项,点击则会进入浏览/预览模式.
, g; }! M) E! S0 q# w
. n# ~1 c* n% I* w4 [1 A简单的拆档使用就这么简单.其他一些设置请查看susie.chm吧~8 c. a7 Z; H$ r9 x* F
/ a) ]/ N& H7 I$ g/ l
如果不介意使用付费工具的话,其实WESTSIDE对大量游戏都制作了资源提取器.当然其中不少也是Susie插件.
; p/ w! N9 k0 l0 m; V. H详细请见其官网, http://www.westside.co.jp/index.html- D5 R. N& X9 i. Z6 l
$ ~8 D7 J, g; t0 }) U S7 U7 b( _想成为一名破解者吗? Part II: 十六进制编辑器' q0 c. _0 [, E5 A& D& N
1 Z' X( L5 S' B( Z
(译者按: PartII介绍的有两部分,其一是资源文件的常见形式,另一部分是在探究一个未知的资源格式时最可靠的伙伴——十六进制编辑器.在这部分的翻译开始前,译者希望能为缺少编程与计算机相关知识的读者先做些背景资料介绍.同时,在翻译过程中也会适当加入一些内容.
2 I4 x4 u7 |) p: ?4 X译者先前也写了点资源格式相关的文字,灯穂奇譚的文件格式与以ef - the first tale. / Trial Version中资源文件的加密的解析简单介绍几个工具中的[准备工作1]部分)
4 `$ K7 ?4 b! w
( g4 j* z9 [5 p* ~(译者按: 背景知识介绍:
z) h! D7 [1 \% `3 ^2 c- K- T8 {# k( \/ [8 Q# y: E
首先,数字的进制问题.
6 }- }( Z- F* D: E5 y+ g所谓"进制问题",小学生也应该知道"逢X进一"的就是X进制,所以也不需要多说...这里特地提到,是因为计算机所使用的进制与我们日常生活中习惯的十进制不同.这里要说的,是二进制和十六进制.
, D v5 Z. x: N$ K9 s \
) X6 \* K: ~, g5 a* V9 f二进制,逢二进一,每位可以是0-1两个状态.
4 i; g' f+ r- h9 v# \9 s% W十六进制,逢十六进一,每位可以是0-9,A-F共十六个状态.下面十六进制数字都使用0x为前缀表示.没有0x前缀的则是普通的十进制数字.0 _6 u- [3 |2 d0 ^5 T$ K
& c3 Y0 s1 i1 J# C' X
不同进制间的换算就不用说了吧...不过2的补码(2's complement)需要说说.2 X% ^3 ]6 I4 O
! G0 ?: f* z6 _/ ~, N j
计算机里常使用所谓"2的补码"的形式来表达带符号的二进制数字.比起单独消耗掉一位来记录正负使0有两个,2的补码完整的利用了整个n位二进制数[-2^(n-1),2^(n-1)-1]范围内的所有数字.其值的计算方式是:-2^(n-1)*最高位+2^(n-2)&*次高位+...+2^0*最低位. Z& \ ~- ~, j% e7 x1 G. F
) G9 k9 O1 e n2 O$ t. M
非负的2的补码的二进制数与直接从十进制换算到二进制的一样,不过首位必须要为0.% M8 \7 `) [$ j" B: ~' M
负的2的补码的二进制数是通过"取补"(complement)操作完成的.先得到数字的绝对值的原码,也就是直接从十进制换算到二进制的数;然后对每一位都"取反"(NOT),0变为1,1变为0,这样就得到了"反码";将反码加1,得到"补码".
3 f- O! _, \9 E! a4 \* N" I) t对2的补码形式表达的二进制数,加法直接进行;减法转换为加上减数的补码;补码的计算就是"取反加一",其作用就是取得原数的相反数(绝对值相等,符号相反).
R; n, Y' p, K+ c6 e
! q( H; i# d' h当前主流的计算机使用的电子元件都只能支持两个状态间的切换,"开"或者"关".因而使用二进制数字来描述计算机上储存的数据非常合适.但是二进制数字写起来还是显示起来都很冗长,为了方便起见,使用2的4次幂=16为底的十六进制来作为二进制数据的简写表达形式.所以,在查看二进制文件时,一般使用的是十六进制编辑器.高级语言像是C++和Java里一般也只允许以八进制或者十六进制作为简写形式来表达二进制数据.
4 q3 V! K$ K5 E& h" p: {/ J4 z. U0 I ^' S
用十六进制来表达二进制数字的方法:
4 }0 o( h/ Q; g+ r' p$ t$ g将原二进制数字以4位为一组,分别换算为十六进制的对应数字作为一个十六进制位.最靠左的一组二进制数字不足四位的在前面补零.这样,一个字节是8个二进制位,用十六进制表达就是2个十六进制位.
. o9 o; _1 ~# k5 C Z L4 v
# Y" F3 f" H# e0 Y+ g然后,"文件"相关.
+ H) \+ B y9 @" e8 B) u7 {
: }. ]' L/ _- X l" b1 D在计算机上,一个"文件"是在次级存储器(如磁盘,磁片,磁带,光盘等)上储存数据所使用的基本结构.在面向对象程序设计里,文件是一种用于描述次级存储器与内存之间数据传输的软件对象.本段的讨论范围并不涉及"管道文件""设备文件"之类的特殊文件,请注意区别.普通的文件,可以分为文本文件和二进制文件两种.其中,
( j8 `+ O d. u/ Z" M% k6 o7 t6 m/ R7 K. K4 _$ `5 V6 P9 E* F
文本文件是能被人直接阅读的文件.一个文本文件里的数据由一串字符所组成.字符都是按照一定的文字编码所表示的,例如ASCII或者UNICODE.可以直接以文本编辑器打开并阅读/编辑.举例的话,一个整数12345,保存到文本文件之后,就变成了"1""2""3""4""5"这五个字符.! k( Z' P" k$ t) U7 f+ m& p" ^
' t, M1 l3 ?9 ^9 j6 T* P+ }
二进制文件是文本文件以外的文件.这种文件更加简洁高效,但是其数据通常是不满足文字编码的一串0和1,不能直接被人阅读.举例的话,同样是整数12345,保存到二进制文件之后,就变成了0x00 0x00 0x30 0x39这4个字节的二进制数据.
& Z4 K( A" l% N% P) [7 ]. n0 y. T3 ]# c9 X; G3 s* ~! ~1 l
Archive,意思是存档,档案文件.本文内将其称为"归档".这个名词应该并不陌生,因为电脑的日常使用中也经常会用到rar,zip,tar,tar.gz,7z等格式的归档文件.游戏多数都不会将其使用的资源直接分散放在游戏的安装目录下,而会将他们"归档"到归档文件里.这样就能够使文件结构更加清晰,而且也可以在一定程度上节省空间,一是归档的时候可能有压缩,二是大量小文件浪费空间的问题被解决了.因而,通常情况下,要提取资源,首要破解目标就是游戏所采用归档的格式.6 ?& h; U+ ~2 M/ ]# s- a# J
" Z( f$ _6 u/ W2 F游戏有可能使用同样的归档文件格式对其所有类型的资源进行归档,也可能对不同类型的资源采取不同的归档格式,也有可能有选择性的归档.即使放入了归档文件,其中的资源也有可能被有选择性的压缩或加密过.7 B1 x! t/ `) N2 X% i( F
; ?, X+ M Z6 n& X. a) Q
另外,还得提一下字节顺序的问题.( n3 G1 c3 N- X& o/ u; C9 s \
当一种数据需要多于一个字节来表达时,就牵涉到字节的顺序问题.可以认为从前向后读,首先读到的是最高位的,称为big-endian序;也可以认为最后读到的是最高位的,称为little-endian序.例如说,如果要读入一个32位的整型数,读入的数据是0x61 0x62 0x630x64的话,按big-endian读是0x61626364,而按little-endian读是0x64636261.同样的数据如果解释为ASCII编码的字符的话,无论采用什么字节顺序读都是"abcd"(也就是0x61 0x62 0x630x64对应的ASCII字符),因为ASCII字符只占一个字节(8位),而字节序只影响同一数据内的字节排列顺序而不影响数据间的顺序或者字节内位的顺序.3 n3 \2 |, n$ f, [, i
在Mac的PowerPC上,数据一般以big-endian顺序储存.而在我们常用的x86兼容的PC机上,数据一般以little-endian顺序储存.请留意.4 i1 f6 H8 {* B' j" G; ]9 Y
) L# M) @3 J# p& q4 Y& S
呵呵,正篇开始前似乎废话太多呢.好了,开始翻译了.)
8 I6 ~) D# [: D% q# o; n) ^, m- c3 A1 K4 P
那么让我们开始吧.今天我们会看看一个简单的范例归档格式,作为讨论一个"标准的"游戏数据文件的各部分的跳板.把这种模板记在心里,尝试理解游戏数据文件里看似无规律的字节时就会轻松不少.3 M( b" s' M! W- H
0 X0 N; z4 @5 P* g& c这次我们作为范例的游戏是Cross+Channel(体验版的下载链接在该页末尾).这个游戏已经有一个翻译计划正在进行中.喜欢的话下载一份体验版,然后我们来看看.
, v$ Y$ a' V! s- N+ S
1 H) b# ^+ V; Q4 W$ L) I% ~(译者按: 嗯,原文里提到的翻译计划自然是英文化的翻译计划.现在其实也有中文化计划正在进行中.到时候或许就能见到成果了吧 ^ ^)
3 d U9 [3 J7 R7 a$ X$ H& o7 S
4 z6 r- |* b$ P游戏安装后,安装目录下除了可执行文件之类以外,我们还能看到几个文件:bgm.pd, cg.pd, script.pd, se.pd, 还有voice.pd.这非常典型: 多数游戏不会让每个音频文件和图像文件独立放在外面,而会把它们集合起来放在几个归档文件里,然后游戏引擎就可以随机访问它们了.把原始的独立文件从归档文件中分离提取出来是攻破游戏的第一步.
. N# Q- {* L; k- q
$ K9 R# N. t5 R5 V# d8 s8 ~, P现在就该启动破解者最喜欢的工具,十六进制编辑器.这是一类相对简单的工具软件,只是显示文件的原始字节及对应地址...常见的高级功能包括将数据解释为常见数据形式(整数或者浮点数,之类),比较文件,还有搜索特定数据模式等.我个人习惯用Mac上的工具,HexEdit.如果你有喜欢的Windows或者Linux上的十六进制编辑器的话,可以在回复的时候提及,让其他读者也留意一下., ]' |1 e# E% l$ l7 D7 p
|$ B+ {" z; K0 P4 k" D% ~& G4 Y& A(更新: 至今提及的推荐的工具,在Windows上有WinHex,HView,XVI32,Hex Workshop,和文本/十六进制混合编辑器UltraEdit-32.Unix系的操作系统上有HexCurse.谢谢!)
, U7 }/ D: ~! k" S( C
4 A9 b7 d; j( w( U l, Y$ ~(译者按: 十六进制编辑器最基本的显示部分有两个:一是以十六进制方式显示的原始的字节数据,每一个字节表示为两个十六进制位,并且每个字节之间会稍微分开一点;二是于前者相对应的以ASCII(或其他编码)方式解释的数据.有了对应的这个解释部分,我们就可以轻松的看出是否存在有明文存在的文本/标记.
+ M G+ Q0 h/ q n: S" R; Y0 T
8 y* M+ e3 ~6 ?. W译者想重点介绍一下WinHex.它是不但可以打开一般文件,还可以像打开文件一样打开正在运行中的进程的内存,同时还有别的一些方便的工具,像是计算器,十进制与十六进制转换器,磁盘编辑器等...0 E, ]8 ~! A1 f! G* C# ~3 e9 d2 A
6 T1 a) N$ X, z: K
例如说,在Data Interpreter窗口里,可以直接读出光标当前位置之后(包括当前位置)的8位,16位和32位数据(以little-endian字节序解释)的十进制值,非常方便.)/ Y% |$ ~' w5 K4 v$ {/ e: N2 \
3 S0 f3 `& p" _9 m+ ~$ Q
那这些后缀为pd的文件看起来是什么样的呢? 下图是cg.pd的开头部分,我们可以肯定的猜测这个文件存有游戏的图像文件.
2 {( B( [7 q- s8 q+ E0 M+ m& {9 v7 V. v8 S1 [2 c
好极了,来看看: 可以辨认的文件名! (如bgcc0000e.png) 如果你看到的是类似这样的东西,而不是一些看似随机的字节,你应该庆幸.虽然你可能还不知道这些数据到底代表着什么,很明显能看到继续解释这些数据的前路.# i- r& z. _: M t/ X* Y2 M' D% B, q
& i1 c) _- c% U为了更好的解释我们看到的数据是什么,是时候来简单介绍一下典型的游戏归档的内容了.
; p; D# z: V9 F" q5 N
% C7 T8 k! H4 ^; k0 n* T0 G# m, p- 1. 文件头- B- r4 d2 `+ N/ b/ b- U
文件头里包含的是关于整个文件的一般信息.并不是必须的.不过如果在一个归档文件的开头就看到一串不像是文件名的字符串的话,多半就是有文件头的了.0 k$ c! s! v% ~3 a2 M* D; z
-- 1. 特征标记(signature)
3 L; Z9 m" p* x! N 通常一个归档文件都会以某种特征标记字符串开头,好让程序能辨认出归档的格式和版本.你可以通过这个标记来确认当前处理的文件是否属于正确的类型.: v. x; u+ P5 O( z+ \! d
-- 2. 索引位置
$ I/ c$ H* n5 J( [8 \! X; y 大多数情况下,紧接着特征标记就会是归档内的内容索引了.不过有时候索引实际上位于归档的末尾,毕竟归档打包程序要等到归档内的内容都处理完了才会知道索引有多大(例如说内容索引本身就被压缩过的情况).如果是那样的话,会有一个指向索引所在位置的指针.% k7 D9 W, m4 w- t; o) g9 L
- 2.内容索引
3 T* @' g+ T0 S 归档内容的索引是你需要掌握的重要结构,因为你要通过它才能知道如何提取出归档里的文件.
- [# J+ t% b: U. Z) o-- 1. 索引大小
4 k3 S" e, m; j( ~9 u+ ^/ w# y 索引一般会以一个表示大小的值开始.很多时候这个值就是归档所包含的文件数量,也可能是索引所占的字节数.不过索引大小并不一定存在,因为有时候内容索引会一直延续直到遇到一个特殊的结束记录(例如说,包含负的文件大小或者空的文件名的记录,等等).& J& a6 v) ]6 j
-- 2. 文件记录的列表( D. n, I0 T1 J/ o8 o
接下来会是归档内所包含的每个独立文件的记录的列表.这可以是定长或者变长的数据结构,取决于文件名是如何处理的.有时候还会有表示目录树装结构的路径层次结构.每个记录有含有一定数量的标准信息:
% B; Z1 [8 k$ r2 O& w5 H0 E 1. 文件名/文件路径: 可能是以0x00结尾的字符串,若不是也可能明确给出了字符串的长度.信不信由你,文件名其实不是必需的.我至少遇到过一个例子,只保存了文件名的哈希值.& O' {( {: J- E7 E5 F% l4 D0 ^, V& O
2. 起始位置: 这会是一个相对某个位置的偏移量,通常是一个32位的整数.这"某个位置"可以是归档文件的开始,可以是内容索引的开始,或者有时候是"文件区"的开始(就是说,归档内第一个文件的起始地址,也可以说是内容索引的结束之后).
, g0 r3 _6 d: l$ c h* L 3.文件大小:文件的在归档内所占的空间大小,或者是文件的原始大小,通常是32位的整数.当文件在归档内所占空间与其原始大小相等时,文件大小要么有一个,要么干脆就省略掉了,因为可以从下一个文件的起始位置来计算出文件的大小;不相等时,通常说明文件被压缩过.则压缩后所占空间与原始大小都需要在内容索引里记录下来.7 ^0 D% v- t4 l8 w
4. 标志位: 标明文件是否被压缩过,有的话用了何种算法;或者是否被加密过,有的话是否有相应的密钥或者初始值等.1 l' n0 \/ [7 b l8 D4 b
5. 校验和(checksum): 为确保数据的完整性,有时候会记录下文件的校验和.这对破解者来说可能有点烦,因为这意味着修改归档的内容后我们还得把校验算法也跟出来,才能计算出修改过的新数据的正确校验和(不然想办法禁止掉可执行文件里的校验检查也可以). Q. l4 Y3 @" ?& t: |
注意: 有时候这些信息可能会分散在不同位置.例如说,文件的起始位置与文件名放在了索引的记录里,而文件大小和相关的一些标志位却在那个起始位置给出,紧接着的就是相应的原始文件.! X+ I) K" i7 q, G9 U
- 3.原始文件& H- c* n- n+ P. \, J3 ~
原始文件的数据基本上就是头尾相接的放置在归档文件里了.这些数据有可能被压缩过也有可能被加密过.游戏引擎可以通过索引快速的定位到这些数据,因而可以任意使用它需要的文件数据.
( \% G) c* s0 l1 w7 c( p9 D' Z% I* X4 }* N" B+ J6 ?9 W# v
好吧,了解了这个标准模板后,让我们来看看它能如何解释cg.pd里的数据.最开始的PackOnly看起来像是个特征标记.接下来是一串0x00字节,直到我们来到地址0x40,在一串可辨认的ASCII字符串之前有这么一组数据: 0x21 0x02 0x00 0x00 0x00 0x00 0x00 0x00.
" l: \0 t/ Q$ x" m+ K$ ?1 L! _7 J& ~* w' I6 k% h( q
这会是内容索引的大小吗? 说起来,我们应该如何解释这几个字节呢? 我们有几种选择: r9 o% M; |1 ~7 @
6 ? }6 }* w8 s3 R6 Z3 q% [
·随机的标志位. 这里可以看成3个位被置位(set)了: 第一个字节中的0x20和0x01,以及第二个字节中的0x02.(注意这里最好转换到二进制观察每一位的值,是置位(set)还是清零(clear).例如0x21=00100001=0x20 AND 0x01.)这么解释算是有点道理,不过暂时没什么价值.看看其他可能吧.
$ d/ h4 n$ a0 i3 u2 L+ f% M8 B- n6 i' g
·little-endian整数. 这里,0x21是最低字节,0x02是次低字节,依此类推.所以这个整数的实际数值是0x0000000000000221,十进制就是545.这可能是一个合理的索引大小值.$ Z/ d0 O+ b$ }2 K9 B; M4 a
6 K0 N# z3 n% e0 P( @$ m1 ^
·big-endian整数.按这种字节序的话,0x21是最高字节,0x02是次高字节,依此类推,整数值是0x21020000,也就是十进制的553,779,200(这已经是忽略了后面那4个0x00了).这个数据比较不合理,因为整个归档文件才不到500MB.所以如果想以big-endian方式来解释的话,得换个长度,例如说这可能是个16位的整数: 0x2102= 8450,这个值或许有可能,例如说是索引数组的字节数之类.# L3 U" z. A3 j3 H( i
& A# F N% |0 H' G, L4 ?8 N' o让我们来具体看看.从归档文件的开头开始看下来,似乎文件名是在0x48,0xD8,0x168,0x1F8,0x288等位置开始的.也就是说每个文件名的开始到下个文件名的开始之间有0x90 = 144字节.最后一个文件名(TCYM0005c.png)是在0x13248开始的,说明大概有(0x13248-0x00048)/0x90 + 1 = 545个文件记录.
% ? k( U1 P" E! R0 A* q2 A* c, L3 j* E/ l
你看到了什么了么? 没错,就是545! 以little-endian方式来解释0x21 0x02 0x00 0x00 0x00 0x000x000x00应该正确的告诉了我们在内容索引里有多少条记录.而且更重要的是,现在我们就可以专注于那些144字节的数据,确信这些就是一个个的索引记录.而且,我们知道这个归档青睐little-endian字节序,也可能使用8字节(64位)的整数.
% f' C8 U# _2 j Q4 ?6 l3 b7 P& O
* X2 \9 \4 B' }& ]: @那就让我们来看看这些索引记录.不过(这里有个小技巧了)不要看第一个记录.很多数据在第一个记录里都有可能是零,我们就看不出数据的意义了.来看看第二条记录吧,在地址0xD8到0x167:- a2 q# }0 @2 F$ A! y1 U
) ^" v% k8 t, z) e
我们看到了一个文件名,一堆零,和看起来像是8字节的little-endian整数.回忆一下典型游戏归档的模板...我们在寻找的是文件大小和位置的信息,或许还有一些标志位之类.那堆0x00可能会是标志位,不过现在还没办法确定.
9 b1 ]' x' J3 R5 H' p
( d( H7 t( }0 T, a眼下先假设那些零是文件名所属的数据结构的一部分:这样就刚好给文件名分配了128个字节,是一个(懒惰的?)程序员会做的合理的事情.剩下的信息是两个整数,0x002420FA和0x0008370E.暂时还不知道这些是什么数据...还是先多看几个记录吧.内容索引的头几条记录里对应位置的数据是什么样的呢?
5 k O+ o% Y1 c) g# |( [! p9 _& F: ?% w
File 1 0x00240048 0x000020B2
- G6 v0 I/ e3 ~' |4 pFile 2 0x002420FA 0x0008370E ! P% x6 o, _4 u1 ?# [* P8 Y
File 3 0x002C5808 0x00002FA6 , F* B) B: A, c; K. Q& ]) a
File 4 0x002C870E 0x00063B8A 8 r4 k9 Y5 z" o5 u4 x1 N) b
File 5 0x0032C338 0x0006A7CB
* ~! w3 w! r) w& _/ b! \" C" g8 M- \* z( o* @6 S. N
现在这些数据有点看头了.第一列数据总是越来越大,更重要的是它们总是以第二列的值增大! 这正是经典的位置+文件大小的进行.
' V/ j. Z- U: e. I, i- x. [
5 B! t0 }8 y- U" {% }5 O3 r& [- c6 t( a如果我们的假设是正确的,那么第一个文件,bgcc0000e.png,应该有0x000020B2 =8370字节这么长,而且应该在归档内的地址0x00240048附近开始.我们不能完全肯定,因为不知道这个偏移量是相对归档文件开头还是特征标记后还是哪里,而且这个值有点诡异,因为我们知道内容索引是在0x000132D7结束的,还记得吗? 总之先到那个地址去看看吧,因为文件的顺序可能被打乱了, ?3 M: k3 m- }
% u. D' W9 M( Z) Y& D
爽! 我们猜得完全正确,地址0x00240048正是一个PNG文件的开头.无压缩,无加密.事实上,要是我们从这个地址开始把接下来的8370字节复制粘贴到一个新文件里(当然也是在十六进制编辑器里),然后用图像浏览器打开的话,我们就得到了...一张640×480的空白白色图.
, Q$ \; z* ?: e, _: Q
3 `% Z7 @7 }( O0 r- _. z( s: E7 k呃.无论如何,游戏也是需要纯白图的,至少这图的尺寸没错.那么为保险起见,再试试下一张图片.从地址0x002420FA开始复制出0x0008370E个字节,我们得到的是:
( o/ b* y5 I+ a6 _# E3 H" C; x- d, d% R3 p5 e
好耶! 胜利是属于我们的!# H% F- D3 p! W0 W U! O
9 Y$ b4 [; j3 A* \3 f+ K+ R4 l现在让我们来总结一下.我们在这个阶段,认为.PD格式包括:9 L, f+ ~1 W4 f) Q, x
) _. A$ ?: e# a2 A6 `特征标记字符串"PackOnly"
7 p" o& n; z( K# l56字节的0x00
) q. a9 j+ E" S! c8字节little-endian的文件数量值
/ t# q) K& y, a |3 U, X多个144字节的索引记录,每个包括:
; @% |3 Y' C% U3 ?! J/ B% ^ 128字节的文件名,是以0x00表示结束的字符串
% M. M' d+ M' [$ b k. L 8字节little-endian的文件位置(从归档文件开头算起的偏移量)
! d3 L7 Q. p5 ?: L' B$ ]5 } 8字节little-endian的文件大小
' d( \4 I6 Z6 V$ f0 h+ T最后是原始文件数据,无压缩无加密,正好在索引里给出的位置上开始.# {! A& i8 g) x* `3 j- t: n
% t: W4 l6 V: s% o$ p. G. J! y" Y那么,内容索引之后到第一个原始文件之间的这段空白(全是0x00)该如何解释呢?7 t+ W6 M0 p0 s
其实,那个地址,0x00240048看起来很可疑...一个索引记录是在0x48,也就是说有0x240000字节的空间可用于放置索引记录.每个记录144字节的话,就能装下16384个记录.也就是2^14.所以让我觉得这很像是一个(懒惰的?)程序员会做的事:留下足够多的空间给大量的文件用就算了.
4 @+ p5 x: h1 U. t& n, k; M" t: d- c# S
那这段空白是否必要呢? 说不定我们在重新打包归档的时候可以把这段空白清除掉,省下那么几兆空间.要不然我们把数据移动超过1个字节程序也会崩溃...我们只能等后面实践的时候才知道了.
0 ^. W" u, t$ `* V3 y# o3 z4 ], g, U7 d& Q f7 v
好了,下次就让我们把获取到的知识转换成实际的程序代码吧.当然,我们会遇到些障碍,嘿嘿: c* j, Z. z+ y- G2 v" I9 A+ w/ I& s
4 T; R9 u; o; l5 ~: o U1 }- a(译者按: Part II结束.这个part所讲解的例子是一个非常简单,无加密无压缩的归档文件的格式分析.
4 t4 Z/ P5 Z8 b( Z+ h+ ~7 }% m简单说来,如果被分析的归档文件比较典型且无加密无压缩,那么就按照典型的游戏归档的形式,找到内容索引后,猜测索引内每个数值的意义,并且到归档内猜测的位置寻找原始文件.
9 k0 N" l+ m3 |) A& a: r译者的经验是,如果一开始就以脚本文件为目标的话,过程会比较痛苦.因为脚本文件经常是纯文本文件或者一些特制的格式,不一定有明确的起始标示,不便于确定是否正确定位到了原始文件的位置.所以,可以尝试对估计含有图像的或者音频的归档文件下手,就像本篇的例子以CG归档为破解对象.
/ j1 `2 k$ d# _! _2 y1 x: ` `' p }0 p! _$ q: n7 d e) h* `
为什么要针对图象,音频和视频下手呢? 因为业界在许多时候都会使用标准的格式来储存图像,音频和视频文件.0 S+ X9 ?% f9 M4 H3 d
下面列举几种常见的文件格式,以[格式名]: [特征标识串]表示+ H/ |) t/ A0 _" F% O. i+ n2 P
- j9 N+ g: }% N6 A& t ^7 q
图像:
0 ^. Q! I3 K8 z3 {BMP: 0x41 0x4D (BM)( {! h) b/ }9 M h/ z
PNG: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A (.PNG....)
: k) N2 i3 O) \& x2 XGIF: 0x47 0x49 0x46 0x38 0x37 0x61 (GIF87a) 或 0x47 0x49 0x46 0x38 0x39 0x61 (GIF89a)# A" \6 J* m% G
JPEG: 0xFF 0xD8 0xFF 0xE0 0xxx 0xxx 0x4A 0x46 ( . ...JF)# }& [: K, a7 ]9 i7 r5 h
0x49 0x46 0x00 (IF.)
; D: `! c8 W0 _ c6 n2 `' X, o8 D+ Y. o- w4 l% P
音频:+ r# F( C$ ]+ j8 K" U
OGG: 0x4F 0x67 0x67 0x53 ("OggS")# d' Q) n! t/ m4 W) V! m
WAV: 0x52 0x49 0x46 0x46 0xxx 0xxx 0xxx 0xxx (RIFF....)
7 U, W2 `: d c. P& a 0x57 0x41 0x56 0x45 0x66 0x6D 0x74 0x20 (WAVEfmt )
. {+ s3 I6 D7 k% o( {5 a) f2 \
. z6 y1 c' s b- Q/ ^7 t) a' o! Z视频:& v6 Y! L8 z; j/ M) S( A
MPEG: 0x00 0x00 0x01 0xBx
3 f3 Z1 i9 d% [1 b3 u9 P8 t0 U1 yAVI: 0x52 0x49 0x46 0x46 0xxx 0xxx 0xxx 0xxx (RIFF....)" K4 e4 c6 y' `' m# ^6 I2 i
0x41 0x56 0x49 0x20 0x4C 0x49 0x53 0x54 (AVI LIST)
! A6 X p, ]1 H" w+ Z2 v9 L4 B8 Q" W0 Q: t' s9 R$ U
在找到这些特征标识串后,我们就能轻松确定 1)是否存在某类型文件 2)文件的起始位置/ o" ?3 h. A$ p5 {& [
从而可以与文件头里的信息进行对比,判断数据的意义,然后推广到游戏中同格式的其他归档的处理(例如含有脚本的归档)! b# c% x9 v* O; U- M
9 F+ x. c7 O9 j( w# N更多更详细的文件特征标识串,可以在这里查询: http://www.garykessler.net/library/file_sigs.html K) O F9 A2 z u+ g
( x( J# R) i& v) o. H1 f
Part II里举的例子"太过典型",让我们来看看可能发生什么简单的变化吧.同样是无加密无压缩的归档,灯穂奇譚里的AOD格式的归档虽然也有内容索引,但却被分成了一段段.详细请看灯穂奇譚的文件格式.- G3 K* E( g5 Z5 N8 o4 T
# v0 G. z0 s0 u% |# a另外,区里另一篇帖子的例子更加有趣.ONE ~輝く季節へ FullVoice 汉化实战篇,其中的归档与其索引是分开在不同文件里的.再次提醒我们索引的必要性以及可能需要变通的地方.
* x9 Y- i0 R- ]/ [2 d
6 X) X8 p2 O$ {) T3 t2 v不得不注意到,并不是所有游戏都会把资源都放入归档内的.3 K& J9 q* o9 ? d4 p! Z9 @6 V2 U0 t0 G
举个例子来说,Visual Art's旗下制作组所使用的RealLive,会使用GAMEEXE.INI文件来配置是否使用归档.其中相关的一段:#FOLDNAME.TXT = "DAT" = 1 : "SEEN.TXT"
, V( S- }$ s/ t8 d4 t#FOLDNAME.DAT = "DAT" = 0 : "DAT.PAK": z- {$ d6 g- T" T
#FOLDNAME.ANM = "ANM" = 0 : "ANM.PAK"" C- @& X9 d- q: q' x$ }
#FOLDNAME.ARD = "ARD" = 0 : "ARD.PAK"
* ]: a( n6 k+ ~#FOLDNAME.HIK = "HIK" = 0 : "HIK.PAK"
4 k9 H; d/ R& }: K0 k#FOLDNAME.PDT = "PDT" = 0 : "PDT.PAK"
9 R8 x3 X3 ~" t K' y2 N$ _#FOLDNAME.G00 = "G00" = 0 : "G00.PAK"
8 W" S' v6 s* A" A#FOLDNAME.M00 = "M00" = 0 : "M00.PAK"
9 s# W* R/ ]* ?#FOLDNAME.WAV = "WAV" = 0 : "WAV.PAK"
6 w. A/ X. g1 e+ O$ O8 i( d#FOLDNAME.BGM = "BGM" = 0 : "BGM.PAK"
" V6 N3 w6 A# K i#FOLDNAME.KOE = "KOE" = 1 : ""
: C( O! ^. f/ z/ V#FOLDNAME.MOV = "MOV" = 0 : "MOV.PAK"
; k g: F5 q+ q#FOLDNAME.GAN = "GAN" = 0 : "GAN.PAK" 中间的数字就是说明是否使用归档的,是的话值为1,否的话值为0.这个还要与后面的文件名相配合,即使前面的值为1,假如后面的文件名为空串的话,也不使用归档,而是直接把一个个资源文件独立放置在安装目录下.上面这段引用自智代After的GAMEEXE.INI,可以看到只有脚本资源被放进了归档里,文件是SEEN.TXT.把这个归档文件拆开,就能看到里面实际上是许多小文件,SEEN0628.TXT到SEEN9072.TXT.而这些小文件又是编译过的脚本., x! q/ U! D2 I. I7 f' P
- M& D# r. q( A" M' d$ T( _. k2 V
就算是放进了归档里的资源,要分析其内容索引也不总是这么轻松.假如说有数据被加密或压缩过,情况就会相对复杂一些.
( \: B: @# \9 F6 f$ O' D' w/ G也举个简单的例子吧.呵呵这个可是运气/RP大爆发的例子...
! H1 W! n- q @, g8 u( u; L1 _7 l |& Z) C+ F9 C1 q5 @- d
在はるのあしおと的web_trial里,归档文件的后缀是paz,都被简单加密过.要使用上面的经验来处理加密过的归档显然不实际,至少也得先解决解密问题.怎么办呢?
8 P1 v$ r. _; v/ T0 {: Z3 g! K8 P; f6 v
由于业界经常在音频格式上选择使用OggVorbis,而且在游戏的安装目录下发现了"ogg.dll"和"vorbis.dll"这两个用于处理OggVorbis文件的程序.所以我们大胆猜测bgm.paz里包含的背景音乐文件是采用Ogg Vorbis格式的,并由其入手.7 _/ C) N) Z' A* M G
V5 Y% H4 x8 _% x) x6 i
用十六进制编辑器打开bgm.paz,发现里面都是些无法识别的数据.从头到尾浏览过之后没有发现形似文件头或者内容索引的东西.因此猜测文件被加密过.
: y! }7 |2 d& s6 D# Y% i) G. o2 y4 t! Z5 m( G0 X
上面说明文件的特征标识串时提到了,Ogg格式的文件是以0x4F 0x67 0x67 0x53("OggS")开头的,注意到中间有连续两个字节的值是一样的.如果运气好,文件只是被简单的加密过的话,那么加密后的密文里这两个字节的对应位置的值也应该是一样的;同理,拥有相同值的连续两个字节,其之前和之后的字节的值应该不同.根据这条线索,让我们来找找文件中有符合这个特征的地方.
. |$ m0 Q( P* Y ~- e" Q5 e) w5 D/ a) W8 h( ~- Y2 n
很快我们就注意到了这个地方.红色标记出来的部分就是我们找到的一组符合线索的数据,0xB1 0x99 0x990xAD.先看看0x67可以如何对应到0x99上.还记得本篇开始时背景知识里提到的2的补码吗?把0x67和0x99分别展开,可以看到它们正是符合互为补码的关系.将头尾两个字节也对比一下,发现符合相同的关系.于是可以大胆猜测,整个文件都是以取补码的方式简单加密过的(虽然这个猜测不完全对...嘿嘿不过还是很RP吧...).! J7 }2 ~5 `* Z$ k
0 F$ P$ c9 G: P* y
于是把想法实践一下,发现简单解密处理处理过后的文件已经相当的可读了.这样就可以继续按照典型的游戏归档文件的处理方法来处理了.
$ U! d( D7 i- ~% w4 b2 f5 y注意到,这里bgm.paz的头4个字节我并没有做取补处理.原因见以ef - the first tale. / Trial Version中资源文件的加密的解析简单介绍几个工具中的[准备工作1]部分所写的内容,这里就不重复写了.# C3 E' f. o$ t( k2 b# \
/ l, J. [) x0 a3 S! s8 b, o" C% u虽然通过简单的观察就能破解出归档格式是很RP的事,不过游戏厂商在决定使用什么加密/压缩方式时本来也很RP -- XD6 B/ a2 c2 x; B3 t% W, [. Z
简单观察法适用于无加密无压缩,或者只做了简单加密的归档.简单的加密方法有: 1)加上或减去一个常量;2)取反(NOT)或者取补(complement/negate);3)与常量做简单的异或(XOR).如果是上述的3种情况,那么加密后的密文里字节与字节间的相等/不等关系会得以维持,因而可以让简单观察法顺利进行.
6 I3 H) W! n# ?& s. r0 p
[/ {5 w: |3 \6 _0 [再举个例子的话...嗯,CIRCUS虽然一直不怎么用归档,不过它的游戏的脚本文件(后缀MES)对文本部分加了密,用的就是加上常量的方式,所以很容易由观察得出.) a4 ] s3 @ s7 V3 @0 V
)
$ f, F" A6 `; C) Q. E& y
3 ]+ L* m8 r; C. C, n$ F
2 S, q/ @1 d' }: F, j) F想成为一名破解者吗? Part III: 构建代码原型
- S" ^, b( ~/ n
+ a; k, w# L$ Q( g' L" Q(译者按: PartIII可以说是"实战"的重要部分,也回答了"高级程序设计语言在游戏破解中有什么用"的问题.在这篇里,Ed介绍了以Python语言编程为手段,对Cross+Channel的归档文件进行拆包,打包,修改等为翻译而做的破解中必须经过的一些步骤.在开始翻译正篇前,照例也写点背景资料吧.不过这篇原文里的经验成分就非常丰富了,相对来说译者也没有多少可发挥的空间.毕竟译者也缺乏经验,才入门没多久...)
4 D. `7 J7 j( m6 [" ]. f' J$ W7 ]% \& s, s0 u: I% }9 K7 @4 a
(译者按: 背景知识介绍:
4 `, N! V5 r1 ?$ M4 C- Z: r4 X3 a7 N, q
在开始编程之前,我们需要知道在这篇的范围内,编写程序的意义.这是个相当浅显的事实:程序能做到的,手动也肯定一样能做到.编写程序的意义就是为了提高过程的自动化程序,让电脑做它最擅长的事——完成烦琐的计算和重复的操作.除非...你有毅力,能手动把成百上千,甚至上万的小文件慢慢复制粘贴出来,修改过后再复制粘贴出一个新文件;又或是遇到简单的加密时用原始的纸笔计算来完成整个解密过程...不然,还是写点程序吧.一劳永逸.虽然不否认有人能做到,复杂的加密和无论复杂与否的压缩,徒手完成所有计算不是一个普通人能在合理的时间内完成得了的事 ^ ^
7 A, ~6 S: c ~6 w6 N; F0 V8 N2 f
从某种意义上说,拆包程序的核心就是把我们对归档文件的结构的了解转换为一个循环:- H* E5 S8 D! B% U" J* p
(在归档级做解密或解压-)读取内容索引信息-读取原始文件信息-(在原始文件级做解密或解压)-写出处理过的原始文件数据.
9 O( S8 r8 z& ?) d3 s如果归档里索引位于文件的开头的话,那么打包程序的核心就会是这样的一个循环:
# E) B8 h) M0 g& e @(在原始文件级做加密或压缩-)收集索引所需信息-依次写入内容索引-依次写入处理过后各原始文件(-在归档级做加密,压缩或计算校验和).
3 p6 O( i) K. p% y( O相当的直观吧? 其实拆包/打包程序一点也不神秘,多数情况下都只需要会编写最基本的文件输入/输出程序就能完成.8 h" B& d$ G1 y
# F1 @4 A$ u9 B4 o) ?学习编写拆包/解包的程序,你将能更充分的理解归档内各种数据(特别是索引内,每个记录里的数据)有什么用,如何使用等.从程序的需求的角度出发,一切都会变得明了.切记,归档内没有什么数据是真的没有任何作用的;如果你觉得有"无用"的数据,多数情况下说明你尚未理解该数据的使用方式,反而应该留心注意.
3 U# W- g3 `2 |1 e
! ^$ N8 }3 f# n- o! T: x举例的话,Cross+Channel的归档就可以.就像PartII里提到的,内容索引里记录了文件大小.但是我们知道,文件大小并不总是必要,因为可以从下一个文件的起始位置来推断出当前文件的大小 =当前文件的起始位置 -下一个文件的起始位置.编写Cross+Channel程序的程序员似乎傻乎乎的浪费了空间存了不需要的数据呢.真的是这样吗?阅读下面的正篇你就会知道,Cross+Channel的归档里文件的顺序并不固定;也就是支持乱序存放,索引记录先出现的对应文件不一定在索引记录后出现的对应文件之前.不记录下文件大小,游戏引擎(当然我们将要编写的处理程序也一样)要处理这种归档就会非常麻烦.) |; v/ y( A; }2 x
* v! F/ {: O- `. Q8 d
再举minori的はるのあしおと或ef之类的例子,归档里每个记录都有3个同文件大小相关的值,而且经常3个值或至少其中头2个值是相等的.为了了解其意义,需要寻找3个值都不同的情况并加以分析.
# {' Q9 m) ]6 I0 ]- j. R
6 b6 K6 z* [, e8 ]8 f/ Y! b+ d回到编程的问题.嗯,我们说要编程,但是什么叫做构建代码原型(code prototyping)?
, d! r0 Q; i* T8 E i因为我们对归档格式的猜测很有可能不完善甚至有根本上的缺陷,一开始编写出来的程序代码有可能经过多次不同程度的修改.如果一开始就想把代码向着"最终发行版"的方向写,一味注重程序的运行速度,再写上一堆图形界面相关的内容弄得花里胡哨,注意力就会被分散,且被频繁修改的代码也有可能很乱.所以,我们可以构建代码原型,先把最重要最核心的部分写出来并加以测试,在反复修改确认其正确性后,如果有兴致可以方便顺利的写出所谓的"最终发行版".如果在程序设计语言上有个人偏好,甚至可以在构建原型与后期编写发布用程序时使用不同的语言;有些语言做脏活累活确实是比较方便.... t P2 ]; {( g% D( S
% d1 N/ I2 u3 A! a, l; ^PartIII的正篇里,Ed的一个重要技巧非常值得提倡,那就是不要只顾着以"已知"的理解方式来处理归档,而要记得检验"不确定"的部分是否也与猜测的相同.因为我们在猜测一个归档的结构的时候,多数情况下都不会手动把归档内所有文件都解出来来验证猜测的正确性,所以很有可能漏掉了一些数据或者一些例外.写好原型代码后,我们就能快速的定位到这些例外所在,然后回到十六进制编辑器里继续分析., v+ f) [/ n _6 Y* F
' N, [# S5 e% l; J5 i, }对程序设计语言不熟悉的,请尽快对各种运算符熟悉起来.这样至少读代码的时候能知道在发生什么事.7 |& o1 [* m7 e5 E
许多语言里的运算符都与C的相同或相似,所以以C为例的话,基本有:4 L& Z5 j1 E8 x |/ o7 L0 e
算术运算符: + (二元加), - (二元减), - (一元取相反数), * (乘), / (除), % (取模,也就是相除后的余数), Z' a) @7 |2 K4 V! X
逻辑运算符: && (与, AND), || (或, OR), ! (非, NOT)4 g. f. V8 m0 m/ ]7 }5 n+ ]
按位运算符: & (按位与, bit-wise AND), | (按位或, bit-wise OR), ~ (按位非, bit-wise NOT), ^ (按位异或, bit-wise XOR) A0 n7 c$ I/ u2 M* H. x
移位运算符: << (左移位), >> (右移位)
8 o3 P/ U' T9 z' R注意到单个等号"="是赋值符,在上面除&&和||外其他运算符后接上一个等号是简写的运算并赋值.要验证相等性的话,用两个等号"==",切记.* p! `5 |/ ]! Y4 P: x
在C-like语言里把"="与"=="是常见手误之一,要小心.
* f+ B" ]* N) d+ T3 X; m
% o7 ]# o9 U% f! i' B* z* R7 g! FC-like语言中如Java,C#,JavaScript等会有>>>的按位操作符,意义是无符号右移位.
. n) W( k: a) G z9 l1 r# D$ V, a& a' L& e% E
如果对逻辑运算不熟悉的话,下面列出了4中最常见逻辑运算的真值表,T表示真(true),F表示假(false):0 X0 s; @: _4 S4 Y% V3 _2 g
7 O/ I) M, y! q) |
与 AND: 二元运算.只当所有操作数都为真时,结果为真* y4 ]6 F% b$ A3 h, c6 t& w
* U( w( n% A* u- |( h6 c3 DAND T F
, s/ Q) ?4 U6 u, T- W- L T T F
r+ m! P6 @% g, g F F F: m" j; k$ g C! x" k( f- W
$ O" V: X! @) G: V7 Z
或 OR: 二元运算.只要操作数中有为真的值,结果就为真$ A E3 I* J9 z7 t2 g# A
* `1 w% E+ `% {1 g$ O7 v
OR T F
2 v, t! h+ B- E; l8 RT T T8 Y0 X7 j' p1 b
F T F% N- I* `8 z) t/ O5 o& A5 M V4 b& U
, J6 s! [6 @1 Z/ G- c) l2 X$ W8 `非 NOT: 一元运算.将操作数的真假值反转
1 g5 p. F4 Y# ]* G
" M% z+ R m0 Z: ENOT T F
& n- X! g N0 i+ Q8 g F T! z+ _6 A; F" {, Q# r7 h3 [
9 _; c# Y9 z- W' |1 {5 E* t K异或 XOR, exclusive-or: 二元运算.只有当操作数不同真假时,结果为真8 ^, K: S/ u; Q9 h/ h7 P
( Y2 r4 w( v1 a/ AXOR T F+ u' y0 l9 C9 n) I3 x$ w; J O
T F T: ~8 h5 P3 R/ X8 {' Y
F T F
* g! D# j) C3 p, [. k W- M
9 s4 F2 f" G* \: I由于计算机使用的二进制数字,每一位只能有0和1两种状态,与逻辑运算里的真假正好吻合,所以逻辑运算也用于位对位的二进制数运算上,也就是按位运算.这时,0等价于false,1等价于true.
% u+ N# H2 r7 }: [4 r$ p! q- E
* b, l7 P' D0 |; Y" [3 m2 e! v( i5 R又说多了...赶紧进入正篇 ^ ^)+ J" r# S! {6 b* t$ q2 k/ M
: J1 ^: F8 W4 x3 o# y) K1 m( a4 Z在上一部分里,我们用可靠的十六进制编辑器分析了Cross+Channel的归档文件格式.在概念验证(proof-of-concept)性质的手工抽取图片后,我们相信我们已经知道那个归档文件是怎么一回事.但是,要想确定的话只有一个办法:写一些工具,然后修改游戏试试看!1 z0 w4 d4 A$ Z: p2 y* d4 X4 W; X
& s3 V* o' t4 ?0 j7 G9 I3 K' Q今天我们会尝试快速的代码原型构建,编写一些适用于处理那些归档文件的工具.我的意图是让这过程尽量的简单直观,这样,如果你看完这部分之后会说:"你是说要破解一个游戏只要写那么点代码吗?!" 那么...任务完成.
& T0 Z& L6 z D8 t) N0 n$ n" {: h. j( ]) B
我们应该使用什么语言呢? 平时我会用C,不过我也想从这系列的文章中获取点什么,所以我们转而使用Python,对我来说也是一种学习经验.这种语言的主要优势在于:
. K$ W* W7 h4 w# |7 B3 b( X
3 ?# M1 l/ X6 F+ H+ h% W) @+ Q4 S·跨平台. 它不但在Windows, Linux, Mac OS上都可用,而且还统一了一些操作系统特有的特性,如目录分隔符.+ A! i, h9 I6 S: w# f. @/ Y, q' q$ p
·适用于广泛的范围. 它提供了许多高级数据结构,如哈希字典(hash-table dictionary)和function continuation,同时也提供适当的低层的位操作等.
6 \: ~% g& Y) R0 J( V1 S(译者按: 由于译者见识尚牵,没在中文资料上见过function continuation,翻不出来=_=笼统来说就是可以将当前运行状态保存下来留待以后使用,就像把栈压到了栈里一般.事实上Python还支持一种较少见的数据结构closure(闭包),意味着可以嵌套函数调用并将其返回.)
5 y8 p8 s s+ e. ~·直观的语法. Python几乎如同可执行的伪代码一样,所以如果你知道任何其他语言,你都应该能轻松读懂Python代码.
3 N' o* s6 k4 c& r" F ?8 f P( j# v7 e·交互模式(interactive mode). Python解释器可以在交互模式下运行.如果你想的话,可以在交互模式下把指令一行一行输入,代码会得到立即执行.这对构建原型时避免重复的编辑-重编译-运行测试的过程有相当的好处.5 `& K1 ~/ g3 l; i2 z
, @4 r# A& s; h6 O) p
很不幸的是,Python的主要缺点是运行速度慢.Python在对付位操作时的速度确实不能与C或者其它彻底的编译语言相提并论.我们这里的应用场景下这倒还不算太糟糕,而且就算慢得值得注意的话,其实也可以通过良好的Python/C接口来搭配代码.
% ~- j4 E/ Y/ I+ z Y; C* H+ o! f$ V8 \! Z+ `
(译者按:其实随便拿一种语言都能写出一堆优缺点.要说跨平台,只在运算和基本的文件输入输出上,C/C++,Java,Perl等一样跨平台,虽然可能得重新编译...要说易懂的语法,其实C-like的语言长得都差不多一个样,光是"读懂"恐怕不会是什么问题.译者想再次提起的是,选定了一种语言就用就是了,考虑到我们这里的使用场景,对语言太挑剔是无谓的.
' M/ Q1 g5 O# z, }( V% x# z% g3 Y, z, K
不过Python有个重要优点,Ed似乎没提到.Python是使用缩进来定义代码块的,因而写出来的代码几乎天生就有良好的风格.有些人或许对强制缩进表示反感,译者认为至少这对读代码的人是很大的好事.
% w# S0 f& r4 ^, d: ~5 L; O; H& D& O
说起来haeleth对OCaml很执着...无论到哪里他都不忘宣传一下OCaml的好处,呵呵,原文的回复里他还顺便踩了踩Python...)% b2 Y$ p& I4 `1 J4 Z; Y) B0 K$ |
2 m5 s- u l* |/ x
如果你使用Mac OS X,你很可能已经有现成可用的Python了.如果使用Linux,你可能已经装了,否则也可以轻松下载到一个安装包.如果使用的是Windows,这里也有为你准备的安装程序.& W5 r. W' Q& q$ f# I: _9 X% `0 P
/ w4 |: ]6 D9 P; G1 G! ~
我们需要开始编写一些什么样的例程(routine,在不同语言里也可能叫做函数(function)/过程(procedure)/方法(method))呢? 还是让我们先回顾一下上次确定的归档格式:
. M; e2 ^9 y$ ^
8 K# ?3 E7 w+ l3 p特征标记字符串"PackOnly"
1 r2 d7 K3 M3 r56字节的0x00
/ h# E0 b! U g& H: U8字节little-endian的文件数量值6 y: X/ A; x1 Q# P4 \. c
多个144字节的索引记录,每个包括:
* L$ Q; b1 X8 Y! Q2 d# I 128字节的文件名,是以0x00表示结束的字符串* }0 W# y3 ?( V7 ?# q
8字节little-endian的文件位置(从归档文件开头算起的偏移量)/ P8 i9 c3 `$ |, v1 a' H7 R3 l
8字节little-endian的文件大小- H4 \2 w6 ], i* ] x; }
最后是原始文件数据,无压缩无加密,正好在索引里给出的位置上开始.
4 Y e3 G; j( x' H2 p
+ t, Y C5 S6 C, ~- E那么看起来我们需要下面的一些例程:6 |8 Q8 V4 M, y. n. p
: q) }: Q6 y% c( A- y
·读/写特定长度和字节序的整数
7 ^* [& ?0 g1 ~" C" S1 Y$ k·读/写以0x00结尾的字符串
) {6 {# H& E5 E7 b8 ~4 M·检查特征标记和已知的零串
0 y: @- O) n8 M b L" b/ c& l* S) V0 c* W5 k
轻松! 这类实用例程以后起来会很顺手,所以写了之后我们也可以在后续处理的游戏里使用. 作为例子,让我们先解决掉整数的处理:def read_unsigned(infile, size = 4, endian = LITTLE_ENDIAN) :3 G3 D/ ^+ x5 J) v1 J3 j7 J
result = long(0)* b) M _6 g. c0 r9 \6 P
for i in xrange(size) :9 g3 V9 ^; R. W1 ^( m
temp = long(ord(infile.read(1)))# B1 o; @* F [9 C! P: |" P8 `
if endian == LITTLE_ENDIAN :+ B% N( G: }8 t) D
result |= (temp << (8*i))5 d/ q9 n7 A: {( s! Z* k" \. G; A
elif endian == BIG_ENDIAN :3 `& \5 S2 N8 z
result = (result << 8) | temp( h* k( z9 T6 @) E( y2 F
return result (译者按: 如果你是刚接触Python的,请千万注意缩进.在论坛上贴出来的代码的缩进可能乱了,千万注意)
0 E7 s5 v% J8 b( u, J! R' e ^' ?, S: ]) F, {; J
取决于你的经验,上面这段代码片段可能是微不足道的也可能让你完全摸不清方向.基本上,它只是从文件里一次读取一个字节,然后按照字节序的规定(最低字节在前(little-endian)或者最高字节在前(big-endian))把那些位移动到正确的数位上来构成一个多字节数字.
! j' m5 z& f1 ?3 q& g; H1 f$ v1 k; P! I$ i+ j H; _
我们需要的另外几个例程也是类似的,说实话也没必要在这里详细讨论.我写了一个实用例程包,需要的话可以下载insani.py来用.
: s: ~2 w$ m9 d( H5 _5 n! W7 w N! G3 v5 q
现在让我们来看看问题的关键部分,读写归档!6 B0 _ R3 v/ u2 d' ]2 Q- P; K
+ j, r, B% F; T8 p基本上我们只要遍历一遍上面我们的归档格式,并将其转换为处理代码就可以了.一开始是特征标记和文件头: from insani import *
/ l% y4 z0 s5 C, \( barcfile = open('cg.pd', 'rb')& u% S+ Z+ a- D
assert_string(arcfile,'PackOnly', ERROR_ABORT)3 j! u( Q5 r: C8 v. i% E6 B
assert_zeroes(arcfile, 56, ERROR_WARNING)
: I7 |: T, y# n- Y2 @) znumfiles = read_unsigned(arcfile, LONG_LENGTH)* B4 m, w, `* U4 {) P; j
print 'Extracting %d files...' % numfiles 这里我从insani.py里用了assert_string()和assert_zeroes()这两个实用例程来检查我们预期的特征标记和一串零.常数LONG_LENGTH就是8,归档里使用的整数的长度.Little-endian作为缺省值,实际上隐式表达了.7 k! d. e. f% J" Q5 q# P; y
- t, w, @8 W c
这里要注意的重要问题是错误检查.我不是在说"常规的"错误检查,例如确保文件已打开,或者是否有足够内存读入一个8字节的整数之类.那类东西对一个原型工具来说有点多余: 我们才不在乎文件不可读时程序会崩溃,事实上我们反而预期着这样的行为.
$ w0 |* k! T9 K0 H8 G: Q/ ?& r) X
" N# x+ i$ X" w4 J5 ^不,我所说的错误检查是指检查我们对归档的假设.你看,我们完全可以跳过头64个字节,直接跳到读numfiles的语句.其实那才是我们所需要的数据,不是吗?为什么我们要特地检查特征标记和那些零呢?
$ M) Q5 k, v0 D" v+ O2 d& l
, l& I# X9 D. Q0 X$ u6 T K7 g1 Y- M我们这么做是因为我们还在检验我们对这个归档文件的理解...我们其实只看了一个游戏里一个归档里的很小一部分.那56个零真的没有用吗?总是没用?我们无法确定.所以如果我们写的工具读到了它没有预期到的数据——就是说,我们没预期到的数据——那我们就得知道发生了什么事,要么在"我完全混乱了"的时候中止程序,要么在没那么关键的不符情况至少输出一条警告信息,这样我们可以观察问题并更仔细的改进工具.
. j* ?9 p* w& |" G4 `
1 s$ g$ }/ [0 m. {好了,这个问题说得够多了.相信我: 多写检查你的假设的代码而不要直接跳到"已知重要"的地方. 现在让我们继续读取内容索引: print 'Reading archive index...'( Z1 Z" L; Y4 |' s
entries = []
+ Q( h3 ?$ i/ j9 A8 o$ ~for i in xrange(numfiles) :
0 g; L. x- S& @ filename = read_string(arcfile)3 s2 _1 h0 a: q+ d4 y" X
assert_zeroes(arcfile, 128-len(filename)-1)/ {2 q! i- k7 r$ T
position = read_unsigned(arcfile, LONG_LENGTH)
! t7 s# }, O: k size = read_unsigned(arcfile, LONG_LENGTH)
% G* w5 s) F5 S7 e! b6 B0 U4 Z entries.append( (filename, position, size) )
& f7 L! U: Q. H0 }+ H1 wassert_zeroes(arcfile, 144*(16384-numfiles), ERROR_WARNING) 这里我们用了点Python的神奇之处来让问题更简单.在读取文件名,位置和大小之后,我们把这三个变量收集到一个"tuple"里,以多出的一对括号表示,并把它插入到链表里.在C里做同样的事情会稍微复杂一些,而在这里几乎只是伪代码一般.再次,请注意断言语句(assetion statement): 这个文件里的每一个字节都要得到检查,无论是有用的数据还是我们不感兴趣的地方.- \7 m C3 }0 Q1 W
2 \# v. j' V! T( c9 q) q现在,我们完成整个过程,实际写出文件,并且稍微做一下清理工作: for (filename, position, size) in entries :* M4 X$ c6 p# C
print 'Extracting %s (%d bytes) from offset 0x%X' % \ l# T- f9 O) F3 O! _
(filename, size, position)# [% _; X1 f- l9 h3 Q
outfile=open(filename,'wb')( U; B {# S5 s/ M' S5 V3 c4 M
assert (arcfile.tell() == position)7 ~& b: T0 n% c' h( o
outfile.write( arcfile.read(size) )
' p$ b! @( g, t p outfile.close()) x8 J; \! g3 l; o. ^- B3 u
assert (arcfile.read(1) == '')& a0 e P; Q% f$ C
arcfile.close()
! u! [: Z4 r+ n( M3 }/ |(译者按: 注意这里第二行末尾那个"\",这意味着将接着的换行符转义掉,于是可以把对写在一行上太长的代码写在多行上)& y0 ], _* V" S! A$ p$ u
, @: e5 X4 Q% k# f0 t" ], {这里,循环的结构又是Python的一个好地方: 我们可以毫不费事的遍历entries链表,每次取出整个tuple并且再次使用那些变量.与其用assert语句,我们本来可以使用arcfile.seek(position)指令来直接跳到我们想要的地址,但是我们还在检查我们对归档格式的了解: 归档里文件是否都如我们所料,按顺序放置并且中间没有间隙? 在处理完所有文件后,我们真的来到了归档的末尾吗,还是说还有被遗漏了的数据?
5 }3 D3 b8 K8 c R* R, n! A
: _! K3 d! ?0 b' \如果我们都做对了,那就不应该有任何意外发生.那就让我们来试试看吧.如果想的话,你可以下载crosschannel-extract1.py,不过我建议你自己实现一次,至少自己敲入这些代码来找到这种编程语言的感觉:
/ j% }# q2 D" {/ H# D; w % python crosschannel-extract1.py
" B" s1 ~& ~3 S, eExtracting 545 files...6 p/ ^/ t8 `* z. y" P
Reading archive index...
0 r& }$ S5 M5 q1 WExtracting bgcc0000e.png (8370 bytes) from offset 0x240048
M. d$ W9 \+ TExtracting bgcc0023.png (538382 bytes) from offset 0x2420FA! n7 ]2 b+ s. _/ t
.../ x! @6 N% O, t7 B
Extracting TCYM0005c.png (156881 bytes) from offset 0x566E1B4 , K! i3 U8 D. k, D3 q$ y
- k9 n- e6 ]; j( L/ }8 w
看起来工作正常了,而且所有提取出来的PNG文件都可以用你最惯用的图像编辑器打开.唔,不过之中有一些是NSFW,顺便一提 ^ ^; (NSFW = Not Safe For Work,意味着有糟糕内容 XD)
( a1 u, i2 F, E+ c7 K4 S( @
, x4 f3 {3 `4 b' |8 r验证概念的提取工具成功了.现在我们可以稍微改进一点,因为硬编码在代码里的归档文件名肯定算不上是好事.改进过的版本是crosschannel-extract2.py,里面增加了一些Python的操作系统接口库中的例程来从命令行读取参数,也可以那文件提取到新的子目录里,之类.我也稍微调整了一下错误检查的代码,允许乱序放置的文件,同时也在看到"明显"不正确的地址或文件大小时尽早中止程序.有兴趣的话可以看看,不过那对于继续推进这篇文章的进度并不重要./ ]9 A: Y, o, s9 B
! Q% o5 t: | B
我们现在确实应该做的事是重建归档! 没问题.我们可以把刚才的代码拿来,轻松的改换为对应相反的操作上.首先我们需要收集要打包的文件的信息,从命令行传入的子目录名获得:8 |+ O$ Y5 A$ D2 |; Y+ |4 p
import sys, os
e- h& X+ g7 ?: {' A% x) Hfrom insani import *6 M! o, B1 b) l+ R' l4 R
& P: G8 z. ? h2 z0 }
dirname = sys.argv[1]8 l: s0 t6 R7 F
rawnames = os.listdir(dirname)
+ F: ~4 J ~7 S7 w: ]( i
) R% y7 U; n( i# Enumfiles = 0+ E q( f/ P# x7 i9 a5 x X
position = 144*16384+72 F) y2 V% T- w
entries = []/ D. m ~! T& W5 O# z
for filename in rawnames :
# _! ?, Q9 ^; l7 Q1 |7 ~ fullpath = os.path.join(dirname,filename)6 o+ @! s4 v8 ^/ L% [. r9 ]
if os.path.isfile(fullpath) : # Skip any subdirectories/ M# u# e1 o) p
numfiles += 1
+ o* _6 ~$ C' x size = os.stat(fullpath).st_size8 Q+ v1 Z1 ~- i# z% x' B! Z$ i% R
entries.append( (filename, fullpath, position, size) )
8 `8 x" W; [; x: ?" `0 [9 G# x position += size; 在收集信息的过程中,我们也可以基于文件的大小和已知的文件头大小来计算出每个文件的最终地址.然后我们就可以把它们写出来:
" q( v* |! P- t5 r5 H9 j7 Vprint 'Packing %d files...' % numfiles
~$ X' t" X3 d6 n5 sarcfile = open(sys.argv[2],'wb')
, r7 w& N0 q+ @arcfile.write('PackOnly')* f" o) e* q0 ~5 ^
write_zeroes(arcfile,56)
. H5 q* F4 P& z( o% vwrite_unsigned(arcfile,numfiles,LONG_LENGTH)
\1 t6 ]( ]1 y5 p8 s6 {7 s- g3 T9 H
print 'Writing archive index...'
5 G/ h ?: _2 l% ], _4 pfor (filename, fullpath, position, size) in entries :
j0 G. N) l j7 U { write_string(arcfile, filename)
" T- L5 w$ u; S- g* [3 `: A8 w write_zeroes(arcfile, 128-len(filename)-1)7 U9 a# p7 l1 T! Y3 P
write_unsigned(arcfile, position,LONG_LENGTH)
6 g. X0 S. J# P( @) m' L write_unsigned(arcfile, size,LONG_LENGTH)
4 v6 q9 Q5 g. q+ g- @. vwrite_zeroes(arcfile, 144*(16384-numfiles)) 其实这相对简单些,因为我们不需要再检查什么了: 我们只需要写入我们知道的东西...没有任何不确定因素.你也会注意到这与我们开始所描述的归档结构的对应性:有实用例程是件好事,这样我们就可以用一两行代码来解决结构里的每项内容.现在我们把要打包的文件本身也写入归档里:9 n3 \$ B& C2 B( j/ S
for (filename, fullpath, position, size) in entries :8 [4 a& |6 N! Q% f8 O; T# S( {
print 'Packing %s (%d bytes) at offset 0x%X' % \
0 S! }5 o3 g( Y% L$ z. c (filename, size, position) C0 t9 X( |# {6 {) K1 t) D
assert (arcfile.tell() == position)+ n1 X6 W3 J3 U2 B: h
infile = open(fullpath,'rb'), x3 x& g n% H( M* A
arcfile.write(infile.read(size))
; ]1 b, {- ~1 r4 M% X5 |- _1 g infile.close()! W) c7 V/ {# a) u1 |/ h- G# [2 N3 W
arcfile.close() 这里我们用assert语句来做了点正确性检查,不过那其实只是个"愚蠢的程序员"的检查而不是别的:如果我们写出的数据都是正确的,那么这个断言(assertion)永远都为真,无论归档格式的细节如何.这是个典型的"失败就中止"式的断言用法:用来快速的检查如果一切操作都正确时不可能出错的东西.) I8 t! G" c7 G' I5 S6 v
3 N( S1 ]3 u' ^+ C如果喜欢,你可以下载crosschannel-repack1.py,不过我还是鼓励你自己写一次.让我们来测试一下:- x/ i3 q t' t$ r h+ k
% python crosschannel-repack1.py temp temp.pd) R9 C8 e" e' L a5 e$ n6 p5 {( p7 a7 K
Packing 545 files...
7 d! G6 @0 J& \7 l7 o4 ZWriting archive index...! U1 m; V+ X" E: b3 m# t0 Z/ Q. O
Packing bgcc0000a.png (408458 bytes) at offset 0x240048' i+ t( S Q' R3 n
Packing bgcc0000b.png (436171 bytes) at offset 0x2A3BD2
: A4 O4 d$ Y W- c( C5 H...& i4 X- R0 z9 f' t( \3 P
Packing xiconp.png (2399 bytes) at offset 0x5693D26 一个成功的工具的金牌标准就是,能够把一个归档解包并重新打包后,得到与原来的归档每一位都相等的复制品.我们成功了吗? % diff cg.pd temp.pd
6 l1 B* B: D# v$ a$ tBinary files cg.pd and temp.pd differ (译者按: 在Windows的命令行里,对应这里diff的命令是comp,格式是comp 文件/目录名1 文件/目录名2
5 Z; p- o& z; O+ f# h% N! P呵呵,上面的信息意味着被比较的两个文件内容不相同)7 i7 F& L2 {8 O; C; U
$ Y& z+ f% g) r
呃,糟糕.不过,其实这也是预料中事...文件打包的顺序不一样,因为我们打包的时候文件是按字母顺序排列的,而原始的归档里则更为随机.不过这有关系吗?我们还不肯定.5 }3 ]1 Z* n* A- C9 }" I
9 e$ a8 \ `1 k一个成功的工具的银牌标准就是能够成功的把自己打包的归档解开.所以,我们从原始归档提取,重新打包,然后再尝试提取,以确认两次提取到的数据是否相同. % python crosschannel-extract2.py temp.pd temp2
0 D6 e& d3 {; _- b- |/ sExtracting 545 files...0 b# Q6 C* I, q# J8 T3 T
.... z0 a x$ C( s/ M
% diff -r temp temp2 成功.好吧,至少我们的解包和打包工具自身是兼容的.不过,它们跟游戏相兼容吗?. b+ S: [% ^0 I3 r+ w8 l# U
% g c8 n; z( l% t+ H
这里要介绍个道中小技巧.在第一次测试重新打包的归档时,什么都不要改变.把游戏自身的归档文件解出来,重新打包,然后看看游戏是否还能正常运行.这样你就能看出,例如说游戏在可执行文件里保存了归档文件的MD5校验和,又或者你对归档格式的理解有所遗漏之类.你解决了这些问题,然后再去修改文件并插入英语之类.
7 P u9 I- n8 [9 i; p4 z; t! ~9 h& U0 w9 S7 a7 F
(译者按: 当然对于中文化工作者来说,修改文件插入的会是中文啦 ^ ^.其实如果程序真的用了MD5之类的校验和,我们总是可以通过简单的检测程序来查看游戏的可执行文件是否使用了常见的校验/加密方式.这个留待以后的篇章再说吧.如果游戏既对归档文件使用了CRC32校验和而又用了PNG图像的话,那就有点不走运了...)3 B8 e/ Q% p! d2 h
$ H- L. D4 ^$ w1 x
长话短说,上面所说的程序可以用.所幸的是没有什么诡异的校验和,归档的特征里也没什么我们没能再现的(说来,文件的顺序还是有可能有所谓的...)3 x. Q, s- T( P
, Y8 P3 j/ S2 a; N* l下一步就是尝试修改点什么.不如试试修改标题菜单的背景图吧? 这幅图正好是x0000.png,所以我们可以随便找张640x480的PNG图片替换掉它,重新打包,然后运行游戏看看...7 u+ m5 y( ~* @# t& C8 A% k# d
& E8 U2 W; i# b6 P! W/ Q, L6 a# h
那个高亮的长方形是主菜单的高亮系统的一部分.看来我们的进展很顺利...大部分的图形界面资源都是以PNG方式保存的,所以我们的翻译和改图员应该可以用对应工具来解决掉游戏的界面,没问题.' k5 l8 @- r- L3 ]) Z' `4 K0 e
, |: A# {; U- Q. a0 c
不过,等等...还有脚本呢!; z* ]8 a+ S$ l. G* ~* s
% python crosschannel_extract2.py script.pd script
. j; D) a: M" |( jExpected "PackOnly" at position 0x0 but saw "PackPlus".
4 |& l% ^' ]' _Aborting!
; s3 N! Y* ^# u% Y- [- aTraceback (most recent call last):
' M* Y* T5 \5 d- `3 o... PackPlus?! 哦糟糕.不过我们的错误检查的高明之处终于展现了出来...我们马上就知道脚本归档里有诡异的地方了,而没有解出些垃圾等到后面的步骤才想办法弄清那些是什么.让我们回到十六进制编辑器来看看是怎么回事.+ e! ^) Q% Y3 s( \
0 W" V4 G: L8 C; Q嗯,其实看起来挺正常的.那些零都还在;也有貌似文件数量,文件名,地址,大小等信息的数据.如果你一直向下滚动,直到0x240048之前你会看到都是0x00,跟以前一样.那么整个文件头都跟之前的一样,除了特征标记之外.莫非我们做了无用功?
6 j6 r; B, e: q/ L! l& A
. _8 k( F* y5 ]8 o* ]" B/ a( E啊,还是看看原始文件本身:
) L# \! r% l) z# u0 a/ @2 P h5 P3 e+ o& t
: ~2 M9 K* X) P$ Q- w. c这看起来有点怪.这些字节看起来不怎么随机——有长串的常量值——所以它应该没有被压缩过,也应该没被复杂的加密过.在这之中,有大量大于0x80的值,有点不寻常.
1 A' r: b5 v( I! V5 P" ]$ x6 ]# ^) B4 C; f) }1 e- Q3 V% @
(译者按:要吐槽了.有大量的大值确实不寻常,不过考虑到日文字符编码Shift-JIS(CP932)使用了C1控制码作为首字节,其实就算是正常的脚本里也很可能有大量大于0x80的字节...问题不在0x80上 =_=||问题是,如果一个脚本是没编译过的,其中的指令多数情况下用英文表示,编码会是ASCII兼容的,也就是说字节的值应该小于0x7F,这就跟0x80扯上关系了.)3 S9 ^' K2 y' h/ }% Y( F
4 ^( X$ S8 r" L所以我们可以猜测所看到的是某种基本"加密".最常见的就是让数据与一个常量字节进行加(减)/异或,适合于保护文件不被你的六岁妹妹拆掉...假设她不会编程的话,呵呵.
5 q& m2 D) @8 q8 a$ I- b7 @$ q+ s, Y2 G! f" H- c2 m
我们完全可以直接猜出答案...事实上,如果你真的猜的话,说不定一下就猜到了.不过你知道吗,我很懒,所以我准备直接写些代码来暴力破解.arcfile=open('script.pd', 'rb')2 s! F! [4 {" Z5 B" g! J, r8 r
arcfile.seek(0x240076)2 D# K- I. X. R C
data=arcfile.read(16)
9 h7 H8 X. I( q- L" Koutfile=open('bruteforce.dat', 'wb')
5 \% w U6 Z4 R, P; L: t6 `for i in xrange(256) :) y6 t: @ v' h
for temp in data :
! l% i6 n9 b; F- v1 y: n! A outfile.write(chr(ord(temp) ^ i))
( E" Q& j7 A" M- p for temp in data :
( ~* H$ c0 n: Y4 A- W7 E l6 m L outfile.write(chr((ord(temp) + i) & 0x00FF))7 u+ t: ` M3 Y y: Q
outfile.close()" a. `# P3 Y0 X, ~/ H, M0 q/ H
arcfile.close()
# u0 }4 S# G% R* \- [ A9 s1 L9 r我只是选了个看似有趣的地址读取了16个字节的数据(这个长度正好对应我用的十六进制编辑器的宽度),然后对这串数据与一个字节所能包含的所有可能值做异或和加操作,再把结果写到一个文件里方便查看.肉眼从头到尾扫一次这个8KB的文件大概要10秒,我们会发现:
# I j% A I6 F# G
& Z- @2 u7 ]/ \3 M3 H5 ]" V$ o* G4 L$ b" e; G) C
哇,可辨认的文本,在文件的最最后...总是你最后才看到的地方.这里对应的是与0xFF做异或操作,也可以说是把文件的所有位都反转一次(0与1互换,其实也就是做了一次非(NOT)操作);你很可能猜的就是这个.其实我想说的是,当你不确定的时候,不要怕做点小实验...写出上面的代码片段然后把一些可能性扫一次可能会花掉5分钟,到顶了;相反,如果你有时候不够聪明的话,很可能在一些简单却未知的加密上都会碰几小时甚至几天的墙.0 H" q0 }1 K/ {3 f
+ j' e* u# _- I( ]4 T: x! c$ J
现在我们可以更新一下我们的提取工具来对应这种加密.要修改处理文件头部分的代码很简单:signature = arcfile.read(8)
u( ?5 s9 h2 V! Aif signature == 'PackOnly' :
, O/ N: x- B4 n* f0 a- t: l xorbyte = 0
* w3 b h! v4 \elif signature == 'PackPlus' :
% I% V- g' _/ t/ j, W/ D: I xorbyte = 0x00FF" n( ]9 {0 y# L9 f1 q5 r
else :: N; F* ], c% x. [; a8 r
print 'Unknown file signature %s, aborting.' % \
" H) l5 [' u. O$ L, a escape_string(signature)6 T; H! j9 l; _7 m2 F% O# ]
sys.exit(0) 同时修改处理文件本身的部分: data = array('B',arcfile.read(size))
: X# \' \1 ~6 f) b0 p6 u if xorbyte != 0 :, F2 c+ {6 _6 w+ k7 d4 x t
for i in xrange(len(data)) :7 @5 O' {& @, v3 H
data ^= xorbyte9 f/ j) i8 }; |7 z7 j- L* I
outfile.write(data.tostring()) 如果你不熟悉Python的话,这部分看起来可能会有点让你混乱.标准的Python字符串是不可修改的数据类型,所以你要想把所有字节都异或掉的话得创建那个字符串成千上万份的复本.与其那样,我们把数据读入一个可以修改的无符号字节数组.要这么做的话,你得在你的文件顶上加上一行from array import array代码,因为数组模块不是Python语言的核心部分.新版本的提取程序是crosschannel-extract3.py." f0 _3 s! A2 W9 {: f" _1 Z
[blockquote]% python crosschannel-extract3.py script.pd script' z8 g% n) m) d
Extracting 54 files...
1 {2 n# C( U; q; A' _" m( @& IReading archive index...
% b. ?. j4 m$ W. d3 B& [% y8 gExtracting adstart.dsf (236 bytes) from offset 0x240048
5 @* J& q, r& Z; r+ Y$ mExtracting cca0001.dsf (934 bytes) from offset 0x240134
+ P' s3 ~6 [0 l7 A m: N1 \ u...
$ X: Q7 ?7 q/ k; z& }/ f+ qExtracting cca0011c.dsf (6674 bytes) from offset 0x297376[/blockquote] . ^; U! e# \9 ^5 d. z# `3 F. Y
看来我们把问题解决了.解出来的这些文件能用文本编辑器以Shift-JIS编码正常打开.上下看看,我们可以发现cca0001.dsf文件是主要游戏脚本的开头.稍微编辑一下,快速重新打包(我们在自己打包的时候甚至不用理会PackPlus了),变!(呵呵玩魔术么...=_=)/ G8 |; j; G A! r
: d* h. |2 ~0 o! I+ p! g* E% j: D8 D2 S8 F& ]- a
5 |; _8 a9 T; d. C* t3 {
注意到我跳过了最后这步里的一些麻烦的地方,例如说手动解决自动换行,还有确保游戏引擎能接收CR-LF换行而不是单个LF.其实还有些诡异之处...我并不是说脚本格式完全不重要,只是,现在我们已经打开了超过这篇文章范围的实验的可能性.
) E7 b' z& L: C) `( r" r- |1 y& T' K7 w5 a$ [
从此,我们可以走向何方?作为家庭作业,你应该用这段代码实验一下,如果可以自己用你选定的语言来写自己的代码更好.而且,归档里的那2.3MB的0x00该怎么办...如果你把那些零拿掉来节省空间的话,游戏引擎会抗议吗? 你能写个例程,在把脚本打包进归档时自动解决换行问题吗? 窗口标题栏里的汉字又怎么办?你尝试做这个游戏的完整翻译时又会碰到什么别的问题呢?
3 ?' \; o2 x3 r0 R3 w$ G! C3 u J* y$ k
不过,暂时来说,Cross+Channel我只准备用到此为止...我们已经破解了相当大的一部分,而且从这里也没什么好再学的了.这是个合理的成就,因为这是个在市场上的实际游戏,而且人们会有兴趣翻译它.不过它也只是个简单的例子.这里的归档文件没有压缩,也只有轻微的加密;图像文件使用的是标准的格式;脚本是纯文本;而且游戏也没有完整性校验,我们甚至不需要去修改游戏的可执行文件.
; Q9 |. b' P6 t; R4 r d
- Z0 b# E, ]9 W6 u+ m(译者按: 把日语游戏英文化与中文化面对的难题不一样.对英文化工作者来说,他们常担心的是自动换行,还有显示半角字符的问题;而中文化工作者则得解决字符编码的不兼容,通常还是得修改游戏的可执行文件的.)
7 d' U$ b- P/ M8 \$ a2 t' M0 f; q; L9 d: @) A* S4 z
这些特征要是反过来的话,我们就得多做很多工作.所以,在接下来的几部分中,我会挑些别的游戏来作为更复杂的情况的好例子,每次集中关注一个方面.请自由推荐你关心的游戏,不过当然,我不保证那些游戏我都知道,呵.) k- \( y* G1 J
* V- s& g# j7 {
=================== 地味的分界线 ===================( S: u1 B7 k3 M, v1 l
下面是对原文的部分回复,选取翻译,有修改.原文的所有回复,请到Part I前给出的原文地址查看.
# W# `2 P( s1 k0 [' }8 }5 \: b, ]) S# e
Shish:1 ~$ [* F3 l# b- M3 j9 _) ~
一个可预测,但值得注意的观察——这个游戏不在乎归档使用的是两种格式的哪一种.当加密是简单的异或时,这或许没什么用.但是当加密/解密方式很复杂时,把本来加密了的归档替换为无加密的就可能很有用.! X; ~& c* s+ o# C
: h* C# O+ M; U B2 i; W' wEdward Keyes:& X' G* W6 a8 D4 k0 C' p
没错,这种事常有.游戏经常会有几种不同的读取数据的方式,特别是作为一种调试或者打补丁的机制,例如说"先看看文件是否就在磁盘上,没有的话再到归档里找找"之类.这些归档也常有未压缩的格式,为简单我们可以拿来一用...只要压缩标志是格式的一部分,游戏就能正确读取数据.最后,有时候你可以省下许多时间,用比较"蠢"的方式来实现一种图像格式或者一种压缩算法,例如:如果压缩算法允许用转义代码来向压缩流里插入原始字节的话,直接把所有字节都转义掉就可以了! (不过最终发布前还是应该回头好好实现一次的...)2 b. l: x! i9 @+ Z$ {
$ b) x0 B" U$ r; l& Y& F+ X
在完整版的星之梦里,DRM意味着我们要修改文件就得付出很大的代价.还好游戏有个后门,能让游戏引擎读取未加保护的版本.省了不少工夫,也可以说要不是这样整个翻译可能都不可能做出来,因为把DRM完全禁止掉可能给我们造成些道德问题.
( `' ~' d5 _7 u9 K
' H! q- X0 W6 k2 ]" @1 h8 eHaeleth:' t h7 }7 }. D [" l
倒,Python.你应该用OCaml的.它有着Python所有的优点却没有Python的缺点(像驴一样慢,没有预先的类型检查,有大量的空白)./ ]" K7 [- z0 ]) n9 E. [' t/ n
) s6 q- q# F+ {/ |0 V
考虑到我所破解过的系统:Majiro用了一种专有图像格式,与其说它复杂还不如说只是很丑陋(是个把RLE用到极限的傻系统,RLE = runlengthencoding,变长度编码).AVG32有简单的压缩(而没有加密)和相对简易的字节码格式.而且你们也知道,RealLive是个典型的例子,虽然有内建的工具但却破烂不堪,用它们的感觉简直就跟破解程序一样.
/ i7 N9 A. |3 ^, e' i: k2 `, C
3 _9 [' K! W9 P( K% X注意到RealLive的"外部"文件实际上是压缩过也轻微加密过的,因为RealLive是在原始文件级而不是归档级来处理压缩的.所以这么做(用不同版本的文件格式替换掉原来的)对KineticNovel可能有用,因为它在全局归档级上有隐秘的加密,不过对一般的RealLive游戏来说就没什么用了:不管文件是否放进归档,游戏引擎都要你做压缩和加密的苦劳.# q# {% y4 B) x" B+ Z
- Q+ \0 g: k5 V! _" P X4 n相似的,可以看看游戏在接受它们自己专有的格式之外是否也支持标准格式.例如,Majiro似乎在用专有且让人不爽的Rokucho格式来储存图像,不过我没浪费一点时间去编写一个Rokucho压缩程序,因为游戏引擎也会高兴的接受PNG格式.虽然很多RealLive游戏坚持使用专有的NWA格式来储存音频,你如果想编辑或者替换掉那些音频的话,直接用OggVorbis就可以了.$ q9 m! H2 D/ u# v
+ o, x6 [% h6 h9 V& c+ _
(译者按: Part III完毕,为了保持原文的完整性,连原文的回复也选择翻译了上来,融合到原文中.是不是很丰富的一章呢? 希望大家都能从中获得一定经验,也请多多举出各种例子来分享经验.)- m- y; n7 k( c, J& U1 P
6 o- a- r+ a8 }+ ?1 D+ E7 {: J6 j% J$ E
0 ]* R( b+ d! E
7 x3 b7 t0 j" l3 p* \ |