Featured image of post Locale Emulator工作原理

Locale Emulator工作原理

Locale Emulator的工作原理及其在Galgame运行中的应用

本文由AI生成(Gemini 2.5 Pro),故所述内容仅供参考,请以实际情况为准。

功能分析(简洁版)

核心功能

Locale Emulator (LE) 是一个为 Windows 应用程序设计的软件,其主要功能是模拟一个非当前系统默认的区域设置(Locale)来运行指定的程序。

这在以下场景中非常有用:

  1. 运行外语软件:许多为特定语言(如日语、中文)开发的软件(尤其是游戏)在其他语言的 Windows 系统上运行时,可能会出现乱码、无法启动或功能异常的问题。LE 可以为这些程序模拟其原生语言环境。
  2. 软件开发与测试:开发者需要测试其应用程序在不同语言和区域设置下的表现时,可以使用 LE 快速切换环境,而无需频繁更改系统设置或使用虚拟机。

实现方法与技术原理

LocaleEmulator.cpp 实现为一个动态链接库(DLL),它通过 DLL 注入技术被加载到目标进程中,然后在进程内部通过 API Hooking 和 内存修改 的方式来篡改其运行环境,从而达到模拟区域设置的目的。

其技术实现可以分解为以下几个关键步骤:

DLL 注入与初始化

  • 项目本身被编译成一个 DLL。一个外部的加载器程序(Loader)负责将这个 DLL 注入到用户指定的目标应用程序进程中。
  • DLL 的入口点是 DllMain 函数。当 DLL 被加载到目标进程时(DLL_PROCESS_ATTACH),Initialize 函数会被调用,开始执行核心的模拟逻辑。
  • 为了防止目标程序过早地调用某些系统函数而导致模拟失败,代码中使用了 LdrDisableThreadCalloutsForDll 来禁止在加载此 DLL 时对线程创建/销毁事件的通知,提高了初始化的稳定性。

伪造区域设置数据

Windows 系统通过一套名为 NLS (National Language Support) 的机制来管理区域设置。相关数据(如代码页、排序规则、日期格式等)存储在一些 .nls 文件中。

LE 的核心原理就是用目标模拟区域的 NLS 数据替换掉进程中默认的系统 NLS 数据。

  • 加载自定义 NLS 文件:LeGlobalData::Initialize 函数会根据配置(例如,要模拟的日语环境),通过 GetNlsFile 和 GetLangFile 等函数定位并读取对应的 .nls 文件(如 c_932.nls 代表日语 Shift-JIS 代码页)。
  • 创建内存映射:程序将读取到的 ANSI 代码页、OEM 代码页和 Unicode 语言文件(l_intl.nls)等数据加载到一个连续的内存块 (CodePageMapView) 中。
  • 修改 PEB (Process Environment Block):PEB 是每个进程都有的一个内核结构,存储了大量的进程信息,其中就包括指向 NLS 数据表的指针。LE 直接修改当前进程 PEB 中的 AnsiCodePageData、OemCodePageData 和 UnicodeCaseTableData 等成员,让它们指向 LE 自己加载到内存中的伪造 NLS 数据。
  • 重置 NLS 表:修改 PEB 指针后,调用 RtlResetRtlTranslations 来通知 NTDLL 使用新的 NLS 数据表。

完成这一步后,所有通过标准 API(如 MultiByteToWideChar)进行字符串转换的操作都会自动使用 LE 提供的伪造数据,从而正确地处理目标语言的字符编码,解决了乱码的根源问题。

API Hooking

仅仅替换 NLS 数据还不够,因为很多程序还会通过其他 API(如查询注册表、获取系统字体)来获取区域信息。因此,LE 必须拦截(Hook)这些 API 调用,返回伪造的信息。

  • Hook NTDLL:HookNtdllRoutines 函数负责 Hook 底层的 ntdll.dll 中的函数,这是最基础的拦截。
  • 动态 Hook 其他系统 DLL:LE 使用 LdrRegisterDllNotification 注册一个 DLL 加载通知。当 USER32.dll、GDI32.dll、KERNEL32.dll 等关键系统库被加载时,LE 会收到通知,并立即调用相应的 Hook 函数(如 HookUser32Routines)来拦截其中的 API。
  • 延迟初始化技巧:代码中使用了一个名为 DelayInitDllEntry 的巧妙技巧。如果 LE 发现 kernel32.dll 等模块虽然已加载但其 DllMain 尚未执行,LE 会将其入口点临时替换为 DelayInitDllEntry。这样可以确保 LE 的 Hook 代码在目标 DLL 的任何初始化代码之前执行,从而避免错过早期的 API 调用。

注册表重定向

应用程序经常从注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage 等路径下读取默认的代码页(ACP、OEMCP)等信息。

  • InitRegistryRedirection 函数设置了一套重定向规则。当被 Hook 的注册表 API 检测到程序正在查询这些敏感键值时,它不会返回系统的真实值,而是返回 LE 根据模拟区域设置伪造的值(例如,将 ACP 的值返回为 “932”)。

特殊兼容性处理

  • 代码中有一个名为 FuckStupid2DJGame 的导出函数,这显然是一个针对特定游戏引擎(“2DJGame”)的兼容性补丁。它 Hook 了 GetFileAttributesA 函数,当检测到查询 2djgame.txt 文件时,直接返回一个正常属性,绕过了某些游戏的特定检查。这体现了此类工具为了兼容性需要做很多细致的适配工作。

总结

Locale Emulator 是一个构思精巧、技术实现复杂的工具。它并非简单地修改几个 API,而是通过注入 DLL,从底层替换进程的 NLS 数据,并结合广泛的 API Hooking 和注册表重定向,为目标程序构建了一个几乎完全隔离和可信的虚拟区域环境。这种多层次、系统性的方法是其能够成功模拟各种复杂应用程序运行环境的关键。

功能分析(完整版)

根据文件名和代码内容,这个项目是一个区域模拟器的核心组件。它的主要功能是让一个应用程序(通常是为特定语言,如日语、中文开发的游戏或软件)误以为自己运行在一个不同语言和区域设置的 Windows 系统上,从而解决乱码或无法运行的问题。

这是一个通过 DLL 注入技术实现的工具。它作为一个 DLL 文件被注入到目标进程中,然后通过修改进程内存中的数据和挂钩(Hook)系统 API 调用来改变应用程序的行为。


主要功能模块和实现方法

下面是该项目核心功能的分解说明,以及它们在 LocaleEmulator.cpp 中对应的代码实现。

1. DLL 入口与初始化

功能:作为 DLL,它需要一个入口点 (DllMain)。当 DLL 被加载到目标进程时,它会执行初始化操作;当 DLL 被卸载时,它会执行清理操作。

实现方法:

  • DllMain 函数是标准的 DLL 入口点。
  • 在 DLL_PROCESS_ATTACH 事件中,它调用 Initialize() 函数来启动模拟环境。
  • 在 DLL_PROCESS_DETACH 事件中,它调用 UnInitialize() 函数来清理资源,恢复被修改的内容。

对应代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
BOOL Initialize(PVOID BaseAddress)
{
    // ...
    // 创建核心数据结构 LeGlobalData
    GlobalData = new LeGlobalData;
    // ...
    // 调用 LeGlobalData 的初始化函数
    Status = GlobalData->Initialize();
    // ...
}

BOOL UnInitialize(PVOID BaseAddress)
{
    // 释放 LeGlobalData 并执行清理
    LeReleaseGlobalData();
    ml::MlUnInitialize();
    return FALSE;
}

BOOL NTAPI DllMain(PVOID BaseAddress, ULONG Reason, PVOID Reserved)
{
    switch (Reason)
    {
        case DLL_PROCESS_ATTACH:
            // 初始化失败则执行清理
            return Initialize(BaseAddress) || UnInitialize(BaseAddress);

        case DLL_PROCESS_DETACH:
            UnInitialize(BaseAddress);
            break;
    }

    return TRUE;
}

2. 核心区域模拟:替换 NLS 数据

功能:Windows 使用一套名为 NLS (National Language Support) 的数据表来处理字符编码(代码页)、排序规则、日期格式等。区域模拟最核心的一步就是将进程默认的 NLS 数据替换为目标区域的数据。

实现方法:

  • LeGlobalData::Initialize() 是实现此功能的核心。
  • 它首先从一个名为 LEPEB (Locale Emulator Process Environment Block) 的共享内存区域读取用户配置的区域信息(如目标代码页、区域ID)。
  • 然后,它根据这些信息找到系统目录 (System32) 下对应的 NLS 文件(例如 c_932.nls 对应日语 Shift-JIS 代码页)。
  • 它将这些 NLS 文件读入自己分配的一块新内存中。
  • 最关键的一步:它修改当前进程环境块 (PEB) 中的几个指针,让它们指向这块新内存。这样,当应用程序请求代码页或区域信息时,系统就会从被替换的数据中读取。

对应代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 在 LeGlobalData::Initialize() 函数中
// ...
// 根据配置获取 NLS 文件名
Status = GetNlsFile(&NlsFileName, GetLeb()->AnsiCodePage, REGPATH_CODEPAGE);
Status = GetNlsFile(&OemNlsFileName, GetLeb()->OemCodePage, REGPATH_CODEPAGE);
// ...
// 读取 NLS 文件内容
Status = ReadFileInSystemDirectory(AnsiFile, &NlsFileName);
// ...
// 分配新内存用于存放伪造的 NLS 数据
Status = AllocVirtualMemory(&CodePageMapView, ...);
// ...
// 将 NLS 文件内容拷贝到新内存
CopyMemory(PtrAdd(CodePageMapView, AnsiCodePageOffset),     AnsiFile.GetBuffer(),   AnsiFile.GetSize32());
// ...
// 修改 PEB 指针,使其指向新的 NLS 数据
Peb = CurrentPeb();
Peb->AnsiCodePageData       = (PUSHORT)PtrAdd(CodePageMapView, AnsiCodePageOffset);
Peb->OemCodePageData        = (PUSHORT)PtrAdd(CodePageMapView, OemCodePageOffset);
Peb->UnicodeCaseTableData   = (PUSHORT)PtrAdd(CodePageMapView, UnicodeCaseTableOffset);
// ...

3. API 挂钩 (Hooking)

功能:仅仅替换 NLS 数据有时是不够的,因为很多应用程序会直接调用 Windows API 来获取区域信息或处理文本。因此,需要拦截(Hook)这些 API 调用,返回伪造的结果。

实现方法:

  • 动态挂钩:使用 LdrRegisterDllNotification 注册一个 DLL 加载通知。当 kernel32.dll, user32.dll, gdi32.dll 等关键系统 DLL 被加载时,模拟器会收到通知。
  • 查找并应用钩子:LookupDllHookEntry 函数维护了一个列表,记录了哪些 DLL 需要被挂钩,以及对应的挂钩/卸载函数。
  • 延迟挂钩:为了处理某些 DLL 在模拟器完全初始化前就被加载的情况,代码采用了一种巧妙的“延迟初始化”策略。它会暂时替换目标 DLL 的入口点为 DelayInitDllEntry。这个函数会先执行 DLL 原本的初始化代码,然后再应用 API 钩子,确保时机正确。
  • 挂钩具体函数:HookNtdllRoutines, HookKernel32Routines 等函数(在此文件中只看到引用,具体实现在其他文件)会使用某种技术(如 Inline Hook)将目标 API 的开头指令替换为一个跳转,使其跳转到模拟器自定义的函数。

对应代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 在 LeGlobalData::Initialize() 函数中
// 注册 DLL 加载/卸载的通知回调
LdrRegisterDllNotification(0,
    [] (ULONG NotificationReason, PCLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context)
    {
        return ((PLeGlobalData)Context)->DllNotification(NotificationReason, NotificationData);
    },
    this,
    &DllNotificationCookie
);

// ...

// DLL 通知处理函数
VOID LeGlobalData::DllNotification(ULONG NotificationReason, PCLDR_DLL_NOTIFICATION_DATA NotificationData)
{
    // ...
    // 检查是否是需要挂钩的 DLL
    if (LookupDllHookEntry(DllName) == nullptr)
        return;
    
    // ...
    // 应用或移除钩子
    HookModule(DllBase, DllName, NotificationReason == LDR_DLL_NOTIFICATION_REASON_LOADED);
}

// 查找需要挂钩的 DLL
PDLL_HOOK_ENTRY LookupDllHookEntry(PCUNICODE_STRING BaseDllName)
{
    static DLL_HOOK_ENTRY DllHookEntries[] =
    {
        { RTL_CONSTANT_STRING(L"USER32.dll"),       &LeGlobalData::HookUser32Routines,      &LeGlobalData::UnHookUser32Routines },
        { RTL_CONSTANT_STRING(L"GDI32.dll"),        &LeGlobalData::HookGdi32Routines,       &LeGlobalData::UnHookGdi32Routines },
        { RTL_CONSTANT_STRING(L"KERNEL32.dll"),     &LeGlobalData::HookKernel32Routines,    &LeGlobalData::UnHookKernel32Routines },
    };
    // ...
}

4. 注册表重定向

功能:有些程序会从注册表中读取区域设置。为了彻底模拟,也需要欺骗这些注册表查询。

实现方法:

  • LeGlobalData::InitRegistryRedirection 函数负责初始化重定向规则。
  • 它维护一个重定向条目列表。每个条目包含一个“原始”注册表键值对和一个“重定向”后的键值对。
  • 当 API 挂钩模块拦截到注册表读取函数(如 NtQueryValueKey)时,它会检查查询的键是否在重定向列表中。
  • 如果匹配,它不会去读真正的注册表,而是直接返回预设的“重定向”数据。例如,当程序查询系统的默认代码页时,返回模拟的代码页(如 932)。

对应代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 在 LeGlobalData::Initialize() 中,为加载器和被加载进程设置重定向规则
// ...
// 这是一个重定向规则的例子,将查询 ACP(Ansi Code Page) 的结果重定向到预设值
REGISTRY_REDIRECTION_ENTRY64 *Entry, Entries[] =
{
    {
        { (ULONG64)HKEY_LOCAL_MACHINE, USTR64(REGPATH_CODEPAGE), USTR64(REGKEY_ACP), REG_SZ, },
        { (ULONG64)HKEY_LOCAL_MACHINE, USTR64(REGPATH_CODEPAGE), USTR64(REGKEY_ACP), REG_SZ, DefaultACP, DefaultACPLength },
    },
    // ...
};

Status = this->InitRegistryRedirection(Entries, countof(Entries), nullptr);
// ...

// InitRegistryRedirection 函数将这些规则解析并存储到内存中,供挂钩函数使用
NTSTATUS LeGlobalData::InitRegistryRedirection(PREGISTRY_REDIRECTION_ENTRY64 Entry64, ULONG_PTR Count, PVOID BaseAddress)
{
    // ...
    // 遍历所有规则,将其转换为内部格式并存储
    FOR_EACH(Entry64, Entry64, Count)
    {
        // ...
        this->RegistryRedirectionEntry.Add(LocalEntry);
        // ...
    }
    // ...
}

总结

LocaleEmulator.cpp 实现了一个功能强大且技术复杂的区域模拟引擎。它综合运用了 DLL 注入、PEB 修改、API 挂钩、注册表重定向等多种底层技术,从多个层面“欺骗”目标应用程序,为其创造一个完全虚拟的系统区域环境。这种多管齐下的方法确保了模拟的彻底性和高兼容性。

载入天数...载入时分秒...
根据 CC BY-NC-SA 4.0 协议进行授权
使用 Hugo 构建
主题 Stack 由 Jimmy 设计