首先,分析游戏的文字显示方式。玩游戏的时候最怕打开游戏看到一片乱码,现在汉化游戏却是最希望打开游戏看到一片乱码,因为一般在非日文环境下能正常显示日文的游戏都使用的字符集限定,为汉化增加了不少的难度…… . A/ b# R& @' z
一起采取Textout方式的年代,日文、繁体中文十个有九个是乱码,于是有了Apploc和NL,后来比尔大叔推出了CreateFontA函数……CreateFontA能够直接定义程序使用和操作系统不同的字符集,于是乱码没有了,而汉化者噩梦开始了,因为汉化后的中文内容在经过日文字符集的编译后变为日语状态下的乱码。
1 y( j1 {$ P0 d& `以下是比尔大叔的MSDN Library对CreateFontA给出的函数原形
" N+ e! { w* S- p( J5 d. @/ m! x1 |3 F' E% }
HFONT CreateFont( # F- {3 k/ j7 Z& V
int nHeight, // height of font $ W- _' b+ X* s+ O1 F
int nWidth, // average character width
9 x- u F' \& D! o: x3 X! }3 {3 n: eint nEscapement, // angle of escapement
* L; \% h: x) W8 M3 s1 _. s& }int nOrientation, // base-line orientation angle
% M8 S& C/ @" Rint fnWeight, // font weight 3 i! n" G- M0 v* e2 V0 q
DWORD fdwItalic, // italic attribute option
! H; x( `& L' gDWORD fdwUnderline, // underline attribute option
! p5 X+ G0 t* b! k) FDWORD fdwStrikeOut, // strikeout attribute option * k* r& L- x6 v G: P; r( E2 i
DWORD fdwCharSet, // character set identifier
# O4 t' { W) b8 K4 N5 SDWORD fdwOutputPrecision, // output precision & L& l. _4 ~. j
DWORD fdwClipPrecision, // clipping precision
5 ]1 Q( D: ? c: F! ]DWORD fdwQuality, // output quality
' a8 y8 `9 F+ X( a& IDWORD fdwPitchAndFamily, // pitch and family
8 C7 O' V( V" O. d. D ALPCTSTR lpszFace // typeface name
# G% Z9 O% n; z2 d, d* M X( m$ W);
* Z# j) k/ d5 Y4 ?7 F
+ S# y3 R+ I7 n8 u9 Q. y/ L+ qHFONT CreateFontIndirect( ; I, o5 o/ [3 ~: C+ t' O
CONST LOGFONT* lplf // characteristics
! P7 C3 c9 A' @ v0 w);
0 u8 }5 q) C8 W, N7 x/ e其中 LOGFONT的声明如下: 4 ?# u; [0 R% I2 k0 @! t
9 L9 B; o I7 ?( n; ?, P% G. htypedef struct tagLOGFONT {
% t$ Z, z0 r! r3 xLONG lfHeight;
! y4 I; [3 v9 pLONG lfWidth; 8 u+ N$ D6 f+ w8 C" A
LONG lfEscapement;
* \" I# v' h. P9 P3 |# W/ y( lLONG lfOrientation; ; P; d: V5 ~. H( L' y1 o+ @
LONG lfWeight; % h! A4 s+ B5 i( j; c1 `
BYTE lfItalic; ' [; S# Q: |2 g, c! r3 k/ e+ C
BYTE lfUnderline; ) G7 p9 f \) {
BYTE lfStrikeOut; - i- B2 q; l2 M% Y
BYTE lfCharSet;
$ g3 D7 M: {8 @4 DBYTE lfOutPrecision;
' S: q- `% G& _/ PBYTE lfClipPrecision; 5 {; G! m* K) b6 `
BYTE lfQuality;
* J4 ]' v1 E0 W. O9 eBYTE lfPitchAndFamily; # r7 o A! ~" {, Q( J# J" E8 ]3 A
TCHAR lfFaceName[LF_FACESIZE];
, r2 f2 N e8 {5 U7 d& J2 j} LOGFONT, *PLOGFONT;
7 E3 @" O, N) C% m4 e" M. u: W==================分割线=================
$ d6 h/ F7 _# G. O要改变程序支持的字符集,就要改变程序调用上面两个函数时的fdwCharSet或lfCharSet的值
) {5 Y% H$ ]( l# a* B+ o$ H其中各字符集所对应的值如下:
N4 W, |, C+ N( k7 [
& V' v; Q3 E$ T字符集 值(十进制) 6 y9 z& i2 W$ J# J+ p. b7 @
ANSI_CHARSET 0 $ P3 {8 A! @) |2 F5 \# p
DEFAULT_CHARSET 1 $ O# t5 Z6 |9 J$ A3 w
SYMBOL_CHARSET 2
8 h* S. B0 z, C. W4 @! b& QMAC_CHARSET 77 + v& G; L8 \0 O; L) C6 } H
SHIFTJI_CHARSET 128 % C! c) z5 S9 O+ k, y
HANGEUL_CHARSET 129 ) v! J9 r$ o( X1 Y! |2 S
HANGUL_CHARSET 129 9 D5 F4 }9 @3 h7 o- r
JOHAB_CHARSET 130 1 x! y: e' b2 E/ u x
GB2312_CHARSET 134
6 t6 J+ `+ u' {6 k6 n% P8 [CHINESEBIG5_CHARSET 136 - I6 m! e( Q* e5 s
GREEK_CHARSET 161 ( z C! x" T& u/ m$ u: w* E% f8 \6 c
TURKISH_CHARSET 162 7 k; T" v+ y9 T9 [5 C; z
VIETNAMESE_CHARSET 163 " F1 P& ~1 }( B# y: G
HEBREW_CHARSET 177
* o1 u' _! Q( C( J$ gARABIC_CHARSET 178 |! j% S! y& J1 ~7 n& _" b
BALTIC_CHARSET 186
' y- ^2 Z6 ^0 N [4 QRUSSIAN_CHARSET 204
7 ^& [( {0 Y8 j& c7 D" q, \" DTHAI_CHARSET 222
- E, m3 y" |( [& C/ _# V2 ZEASTEUROPE_CHARSET 238
8 a% O0 c! [ m9 B1 AOEM_CHARSET 255 9 R) L+ [. K5 E/ J. g S
可以看到简体中文是86(Hex),日语是80(Hex),繁体中文是88(Hex) ) Q& h: R Q$ M- T8 n$ a
我们要做的就是找到游戏中定义调用字符集的部位,将80改为86,这样就能让程序正确的显示中文。
3 }1 {5 U8 K' m, t r5 M( _; m" i用pexplorer打开HANABIRA.exe,进入反编汇模式,查找Font字符串,我们发现调用CreateFontA函数的位置是唯一的: " q5 G( B! a) T% F! f- n2 q, w
于是在以下代码我们停下来 2 [* t5 k* v5 H% ~5 q
L0041E365: ) V1 L* I5 L3 s6 [* E
mov edi,[esp+14h] % I, K ]$ Q) C. W( n' A
mov ebx,[esp+28h] 9 d0 ~0 `) s* L A
mov ebp,[esp+24h] 8 a& y; `& `8 J3 o
mov eax,[esp+20h] 9 |9 H' h" z u7 s
mov ecx,[esp+1Ch]
: G( Y0 d0 p# t! V& Ipush edi
" C! W3 n2 ?$ D* `8 }mov edx,[esp+30h] % j7 W8 T4 u Y# O0 U, P7 _
push 00000031h ( J" f5 D' q0 a7 Q7 t2 Z5 c
push 00000002h * g6 @# ~; O) t/ a; u
push 00000000h
) J1 w' V$ V) [6 A$ Xpush 00000000h
9 h8 n B9 v2 M2 \, v# b s, m8 `push 00000080h & z9 O- `! V2 N& S
push ebx
- x0 z9 o2 k( v5 R6 @2 H- Mpush ebp
' v( \/ F( A* u( j2 Z6 a8 jpush eax 1 x* ] q: t; G; e; G- _$ ?. T3 H
mov eax,[esp+3Ch] 4 r6 K: N. N$ P: j J" W8 g' J
push ecx 5 v! S5 u; Z9 {6 M. |
push 00000000h : R: M5 m, ~# C2 B9 d6 I1 K7 L. Z
push edx % |/ H- Q6 U9 W. b, p% v
push 00000000h 9 b( t. K8 e0 d0 H. ~( q0 T
push eax
6 l! U |' q) [* q6 J! X; k' r' O/ jcall [GDI32.dll!CreateFontA] 9 N5 f- R9 M; j, w. p' u* s
lea ecx,[esi+28h] # [' i. L' ?9 F9 N& m6 e
mov [esi+00000138h],eax 6 ?3 ^* _1 k2 J0 C) l' V7 u6 W* K
mov eax,edi 9 e1 ]8 P9 i Z) @9 o7 a
sub ecx,edi : Q* ]$ T. ]2 Y0 ^4 U& Z
lea esp,[esp+00h]
6 V9 p y8 S& g" M6 Y2 f) H% h注意这就是调用GDI32.dll中的CreateFontA函数了,我们在这个堆栈中寻找将80这个值传递给CreateFontA的部位。 ( @) h$ ^6 c4 s& n a; N: v
push 00000080h 6 X Q* j# W9 Z% n3 \6 l& \
就是这里将80值压入传递中
4 L/ N: _' r& Y$ Z8 RPE中标记了这段赋值的Hex数值,用UE打开文件,找到
+ R8 W; c- k5 W' h. T/ A5 E7 [68800005355 : X% i# A4 p) X. s
将其改为
L1 y$ E7 C) o8 y1 k4 ]68860005355
! ]* Y4 ^0 ?7 t9 a保存之。 T, |; S9 U, P6 a( r! A- r Y+ b
这样PE中看到这段压入就成了 $ F1 {( p6 A1 ^+ }/ u5 q+ |$ q
push 00000086h 0 G5 l- n& n7 m" ~- J& W
初战告破,运行游戏,你会看到日文全变成了乱码,说明程序已经在使用中文的字符集了 * u" f" ~3 B2 f9 z( g: p
修改游戏脚本,加入几个中文看看……
! R. ~. b- Y8 ]$ ]- O& |为什么我添加的中文全部是“□”? ; o! m L, {; Q8 @
这就是需要解决的第二关卡,字符集边界检查。 * U8 B: f6 E2 U. ^
1 N. ~$ ~! f* X$ a0 ]
既然已经设定了字符集,为什么还要边界检查呢?这是为了防止当游戏文本中含有某些非法的字符串时产生缓存溢出。于是在字形传递到GDI32.dll描绘字体准备显示在屏幕之前对其进行检查,发现超出了设定的缓存大小就将其拦截下来,于是屏幕上就显示出一个“□”。我们知道,由于日文的字符比中文少得多,所以这个缓存也小的多,换言之就是边界太窄。
: F& d/ G% K0 o3 S Y. H边界检查的例子:
/ m3 ?% H# w2 n9 p' I9 _9 Dcmp al,80 9 D/ o/ _$ |& H+ o+ R/ A# f
jbe xxxxxxxx ; \7 [! M( h _% @: h$ L
cmp al,09F 1 W& k& h, T* b5 U; r: ?" u
jb xxxxxxxx
1 C0 `6 Z& o/ B. ]5 kcmp al,0E0
; J8 _ T! q: J3 I4 }jb xxxxxxxx
9 O$ v0 b; h+ K* Scmp al,0FC 8 b9 z" i, F% V
ja xxxxxxxx 9 s# i' K- ^9 v. h) {8 I
2 V% c+ l$ L- L2 q即看字符是否在80-9F(前两位)和e0-fc(后两位)之间
$ }# {! B( k2 m( {* z1 o1 ]=================================== 8 I$ d/ o8 Z. n
如何具体查找程序的字符集边界呢?常用的方法是下断,用OllyDbg载入游戏主程序运行,一步一步断下去,在出现一堆“□”的时候停住,然后转到ASM模式查看停在哪里。 . C! C$ Y7 c; Y c, S
L0043BA00: 5 H, c9 x, ?# g+ |/ ^8 E# O- C" G
cmp al,80h 7 q' p' M* z6 @6 P$ B5 Y5 k
jc L0043BA08
6 C J" R+ r# K) A @3 c- g% ]* d, Scmp al,9Fh
9 a9 D% k6 o6 }; N0 Qjbe L0043BA10
- h8 u; E+ o- e2 j0 |. E" ?L0043BA08: : z* D% T/ o) d2 B
cmp al,E0h
, M7 ^0 W2 X& M1 v, C9 C/ Ljc L0043BA30
# R7 C8 b2 Z3 q" X6 u1 }cmp al,FFh
" g* y2 n+ T4 ^. ^ja L0043BA30
9 c1 d, p; K. B花瓣的上边界为80~FF,而下边界为E0~FF,好,开始动手
8 Z, ~& J. t. b% R! O1 t, _% @% q E这里我们将上边界改为80~FF,而下边界范围足够宽广,就不用改了。 * z0 c* B: r" [. m# x! J
这里为什么使用80~FF而不改为20~FF呢?因为我们需要让游戏文本中原本的日语空格(8140)不显示为乱码,于是8140刚好在边界外,就不会被送去CreateFontA进行显示,就会显示为日语的缺字码,一个空白——而它刚好就是空白,在显示上两者没有任何区别。 : a t7 l q: `
如法炮制,打开EU修改之,保存测试,OK,正常显示了& w' f5 L% b5 `# X( E* Y8 |
- ]. o9 D$ p( d4 W$ U原文
+ `+ k& ?8 b7 R' }6 \http://blog.potatoneko.cn/ |