首先,分析游戏的文字显示方式。玩游戏的时候最怕打开游戏看到一片乱码,现在汉化游戏却是最希望打开游戏看到一片乱码,因为一般在非日文环境下能正常显示日文的游戏都使用的字符集限定,为汉化增加了不少的难度…… - z; I1 ]5 f2 ]0 G B' Z; j @
一起采取Textout方式的年代,日文、繁体中文十个有九个是乱码,于是有了Apploc和NL,后来比尔大叔推出了CreateFontA函数……CreateFontA能够直接定义程序使用和操作系统不同的字符集,于是乱码没有了,而汉化者噩梦开始了,因为汉化后的中文内容在经过日文字符集的编译后变为日语状态下的乱码。 " z/ Z4 I+ h( S5 J2 J9 Q' M
以下是比尔大叔的MSDN Library对CreateFontA给出的函数原形
- P) O4 x0 Z, S
9 f5 E9 @2 u, } {HFONT CreateFont(
9 K; x, w% z4 R9 |3 lint nHeight, // height of font % ?* z, T" c+ J& {& X" h% z0 a
int nWidth, // average character width 5 w& B3 y6 J( }+ d
int nEscapement, // angle of escapement
, l- B) c0 u; ^int nOrientation, // base-line orientation angle 8 c8 r S% f5 F) n/ n' x2 v! {( A, W T
int fnWeight, // font weight " K6 v4 a/ k0 ?' |, e7 u/ {1 D/ L
DWORD fdwItalic, // italic attribute option ) _# f2 A4 `- }* h+ u, j
DWORD fdwUnderline, // underline attribute option
% O5 c7 C0 I/ r! a3 `4 KDWORD fdwStrikeOut, // strikeout attribute option 7 s, T8 t* f/ X: x, i2 Z, K# X; N w
DWORD fdwCharSet, // character set identifier
: N3 F1 @, D* uDWORD fdwOutputPrecision, // output precision
+ v+ n8 V3 z" @DWORD fdwClipPrecision, // clipping precision
1 J1 W2 t* s8 s3 EDWORD fdwQuality, // output quality
! m$ t7 n4 s& i1 ?- e1 O/ {DWORD fdwPitchAndFamily, // pitch and family & M/ g j7 o: a
LPCTSTR lpszFace // typeface name $ L9 o7 O" x% c* i
); : [3 Y( b# Y9 p
4 W1 s3 z0 M) B* f8 c$ R8 H9 QHFONT CreateFontIndirect( ; b' X7 ?# y/ S i
CONST LOGFONT* lplf // characteristics r& g) j- b p% e J6 s% n% h
);
6 k+ v; D: ~* i0 p) P3 q/ ]/ X其中 LOGFONT的声明如下:
* J3 ^1 I- h/ j9 S3 q8 W3 H
( K9 D1 ?4 Y" Y1 W* {: [; Ktypedef struct tagLOGFONT {
' H" H$ P$ S8 OLONG lfHeight; ' Q `: X% m# m* R L8 X
LONG lfWidth;
$ }# l- z f& p! ~# WLONG lfEscapement;
& M! A2 @; I5 ^0 C3 _LONG lfOrientation; & T! J$ Q$ u( o1 a: o: Z
LONG lfWeight;
4 }, F7 P; g) c/ I; lBYTE lfItalic; / E8 }+ S0 @0 I2 U+ q, M% K& U
BYTE lfUnderline; 1 T2 ?& Z) V. g6 T" g( o
BYTE lfStrikeOut; ; E1 p) J/ V9 n. W
BYTE lfCharSet;
6 n) M2 w1 h9 k) G/ A8 a& uBYTE lfOutPrecision;
+ G! {% o) D: w* I' DBYTE lfClipPrecision; & n$ E5 g# B* f& I
BYTE lfQuality;
+ J w3 H( E) O* Y1 `4 kBYTE lfPitchAndFamily;
) N+ q. {/ A6 N2 i2 LTCHAR lfFaceName[LF_FACESIZE]; 7 R9 W- X$ K, h% Z4 X
} LOGFONT, *PLOGFONT;
' s ?+ \1 m r( G& H==================分割线=================
( x$ r& }1 q3 Y6 i* T/ X/ x6 |1 H+ A1 D要改变程序支持的字符集,就要改变程序调用上面两个函数时的fdwCharSet或lfCharSet的值 4 q$ m) b+ j: [& _! x, z5 O
其中各字符集所对应的值如下:
' T' R# C u9 y( O6 M0 H
2 h; c1 e6 `/ k! U7 }% O1 T字符集 值(十进制)
: B7 A, K8 D. D% M. @" z) L; A3 d- NANSI_CHARSET 0
1 u3 ]; b$ \* `* tDEFAULT_CHARSET 1 , E3 Q9 O8 K! V: l2 z4 v
SYMBOL_CHARSET 2
1 B9 I. H9 z5 {+ I3 f! s! ?' rMAC_CHARSET 77 4 N, T( J: [6 g! Y& Y E
SHIFTJI_CHARSET 128 ( W8 \; v2 I6 E& C
HANGEUL_CHARSET 129
/ Z4 W: D0 V0 `1 U( Z7 ^HANGUL_CHARSET 129 # t3 d5 p2 |- R: X% S
JOHAB_CHARSET 130
' | a% K1 m: h5 M, bGB2312_CHARSET 134
1 V3 h G! E, O. jCHINESEBIG5_CHARSET 136 0 @' z ?! `3 B- {6 z, Y
GREEK_CHARSET 161
2 L6 P- Z6 l: R- RTURKISH_CHARSET 162 8 V$ p m' x# @- ?. D6 ^. l) D m
VIETNAMESE_CHARSET 163 + q3 o" m+ E( c
HEBREW_CHARSET 177
; S9 Y$ P; G% A# {" C6 DARABIC_CHARSET 178 4 ^( r8 m" G5 R
BALTIC_CHARSET 186
% ^/ F$ r! W! `2 q& j3 ~RUSSIAN_CHARSET 204 # _8 y( U# W* T8 K" }- S
THAI_CHARSET 222
; t0 b; U; c, tEASTEUROPE_CHARSET 238
, C, N N8 M {5 M. K+ G: FOEM_CHARSET 255
$ M o( i F, p* T可以看到简体中文是86(Hex),日语是80(Hex),繁体中文是88(Hex)
9 j% R* g( y& e" l+ b7 t我们要做的就是找到游戏中定义调用字符集的部位,将80改为86,这样就能让程序正确的显示中文。
9 o- i# H, U `& }用pexplorer打开HANABIRA.exe,进入反编汇模式,查找Font字符串,我们发现调用CreateFontA函数的位置是唯一的:
: h! e5 M) o/ u7 c q于是在以下代码我们停下来
& W# }# b; T3 @3 m8 B! C: zL0041E365:
2 p" q: ^+ m- `mov edi,[esp+14h] ) s8 ?! W/ q, f* b' _" }
mov ebx,[esp+28h] 6 D. `# O5 J: G0 `
mov ebp,[esp+24h] 8 M( {5 j4 y/ k
mov eax,[esp+20h]
1 J1 j2 E0 b, J; e- }- E5 Emov ecx,[esp+1Ch]
: l7 O' p3 U+ i- x. C$ npush edi
, R* _" d" M0 umov edx,[esp+30h]
" t. S! d( B' ]# K" |push 00000031h
6 }$ x* ]4 t1 {: F( `push 00000002h
& l: o" W! ^* e+ ? c2 M7 ~push 00000000h `, {; U" C6 p& C1 A3 M* X: c
push 00000000h " Q/ D! I& b3 U- U4 ^
push 00000080h . L! [( c# V: {
push ebx 4 v; \& n6 p" \: G7 @: {$ X
push ebp
& T* Y6 H/ N+ P. H. S$ e$ R+ D" ~push eax
% o1 q. \* U0 n) _5 pmov eax,[esp+3Ch]
) Q5 M- N) r# {2 C$ Epush ecx & E$ D, I& x! g3 c5 F( s+ p
push 00000000h
1 U* R) B8 S6 P3 N F7 |push edx
0 ] n3 Q# {4 C5 t! Dpush 00000000h
- q& X# U- G, z) t2 Wpush eax + \' L6 B$ ]; D, D
call [GDI32.dll!CreateFontA] . R9 r$ K$ C# w& L+ p- u$ E: }; v
lea ecx,[esi+28h] , K# S. o9 o, Z L' m
mov [esi+00000138h],eax 9 [# p# }+ H) _& ^3 e; F$ }* j# q
mov eax,edi
! V% H( K3 a; asub ecx,edi
- T$ {& L8 Y! a% Dlea esp,[esp+00h]
( e$ K7 c0 B- i9 c注意这就是调用GDI32.dll中的CreateFontA函数了,我们在这个堆栈中寻找将80这个值传递给CreateFontA的部位。 * Y) @5 S' z. l$ ~
push 00000080h
4 v7 O# W5 I2 `7 i就是这里将80值压入传递中
& J, l. z8 q) q# k% ~PE中标记了这段赋值的Hex数值,用UE打开文件,找到 ! X+ m7 Y. d1 U/ @
68800005355 6 v) W. y6 ]" C* |3 o; d; T5 h" G; P
将其改为 & l0 q0 ?" A: `0 U
68860005355 ! C S) V9 u3 Z. L
保存之。
6 m+ U" Z8 h; y这样PE中看到这段压入就成了 3 \# c, ]3 N4 z* r* ?5 E
push 00000086h
$ T; \3 c* j; V3 B. Z初战告破,运行游戏,你会看到日文全变成了乱码,说明程序已经在使用中文的字符集了 - e3 w4 p/ N0 ? U6 [- q
修改游戏脚本,加入几个中文看看……
# I$ s- c& P, p4 ~6 U. Z# @+ K/ w为什么我添加的中文全部是“□”?
: k s/ j' j5 M这就是需要解决的第二关卡,字符集边界检查。
3 k& Y& u2 d6 y* j! c
- ~6 m. v, g5 [! X; \( W: d既然已经设定了字符集,为什么还要边界检查呢?这是为了防止当游戏文本中含有某些非法的字符串时产生缓存溢出。于是在字形传递到GDI32.dll描绘字体准备显示在屏幕之前对其进行检查,发现超出了设定的缓存大小就将其拦截下来,于是屏幕上就显示出一个“□”。我们知道,由于日文的字符比中文少得多,所以这个缓存也小的多,换言之就是边界太窄。
1 Q) ^+ I; r3 P& Y边界检查的例子: ' \# b2 w" I) s K
cmp al,80 q- V( A9 ?+ H( ^) A/ T8 x
jbe xxxxxxxx 1 X2 P; v* m% t% y1 m* m" K
cmp al,09F
; {2 t0 F9 O+ G9 Z: h4 b# F! pjb xxxxxxxx
& u! C1 T# {: a1 ?: N* e! }+ G: W8 X' Ucmp al,0E0 X0 R& N$ q# L6 S8 d
jb xxxxxxxx
3 V. }/ k/ [9 s# K& pcmp al,0FC B5 N2 e, ~- O. G5 G! a9 E
ja xxxxxxxx
" z. i% R( U" f) u! A5 Z$ x/ x. A' U4 h! x6 R
即看字符是否在80-9F(前两位)和e0-fc(后两位)之间 {9 ^# z: d8 d
===================================
* _4 Y( y a0 V# b; w) q如何具体查找程序的字符集边界呢?常用的方法是下断,用OllyDbg载入游戏主程序运行,一步一步断下去,在出现一堆“□”的时候停住,然后转到ASM模式查看停在哪里。
- |# s' H. L5 [( q& wL0043BA00: % P5 F: v, V7 m* z) |' o
cmp al,80h 3 g# ~ {$ y1 k: o: ~' A; ~
jc L0043BA08
9 L( Z$ l6 F2 \) Q* c0 Rcmp al,9Fh ! a9 [% t2 a% @* M
jbe L0043BA10
# \4 x/ @9 l7 V0 xL0043BA08: 0 R* r8 ?* ^& Q/ n" D- k
cmp al,E0h
2 W* e0 F8 {2 ?; ijc L0043BA30 * r% T4 x* a( T
cmp al,FFh ! E9 Q3 t9 i6 W: [, c/ m
ja L0043BA30
0 Y! m- C; z- P- Q花瓣的上边界为80~FF,而下边界为E0~FF,好,开始动手 & x2 {+ m- N6 l) |
这里我们将上边界改为80~FF,而下边界范围足够宽广,就不用改了。 : P4 T2 R9 ~8 t& z8 x1 m
这里为什么使用80~FF而不改为20~FF呢?因为我们需要让游戏文本中原本的日语空格(8140)不显示为乱码,于是8140刚好在边界外,就不会被送去CreateFontA进行显示,就会显示为日语的缺字码,一个空白——而它刚好就是空白,在显示上两者没有任何区别。 / z5 M6 E4 t/ z/ ]
如法炮制,打开EU修改之,保存测试,OK,正常显示了: J, a* F- ^6 c/ S
' \5 m, S7 q$ \- X3 N7 z
原文/ {& O; z6 o E0 P9 O0 K. y+ V
http://blog.potatoneko.cn/ |