查看原文
其他

Galgame汉化中的逆向(二):系统字库与文字编码

devseed 看雪学院 2021-03-07

本文为看雪论坛优秀文章

看雪论坛作者ID:devseed





0x0 前言




先上预览图。上节我们谈了谈如何找到解密文本的函数,以及如何来反汇编分析。这节来谈谈有关汉化调用系统字库与编码的问题、如何解决乱码、以及windows上常用的相关函数。通常我们要做的是:

  • 解除非日文系统区域限制

  • 修正游戏由于编码问题无法找到对应文件

  • 修正游戏中的乱码,标题中的乱码

  • 修改游戏中字符的限制,为中文汉化做准备


此系列与我在隔壁还有贴吧发的相同。

上期:
Galgame汉化中的逆向 (一):文本加密(压缩)与解密

ps. 点击文字即可跳转查看~





0x1 文字编码


以winodws系统来看,字符编码主要有两种:
  • Multibyte(类型表示为 char *, LPSTR, ),

  • WideChar(类型表示为 wchar_t *, LPWSTR, L"")。


Multibyte为变长字符,中ASCII字符为1字节,中文或日文等为两字节,不同系统编码不同,必须要指定对应的codepage才能正确显示。

其中GB2312与shift-jis属于MultiByte;WideChar可以看作是unicode,或者说utf-16,两字节宽字符,所有系统编码都相同。

(1) utf-8


utf-8是一种Multibyte编码,也是unicode的一种存储形式(可以理解为unicode是一种标准,utf-8是一种实现方式),最短为8位,通过位运算可以很容易和unicode进行变换。如下,将x位拼起来就是unicode,其中汉字通常为,0xEx开头。
 
1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx


(2) shift-jis, SHJIS, cp932


shift-jis即 JIS X 0208子集的所有字符,可以参考sjis字符表。

第一位字节 使用0×81-0×9F、0xE0-0xEF (共47个)

第二位字节 使用0×40-0×7E、0×80-0xFC (共188个),没有7F。
 
一般查字库的时候多用这几个边界处字符定位,0x8141[、], 0x889f[亜], 0xe040[様]。


(3) gb2312, cp936


gb2312简中编码和sjis差不多,但是中间没有断层,这点要方便不少。

范围0xA1A1~0xFEFE,其中汉字编码范围是0xB0A1~0xF7FE,详见gb2312字符表。





0x2 字符转换


不同编码集的字符转换通常要通过WideChar为桥梁,转换函数没MultiByteToWideChar、WideCharToMultiByte。

通过CodePage来进行不同字符集到WideChar转换,其中932为sjis编码,936为gb2312编码。

int MultiByteToWideChar( UINT CodePage, DWORD dwFlags, _In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar);

有时候游戏调用CreateFileW等相关需要宽字符的函数,而MultiByteToWideChar中codepage为0(跟随系统),这时候就可能找不到文件,如下图:
 
 
我们需要把代码改成0x3A4。可以通过hook此函数,hook不好用就直接改exe,如果利用push eax相关的短指令则需要用code cave了。
 
Frida hook MultiByteToWideChar

var kernel32_MultiByteToWideChar = Module.findExportByName("kernel32.dll", "MultiByteToWideChar");console.log('MultiByteToWideChar at '+ kernel32_MultiByteToWideChar);var g_multibyte_ptr;var g_wide_ptr;var g_n=0; Interceptor.attach(kernel32_MultiByteToWideChar, { onEnter: function (args, state) { g_multibyte_ptr = args[2]; g_wide_ptr = args[4]; console.log(g_n +", cp"+args[0].toString(10)) args[0] = ptr('932'); g_n++;}, onLeave: function (retval) { console.log(g_multibyte_ptr.readAnsiString() + ", " + g_wide_ptr.readUtf16String()) }

Code Cave






0x3 调用系统字库


游戏中通常用CreateFontIndirectA来选择字库,函数如下:

HFONT CreateFontIndirectA( const LOGFONTA *lplf);typedef struct tagLOGFONTA { LONG lfHeight; LONG lfWidth; LONG lfEscapement; LONG lfOrientation; LONG lfWeight; BYTE lfItalic; BYTE lfUnderline; BYTE lfStrikeOut; BYTE lfCharSet; //0x17 BYTE lfOutPrecision; BYTE lfClipPrecision; BYTE lfQuality; BYTE lfPitchAndFamily; CHAR lfFaceName[LF_FACESIZE];} LOGFONTA, *PLOGFONTA, *NPLOGFONTA, *LPLOGFONTA;

日文游戏显示乱码原因多半是lfCharSet值输入为1(ANSI_CHARSET 0 BALTIC_CHARSET 1,跟随系统),语言根据徐通决定,我们要改成0x80 日语,汉化后要改成0x86 中文。其中lfCharSet大多在CreateFontIndirectA上面。

比如说改为mov dword ptr ds:[532798], 80000000,修改乱码如图所示:





0x4 枚举系统字库


游戏中选择字体的时候通常用EnumFontFamiliesA或EnumFontFamiliesExA来枚举可用字库,通常会在FONTENUMPROCA回调函数中,通过charset来过滤其他字库。如:

mov edi, dword ptr ss:[ebp + 8] ;arg1 cmp byte ptr ds:[edi + 17], 80 ;japanese

这个函数声明如下, charset就在LOGFONT结构体里, charset 0x80为日语, 0x86为中文。

int CALLBACK EnumFontFamProc( ENUMLOGFONT FAR *lpelf, // pointer to logical-font data NEWTEXTMETRIC FAR *lpntm, // pointer to physical-font data int FontType, // type of font LPARAM lParam // address of application-defined data ); int EnumFontFamiliesExA( HDC hdc, LPLOGFONTA lpLogfont, FONTENUMPROCA lpProc, LPARAM lParam, DWORD dwFlags); typedef struct tagENUMLOGFONT { // elf LOGFONT elfLogFont; //这项结构如tagLOGFONTA所示 BCHAR elfFullName[LF_FULLFACESIZE]; BCHAR elfStyle[LF_FACESIZE];} ENUMLOGFONT;




0x5 其他


(1) 系统语言检测相关


一般来说常用的函数是GetSystemDefaultLCID,返回值是系统语言代码,英文 0x409,日文 0x411, 简中0x804,繁中0x404。


(2) 游戏窗口乱码修复


游戏窗口标题可能存储在游戏资源文件里,不一定在exe里面,通过CreateWindowEx来初始化窗口标题,或者SetWindowText。


(3) 点阵字模


GetGlyphOutlineA 将字符转换成点阵来输出,有些游戏的字体输出都是调用此函数,一般都会在调用这个函数之前进行字符边界校验。具体与上面类似,不再赘述。函数声明为:

DWORD GetGlyphOutlineA( HDC hdc, UINT uChar, UINT fuFormat, LPGLYPHMETRICS lpgm, DWORD cjBuffer, LPVOID pvBuffer, const MAT2 *lpmat2);



- End -


看雪ID:devseed

https://bbs.pediy.com/user-617776.htm

  *本文由看雪论坛 devseed 原创,转载请注明来自看雪社区。



推荐文章++++

* Frida加载和启动XServer

* 记一次so文件动态解密

* 阿里2015第二届安全挑战赛第三题题解

* VSCode搭建轻量驱动开发环境

* 恶意代码分析之反射型DLL注入





好书推荐










公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点分享点赞在看,支持看雪哦~


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存