unity系列逆向博客第一篇……整个系列结构会是:
代码逆向 & 调试(c#) ←本篇here
代码Hook框架 & mod教程(c#)
资源逆向(UABE/AssetStudio) & 导出 & 修改
代码逆向 (IL2CPP篇)
代码mod教程 (IL2CPP篇) 会有吗
至于后面还有没有,看缘分(手动狗头),也许5年内能填完。
1. 如何区分Unity应用是c#还是IL2CPP 大部分unity游戏都可以套以下2条公式:
在${game_name}_Data目录下有Managed文件夹,且该文件夹下有Assembly-CSharp.dll这个文件。这类游戏跑的就是c#代码(严格意义上来说,是用c#生成的一种中间语言IL,用mono执行。这好比于java生成的字节码,后面用JVM执行一样……)比如:
在${game_name}_Data目录下有il2cpp_data文件夹,且该文件夹里有etc、Metadata和Resources三个子文件夹的,就是用il2cpp跑的代码了,比如:
按逆向的难度来说,前者和后者的难度相比起来可以说是从简单直接跳到地狱难度了。所以,我个人是不太建议深入钻研il2cpp相关的逆向的,除非工作需求or自己学习的兴趣确实能啃得下这块硬骨头。
2. c#逆向工具比对 市面上主流的大概有2款工具,.net Reflector和dnSpy 。.net Reflector是付费&比较老的工具了,初学者一般而言,无脑用dnSpy就行了。前者的功能后者都有,而且是开源的(划重点),不止能看代码,甚至只要你会玩,还能进行代码调试。打开工具选择Assembly-CSharp.dll,就能看到里面的代码了,也没太多可介绍的。
像我之前无聊写的戴森球计划存档解析工具dsp_save_parser ,就是抄这里面的代码逻辑。
3. 代码调试 本片重头戏。dnspy调试unity游戏,需要一个debug build的游戏版本(不现实),或者给mono-2.0-bdwgc.dll打个补丁,使其支持调试。而大部分unity版本的补丁,都能在dnSpy-Unity-mono 这个repo里找到,还有个别PR支持到2021.X了。参考里面的readme流程走就行了。
3.1 确认游戏的unity版本 右键UnityPlayer.dll,选到详细信息tab,用眼睛去感受。戴森球用的是2022.3.62,再随便找了个游戏,是2019.4.22的……
3.2 跟着走readme流程 首先告诉你clone dnSpy-Unity-mono 和mono 这两个仓库,那就直接clone:
git clone https://github.com/dnSpyEx/dnSpy-Unity-monogit clone -b unity-2022.3-mbe https://github.com/Unity-Technologies/mono --recurse-submodules
已经在偷懒了……直接带上分支,submodule也干脆直接顺带一块给clone下来。
上一步找到的unity版本号,直接在mono的GitHub仓库页面搜大版本,基本上能搜到分支名,直接用就行,免得后面还得去切分支。
然后编译umpatcher,拿到游戏里的mono-2.0-bdwgc.dll的编译时间,就省了需要下unity安装包的麻烦了:
然后在上面clone到本地的mono 仓库,查看这个时间点附近的最近一次merge提交(建议用gitk看):
把这次的commit sha1 98b53367f6099821deac68d1b7de2e2fbb1e1532(左下角的那串东西)记下来,然后就应该是复制内容然后生成一个sln,直接编译就行了。
看着有点不对劲……修复了这个后,又发现dnSpy-Unity-mono 这个仓库的dnSpy分支少了最新的unity 2022的sln,淦。
3.3 在mono仓库手动改 既然捡不了漏,那就干脆看下代码是干嘛的,自己改得了。
大部分的逻辑是在这段代码文件里的,还有极个别是在项目里加上新增的几个文件,很好,自己动手工作量也不大,go on,顺便也看一下别人fork之后patch 2021.x的代码dnSpy-Unity-mono-unity2021.xx 。最终改动的地方,直接给个diff吧。
前提级:注意改之前要checkout到上一步的commit sha1
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 @@ -8593,9 +8593,13 @@ ves_icall_get_resources_ptr (MonoReflectionAssemblyHandle assembly, gpointer *re #endif /* ENABLE_NETCORE */ +extern gboolean dnSpy_hideDebugger; + MonoBoolean ves_icall_System_Diagnostics_Debugger_IsAttached_internal (void) { + if (dnSpy_hideDebugger) + return 0; return mono_is_debugger_attached (); } @@ -100,7 +100,8 @@ free_debug_handle (MonoDebugHandle *handle) void mono_debug_init (MonoDebugFormat format) { - g_assert (!mono_debug_initialized); + if (mono_debug_initialized) + return; if (format == MONO_DEBUG_FORMAT_DEBUGGER) g_error ("The mdb debugger is no longer supported."); @@ -91,6 +91,8 @@ #include "mono/metadata/custom-attrs-internals.h" #include "mono/metadata/unity-utils.h" +#include "mono/mini/dnSpy.h" + /* * On iOS we can't use System.Environment.Exit () as it will do the wrong * shutdown sequence. @@ -854,6 +856,7 @@ debugger_agent_parse_options (char *options) agent_config.keepalive = atoi (arg + 10); } else if (strncmp (arg, "setpgid=", 8) == 0) { agent_config.setpgid = parse_flag ("setpgid", arg + 8); + } else if (dnSpy_debugger_agent_parse_options (arg)) { } else { print_usage (); exit (1); @@ -1043,6 +1046,7 @@ debugger_agent_init (void) mono_profiler_set_gc_finalizing_callback (prof, gc_finalizing); mono_profiler_set_gc_finalized_callback (prof, gc_finalized); + dnSpy_debugger_init_after_agent (); mono_native_tls_alloc (&debugger_tls_id, NULL); /* Needed by the hash_table_new_type () call below */ @@ -141,6 +141,7 @@ insert_breakpoint (MonoSeqPointInfo *seq_points, MonoDomain *domain, MonoJitInfo BreakpointInstance *inst; SeqPointIterator it; gboolean it_has_sp = FALSE; + SeqPoint found_sp; if (error) error_init (error); @@ -149,9 +150,14 @@ insert_breakpoint (MonoSeqPointInfo *seq_points, MonoDomain *domain, MonoJitInfo while (mono_seq_point_iterator_next (&it)) { if (it.seq_point.il_offset == bp->il_offset) { it_has_sp = TRUE; - break; + if (!(it.seq_point.flags & MONO_SEQ_POINT_FLAG_NONEMPTY_STACK)) { + found_sp = it.seq_point; + break; + } + found_sp = it.seq_point; } } + it.seq_point = found_sp; if (!it_has_sp) { /* @@ -111,6 +111,8 @@ #include "mono/metadata/icall-signatures.h" #include "mono/utils/mono-tls-inline.h" +#include "mono/mini/dnSpy.h" + static guint32 default_opt = 0; static gboolean default_opt_set = FALSE; MonoMethodDesc *mono_stats_method_desc; @@ -4366,6 +4368,7 @@ mini_init (const char *filename, const char *runtime_version) CHECKED_MONO_INIT (); + dnSpy_debugger_init (); #if defined(__linux__) if (access ("/proc/self/maps", F_OK) != 0) { g_print ("Mono requires /proc to be mounted.\n"); @@ -175,6 +175,7 @@ <ClCompile Include="$(MonoSourceLocation)\mono\mini\debugger-agent.c"> <Filter>Source Files$(MonoMiniFilterSubFolder)\common</Filter> </ClCompile> + <ClCompile Include="$(MonoSourceLocation)\mono\mini\dnSpy.c" /> <ClInclude Include="$(MonoSourceLocation)\mono\mini\debugger-engine.h"> <Filter>Header Files$(MonoMiniFilterSubFolder)\common</Filter> </ClInclude> @@ -216,6 +216,8 @@ <ItemGroup> <ClCompile Include="$(MonoSourceLocation)\mono\mini\mini-windows-dllmain.c" /> <ClCompile Include="..\mono\metadata\oop.c" /> + <ClCompile Include="..\mono\mini\dnSpy.c" /> + <ClCompile Include="..\mono\mini\dnSpy.h" /> </ItemGroup> <Import Project="clrcompression.targets" /> <Import Project="eglib.targets" /> @@ -35,5 +35,11 @@ <ClCompile Include="..\mono\metadata\oop.c"> <Filter>Source Files\libmini\unity</Filter> </ClCompile> + <ClCompile Include="..\mono\mini\dnSpy.h"> + <Filter>Header Files</Filter> + </ClCompile> + <ClCompile Include="..\mono\mini\dnSpy.c"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> </Project>
然后是submodule bdwgc的修改,只加了一行:
1 2 3 4 5 6 7 8 9 10 11 @@ -24,6 +24,7 @@ #ifndef GCCONFIG_H #define GCCONFIG_H +#define GC_DISABLE_INCREMENTAL #define MANUAL_VDB
最后得新增2个文件,抄的原来的dnSpy.c,加在mono/mini里,然后VS里也加上(在上面的diff里已经加了):
dnSpy.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #pragma once #include <mono/metadata/profiler.h> #include <mono/metadata/mono-debug.h> #include "debugger-agent.h" #define DNUNITYRT 1 typedef void *MonoLegacyProfiler;typedef void (*MonoLegacyProfileFunc) (MonoLegacyProfiler *prof) ;MONO_API void mono_profiler_install (MonoLegacyProfiler *prof, MonoLegacyProfileFunc callback) ;#define DEFINED_LEGACY_PROFILER extern gboolean dnSpy_hideDebugger;void dnSpy_debugger_init () ;int dnSpy_debugger_agent_parse_options (char * arg) ;void dnSpy_debugger_init_after_agent () ;
dnSpy.c
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #include "dnSpy.h" #ifndef DNUNITYRT #define DNUNITYRT 0 #endif #ifndef DEFINED_LEGACY_PROFILER #define MonoLegacyProfiler MonoProfiler #endif #define ENV_VAR_NAME_V0 "DNSPY_UNITY_DBG" #define ENV_VAR_NAME_V1 "DNSPY_UNITY_DBG2" #if DNUNITYRT == 0 #define ENV_VAR_NAME ENV_VAR_NAME_V0 #elif DNUNITYRT == 1 #define ENV_VAR_NAME ENV_VAR_NAME_V1 #else #error Invalid DNUNITYRT value #endif gboolean dnSpy_hideDebugger = 1 ; void dnSpy_debugger_init () { gboolean fixDefer = FALSE; char * envVal = getenv (ENV_VAR_NAME); #if DNUNITYRT != 0 if (!envVal) { envVal = getenv (ENV_VAR_NAME_V0); fixDefer = TRUE; } #endif if (!envVal) { envVal = "--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y" ; fixDefer = TRUE; } #if DNUNITYRT != 0 #define defer_y "defer=y" #define suspend_n "suspend=n" const char * s = strstr (envVal, defer_y); if (s && fixDefer) { int envVal_len = strlen (envVal); int defer_y_len = strlen (defer_y); int suspend_n_len = strlen (suspend_n); int newStr_len = envVal_len - defer_y_len + suspend_n_len; char * newStr = (char *)malloc (newStr_len + 1 ); memcpy (newStr, envVal, s - envVal); memcpy (newStr + (s - envVal), suspend_n, suspend_n_len); memcpy (newStr + (s - envVal) + suspend_n_len, s + defer_y_len, envVal_len - (s + defer_y_len - envVal)); newStr [newStr_len] = 0 ; envVal = newStr; } #endif char * argv[] = { envVal }; mono_jit_parse_options (1 , (char **)argv); mono_debug_init (MONO_DEBUG_FORMAT_MONO); } int dnSpy_debugger_agent_parse_options (char * arg) { if (strcmp (arg, "no-hide-debugger" ) == 0 ) { dnSpy_hideDebugger = 0 ; return 1 ; } return 0 ; } typedef struct { void * dummy; } DebuggerProfiler;static DebuggerProfiler dnSpy_dummy_profiler;static void dnSpy_runtime_shutdown (MonoLegacyProfiler *prof) {} void dnSpy_debugger_init_after_agent () { mono_profiler_install ((MonoLegacyProfiler*)&dnSpy_dummy_profiler, dnSpy_runtime_shutdown); }
3.4 生成patch dll 打开msvc/mono.sln:
上面选Release和x64,然后右边右键libmono-dynamic
点生成……
如果有奇怪的链接报错,右键右边的dnSpy.c,选编译,然后重新生成一遍就行了,大概是这文件放错位置了,后面再说……
然后在msvc/build/boehm/x64/bin/Release里找到需要的mono-2.0-bdwgc.dll。
把游戏里的原本的dll备份一遍,然后把它复制到游戏路径下的MonoBleedingEdge/EmbedRuntime里,打开游戏就行了……
懒的同学,这里就放一份预编译好的2022.3.62的mono-2.0-bdwgc.dll ,下载下来直接用就行。
3.5 开启调试 先打开游戏,然后就可以在dnspy上调试了:设置好断点,按F5,选Unity(连接)
最后想怎么用就怎么用,手感跟vs差不多。
4. 题外话环节 跟本篇无关哈……
之前一直抱怨戴森球这破游戏蓝图怎么不能复制地基……好了,这下功能实装了,该把之前搁浅了老久的痛球代码重拾一下做到蓝图里了……
↑预览图就这样的,适配了戴森球的坐标网格,额外带球体透视校正。远看应该还挺唬人的。最近写这篇博客,主要也是用来debug这游戏里面的蓝图代码啦。虽然之前已经支持蓝图解析了,但要生成蓝图那还是得读生成蓝图的那堆代码才行……下一篇预计是跟本次无关的博文:戴森球蓝图生成代码解读……