首先,分析游戏的文字显示方式。玩游戏的时候最怕打开游戏看到一片乱码,现在汉化游戏却是最希望打开游戏看到一片乱码,因为一般在非日文环境下能正常显示日文的游戏都使用的字符集限定,为汉化增加了不少的难度…… ! @/ R8 w5 A6 {" u9 S/ Y& j
一起采取Textout方式的年代,日文、繁体中文十个有九个是乱码,于是有了Apploc和NL,后来比尔大叔推出了CreateFontA函数……CreateFontA能够直接定义程序使用和操作系统不同的字符集,于是乱码没有了,而汉化者噩梦开始了,因为汉化后的中文内容在经过日文字符集的编译后变为日语状态下的乱码。
' @0 @" e+ G+ ~, N以下是比尔大叔的MSDN Library对CreateFontA给出的函数原形 , g$ U* ~4 N# u
- L( D8 {3 D3 p9 gHFONT CreateFont(
0 p9 V! o9 B% F( C* `# nint nHeight, // height of font ' R) w0 [/ U: S8 @
int nWidth, // average character width ' G, o( |: z8 W' Z E2 L
int nEscapement, // angle of escapement
# ?, Z- Q2 ]7 [6 d5 B' Fint nOrientation, // base-line orientation angle
- u( f# ?; p1 b8 g& G, v7 O% r- zint fnWeight, // font weight ! [ v* ?1 ]4 ?: N' H
DWORD fdwItalic, // italic attribute option 8 m6 L/ D1 P0 d; {/ b/ t
DWORD fdwUnderline, // underline attribute option
( n( w, \2 a0 |+ q6 H" F4 ^: b! QDWORD fdwStrikeOut, // strikeout attribute option 1 }) u3 a: D# }" J' |6 e( i- v* f1 i9 I% P4 g
DWORD fdwCharSet, // character set identifier ' O, u2 F6 b! P* Q
DWORD fdwOutputPrecision, // output precision l. o/ z- @+ q2 b& u7 y$ S
DWORD fdwClipPrecision, // clipping precision
* a4 v; K* u( D, E" BDWORD fdwQuality, // output quality
' b8 T/ S- R; z& I2 X2 q, r9 FDWORD fdwPitchAndFamily, // pitch and family ( C+ y8 H9 o9 W6 \. k
LPCTSTR lpszFace // typeface name
% A. G& q0 _/ X8 W X I3 e);
6 k2 ?4 s3 T+ q' P: }& p( J; y! `* p
HFONT CreateFontIndirect( + E' x2 e$ W% |1 M$ H. V8 k
CONST LOGFONT* lplf // characteristics " {% W" j- ^' l( |2 H/ ?" H
); 4 Y$ J" G6 l& b$ H& i! _
其中 LOGFONT的声明如下: 8 R5 x5 p2 o! W, F/ c: J
& s% l1 }* u* Y3 Q2 T: Rtypedef struct tagLOGFONT {
" `! d9 i0 r' q i, S% Z9 wLONG lfHeight;
4 h, W. ^' m! HLONG lfWidth; . Z; Y5 B0 J3 z6 {& C% s
LONG lfEscapement; 3 ?4 F, M; Y6 h: b
LONG lfOrientation; 7 N; M3 e3 T" M( k$ Y4 U/ E0 D
LONG lfWeight;
: A& L7 S# p8 k4 f0 [BYTE lfItalic; ) }9 w5 e! n- f
BYTE lfUnderline;
& W9 E) Y; S& Z1 _" A2 ~8 t& zBYTE lfStrikeOut; : a/ G& d3 [ y4 f3 j
BYTE lfCharSet; ( r7 i" K& r. v s0 q/ @
BYTE lfOutPrecision; ! S% P( K" d& {" o. L
BYTE lfClipPrecision; * Q; g& }: R' s# m; J3 r5 n
BYTE lfQuality;
; m4 R5 B w4 ?! m* o4 x( NBYTE lfPitchAndFamily;
/ X# O2 B$ Z' e! O5 `0 eTCHAR lfFaceName[LF_FACESIZE];
- ] c; i6 D1 Y} LOGFONT, *PLOGFONT;
- u9 m- c4 J! K2 Q4 T' s" F==================分割线================= 6 J, X5 t& r( i# M& @1 d$ X" }
要改变程序支持的字符集,就要改变程序调用上面两个函数时的fdwCharSet或lfCharSet的值 5 [9 u% o8 z h$ p# O8 s- [
其中各字符集所对应的值如下:
! j2 u5 Z# N; f- n6 j6 Y' F3 p
5 b: ?% m# ^3 c: m: f字符集 值(十进制)
3 i& N4 u6 @9 K- QANSI_CHARSET 0
. l1 H) P! k# |DEFAULT_CHARSET 1 ; R+ S) C3 I6 `/ @1 c; l
SYMBOL_CHARSET 2 . O( {6 v9 a0 X
MAC_CHARSET 77 ' h( W$ B2 @# f3 B
SHIFTJI_CHARSET 128
) P, N8 {4 F4 G9 L! q: KHANGEUL_CHARSET 129
3 R6 _/ F+ f- @6 A5 j( U* w. qHANGUL_CHARSET 129
8 Y3 F/ G6 g e- K; ^' o4 oJOHAB_CHARSET 130 }* T7 L" {; ]
GB2312_CHARSET 134
+ ]8 ~. O9 t! y. hCHINESEBIG5_CHARSET 136 5 m+ y L. e9 J) i4 r
GREEK_CHARSET 161 ) {' o: K; e0 m# |# V6 p P# O1 l
TURKISH_CHARSET 162 $ B( @ Q+ @6 G4 D! n
VIETNAMESE_CHARSET 163
9 `& c) O+ p) y# t9 {7 N, q/ s8 v: R, sHEBREW_CHARSET 177 7 q/ l: v9 g s- Z1 h" P
ARABIC_CHARSET 178 $ h w+ }. _, G' ^
BALTIC_CHARSET 186 ) N4 m% Q8 l( H' `4 w1 V( U
RUSSIAN_CHARSET 204 ' V" {( v4 r& z5 @" f: b
THAI_CHARSET 222 5 l) j6 e$ ~* D. J
EASTEUROPE_CHARSET 238 . j4 Y5 G( E' b
OEM_CHARSET 255 6 K0 h* Q" R4 B# ]
可以看到简体中文是86(Hex),日语是80(Hex),繁体中文是88(Hex) + i6 p) ]6 L4 {: P
我们要做的就是找到游戏中定义调用字符集的部位,将80改为86,这样就能让程序正确的显示中文。
* Z. D& U) S9 G, r) q z用pexplorer打开HANABIRA.exe,进入反编汇模式,查找Font字符串,我们发现调用CreateFontA函数的位置是唯一的:
/ m2 N# c: C4 `) C$ z于是在以下代码我们停下来
Z1 J+ Z) s- X7 l7 t( y% H; UL0041E365: 8 n7 T. l) J: R! N
mov edi,[esp+14h] 1 h2 H- A: @* U" P0 ?' a# Y
mov ebx,[esp+28h]
8 @ N# u3 G2 M8 xmov ebp,[esp+24h]
! s i/ z3 i8 l, B8 x- q1 Zmov eax,[esp+20h] + g+ S2 U$ \! `3 C
mov ecx,[esp+1Ch] ! X. s6 r- x% W t/ V
push edi 7 N2 N2 f$ B) h% D9 e
mov edx,[esp+30h] 3 _$ ]: @ I1 w2 h& b
push 00000031h 9 k. r5 f' X' g+ [
push 00000002h 4 r$ r" [$ I: ^( j3 x4 P F
push 00000000h
7 B8 E; P! O' V( A# l3 npush 00000000h 9 _- I0 \6 h/ m" M* @
push 00000080h
: B. K; c3 C; B( ypush ebx
$ ]- J$ p7 ~, C2 T0 W2 ]1 Ipush ebp 6 s0 {2 @1 G' g9 x
push eax % Z8 ]1 {. @9 I6 {
mov eax,[esp+3Ch]
( H2 F: `! M, n% tpush ecx ' t2 A K4 J9 p6 j2 R% L. }% ~2 h) H
push 00000000h
$ I' F) P7 a4 W; O/ d% epush edx # J$ j; H3 B+ Y% }9 o, \
push 00000000h
% d' Y; y( R; B$ s; ppush eax
: R/ d. f o8 G- Ncall [GDI32.dll!CreateFontA] $ M- f9 T6 a; u3 j3 S) ?( I
lea ecx,[esi+28h]
% }) T( [ M5 v" }" L, ~0 Bmov [esi+00000138h],eax
: m; t/ p! D* k2 K/ t9 x+ h. ^9 Xmov eax,edi & {3 n6 [7 x3 G& H3 w
sub ecx,edi ) I1 k) n* w" r
lea esp,[esp+00h]
" w( @" ^' i. o. v' i$ m6 x, o2 `! W注意这就是调用GDI32.dll中的CreateFontA函数了,我们在这个堆栈中寻找将80这个值传递给CreateFontA的部位。
+ [$ V4 p, {- T J) [% o" E# Epush 00000080h
8 a h) T6 Z2 c2 Q就是这里将80值压入传递中
6 [( x! _# i2 m2 }9 ]: vPE中标记了这段赋值的Hex数值,用UE打开文件,找到
& m+ e, o, V: g68800005355
* j9 d" S* Y" ]# |7 }) H0 u* w将其改为 * E) ^& J3 C3 [8 A; ~ h
68860005355
: V) u& b* N7 F: [保存之。
( ?( F8 W& W2 d+ }这样PE中看到这段压入就成了 ) Y+ ]. v Q5 @
push 00000086h
- V6 \5 G, ]' u* L初战告破,运行游戏,你会看到日文全变成了乱码,说明程序已经在使用中文的字符集了
+ c* ]1 C( @2 e: T/ {( Q0 g修改游戏脚本,加入几个中文看看…… % ]# F0 d: |9 a8 }
为什么我添加的中文全部是“□”? 0 r8 W0 P, B1 H% x, r# t" P
这就是需要解决的第二关卡,字符集边界检查。
/ Z; f9 A9 a6 A6 u1 H) ~ j2 _
0 d5 k- r' X: N+ C既然已经设定了字符集,为什么还要边界检查呢?这是为了防止当游戏文本中含有某些非法的字符串时产生缓存溢出。于是在字形传递到GDI32.dll描绘字体准备显示在屏幕之前对其进行检查,发现超出了设定的缓存大小就将其拦截下来,于是屏幕上就显示出一个“□”。我们知道,由于日文的字符比中文少得多,所以这个缓存也小的多,换言之就是边界太窄。
# i5 S$ {6 I5 M ^! G" v6 M& b边界检查的例子:
1 t. h4 A9 _1 _* Kcmp al,80
- N9 ~# U8 J, ]* }- Mjbe xxxxxxxx 1 W0 A- x J4 o/ l4 ]! u
cmp al,09F / {0 R5 _, p' f: ?' c* y
jb xxxxxxxx
2 |# n J, \9 l3 tcmp al,0E0 - g4 Y. O' r* ]0 M9 z) o
jb xxxxxxxx ) ?# T6 o( \( E- j( y
cmp al,0FC - \, [) R8 K! |; W: m+ ?) V% e
ja xxxxxxxx 7 g( s. R( q" `6 {' V* n: B& Y
C4 |& M% ~/ m即看字符是否在80-9F(前两位)和e0-fc(后两位)之间 9 X5 g! x$ W. L; ^0 `
=================================== - W, s) z2 k( l: F' N: E! V
如何具体查找程序的字符集边界呢?常用的方法是下断,用OllyDbg载入游戏主程序运行,一步一步断下去,在出现一堆“□”的时候停住,然后转到ASM模式查看停在哪里。
1 v8 E; [4 a2 [8 ]* D8 ]: iL0043BA00:
/ w$ |' v) x. o4 I, Scmp al,80h 3 ]+ T& ]4 a" E2 H; l/ J. g
jc L0043BA08 1 q4 d" P1 v; _9 v/ ~- K+ H& i! |
cmp al,9Fh
5 o/ \. m6 @$ m" O0 y# M) ]0 kjbe L0043BA10 $ d2 j2 e" `. M9 M; u0 F
L0043BA08: 5 Q8 }( @) p% Z' e/ w+ D; w$ A
cmp al,E0h ) |' [1 J% S0 }1 `/ M
jc L0043BA30
" k! x9 ^- e- K' Y4 ~# A5 bcmp al,FFh 0 T8 A$ L @- l' ~0 y
ja L0043BA30 + _% `) _! ]% }1 v0 ~
花瓣的上边界为80~FF,而下边界为E0~FF,好,开始动手 ! ?7 \, q. ~% \0 F; j j
这里我们将上边界改为80~FF,而下边界范围足够宽广,就不用改了。 " ~! @. l" ^, Y% n J0 V) X
这里为什么使用80~FF而不改为20~FF呢?因为我们需要让游戏文本中原本的日语空格(8140)不显示为乱码,于是8140刚好在边界外,就不会被送去CreateFontA进行显示,就会显示为日语的缺字码,一个空白——而它刚好就是空白,在显示上两者没有任何区别。
7 F2 I! e: w, j1 I2 o$ `& G如法炮制,打开EU修改之,保存测试,OK,正常显示了
! W9 G. c5 @. v% _$ c8 }
h, E$ J/ m8 J- a原文
. h2 S( w& E/ V* D) y6 t# whttp://blog.potatoneko.cn/ |