Background

When analyzing some logical code of unity games, we could use dnspy to view Assembly-CSharp.dll. However, there's something really inconvenient. If you aren't familiar with a new game, it is difficult to grasp the general execution process of the code. Fortunately, dnspy has supported debugging for unity games on mono VM. Just follow my step, and you'll find it easy to debug a unity game with dnspy!

Download Dnspy and Dependence

If you don't know what dnspy is, you can get some information here. dnspy Page

In short, it is a tool that can be used to decompile binary .Net framework program code, with powerful analysis, decompilation, debugging functions.

You can download the latest version here on the release page. Releases · dnSpy

In addition to downloading the dnspy main binary, we also need to download the mono replacement file needed for debugging unity, which is under the unity branch of the release page.

1607699123890.png

Here you need to download it according to your game version. As for the method to view the game version, you can find the main executable program of the unity game. Right-click the program properties. The file version found in the details column is your unity version.

1607699191680.png

After downloading the corresponding replacement file, open the folder corresponding to the unity version, and you will find that there are two folders: Win32 / win64. It depends on the platform your game runs on. If you are not sure about the number of bits in the game, you can first try to replace the 32-bit file. If the game does not work properly, try to replace the 64-bit file.

Replace mono.dll for Debugging

Here you just need to find the root directory of your game, that is, the directory containing the game executable files and game data files. Its structure is usually as follows:

/GameName.exe
/GameName_Data/
/OtherFolder/

Open the folder GameName_Data, and replace the following file (ignored if it doesn't exist):

<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

Exceptional Case

Given that some games may verify mono.dll to confirm that it has not been replaced, we need to do some operation to load the debug version mono.dll. The solution I came up with is to inject DLL and hook LoadLibraryExW. When the game loads mono module, we can directly load our own debugging version module. Of course, this method requires that your injection method time must be very early, which is earlier than the loading time of mono module. As for how to implement it, registry injection and hijacking injection can be implemented. Here, I will not talk about that.

My implementation is like this

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 Load Failed!");
                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); //replace our own debug module
        Log("Patched Load Monodebug.dll");
        return res;
    }
    return oriLoadLibraryExW(lpLibFileName, hFile, dwFlags);
}

In this way, we have run the game through the debug version of mono, and the next step is to debug it with dnspy

Debug the Game With DnSpy

First, let's configure the environment variables according to the instructions on the dnspy official website

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.

According to the version of our unity game, we can configure environment variables to customize the debugging port. By the way, the debugging port of dnspy is 55555 by default, if no environment variable is set.

Here we take win10 as an example to teach you how to set environment variables

MyComputer-RightClick-properties

Advanced system settings

1607699933028.png

Advanced-Environment Variables

1607699955128.png

Click New and input the Variables name and Variables value

1607699999534.png

click OK and the environment variable is set

Then start the game and keep it running

Launch dnspy,and you need to note that it must be opened with administrator authority

Then according to the method of static analysis, open the main DLL of the game

Then go to Debug-Start Debugging

and select Unity(Connect),you don't need to input IP as it will be set as localhost by default

1607700184233.png

If there is no problem with the settings, the game should be already connected. You can try the add a breakpoint to see if it can be hitted

1607700246840.png

1607700265069.png

We can see that the breakpoint is triggered successfully, and the functions such as stack backtracking and variable monitoring work normally

In this way, it is very convenient to view the process of the game through single step, which is more convenient for us to analyze the game.

Reference

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

Reprint must be marked with copyright