代码与孤独与Galgame

3.3k 词

1. 引言

Galgame是美丽而独特的游戏形式,我一直如此相信着。但长文本时代已经日薄西山,Galgame业界江河日下。优秀的作品减少,爱好者和社区自然也凋零。对中国阿宅来说,更重要的是汉化组不像以往那样百花齐放了。而Galgame汉化,又比动画漫画轻小说更复杂,除了需要懂日语的人,还需要程序员,从解包到汉化到校对到封包,是件相当麻烦的事。

很凑巧的是,我刚好了解与Galgame逆向相关的知识,所以希望自己能为社区建设贡献微小的力量,因此仿照MisakaHookerFinder的功能写了类似的文本提取器。经过测试,在某些游戏上(例如秽翼的尤斯蒂娅),有更好的效果。但当然Bug更多,如果有人对inline Hook或者Win32编程感兴趣,可以看下下文的实现原理介绍,代码中我也写了详细的注释。

Have fun:)

不过在跨年夜写代码推Gal,还真是孤独的夜晚啊。

2. 相关技术简介

2.1 Win32 API

Windows是Windows操作系统,也是一个应用程序,Windows 提供了不同的服务,这些服务通过一些特定的方式进行调用、使用;这些服务可能是 开启一个窗口、打开一个应用程序、通过一个方法设置系统的休眠时间等;这些不同的服务,做成了接口的方式使用。

简而言之,Windows API的作用就是调用WIndows提供的服务,详细的API文档可以参见微软官网

也可以用中文版本,但机翻比较难看懂。

2.2 API Hook

API Hook是一种技术,可以拦截目标程序调用的函数,并将其重定向到自定义的函数中,从而实现对程序行为的监视、干预和修改。

具体来说,API Hook的实现过程大致包括以下几个步骤:

  1. 找到目标函数:API hook需要先找到目标程序中需要拦截的函数。通常可以通过静态分析、动态调试等方法来获取目标函数的地址或名称。
  2. 替换函数指针:通过修改目标程序的数据段,将目标函数的指针替换为自定义函数的指针。这样,当程序调用目标函数时,会自动跳转到自定义函数中执行。
  3. 处理参数和返回值:自定义函数需要处理目标函数的参数和返回值,并在必要时对其进行修改。这可以通过获取目标函数的参数列表和返回值类型,并在自定义函数中进行相应的处理来实现。
  4. 恢复原始函数:在完成自定义函数的操作后,需要将目标函数的指针恢复为原始的函数指针,以确保程序正常运行。

pivZsGn.png

当然Windows操作系统也提供了类似功能的钩子函数,即SetWindowHookEx这种,但本次设计使用更灵活的修改目标函数地址的机器码的inline Hook方式实现。

2.3 DLL注入

使用Hook可以实现监控本进程的Win32 API,但是不能监控其他进程的Win32 API。因为其他进程调用的函数,实际上是将系统提供函数映射到进程私有的内存空间,不能被其他进程修改。

因此要实现Hook其他进程,还需要用到DLL注入,将Hook代码写成动态链接库,然后让目标进程加载该DLL。既然目标进程加载的,自然可以随意操作目标进程的内存空间了。所以问题就转变为如何让目标进程加载我们构造好的动态链接库了。

本项目使用 Windows 远程线程机制实现DLL注入,在本地进程中通过 CreateRemoteThread()函数在其他进程中开启并运行一个线程CreateRemoteThread函数原型如下:

1
2
3
4
5
6
7
8
9
10
HANDLE WINAPI CreateRemoteThread (
HANDLE hProcess, // 远程进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
SIZE_T dwStackSize, // 线程栈的大小
LPTHREAD_START_ROUTINE lpStartAddress, // 线程入口函数的起始地址
LPVOID lpParameter, // 传递给线程函数的参数
DWORD dwCreationFlags, // 线程是否立即启动
LPDWORD lpThreadId // 用于保存内核分配给线程的ID
)

主要关注三个参数:hProcesslpStartAddresslpParameterhProcess是要执行线程的目标进程句柄;lpStartAddress 是线程函数的起始地址,且该函数必须位于目标进程内;lpParameter 是传递给线程函数的参数。

为了使远程线程加载DLL,把 LoadLibrary 函数作为 CreateRemoteThread的线程函数,然后加载的 DLL 路径作为线程函数的参数即可。

3. 系统设计实现

3.1 整体设计

理解了上面三个技术的基础,就可以开始设计整体Hook流程了。

很明显,系统应该分为两个部分:

  1. 将DLL注入到目标进程的主窗口程序:这是Windows API编程的部分,我使用原生Win32 API完成。主要包括对进程的查找和操作,以及对进程通信和消息处理;

  2. 注入到目标进程的动态链接库:其中包含对目标进程的API Hook代码。因为是在目标进程中加载调用,所以可以直接使用当前进程句柄开发即可。这是系统设计的核心,所有hook操作和进程通信都在动态链接库中完成.

3.2 主窗口程序

3.2.1 GUI窗口实现

Win32设计窗口比较复杂,但流程很固定:

  1. 调用 WinMain 函数,注册窗口类,创建主窗口并显示。

  2. 创建窗口过程函LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)。窗口过程函数是一个回调函数,用于处理窗口消息和事件。

  3. 创建消息循环,等待事件的发生。

  4. 当一个事件发生时,系统将该事件打包成一个消息,并发送到应用程序的消息队列中。

  5. 从消息队列中取出消息,并根据消息类型调用相应的窗口过程函数进行处理。

  6. 处理完消息后,返回到消息循环,继续等待事件的发生。

大部分代码用在消息处理中,有一个大的switch将获取到的各种消息分发到函数,在逆向窗口程序时也很常见到。我的窗口UI设计比较简洁,因为Win32设计UI比较麻烦,就算用VS2022也没办法用资源管理器轻松设计窗口,所以主要创建了四个窗口:主窗口、ListBox控件、文本框控件和按钮控件。然后根据效果调整了UI布局,最后效果如下图:

pivZy2q.png

主窗口UI如上,在打开窗口时对进程拍摄快照,获取进程名称和PID,然后鼠标选中进程即可查看详细信息,以便确认是否是想要hook的进程(可以使用微软官方的process explorer选中要hook的进程查看PID):

pivZ6x0.png

窗口选中要Hook的进程后,点击[API Hooking!]按键,开始DLL注入进程。

3.2.1 DLL注入实现

DLL注入有很多种方式,其中最成熟也最方便的方法是建立远程线程的方式加载动态链接库,本项目使用这种方式完成DLL注入。

  1. 通过建立远程线程的方式注入DLL主要有如下流程:
  2. 使用OpenProcess()打开目标进程;
  3. 在目标进程中使用VirtualAllocEX()分配内存空间;
  4. WriteProcessMemory将DLL路径写入进程内存空间;
  5. 获取LoadLibrary函数地址:因为这个函数原型在Kernel32.dll中,所有进程都会将内核模块加载进内存,所以LoadLibrary的地址偏移量是可以确定的;
  6. 在目标进程创建远程线程执行LoadLibrary
1
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(pLoadLibrary), pRemoteMemory, 0, NULL);

此时如果正常执行,我们建立的Dll会加载进目标进程中,可以使用OpenArk或者Process Explorer工具查看验证,我这里使用OpenArk查看HookDll.dll是否加载进目标进程模块:

pivZfZF.png

3.3 HookAPI动态链接库

这部分是本项目核心代码,即Hook具体功能的实现以及与主窗口的进程间通信。如前所述,本项目使用inline Hook方式实现对API的监听,那么主要流程如下:

  1. 使用getLibraryProcAddress获取目标API函数在目标模块中的地址指针:因为无论映射到哪个进程,模块库中函数的相对地址都是一样的:
1
pFunction = getLibraryProcAddress(moduleName, functionName);
  1. ReadProcessMemory将目标进程中,目标函数地址的第一条指令读取出来存储在字节数组中
1
ReadProcessMemory(GetCurrentProcess(), pFunction, messageBoxOriginalBytes, 6, &bytesRead);

这是为了方便unhook,在完成我们写好的操作后,再修改回原本的指令

  1. 构造覆盖目标函数地址的第一条指令的机器码

这一步是核心,因为只有正确构造跳转指令,才能将对API函数的调用转到我们定义的HookedAPI函数。说到跳转,很容易想到的是jmp指令,但因为jmp指令指向的地址是相对的偏移量,因此还要获取基址,算起来比较麻烦,所以本项目采用堆栈实现类似的跳转操作。

构造的HookCode形式为:push 0x12345678 ret

即将我们构造的函数地址压栈,然后ret将其作为返回地s址。实际上和jmp完成的跳转是一样的。

Push和ret各为1个字节的指令,因为要注入的是x86架构的32位进程,所以地址为4位,因此要构造的HookCode长度为6个字节:

1
2
3
4
char patch[6] = { 0 };
memcpy_s(patch, 1, "\x68", 1); //push
memcpy_s(patch + 1, 4, &hookedMessageBoxAddress, 4); //32位地址为4字节长度
memcpy_s(patch + 5, 1, "\xC3", 1); //ret\

要注意这段写法只能适用于32位,如果是64位的宽字符要修改不少地方。

  1. 将构造好的机器码写入目标函数第一条指令的地址:
1
WriteProcessMemory(GetCurrentProcess(), (LPVOID)pFunction, patch, sizeof(patch), &bytesWritten)

此时已经完成对目标函数的Hook,可以调试看下是否正常将指令覆盖掉了:

这是MessageBoxA原本的机器码:

pivZLqO.png

完成Hook后再查看:

pivZjde.png

发现已经正确完成覆盖了,此时目标进程调用MessageBoxA时,就会被重定向到我们的函数地址了。那么下面就是设计我们的HookedAPI函数了,需要注意的是这个函数必须有与原API函数相同的签名,否则会出现堆栈不平衡,影响后续程序执行。

  1. 设计Hooked函数
1
2
3
4
5
6
7
8
9
10
int __stdcall HookedMessageBoxA(HWND hwnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) 
{
UnHook();
/*========通过向指定窗口进程发送消息实现进程通信(Work) =========*/
SendCustomMessage(lpText);
// call the original MessageBoxA
int result = MessageBoxA(NULL, lpText, lpCaption, uType);
HookAPI("user32.dll", "MessageBoxA", &HookedMessageBoxA);
return result;
}

注意这个函数是MessageBoxA,所以字符指针是LPCSTR,如果是MessageBoxW则需要用长指针LPCWSTR。因为不能干扰目标进程正常执行,所以还需要UnHook()将指令修改回来,但是为了持久化Hook以监听每一次API调用,还要在函数返回前再调用一次Hook函数。此时已经完成了Hook的基本操作,但我们前面设计的主窗口没有收到消息,用户也无从知道API函数被调用,所以需要做进程通信。

  1. 向目标窗口发送自定义消息,进程通信
1
2
3
4
5
6
7
8
9
10
11
12
13
void SendCustomMessage(LPCSTR lpText)
{
HWND targetHwnd = FindWindow(NULL, L"ProcessHooker"); // 根据窗口标题查找目标窗口句柄
if (targetHwnd != NULL) {

// WCHAR buffer[] = L"Hello world";
// 将Hook到的数据写入剪切板(x86字符)
CopyTextToClipboard(lpText);

SendMessage(targetHwnd, WM_HOOKED_MESSAGE, 0, 0); // 发送消息
}
}

进程通信有很多实现方式,可以用共享内存、管道或者直接用消息(但操作系统不允许SenMessage传递指针),本项目使用最稳妥的剪切板实现。发送消息到主窗口后,主窗口进程会读取到数据,并数据和调试信息显示在文本框中,此时完成全部过程:

piveSJA.png

5. 系统调试与测试

因为大部分代码在动态链接库中完成,所以调试是件比较头疼的事。甚至主窗口程序在原生Win32实现下,也不好显示调试信息。所以就出现一个问题:如果Hook没有正常完成,可能是注入有问题,也可能是注入的DLL代码有问题,让问题难以定位。

因此本次项目开发中用到了开源工具OpenArk的DLL注入功能,大大便利了DLL注入的过程,让我可以安心开发Hook程序,虽然后面实现DLL注入后不需要了,但对调试确实很便利,而且有丰富的进程操作工具。

留言