0. 背景
近日,一家老牌游戏厂商发布了一款MMO手游。 听说图形非常好,而且是基于unity3d的。 很想看看效果如何,可惜测试时间太短,而且需要激活码。 我必须等到安装它。 apk,输入后发现没有激活码,测试结束。 不过,登录界面看起来确实不错,所以我不得不对其进行逆向工程并看一下。
十年前,我们在微软的dotnet平台下研究了一些安全相关的东西。 十年后,由于mono项目和unity3d的流行,dotnet技术在移动平台上开始流行。 凭着断断续续的记忆和当年的一些资料,我就做了一个游戏逆转。
在移动平台上,dotnet dll没有签名,这在一定程度上使得逆向工程和修改变得更加容易。
本文简单介绍逆向过程,省略与游戏相关的内容。
1. apk解包、重新打包和签名
您可以使用“Android逆向助手”或ApkStudio。 我们使用ApkStudio进行解包,使用Android逆向助手进行重新打包和签名。
2.棒棒加解密
解压后的一级目录如下:
AndroidManifest.xml
apktool.yml
资产/
建造/
库/
原来的/
资源/
斯马利/
首先,使用Reflector或IlSpy打开assets/bin/Data/Managed目录下的标准unity3d游戏模块Assembly-CSharp.dll。
无法打开,文件已加密。 。
打开lib/armeabi-v7a目录可以看到依赖的so文件如下:
libAkSoundEngine.so
libBlueDoveMediaRender.so
libCrasheyeNDK.so
libDexHelper.so
libKGAudio.so
libmain.so
libmono.so
libmsc库
libslua.so
自由库
力布娃网
libweibosdkcore.so
看文件名,大部分都是功能模块。 只有 libDexHelper.so 是可疑的。 我上网一查,发现是棒棒的东西。 在网上找不到任何关于Bangbang的unity3d游戏的加密原理和解决方案的资料,只好自己看一下。
这时候就得用ida pro了。 我使用的是6.6版本。 首先,看一下 libmono.so。 这是mono的运行时库。 dotnet的加载和运行支持都在这里。 直接看一下mono_image_open_from_data_with_name函数。 经检查,没有发现明显的改装痕迹。 我也搜索了几个相关的加载相关函数,但没有发现修改的痕迹。 。
我怀疑 libmono.so 根本没有被修改。 unity3d的版本信息在它的资源文件中比较容易找到。 只需在assets/bin/Data下打开一个带有assets后缀的文件即可(记得使用十六进制编辑器)。 您可以在文件的开头看到 5.3.3p2。 它实际上使用的是补丁版本,而不是像f1那样的官方版本。 去unity3d网站下载对应版本的编辑器+android发布包。 安装后找到Unity5.3.3p2\Editor\ Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Libs\armeabi-v7a\libmono.so使用Beyond Compare等支持二进制比较的工具与libmono.so进行比较游戏。 一模一样。看来Bangbang不能静态修改。
libmono.so。 想一想。 像unity3d这样的版本更新频繁。 如果静态地改变每个版本2d游戏素材,那就相当被动了。 不过某厂的加密是静态修改的libmono.so(其实是我修改了源码重新编译的)。
我无法弄清楚 libDexHelper.so 是如何工作的。 我不是逆向工程专业人士。 我用ida pro打开看了一下。 没找到任何线索就放弃了(在ida pro中找不到Assembly-CSharp.dll这样的字符串,但是用十六进制编辑器在libDexHelper.so文件末尾看到了这个字符串。有兴趣的同学可以研究一下它的加密原理)。
我改变主意,获取解密后的dll,即以libmono.so开头。 我已经发现Bangbang并没有对这个文件进行静态处理,所以我们更容易随心所欲地处理它。 查看mono的源码,我们知道是在mono_image_open_from_data_with_name函数中。 dll 文件的内容将显示为完整的内存映像。
那么获取解密的dll至少有以下几种方法:
我在断点后转储了内存。 我的电脑上用的是虚拟机,破解不了。 。
修改mono源代码,添加dump代码并替换游戏的libmono.so。 这在理论上是可行的,但是对于这么小的事情来说就有点费力了。
直接修改libmono.so并手动打补丁。 查看mono源代码后,发现mono_image_open_from_data_with_name函数的开头有
一般情况下一段时间的空检查是没有用的。 我在ida pro中检查了这段代码占用的字节数,足以用于修补:)
我们还需要找到一个地方来放置我们的补丁代码,这需要一个很少使用的功能。 连孟黛猜测我选择的是mono_load_remote_field。 这个函数的空间足够写很多代码了。
我们要在函数开头添加的代码如下:
如果(数据长度>6000000){
文件* fp = fopen("/data/local/tmp/test.dll","wb");
如果(fp){
fwrite(数据,1,data_len,fp);
fclose(fp);
最初的字节数判断是只转储想要解密的dll。 因为这个dll很大,你可以通过它的大小来判断它,这样可以节省字节:)
我们需要手动打补丁,流程大致如下:
1)首先翻译成字节码。 这里我使用ADS 1.2来编译代码片段。 字节码和反汇编如下:
这是相当不错。 它只是将字符串放在代码末尾,这特别适合在代码中打补丁。
2)、然后复制到目标函数中
这里我使用Hex Workshop,首先找到ida pro中函数对应的起始位置,然后nop掉之前提到的无用代码,添加一个
对于 mono_load_remote_field 的调用,只需将之前的字节码粘贴到函数 mono_load_remote_field 的开头即可。
3)。 然后手动重新定位系统功能。 。
因为我们使用了3个C语言库函数fopen/fwrite/fclose,为什么要用这3个函数呢? 因为一般程序都应该引入这个库,所以只需要修改偏移量即可(实际上ADS编译的指令中的这些调用也是为以后重定位保留的)。 在ida pro中找到这三个函数的导入代码(即三个进程)的地址,然后计算出三个调用位置。 目标偏移量(这里我不太明白,必须使用“目标地址-调用指令地址-8”。我在ARM手册中没有找到这个要求,如果有人知道更多,请告诉我)。 修改指令的最后3条。 字节可以用作偏移量。
修改 mono_image_open_from_data_with_name
;以下是原代码
.text:00190AE8 LDR R3, [R11,#src]
..
修改mono_load_remote_field
;以下是原代码
现在您应该能够在重新打包和签名后获得解密的 dll。 我只是还无法进入游戏。 。
3. 消除对bangbangso的依赖
这个就按照网上说的来吧(我有点忘记了manifest的具体修改点了……)
1)。 修改解压包中的AndroidManifest.xml,去掉面向Bangbang的Activity。
2)修改解压后的smali文件,主要是smali\com\secneo\apkwrapper目录,注释掉加载DexHelper的代码。
现在将apk重新打包、签名并安装在虚拟机中并运行。 就可以进入游戏了(因为之前dll已经被解密了,这次需要把libmono.so替换成原来的unity3d的)。
4. 编写自己的调试模块
哈哈,终于可以从ARM的组装中回到人类世界了。
现在游戏dll已经解密,重新打包后就可以运行了。 所以我们可以尝试修补逻辑。 嗯,其实我想研究一下它有没有什么新花样。
我不会谈论编程。 大概是基于DebugConsole.cs(),然后添加一些我们需要的命令,比如动态加载Assembly,使用反射API调用函数等。 嗯,这在Android上确实是可行的。 ,对于dotnet来说,这基本上不是问题(所以详细的就不写了。需要注意的是,读取dll时,必须先使用文件API读取byte[],然后是Assembly.Load。另外就是dll所在的目录必须有权限读取,比如sdcard上的目录更好)
5. 将调试模块合并到目标游戏中
我们根据DebugConsole.cs的修改unity 打包 dll,编译了自己的dll。 下一步是将这个dll合并到目标Assembly-CSharp.dll中,并修改Assembly-CSharp.dll中的代码,该代码将在运行时到达以调用我们的代码。 。
微软开发了一个非常好的工具来合并dll(微软研究院经常做一些很奇怪的事情橙光游戏,比如Detours项目,还有这个IlMerge工具)。 除了合并dotnet dll文件之外,这个工具实际上是一个dotnet PE。 文件处理的源代码库(实际上找不到源代码,但使用IlSpy或Reflector基本没问题)。 另外,它还可以用来重新组织我们修改后的dotnet可执行文件。
6.修改目标游戏代码调用调试模块
这一步我使用了自制的工具。 如果你想手动处理,也可以使用CFF Explore,这里不再赘述。
我们简单说一下我们实际做的事情:
1)。 游戏的启动类Game在Update中调用TestInput。
私有无效更新()
尝试
...
this.TestInput();
catch(异常异常)
日志异常(异常);
私有无效测试输入()
TestInput 是一个空函数。 该函数不访问 Game 实例的任何变量。 它与静态函数具有相同的效果。 我们用我们提供的函数替换它的方法体(之前使用 IlMerge 将其合并到 Assembly-CSharp.dll 中)。 代码),这样我们的静态注入代码就有机会执行:
私有无效测试输入()
DebugConsoleHelper.Init(base.gameObject);
DebugConsoleHelper.Tick();
修改的原理就是将Game类的TestInput方法的元数据中的RVA改为我们合并到的类GamePatch的TestInput方法的RVA。
修改后,Game.TestInput和GamePatch.TestInput方法实际上共享相同的指令,这在dotnet PE文件中没有问题。
使用 CFF Explore 手动修改此步骤也更容易,因为它仅替换元数据。
因为我为了学习需要多次修改dll,所以我使用了自己的工具执行脚本来自动处理它(见下文)。
2)在游戏的PlayerController类的Update函数中,我们需要修改它来实现移动(主要用于在不连接服务器的情况下浏览场景)
私有无效更新()
if (activeController == this)
this.ProcessJoystick();
this.m_moveElapseTime += Time.deltaTime;
if (this.m_moveElapseTime > m_moveInterval)
DebugConsoleHelper.Move(this.m_player, this.m_moveElapseTime);
this.m_moveElapseTime = 0f;
this.ProcessSkillCD();
这里我们需要修改字节码来实现我们的代码。 它本质上与修改传统的可执行文件相同。 首先NOP一段代码,然后编写我们的代码。
.method 私有 hidebysig 实例 void Update() cil 管理
.maxstack 8
L_0000:调用类 PlayerController PlayerController::get_activeController()
L_0005:ldarg.0
L_0006: 调用 bool [UnityEngine]UnityEngine.Object::op_Equality(类 [UnityEngine]UnityEngine.Object, 类 [UnityEngine]UnityEngine.Object)
L_000b:brfalse L_0076
L_0010:ldarg.0
L_0011:调用实例 void PlayerController::ProcessJoystick()
L_0016:ldarg.0
L_0017:重复
L_0018: ldfld float32 PlayerController::m_moveElapseTime
L_001d:调用 float32 [UnityEngine]UnityEngine.Time::get_deltaTime()
L_0022:添加
L_0023: stfld float32 PlayerController::m_moveElapseTime
L_0028:ldarg.0
L_0029: ldfld float32 PlayerController::m_moveElapseTime
L_002e: ldsfld float32 PlayerController::m_moveInterval
L_0033:ble.un L_0076
;以下是我们修改后的代码
L_0038:ldarg.0
L_0039: ldfld 类玩家 PlayerController::m_player
L_003e: ldarg.0
L_003f: ldfld float32 PlayerController::m_moveElapseTime
L_0044:调用 void DebugConsoleHelper::Move(类 Player,float32)
L_0049:没有
L_004a:无
L_004b:无
L_004c:无
L_004d:无
L_004e:无
L_004f:无
L_0050:无
L_0051:没有
L_0052:没有
L_0053:没有
L_0054:没有
L_0055:没有
L_0056:没有
L_0057:没有
L_0058:没有
L_0059:没有
L_005a:无
L_005b:无
L_005c:无
L_005d:无
L_005e:无
L_005f:无
L_0060:没有
L_0061:没有
L_0062:没有
L_0063:没有
L_0064:没有
L_0065:没有
L_0066:没有
L_0067:没有
L_0068:没有
L_0069:没有
L_006a:无
;修改结束
L_006b:ldarg.0
L_006c:ldc.r4 0
L_0071: stfld float32 PlayerController::m_moveElapseTime
L_0076:ldarg.0
L_0077:调用实例 void PlayerController::ProcessSkillCD()
L_007c:ret
我还使用自制工具自动处理此步骤。 也可以使用CFF Explore和Hex Workshop手动修改,但如果需要多次修改就有点烦人了。
7.自制工具
我自己做了一个小工具,用于前面提到的方法体替换和方法代码修改(是从10年前的DeObfuscator修改而来的:)
对于方法体替换,由于多个类会共享方法体,这样的方法无法访问类的实例变量,这意味着它一般用于静态方法。
工具直接支持方法体替换。 添加目标文件后,输入替换的类和替换类,点击“方法实现替换”。
这个小工具主要是利用脚本来自动处理上面的修改。 这些脚本是基于DSL(我的另一个开源项目)实现的。
本文涉及的修改所使用的脚本如下:
过程(主要)
$files = getfilelist();
begin("开始脚本处理");
循环列表($文件){
$文件=$$;
beginfile($file,"开始修改"+$file+"...");
开始替换($文件);
替换($文件,“游戏”,“GamePatch”);
结束替换($文件);
开始修改($文件);
writeloadarg($file,"PlayerController","更新",0x38,0);
writeloadfield($file,"PlayerController","更新",0x39,"PlayerController","m_player");
writeloadarg($file,"PlayerController","更新",0x3e,0);
writeloadfield($file,"PlayerController","更新",0x3f,"PlayerController","m_moveElapseTime");
writecall($file,"PlayerController","更新",0x44,"DebugConsoleHelper","移动");
writenops($file,"PlayerController","更新",0x49,0x22);
结束修改($文件);
结束文件($文件);
};
end("结束脚本处理");
};
我把这个小工具开源了unity 打包 dll,欢迎使用:
观看雪地测试:
看学论坛:
-----微信ID:ikanxue-----
勘学研究院致力于安全研究16年!