【文章标题】: 基于unity3d游戏的android版本逆向初探
【文章作者】: dreaman
【作者邮箱】: [email]dreaman_163@163.com[/email]
【作者主页】: https://github.com/dreamanlan
【软件名称】: 匿了
【软件大小】: 好几百MB
【下载地址】: 自己搜索下载
【加壳方式】: 梆梆加密
【保护方式】: 梆梆
【编写语言】: unity3d
【使用工具】: 见后面总结
【操作平台】: android
【软件介绍】: 一款MMO游戏
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
0、背景
最近某游戏老厂出了一款MMO手游,听说画面很好,是基于unity3d的,很想看一下都是怎样的效果,但可惜
测试时间太短而且还要激活码,等我装好apk,进去时发现我既没有激活码,而且测试也结束了。。不过登录
的界面看起来确实很好,所以,只好逆向一下看看。
10年前我们在微软的dotnet平台下研究过一些安全相关的东东,10年后由于mono项目与unity3d的流行,dotnet
技术竟然在移动平台流行起来,凭着当年断断续续的记忆与一些文档,我就这么搞了一次游戏逆向,呵呵。
在移动平台,dotnet dll是没有签名的,这在一定程度上让逆向与修改更容易了。
本文简要介绍一下本次逆向的过程,与游戏相关的内容就略去了。
1、apk解包、重打包与签名
使用“Android逆向助手”或ApkStudio都可以,我们使用ApkStudio解包,Android逆向助手重新打包并签名。
2、梆梆加密解密
解包后的一级目录如下:
AndroidManifest.xml
apktool.yml
assets/
build/
lib/
original/
res/
smali/
首先用Reflector或IlSpy打开assets/bin/Data/Managed目录下的Assembly-CSharp.dll这个标准的unity3d游戏模块,
打开失败,文件加密了。。
打开lib/armeabi-v7a目录,可以看到依赖的so文件如下:
libAkSoundEngine.so
libBlueDoveMediaRender.so
libCrasheyeNDK.so
libDexHelper.so
libKGAudio.so
libmain.so
libmono.so
libmsc.so
libslua.so
libunity.so
libuwa.so
libweibosdkcore.so
从文件名看了看,多数是功能性模块,只有libDexHelper.so比较可疑,上网搜了一下知道是梆梆的东东。网上没找到
梆梆用于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对比一下,完全相同。看来梆梆是不会静态修改
libmono.so的了,想想也是,像unity3d这种版本频繁更新的,梆梆要是每个版本都静态改一下,还是挺被动的。不过
话说某厂的加密就是静态修改的libmono.so(其实是自己修改源码了重新编译的)。
搞不清libDexHelper.so是怎么工作的,我不是专业搞逆向的,用ida pro打开看了看,没发现线索就放弃了
(在ida pro里没找到Assembly-CSharp.dll这样的字符串,不过用十六进制编辑器在libDexHelper.so文件尾是看到有
这个字符串的,有兴趣的同学可以研究下它的加密原理)。
我换了个思路来得到解密后的dll,就是从libmono.so动手,前面已经发现梆梆没有对这个文件进行静态处理,所以我们
想怎么处理都比较容易,看mono的源码知道在mono_image_open_from_data_with_name函数里dll文件的内容会以完整的内
存映像出现。
所以至少有几种办法得到解密后的dll:
1)、断点后dump内存,我用的是电脑上的虚拟机,断不了。。
2)、修改mono源码,加入dump代码再替换游戏的libmono.so,这个理论上是可行的,但为了这么点事有点费劲了
3)、直接修改libmono.so,手动打补丁,看了下mono源码后,发现mono_image_open_from_data_with_name函数的开头有
一段判空检查,正常情况没什么用,看了下ida pro里这段代码占用的字节数,足够打补丁的了:)
我们还需要找一个能放我们补丁代码的地方,这需要一个不怎么被使用的函数,连蒙代猜的,我选择了
mono_load_remote_field,这个函数的空间足够写很多代码了。
我们要在函数开头加的代码如下:
if(data_len>6000000){
FILE* fp = fopen("/data/local/tmp/test.dll","wb");
if(fp){
fwrite(data,1,data_len,fp);
fclose(fp);
}
}
开始的字节数判断是为了只dump想解密的dll,因为这个dll个头很大,用大小就可以判断了,这样还比较省字节数:)
我们要手动打补丁,流程大概如下:
1)、先翻译成字节码,这里我使用ADS 1.2来编译代码片断,字节码与反汇编如下:
$a
.text
0x00000000: e92d4070 p@-. STMFD r13!,{r4-r6,r14}
0x00000004: e1a06000 .`.. MOV r6,r0
0x00000008: e59f003c <... LDR r0,0x4c
0x0000000c: e1a05001 .P.. MOV r5,r1
0x00000010: e1510000 ..Q. CMP r1,r0
0x00000014: 98bd8070 p... LDMLSFD r13!,{r4-r6,pc}
0x00000018: e28f0034 4... ADD r0,pc,#0x34 ; #0x54
0x0000001c: e28f102c ,... ADD r1,pc,#0x2c ; #0x50
0x00000020: ebfffffe .... BL fopen
0x00000024: e1b04000 .@.. MOVS r4,r0
0x00000028: 08bd8070 p... LDMEQFD r13!,{r4-r6,pc}
0x0000002c: e1a03004 .0.. MOV r3,r4
0x00000030: e1a02005 . .. MOV r2,r5
0x00000034: e3a01001 .... MOV r1,#1
0x00000038: e1a00006 .... MOV r0,r6
0x0000003c: ebfffffe .... BL fwrite
0x00000040: e1a00004 .... MOV r0,r4
0x00000044: e8bd4070 p@.. LDMFD r13!,{r4-r6,r14}
0x00000048: eafffffe .... B fclose
$d
0x0000004c: 005b8d80 ..[. DCD 6000000
0x00000050: 00006277 wb.. DCD 25207
0x00000054: 7461642f /dat DCD 1952539695
0x00000058: 6f6c2f61 a/lo DCD 1869360993
0x0000005c: 2f6c6163 cal/ DCD 795631971
0x00000060: 2f706d74 tmp/ DCD 795897204
0x00000064: 74736574 test DCD 1953719668
0x00000068: 6c6c642e .dll DCD 1819042862
0x0000006c: 00000000 .... DCD 0
还挺好的,恰好把字符串放在代码后面了,特别适合在代码里打补丁。
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:00190A94
.text:00190A94 ; =============== S U B R O U T I N E =======================================
.text:00190A94
.text:00190A94 ; Attributes: bp-based frame
.text:00190A94
.text:00190A94 EXPORT mono_image_open_from_data_with_name
.text:00190A94 mono_image_open_from_data_with_name ; CODE XREF: sub_133E34+170p
.text:00190A94 ; mono_image_open_from_data_full+3Cp
.text:00190A94
.text:00190A94 var_24 = -0x24
.text:00190A94 var_20 = -0x20
.text:00190A94 n = -0x1C
.text:00190A94 src = -0x18
.text:00190A94 var_10 = -0x10
.text:00190A94 var_C = -0xC
.text:00190A94 dest = -8
.text:00190A94 arg_0 = 4
.text:00190A94 arg_4 = 8
.text:00190A94
.text:00190A94 STMFD SP!, {R11,LR}
.text:00190A98 ADD R11, SP, #4
.text:00190A9C SUB SP, SP, #0x20
.text:00190AA0 STR R0, [R11,#src]
.text:00190AA4 STR R1, [R11,#n]
.text:00190AA8 STR R2, [R11,#var_20]
.text:00190AAC STR R3, [R11,#var_24]
.text:00190AB0 LDR R2, [SP,#0x24+src]
.text:00190AB4 BL mono_load_remote_field
.text:00190AB8 CMP R3, #0
.text:00190ABC CMP R3, #0
.text:00190AC0 CMP R3, #0
.text:00190AC4 CMP R3, #0
.text:00190AC8 CMP R3, #0
.text:00190ACC CMP R3, #0
.text:00190AD0 CMP R3, #0
.text:00190AD4 CMP R3, #0
.text:00190AD8 CMP R3, #0
.text:00190ADC CMP R3, #0
.text:00190AE0 CMP R3, #0
.text:00190AE4 CMP R3, #0
;后面是原来的代码了
.text:00190AE8 LDR R3, [R11,#src]
...
修改后的mono_load_remote_field
.text:001F9A4C
.text:001F9A4C ; =============== S U B R O U T I N E =======================================
.text:001F9A4C
.text:001F9A4C
.text:001F9A4C EXPORT mono_load_remote_field
.text:001F9A4C mono_load_remote_field ; CODE XREF: mono_image_open_from_data_with_name+20p
.text:001F9A4C STMFD SP!, {R4-R6,LR}
.text:001F9A50 MOV R6, R0
.text:001F9A54 LDR R0, =0x5B8D80
.text:001F9A58 MOV R5, R1
.text:001F9A5C CMP R1, R0
.text:001F9A60 LDMLSFD SP!, {R4-R6,PC}
.text:001F9A64 ADR R0, aDataLocalTmpTe ; "/data/local/tmp/test.dll"
.text:001F9A68 ADR R1, dword_1F9A9C ; modes
.text:001F9A6C BL fopen
.text:001F9A70 MOVS R4, R0
.text:001F9A74 LDMEQFD SP!, {R4-R6,PC}
.text:001F9A78 MOV R3, R4 ; s
.text:001F9A7C MOV R2, R5 ; n
.text:001F9A80 MOV R1, #1 ; size
.text:001F9A84 MOV R0, R6 ; ptr
.text:001F9A88 BL fwrite
.text:001F9A8C MOV R0, R4 ; stream
.text:001F9A90 LDMFD SP!, {R4-R6,LR}
.text:001F9A94 B fclose
.text:001F9A94 ; End of function mono_load_remote_field
.text:001F9A94 ; ---------------------------------------------------------------------------
.text:001F9A98 dword_1F9A98 DCD 0x5B8D80 ; DATA XREF: mono_load_remote_field+8r
.text:001F9A9C dword_1F9A9C DCD 0x6277 ; DATA XREF: mono_load_remote_field+1Co
.text:001F9AA0 aDataLocalTmpTe DCB "/data/local/tmp/test.dll",0
.text:001F9AA0 ; DATA XREF: mono_load_remote_field+18o
.text:001F9AB9 DCB 0, 0, 0
.text:001F9ABC ; ---------------------------------------------------------------------------
;后面是原来的代码了
.text:001F9ABC MOV R2, R3
.text:001F9AC0 LDR R3, =(aObject_c - 0x1F9ACC)
.text:001F9AC4 ADD R3, PC, R3 ; "object.c"
.text:001F9AC8 BL sub_29D7F8
...
现在重新打包、签名后应该已经可以得到解密的dll了。只是还不能进入游戏。。
3、去除对梆梆so的依赖
这个就是按网上说的做就可以了(对manifest的处理有点忘了具体的修改点了。。)
1)、修改解出的包里AndroidManifest.xml,把里面对梆梆的Activity去掉。
2)、修改解压出的smali文件,主要在smali\com\secneo\apkwrapper目录下,把加载DexHelper的代码注掉。
现在再重新打包、签名后在虚拟机里安装apk后运行,可以进游戏了(因为之前已经把dll解密了,这次要把libmono.so换
成原版unity3d的)。
4、编写自己的调试模块
呵呵,终于可以从ARM的汇编回到人类世界了。
现在游戏的dll已经解密,并且重新打包后也可以运行了。所以我们可以试着在逻辑上打补丁了,好吧,其实我是为了研究
一下它有没有什么新技巧。
编程序的事情就不说了,大概就是基于DebugConsole.cs (http://wiki.unity3d.com/index.php?title=DebugConsole),
然后添加一些我们需要的命令,比如动态加载一个Assembly,再比如利用reflection API调用函数,嗯,这在android上确
实是可行的,对dotnet来说,这基本不是事(所以我就不写细节,要注意的就是读dll一定要先用文件API读出byte[],然
后再Assembly.Load。另外一个是dll所在的目录必须是有权限读的,比如sdcard上的目录就比较好)
5、合并调试模块到目标游戏
我们基于DebugConsole.cs修改编译了一个自己的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
private void Update()
{
try
{
...
this.TestInput();
}
catch (Exception exception)
{
Log.Exception(exception);
}
}
private void TestInput()
{
}
TestInput是一个空函数,这个函数不会访问任何Game实例的变量,和静态函数效果一样,我们将它的方法体替换成我们提
供的一个函数(就是前面用IlMerge合并到Assembly-CSharp.dll里的代码),这样我们静态注入的代码就有了执行的机会:
private void TestInput()
{
DebugConsoleHelper.Init(base.gameObject);
DebugConsoleHelper.Tick();
}
修改的原理是将Game类的TestInput方法元数据里的RVA改为GamePatch即我们合并进的类的TestInput方法的RVA。
修改后,Game.TestInput与GamePatch.TestInput方法其实是共享了同一块指令,这在dotnet PE文件里是没问题的。
这一步用CFF Explore手动修改也比较容易,因为只是替换一下元数据。
我因为学习的需要要多次修改dll,所以用了自己的工具执行一段脚本来自动处理(见后)。
2)、游戏的PlayerController类的Update函数里,我们需要修改一下来实现移动(主要用于没有连接服务器的情况下浏览场景)
private void 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 private hidebysig instance void Update() cil managed
{
.maxstack 8
L_0000: call class PlayerController PlayerController::get_activeController()
L_0005: ldarg.0
L_0006: call bool [UnityEngine]UnityEngine.Object::op_Equality(class [UnityEngine]UnityEngine.Object, class [UnityEngine]UnityEngine.Object)
L_000b: brfalse L_0076
L_0010: ldarg.0
L_0011: call instance void PlayerController::ProcessJoystick()
L_0016: ldarg.0
L_0017: dup
L_0018: ldfld float32 PlayerController::m_moveElapseTime
L_001d: call float32 [UnityEngine]UnityEngine.Time::get_deltaTime()
L_0022: add
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 class Player PlayerController::m_player
L_003e: ldarg.0
L_003f: ldfld float32 PlayerController::m_moveElapseTime
L_0044: call void DebugConsoleHelper::Move(class Player, float32)
L_0049: nop
L_004a: nop
L_004b: nop
L_004c: nop
L_004d: nop
L_004e: nop
L_004f: nop
L_0050: nop
L_0051: nop
L_0052: nop
L_0053: nop
L_0054: nop
L_0055: nop
L_0056: nop
L_0057: nop
L_0058: nop
L_0059: nop
L_005a: nop
L_005b: nop
L_005c: nop
L_005d: nop
L_005e: nop
L_005f: nop
L_0060: nop
L_0061: nop
L_0062: nop
L_0063: nop
L_0064: nop
L_0065: nop
L_0066: nop
L_0067: nop
L_0068: nop
L_0069: nop
L_006a: nop
;修改结束
L_006b: ldarg.0
L_006c: ldc.r4 0
L_0071: stfld float32 PlayerController::m_moveElapseTime
L_0076: ldarg.0
L_0077: call instance void PlayerController::ProcessSkillCD()
L_007c: ret
}
这一步我也是用我的自制工具自动处理的,用CFF Explore与Hex workshop手动修改也可以,但如果需要多次修改的话就
有点烦了。
7、自制工具
前面说到的方法体替换与方法代码修改我自己做了一个小工具
(是在10年前的DeObfuscator上修改而成:http://bbs.pediy.com/showthread.php?threadid=34127)
对于方法体替换,由于多个类会共享方法体,所以这样的方法不能访问类的实例变量,也就是一般要用在静态方法上。
方法体替换在工具里是直接支持的,添加好目标文件后,输入被替换类与替换类,点“方法实现替换”即可。
这个小工具主要是通过脚本来自动处理上面的修改,脚本是基于我的另一个开源项目DSL实现的。
本文涉及的修改使用的脚本如下:
proc(main)
{
$files = getfilelist();
begin("开始脚本处理");
looplist($files){
$file=$$;
beginfile($file,"开始对"+$file+"进行修改。。。");
beginreplace($file);
replace($file,"Game","GamePatch");
endreplace($file);
beginmodify($file);
writeloadarg($file,"PlayerController","Update",0x38,0);
writeloadfield($file,"PlayerController","Update",0x39,"PlayerController","m_player");
writeloadarg($file,"PlayerController","Update",0x3e,0);
writeloadfield($file,"PlayerController","Update",0x3f,"PlayerController","m_moveElapseTime");
writecall($file,"PlayerController","Update",0x44,"DebugConsoleHelper","Move");
writenops($file,"PlayerController","Update",0x49,0x22);
endmodify($file);
endfile($file);
};
end("结束脚本处理");
};
这个小工具我已经开源了,欢迎使用:https://github.com/dreamanlan/DotnetPatch
考虑到伟大的墙,我在看雪也放一个目前的版本。
--------------------------------------------------------------------------------
【经验总结】
所用工具列表:
1、ApkStudio
2、Android逆向助手
3、Reflector 8
4、ida pro 6.6
5、hex workshop
6、cff explore
7、IlMerge
8、ADS 2.1
9、自制工具DotnetPatch
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2016年09月01日 20:23:13
[注意]APP应用上架合规检测服务,协助应用顺利上架!