Unity IL2CPP 游戏分析入门

公众号: 奋飞安全
• 阅读 617

一、目标

很多时候App加密本身并不难,难得是他用了一套新玩意,天生自带加密光环。例如PC时代的VB,直接ida的话,汇编代码能把你看懵。

但是要是搞明白了他的玩法,VB Decompiler一上,那妥妥的就是源码。

Unity 和 Flutter 也是如此。

最近迷上了一个小游戏 Dream Blast,今天就拿他解剖吧。

com.rovio.dream

二、步骤

侦测敌情

从apk包里面发现libil2cpp.so,就足以证明是Unity写的游戏了。

在Android下Unity有两种玩法,一种是Mono方式打包,我们可以从包内拿到Assembly-CSharp.dll,如果开发者没有对Assembly-CSharp.dll进行加密处理,那么我们可以很方便地使用ILSpy.exe对其进行反编译。这样看到的就是妥妥的C#源码了。

由于总所周知的原因,这种玩法肯定会被公司开除的。现在工作这么难找,所以大家都采取第二种玩法了,使用IL2CPP方式打包,就没有Assembly-CSharp.dll。这样就不会让人轻易攻破了。

这时候就需要召唤出IL2CPP界的Decompiler了。

Il2CppDumper

https://github.com/Perfare/Il2CppDumper

Il2CppDumper 通过 assets/bin/Data/Managed/Metadata/global-metadata.dat 字符串文件 和 lib/armeabi-v7a/libil2cpp.so 游戏二进制文件来还原C#写的代码逻辑。

目前只有编译好的windows可执行文件,所以目前只能在win下使用。(本例演示的是Arm32)

1、先把global-metadata.dat 和 libil2cpp.so 这两个文件拷贝到同一个目录。

2、运行 Il2CppDumper-x86.exe,在弹出的文件选择框里面,先选择 libil2cpp.so,然后再选择 global-metadata.dat。

Initializing metadata...
Metadata Version: 27
Initializing il2cpp file...
Applying relocations...
WARNING: find JNI_OnLoad
ERROR: This file may be protected.
Il2Cpp Version: 27
Searching...
Change il2cpp version to: 27.1
CodeRegistration : 205f9c8
MetadataRegistration : 205ff3c
Dumping...
Done!
Generate struct...
Done!
Generate dummy dll...
Done!
Press any key to exit...

这就算反编译成功了。

一共会生成 DummyDll 目录, script.json,stringliteral.json,dump.cs,il2cpp.h 等文件。

script.json和stringliteral.json是辅助ida 和ghidra 分析的,可以用 ida.py 这个脚本导入到ida里面去。

这会我们只关心 dump.cs。

存盘文件

为了 好好 玩一个游戏,除了改内存,还一个重要的方案就是改配置文件甚至改存盘文件了。

遥想当年帝国时代非得搞个200的人口上限,直接hook一下,把200改成2000他不香吗? (电脑拖崩溃了)

细心 分析了一下,这个游戏的存盘文件在

/sdcard/Android/data/com.rovio.dream/files/usesr/XXX-XXX-XXX/prefs.json

改它,改它,可是它加密了

分析

这时候显示出 dump.cs 的用处了,这可是活地图呀。

在里面搜一下 "prefs.json"

[CreateAssetMenuAttribute] // RVA: 0x3979B8 Offset: 0x3979B8 VA: 0x3979B8
public class UserPrefs : UserPrefsBase, IInitializable, IInitializableInit // TypeDefIndex: 7278
{
        // Fields
        private const string EK = "8CSstq6cz1Gp9YSQpr2l";
        private const string PrefsFileName = "prefs.json";
   ....
           // RVA: 0xAAE690 Offset: 0xAAE690 VA: 0xAAE690 Slot: 42
        public void Init() { }
    ....

从这里得到两个有用的信息,一个是存盘文件在UserPrefs类里面处理,再一个EK可能就是密钥或者密钥的一部分。

可以上ida了,打开libil2cpp.so细嚼慢咽一下。

首先运行 Il2CppDumper-v6\ida_py3.py (低版本的ida请跑ida.py)

然后 在弹出的文件选择框里面 ,选择刚才反编译出来的script.json,最后再跑一次ida_py3.py 把stringliteral.json 也加进来。

万事俱备了,我们去分析一下 UserPrefs_Init() ,地图告诉我们它在 0xAAE690,

ida里面去到 0xAAE690, 然后Create Function, 再F5以下,代码就出来了。

代码看上去还是有点懵,它似乎 System_Guid__NewGuid(v47, 0); 生成了个guid,然后再加上了EK

v43 = System_String__Concat_23810904(*(_DWORD *)(a1 + 28), StringLiteral_1313, 0);

StringLiteral_1313就是 EK。

不过好消息是 最后 它要初始化一个 CryptoUtility___ctor

int __fastcall CryptoUtility___ctor(int a1)
{
  int v2; // r6
  _DWORD *UTF8; // r0

  if ( !byte_2173DF8 )
  {
    sub_48CE2C(&System_Security_Cryptography_AesManaged_TypeInfo);
    sub_48CE2C(&System_Security_Cryptography_Rfc2898DeriveBytes_TypeInfo);
    sub_48CE2C(&StringLiteral_1149);
    byte_2173DF8 = 1;
  }
  v2 = sub_48CF00(System_Security_Cryptography_AesManaged_TypeInfo);
  System_Security_Cryptography_AesManaged___ctor(v2, 0);
  *(_DWORD *)(a1 + 16) = v2;
  System_Object___ctor(a1, 0);
  UTF8 = (_DWORD *)System_Text_Encoding__get_UTF8(0);
  if ( !UTF8 )
    sub_48CF08();
  return sub_9DB34C(*UTF8, &StringLiteral_1149, *(_DWORD *)(*UTF8 + 344), *(_DWORD *)(*UTF8 + 340));
}

很明显,算法是 AES, 那么key是啥呢? aes还有cbc和ecb,又应该是哪一个呢?

Rfc2898DeriveBytes

幸亏咱还是懂点C#的,一个优秀的C#程序员,看到AesManaged和Rfc2898DeriveBytes,就知道套路了。

Rfc2898DeriveBytes的入参是一个password和salt,然后生成一组key和iv,后面就是aes做AES-128-CBC了。

目标很明确了,搞到pwd和salt。

ida双击进到 sub_9DB34C

void __fastcall sub_9DB34C(
        int a1,
        _DWORD *a2,
        int a3,
        int (__fastcall *a4)(int, _DWORD),
        int a5,
        int a6,
        int a7,
        int a8,
        int a9,
        int a10)
{
  int v10; // r4
  int v11; // r5
  int v12; // r6
  int v13; // r7
  int v14; // r6
  int v15; // r0

  v13 = a4(v12, *a2);
  v14 = sub_48CF00(System_Security_Cryptography_Rfc2898DeriveBytes_TypeInfo);
  v15 = System_Security_Cryptography_Rfc2898DeriveBytes___ctor(v14, v11, v13, 0);
  if ( !v14 )
    sub_48CF08(v15);
  ...

真相只有一个,hook 这个 System_Security_Cryptography_Rfc2898DeriveBytes___ctor 就可以拿到 pwd和salt了。 a2是pwd,a3是 salt。

Tip:

https://github.com/microsoft/referencesource/blob/master/mscorlib/system/security/cryptography/rfc2898derivebytes.cs

int __fastcall System_Security_Cryptography_Rfc2898DeriveBytes___ctor_17396484(int a1, int a2, int a3, int a4)
{
  int v8; // r6

  if ( !byte_2176D99 )
  {
    sub_48CE2C((int)&System_Security_Cryptography_HMACSHA1_TypeInfo);
    byte_2176D99 = 1;
  }
  System_Security_Cryptography_DeriveBytes___ctor(a1, 0);
  System_Security_Cryptography_Rfc2898DeriveBytes__set_Salt(a1, a3);
  System_Security_Cryptography_Rfc2898DeriveBytes__set_IterationCount(a1, a4);
  *(_DWORD *)(a1 + 20) = a2;
  v8 = sub_48CF00(System_Security_Cryptography_HMACSHA1_TypeInfo);
  System_Security_Cryptography_HMACSHA1___ctor_22256684(v8, a2, 0);
  *(_DWORD *)(a1 + 16) = v8;
  return System_Security_Cryptography_Rfc2898DeriveBytes__Initialize(a1);
}

说干就干

var libxx = Process.getModuleByName("libil2cpp.so");
console.log("*****************************************************");
console.log("name: " +libxx.name);
console.log("base: " +libxx.base);
console.log("size: " +ptr(libxx.size));

Interceptor.attach(ptr(libxx.base).add(0x1097304),{
    onEnter: function(args){
        console.log("=== pwd");
        console.log(TAG + hexdump(ptr(this.context.r1), { offset: 0, length: 128, header: true, ansi: true }) );

        console.log("=== salt ");
        console.log(TAG + hexdump(ptr(this.context.r2), { offset: 0, length: 64, header: true, ansi: true }) );


    },
    onLeave:function(retval){
    }
});

这就尴尬了

Error: unable to find module 'libil2cpp.so'

libil2cpp.so 大概率是动态载入的,所以刚启动app的时候木有libil2cpp.so。

如果我们要hook的函数之后会被多次调用,那么可以延迟几秒钟来载入 setTimeout(main, 1000*3);

不过这里我们要hook的都是init和ctor之类的初始化函数,几秒钟之后可能都初始化完成了。

hook_constructor

要第一时间hook 动态载入的so,就需要从so的加载开始搞

function hook_constructor0() {
    if (Process.pointerSize == 4) {
        var linker = Process.findModuleByName("linker");
    } else {
        var linker = Process.findModuleByName("linker64");
    }

    var addr_call_function =null;
    var addr_g_ld_debug_verbosity = null;
    var addr_async_safe_format_log = null;
    if (linker) {
        var symbols = linker.enumerateSymbols();
        for (var i = 0; i < symbols.length; i++) {
            var name = symbols[i].name;
            if (name.indexOf("call_function") >= 0){
                addr_call_function = symbols[i].address;
            }
            else if(name.indexOf("g_ld_debug_verbosity") >=0){
                addr_g_ld_debug_verbosity = symbols[i].address;

                ptr(addr_g_ld_debug_verbosity).writeInt(2);

            } else if(name.indexOf("async_safe_format_log") >=0 && name.indexOf('va_list') < 0){

                addr_async_safe_format_log = symbols[i].address;

            }

        }
    }
    if(addr_async_safe_format_log){
        Interceptor.attach(addr_async_safe_format_log,{
            onEnter: function(args){
                this.log_level  = args[0];
                this.tag = ptr(args[1]).readCString()
                this.fmt = ptr(args[2]).readCString()
                if(this.fmt.indexOf("c-tor") >= 0 && this.fmt.indexOf('Done') < 0){
                    this.function_type = ptr(args[3]).readCString(), // func_type
                    this.so_path = ptr(args[5]).readCString();
                    var strs = new Array(); //定义一数组
                    strs = this.so_path.split("/"); //字符分割
                    this.so_name = strs.pop();
                    this.func_offset  = ptr(args[4]).sub(Module.findBaseAddress(this.so_name))


                    if(this.so_name == "libil2cpp.so") {

                var targetSo = Module.findBaseAddress(this.so_name);

                console.log(TAG +' so_name:',this.so_name);
                console.log(TAG +' ptr:',ptr(targetSo));

                hookDbg(targetSo);
                    }

                }
            },
            onLeave: function(retval){

            }
        })
    }
}

function hookDbg(targetSo){
    Interceptor.attach(targetSo.add(0xAAE690),{
        onEnter: function(args){
            console.log(" UserPrefs_ctor *****************************************************");

        },
        onLeave:function(retval){
        }
    });


    Interceptor.attach(ptr(targetSo).add(0x1097304),{
        onEnter: function(args){
            console.log("=== pwd");
            console.log(TAG + hexdump(ptr(this.context.r1), { offset: 0, length: 128, header: true, ansi: true }) );

            console.log("=== salt ");
            console.log(TAG + hexdump(ptr(this.context.r2), { offset: 0, length: 64, header: true, ansi: true }) );
        },
        onLeave:function(retval){
        }
    });

}

这次的结果就比较完美了

Unity IL2CPP 游戏分析入门

1:rc

Rfc2898DeriveBytes的入参是String,可以看到String在内存中的布局, 0x0C 开始的4个字节是 字符串长度,0x10开始才是真正的字符串。

password 是存档的文件夹名称+EK

salt 是个固定的字符串

带着这个结果我们再回过头去看 UserPrefs__Init的F5的代码,重点关注那几个 System_String_Concat 就更有心得了。

三、总结

为了抵抗Il2CppDumper,敌人变狡猾了,所以作者推出了更帅的 Zygisk-Il2CppDumper

现在套路这么多,技能得不断更新才能跟的上,又要掉头发了。

变来变去的都是外围,万变不离其宗的还是arm汇编,最后的定位还是需要你的汇编功底。

网络游戏改存盘是没用的,一联服务器就把你覆盖了。

Unity IL2CPP 游戏分析入门

1:ffshow

富贵故如此,营营何所求

Tip:

: 本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去 奋飞的朋友们 知识星球自取,欢迎加入知识星球一起学习探讨技术。有问题可以加我wx: fenfei331 讨论下。

关注微信公众号:奋飞安全,最新技术干货实时推送

点赞
收藏
评论区
推荐文章
李志宽 李志宽
2年前
IDA,牛逼!逆向安全应该怎么学?
逆向分析是网络安全从业人员尤其是二进制安全研究人员必备的技能。提到逆向分析,不得不说的就是神器IDA,这个逆向分析的大杀器,可以分析x86、x64、ARM、MIPS、Java、.NET等众多平台的程序代码,可以说是非常强大了!0但强大的同时,IDA复杂的功能让新手小白一时之间不知如何上手。要是有一个详尽的教程,从简单入手介绍逆向工程入门,配合IDA的使用,那
某车联网App 通讯协议加密分析
一、目标李老板:最近刚买了辆新车,他带的App挺有意思,要不要盘一盘?奋飞:我去,加壳了,还挺有意思,搞一搞。v6.1.0二、步骤抓包我的抓包环境是Mac10.14.6httpToolKit,这一步很顺利的抓到包了。1:main可以看到,http请求和返回值都是加密的,我们的目标就是这个 request 和 response的来历的。脱壳
御弟哥哥 御弟哥哥
3年前
Android深入浅出之Binder机制
Android深入浅出之Binder机制一说明Android系统最常见也是初学者最难搞明白的就是Binder了,很多很多的Service就是通过Binder机制来和客户端通讯交互的。所以搞明白Binder的话,在很大程度上就能理解程序运行的流程。我们这里将以MediaService的例子来分析Binder的使用:ServiceMan
某小说App __sig3签名分析
一、目标这个样本和之前的小视频App的套路有点类似。签名的名称和算法估计都是一样的。所以搞明白这个,估计也能搞明白最新版的小视频App。那看小说和看小视频的区别在哪?小说越看越困,小视频越看越清醒。足以证明AI比你还要了解你自己。今
Stella981 Stella981
2年前
Android深入浅出之Binder机制
Android深入浅出之Binder机制一 说明Android系统最常见也是初学者最难搞明白的就是Binder了,很多很多的Service就是通过Binder机制来和客户端通讯交互的。所以搞明白Binder的话,在很大程度上就能理解程序运行的流程。我们这里将以MediaService的例子来分析Binder的使用:lServiceMana
Wesley13 Wesley13
2年前
Unity 2D游戏开发快速入门第1章创建一个简单的2D游戏
Unity2D游戏开发快速入门第1章创建一个简单的2D游戏即使是现在,很多初学游戏开发的同学,在谈到Unity的时候,依然会认为Unity只能用于制作3D游戏的。实际上,Unity在2013年发布4.3版本的时候,就开始提供对制作2D游戏的支持了。例如,提供了一些专用于开发2D游戏的Unit
Wesley13 Wesley13
2年前
Unity打开同一个工程目录
Unity打开同一个工程目录在使用Unity调试项目工程的时候,有时候需要使用Unity打开同一个目录。但是Unity并不支持打开同一个工程,因为路径是相同的。也就是说我们必须要创建一个新目录,然后重新使用svn或者git来拉取项目。如果项目文件很多的话,该过程耗时很长。mklink既然Unity不支持
Wesley13 Wesley13
2年前
unity之截屏功能
1.全屏截图  方法一:在unity的API中,unity给我们提供了一个现成的API : Application.CaptureScreenshot(imagename)。但是这个API虽然简单,在PC、mac运用没有多大的影响,但是如果是在移动平台上使用的话就显得相当的吃力,因为它会消耗我们很大的CUP,所以在移动端使用
Wesley13 Wesley13
2年前
Unity横屏
Android下发现Unity里面的Player设置,并不能完全有效,比如打开了自动旋转,启动的时候还是会横屏,修改XML添加以下代码<applicationandroid:icon"@drawable/ic\_launcher"                    android:label"@string/app\_name"
专注IP定位 专注IP定位
1年前
网络七层结构是干啥的? 看这篇文章就够了
前言“物理层、数据链路层、网络层、传输层、会话层、表示层和应用层”,今天我们的目标就是把这些个玩意,翻译成“人话”,保证你一看就懂,一学就废网络七层结构,相信是个搞计算机网络的,或者是搞软件开发的,甚至是搞互联网的,99%的人应该都知道,最起码那也得是听过,就是下方这个玩意:(你要是听都没听过,别跟我说你是混迹于互联网界的啊)但是,如果让这些人讲清楚这七层结
公众号:  奋飞安全
公众号: 奋飞安全
Lv1
奋飞,国家高级信息系统项目管理师,独立安全研究员。 http://91fans.com.cn/
文章
59
粉丝
4
获赞
44