前言

在分析一些unity游戏的逻辑代码时,直接使用dnspy查看游戏的Assembly-CSharp.dll逻辑代码静态分析存在一些缺陷,就是对于不熟悉运作流程的新游戏,我们很难把握其代码的大致执行流程,并且很难做到在运行时分析。幸运的是,dnspy在几个版本之前就已经支持了针对mono虚拟机的unity游戏的调试支持,只需要跟随我的步伐,修改几处游戏文件就可以顺利的开始你的dnspy动态调试unity之旅

下载dnspy及调试所需内容

如果你还不知道dnspy是个什么东西,你可以在这里看到它的介绍 dnspy主页

简单来说,它就是一个可以用来反编译二进制.NET架构程序代码的工具,拥有强大的分析,反编译,调试功能。

你可以在release界面直接下载到最新版本,自带中文 Releases · dnSpy

除了下载dnspy主程序,我们这里还需要专门下载调试unity所需的mono替换文件,它在release板块的unity分支栏下

1607699123890.png

这里需要根据你的游戏版本下载,至于查看游戏版本号的方法,你可以找到unity游戏主程序,右键程序属性,在详细信息这一栏所找到的文件版本就是你的unity版本号

1607699191680.png

下载好对应的替换文件后,打开对应版本号的文件夹,你会发现有win32/win64两个文件夹,这个根据你游戏的位数而定,如果你不确定游戏位数,那么可以先尝试使用32位文件替换,如果游戏无法正常运行,再尝试64位文件替换

替换调试所需文件

这里只需要找到你游戏的根目录,也就是包含有游戏可执行文件,和游戏数据文件的目录,它的结构通常是这样:

/游戏名.exe
/游戏名_Data/
/其他文件夹/

打开游戏名_Data 文件夹,并替换以下文件(如果没有这个文件,则忽略)

<root>\<GAME>_Data\Mono\mono.dll
<root>\<GAME>_Data\Mono\EmbedRuntime\mono.dll
<root>\<GAME>_Data\MonoBleedingEdge\EmbedRuntime\mono-2.0-bdwgc.dll
<root>\Mono\EmbedRuntime\mono.dll
<root>\MonoBleedingEdge\EmbedRuntime\mono-2.0-bdwgc.dll

特殊情况

鉴于有一些游戏可能会验证mono.dll是否被替换,我们需要做一些操作手段来加载debug版本的mono.dll,我想到的解决办法是通过注入dll,然后hook LoadLibraryW 的方法,让游戏加载mono模块的时候,直接加载我们自己的调试版本模块。当然这个方法要求你的注入方法时机必须非常早,早于mono模块的加载时机,至于怎么实现,注册表注入,劫持注入这些都可以实现,这里不做赘述。

代码可以大概写成这样

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    char ProcessPath[MAX_PATH] = { 0 };
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        DWORD curPID = GetCurrentProcessId();
        GetModuleFileNameExA(OpenProcess(PROCESS_ALL_ACCESS, false, curPID), 0, ProcessPath, MAX_PATH);
        string myPath = ProcessPath;
        if (myPath.find("SSJJ_BattleClient_Unity") != string::npos) {
            Log("Injected!!");
            Log(myPath);
            if (MH_Initialize() != MH_OK) {
                Log("MH_Initialize 加载失败!");
                return FALSE;
            }
            HookLoadLibraryW();
            return TRUE;
        }
        else {
            return FALSE;
        }
    }
    else if (ul_reason_for_call == DLL_PROCESS_DETACH) { 
        MH_Uninitialize();
    }
    return FALSE;
}
BOOL HookLoadLibraryW() {
    PVOID LoadLibraryWAddr = GetProcAddress(GetModuleHandleA("KernelBase.dll"), "LoadLibraryExW");
    if (!LoadLibraryWAddr) {
        Log("GET LoadLibraryWAddr failed!");
        return false;
    }
    if (MH_CreateHook(LoadLibraryWAddr, MyLoadLibraryExW, reinterpret_cast<LPVOID*>(&oriLoadLibraryExW)) != MH_OK) {
        Log("MH_CreateHook LoadLibraryWAddr failed");
        return false;
    }
    if (MH_EnableHook(LoadLibraryWAddr) != MH_OK) {
        Log("MH_EnableHook LoadLibraryWAddr failed");
        return false;
    }
    Log("Hook LoadlibraryW success");
    return true;
}
typedef HMODULE(WINAPI* ptrLoadLibraryExW)(LPCWSTR, HANDLE, DWORD);
ptrLoadLibraryExW oriLoadLibraryExW;
HMODULE
WINAPI
MyLoadLibraryExW(
    _In_ LPCWSTR lpLibFileName,
    _Reserved_ HANDLE hFile,
    _In_ DWORD dwFlags
) {
    char buf[1000];
    memset(buf, 0, sizeof(buf));
    sprintf_s(buf, "%ws", lpLibFileName);
    string buf_str = buf;
    if (buf_str.find("\\mono.dll") != string::npos) {
        HMODULE res = oriLoadLibraryExW(L"C:\\Users\\15516\\AppData\\Roaming\\Wooduan\\SSJJ-4399\\battle\\6\\SSJJ_BattleClient_Unity_Data\\Mono\\monoDebug.dll", NULL, dwFlags); //替换成我们自己的调试用的Dll
        Log("Patched Load Monodebug.dll");
        return res;
    }
    return oriLoadLibraryExW(lpLibFileName, hFile, dwFlags);
}

通过这样的手段,我们已经通过调试版mono运行了游戏,接下来就是使用dnspy进行调试了

使用dnspy调试

首先来配置环境变量,根据dnspy官网的说明

dnSpy's mono.dll will look for an environment variable called DNSPY_UNITY_DBG (Unity with .NET 2.0-3.5 assemblies) or DNSPY_UNITY_DBG2 (Unity with .NET 4.x assemblies)

DNSPY_UNITY_DBG

--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y or

--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y,no-hide-debugger to enable detection of the debugger.

DNSPY_UNITY_DBG2

--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n or

--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n,no-hide-debugger to enable detection of the debugger.

我们根据自己unity游戏的版本,可以来配置环境变量,以自定义调试端口。当然,dnspy的调试默认以55555为端口,因此不设置环境变量默认情况下,以55555为调试端口

这里我们以win10为例,教大家如何设置环境变量

我的电脑右键-属性

左侧栏点击高级系统设置

1607699933028.png

点击高级-环境变量

1607699955128.png

在系统变量处点击新建,并输入对应的环境变量值进入

1607699999534.png

点确定即可

然后启动游戏,并保持游戏运行

打开dnspy,注意这里一定要用管理员权限打开

然后先按照静态分析的方法,打开游戏的主dll

然后点菜单栏的调试-开始调试

调试引擎选择Unity(连接),ip不用填写,默认为本机,点击确定

1607700184233.png

如果设置没有问题,此时应该游戏就已经连接上了,可以试试下断点看看能否触发

1607700246840.png

1607700265069.png

可以看到,断点成功触发,而且堆栈回溯,变量监控等各功能均正常工作

这样,就可以很方便的通过单步查看游戏的流程,从而更方便我们分析游戏了。

参考资料

Debugging Unity Games · dnSpy/dnSpy Wiki (github.com)

转载必须注明版权