首页
社区
课程
招聘
[原创]内存解析Il2cpp函数地址
2020-12-29 22:07 22462

[原创]内存解析Il2cpp函数地址

2020-12-29 22:07
22462

目的

使用函数名拿到U3D非导出函数地址

起因

  1. 相对u3d游戏进行hook基本上离不开 Il2CppDumper 的使用,有时候想要找个函数什么的就很麻烦,第一步就得使用上面这工具来找到函数地址
  2. 第二就是每次都要用 Il2CppDumper 来找函数地址不能方便的实现 frida 脚本的自动化,既然都用到了 frida 当然是越方便越爽

经过

  • 之前搞了一个通过 Il2CppDumper 拿到的 script.js 加上一点python 筛选,来实现对函数的批量断点,方便实现对点击事件或者是其他关键函数(Like:Show,Click,Button,Rewarded)多函数的批量断点,方便我们分析代码调用
  • 这还是不够优雅,要让他自动去拿到这些地址,我们就得读一点点,在源码层面去hook拿到一些我们需要的关键点

分析

网上也有很多类似的相关文章
比如 [unity]Real-time Dump一套强劲的 Il2cpp Hook 框架IL2CPP的原理
都可以去参照 去细品

 

这里我讲讲我用到的东西
分析的入口点从加载 global-metadata.dat 入手
这是一个非常标志性的入口,不管是在源码还是在IDA中都很容易的定位
找到入口位置
IDA中从字符串窗口搜索该关键词也可以轻松的定位到一下代码

 

源码中的入口
IDA中F5也是随意的改改名字,改改结构体就差不多一个样了

 

记下未初始化的段的两个地址
两个关键地址
在源码中让后看,可以看下面就开始对此处分配出来的内存填数据了,后面我们用的时候也是无需但是未初始化的问题

 

这里的s_ImagesTable是一个指向一个 Il2CppImage 列表的开头,
sizeof(Il2CppImage) = 52,可以去头文件查看

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
typedef struct Il2CppImage
{
    const char* name;
    const char *nameNoExt;
    Il2CppAssembly* assembly;
 
    TypeDefinitionIndex typeStart;
    uint32_t typeCount;
 
    TypeDefinitionIndex exportedTypeStart;
    uint32_t exportedTypeCount;
 
    CustomAttributeIndex customAttributeStart;
    uint32_t customAttributeCount;
 
    MethodIndex entryPointIndex;
 
#ifdef __cplusplus
    mutable
#endif
    Il2CppNameToTypeDefinitionIndexHashTable * nameToClassHashTable;
 
    const Il2CppCodeGenModule* codeGenModule;
 
    uint32_t token;
    uint8_t dynamic;
} Il2CppImage;

这一步我们关注的只有 const char *nameNoExt 以及当前位置的指针位置由此写出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var s_ImagesTable = soAddr.add(0x2DF9134).readPointer()
var s_ImagesCount = soAddr.add(0x2DF9130).readPointer().toInt32()
function list_Images(keywords){
    LOG("-------------------------------------------------------------------------------------",LogColor.COLOR_33)
        for(var t=0;t<s_ImagesCount;t++){
            var tt = s_ImagesTable.add(sizeof_Il2CppImage*t)
            var typeCount = tt.add(p_size*4).readPointer().toInt32()
            var name = tt.add(p_size).readPointer().readCString()
            if (keywords != undefined){
                if (name.indexOf(keywords)!=-1){
                    LOG("[*]"+tt+"\t"+name,LogColor.COLOR_36)
                }else{--tmp}
            }else{
                LOG("[*]"+tt+"\t"+name,LogColor.COLOR_36)
            }
            current_off = current_off.add(typeCount*p_size)
        }
        LOG("----------------------------",LogColor.COLOR_33)
        LOG("  List "+(keywords==undefined?tmp:--tmp)+" Images",LogColor.RED)
        LOG("-------------------------------------------------------------------------------------",LogColor.COLOR_33)
}

运行一下就可以拿到一下内容
列出Images
这些玩意其实就是我们用Il2CppDumper拿到的哪些dll


然后下面就可以进入正题
 

使用命名空间,类名,方法名,参数个数拿到我们需要的函数 内存地址以及IDA中静态分析的地址

 

下面介绍两个主角函数(这里引入的两个函数都是libil2cpp.so的默认导出函数)
位置:D:\Program Files\Unity\Editor\Data\il2cpp\libil2cpp\il2cpp-api.cpp

  • il2cpp_class_get_method_from_name
  • il2cpp_class_from_name
    这里为了能截图在一张中移动了函数位置 ↓
    (ps:这里做的事情其实,就是和做数学一样,把未知量用已知函数去代替,带入到我们能解决即可 )
    函数原型

由此可以写出以下代码进行主动调用

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
function findAddrByName(namespaze){
    for(var t=0;t<s_ImagesCount;t++){
        var t_addr = s_ImagesTable.add(52*t)
        var t_name = s_ImagesTable.add(52*t).add(p_size).readPointer().readCString()
        if (t_name == namespaze) return ptr(t_addr)
    }
    return ptr(0x0)
}
 
function showAddr(namespaze,className,functionName,argsCount){
    Interceptor.detachAll()
    Il2CppImage = findAddrByName(namespaze)
    if (Il2CppImage == 0) {
        console.warn("Il2CppImage addr not found!")
        return
    }
    console.warn("---------------------------------------------------------")
    console.error(namespaze+"."+className+"."+functionName)
    console.warn("----------------------------")
    console.log("Il2CppImage\t---->\t "+Il2CppImage)
 
    //Il2CppClass* il2cpp_class_from_name(const Il2CppImage* image, const char* namespaze, const char *name)
    var il2cpp_class_from_name = new NativeFunction(
        Module.findExportByName(soName,"il2cpp_class_from_name"),
        'pointer',['pointer','pointer','pointer'])
 
    //const MethodInfo* il2cpp_class_get_method_from_name(Il2CppClass *klass, const char* name, int argsCount)
    var il2cpp_class_get_method_from_name = new NativeFunction(
        Module.findExportByName(soName,"il2cpp_class_get_method_from_name"),
        'pointer',['pointer','pointer','int'])
 
    var namespaze_t = Memory.allocUtf8String(namespaze)
    var className_t = Memory.allocUtf8String(className)
    var functionName_t = Memory.allocUtf8String(functionName)
 
    var Il2CppClass = il2cpp_class_from_name(Il2CppImage,namespaze_t,className_t)
    console.log("Il2CppClass\t---->\t",Il2CppClass)
    var MethodInfo = il2cpp_class_get_method_from_name(Il2CppClass,functionName_t,argsCount)
    console.log("MethodInfo\t---->\t",MethodInfo)
    console.log("\x1b[36mmethodPointer\t---->\t "+MethodInfo.readPointer() +"\t ===> \t"+MethodInfo.readPointer().sub(soAddr)+"\x1b[0m")
    console.warn("---------------------------------------------------------")
}

结果展示

实现通过名称拿到函数地址

 

静态dumper出来的结果


 

看到此处的话目的算是勉强达到了,但是还有待优化把,比如一开始的

1
2
var s_ImagesTable = soAddr.add(0x2DF9134).readPointer()
var s_ImagesCount = soAddr.add(0x2DF9130).readPointer().toInt32()

这两个位置需要我们手动IDA去找


新的问题

从上述结果中确实看到了我们可以成功的调用函数拿到函数的返回值,但是仅限于我们已知Il2CppImage*的时候有效,也就是仅仅在我们list_Images中列出的地址对应的子函数可以这么操作(使用il2cpp_class_from_name()获取对应的Il2CppClass),所以我们为了通用得考虑新的思路

 

两个关键结构体:Il2CppImage 和 Il2CppAssembly
两个关键结构体

 

从list_images中取地址

 

查看一下image的内存情况

 

前两个地址对应的是name和nameNoExt
可以看到第三个指针指向的地址的第一个区域由指回来了,就是咋们的Il2CppImage*

 

结合上图

 

上面这张图不是重点,重点在上上张图第二三个圈起来的部分,也就是
TypeDefinitionIndex typeStart;
uint32_t typeCount;
这两位,分别记录了偏移位置和方法个数,偏移位置的开始在(还是去参考IDA拿到地址,同样也是bss段的一个指针)

 

s_MethodInfoDefinitionTable

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
typedef struct MethodInfo
{
    Il2CppMethodPointer methodPointer;
    InvokerMethod invoker_method;
    const char* name;
    Il2CppClass *klass;
    const Il2CppType *return_type;
    const ParameterInfo* parameters;
    union
    {
        const Il2CppRGCTXData* rgctx_data; /* is_inflated is true and is_generic is false, i.e. a generic instance method */
        const Il2CppMethodDefinition* methodDefinition;
    };
    union
    {
        const Il2CppGenericMethod* genericMethod; /* is_inflated is true */
        const Il2CppGenericContainer* genericContainer; /* is_inflated is false and is_generic is true */
    };
    uint32_t token;
    uint16_t flags;
    uint16_t iflags;
    uint16_t slot;
    uint8_t parameters_count;
    uint8_t is_generic : 1; /* true if method is a generic method definition */
    uint8_t is_inflated : 1; /* true if declaring_type is a generic instance or if method is a generic instance*/
    uint8_t wrapper_type : 1; /* always zero (MONO_WRAPPER_NONE) needed for the debugger */
    uint8_t is_marshaled_from_native : 1; /* a fake MethodInfo wrapping a native function pointer */
} MethodInfo;

根据上述SeeHexA(0xcf80ec00)中可知偏移0x0,方法个数0x5c2(即1474个方法),接下来就可以写出以下demo

1
2
3
4
5
6
7
8
9
10
11
12
function a1(){
    var pointerSize = Process.pointerSize
    var a = soAddr.add(0xCC6474).readPointer()
        for (var t = 0;t<1475;t++){
            var tt = a.add(t*pointerSize).readPointer()
            console.warn("---------")
            console.log("srcPointer \t--->\t"+a.add(t*pointerSize))
            console.log("Il2CppImage \t--->\t"+tt.add(pointerSize*0))
            console.log("name \t\t--->\t"+tt.add(pointerSize*2).readPointer().readCString())
            console.log("namespaze \t--->\t"+tt.add(pointerSize*3).readPointer().readCString())
        }
}

效果如下
运行结果

 

上述故意方法数多加一,看到最后空命名空间和<Module>基本就稳了,就是这意思没猜错


拓展

能拓展的东西就挺多的,这就涉及最初起点了

  1. Il2CppDumperTool 脱离 Il2CppDumper 以及python脚本更易用

  2. 搞一些通用的API Hook,比如

    • UnityEngine.GameObject.SetActive(Boolean)
    • UnityEngine.Object.GetName(UnityEngine:Object):String
    • UnityEngine.Application.get_identifier():String
    • UnityEngine.PlayerPrefs.GetInt(String,Int32):Int32
      ......

 

以上展示只是一个简单的分析,其实就上述这样东西还是非常不好用,上述的addr并不能找到所有的方法及其地址,后续就想着让他更加智能一点,就尝试手动去解析一下结构体,然后就得到了一下效果
找关键位置

 

首先是针对我们需要的三个参数的自动解析,主要涉及到ldr寻址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function FuckLDR(addr,isRealAddr){
    var ins = Instruction.parse(addr)
    if (ins.mnemonic != "ldr"){
        LOG("Not LDR Instruction")
        return ptr(0)
    }
    var off = ins.operands[1].value.disp
    // console.log(JSON.stringify(ins.operands))
    var pc_1 = addr.add(p_size*2)
    var pc_2 = addr.add(p_size*3)
    var codeEnd = pc_1.add(off).readPointer()
    var final = codeEnd.add(pc_2)
    return isRealAddr?final:final.sub(soAddr)
}

findins

 

这里为了实现找到我们需要的参数,主要是通过导出函数il2cpp_init的地址往下找找到bl进去然后再找到ldr,并解析到ldr加载的值,如果出现了“mscorlib.dll”,说明我们找到的bl是正确的,然后我们同理找下层的“global-metadata.dat”即可定位到函数 bool il2cpp::vm::MetadataCache::Initialize(),找到上述位置后,我们稍微看一下就可以发现特征 IL2CPP_CALLOC 函数,老规矩通过这个bl指令去解析到函数地址,记录所有调用的该函数的当前地址放进数组,接下来就是 通过数组访问到 [1] [4] [5] 位置就是我们需要的三个初始参数,从当前位置往上解析它,第一条 mov r0,#xx 的立即数就是我们要找的,好了介绍到这里,上效果:

 

找参数

 

这种找法效果还行,能解决大部分问题,也不能解决全部问题,总有些奇葩adr之类的东西,这些就自己手动IDA查看了

 

然后就是挨着列出
列出namespace和class

 

列出method

 

也可以解析的更加详细(其实还可以继续深入解析出方法类型返回值类型的,暂时没有继续深入)
详细method

 

用新的方法来解析找函数就不存在解析不到函数的问题了(随意解析这个so里面的任意函数)

 

解析method


实际用处

随便举个例:断点 public extern void SetActive(GameObject obj,bool value);
1.有了gameObject就可以拿到transform,以及他的名字,再遍历拿到父级名字,整个调用链你都清晰了

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
function HookSetActive(){
 
    var f_getName        = new NativeFunction(find_method("UnityEngine.CoreModule","Object","GetName",1,true),'pointer',['pointer'])
    var f_getLayer       = new NativeFunction(find_method("UnityEngine.CoreModule","GameObject","get_layer",0,true),'int',['pointer'])
    var f_getTransform   = new NativeFunction(find_method("UnityEngine.CoreModule","GameObject","get_transform",0,true),'pointer',['pointer'])
    var f_getParent      = new NativeFunction(find_method("UnityEngine.CoreModule","Transform","GetParent",0,true),'pointer',['pointer'])
 
    Interceptor.attach(find_method("UnityEngine.CoreModule","GameObject","SetActive",1,true),{
        onEnter:function(args){
            if (args[1].toInt32() == 1 || args[1].toInt32() == 0) {
                LOG("\n--------------------------------------",LogColor.YELLOW)
                LOG("public extern void SetActive( "+(args[1].toInt32()==0?"false":"true")+" );",LogColor.COLOR_36)
                LOG("gameObj\t\t--->\t"+args[0],LogColor.COLOR_36)
                LOG("getName\t\t--->\t"+f_getName(args[0]).add(Process.pointerSize*3).readUtf16String(),LogColor.COLOR_36)
                LOG("getLayer\t--->\t"+f_getLayer(args[0]),LogColor.COLOR_36)              
                var m_transform = f_getTransform(args[0])
                LOG("getTransform\t--->\t"+m_transform,LogColor.COLOR_36)
                var layerNames = ""
                try{
                    for (var i=0;i<10;i++){
                        var getName = f_getName(m_transform)
                        var spl =  layerNames == "" ? "" : " <--- "
                        layerNames = layerNames + spl + getName.add(Process.pointerSize*3).readUtf16String()
                        m_transform = f_getParent(m_transform)
                        if (m_transform == 0) break
                    }
                }catch(e){
                    LOG(e,LogColor.RED)
                }
                LOG("hierarchy\t--->\t"+layerNames,LogColor.COLOR_36)
            }
        },
        onLeave:function(retval){
 
        }
    })
}


2.有了gameObject用以上的函数API可以随意的移动,显示隐藏 。。。。
......


补充:

https://github.com/axhlzy/Il2CppHookScripts/tree/master/Il2cppHook



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-5-31 17:22 被唱过阡陌编辑 ,原因: github地址修改
收藏
免费 17
打赏
分享
最新回复 (25)
雪    币: 355
活跃值: (335)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mllaopang 2020-12-30 08:17
2
0
学习                            学习
雪    币: 3836
活跃值: (4142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 2020-12-30 08:50
3
0
等 加精
雪    币: 987
活跃值: (419)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
云天逵 2020-12-30 10:38
4
0
np,np
雪    币: 5855
活跃值: (438)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
NightGuard 1 2020-12-30 18:39
5
0
 来膜拜下大牛,以前都是要用il2cppdumper获得偏移,每次app更新就得更新偏移,这下可以自动支持新版了
雪    币: 199
活跃值: (93)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋日新阳 2021-1-1 21:05
6
0
np,学习了,好地方
雪    币: 60
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
暴强 2021-1-4 14:31
7
0
好东西,刚想自己实现个,你就放出来了。真的好兴奋啊。
雪    币: 1363
活跃值: (1500)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Himeko 2021-1-10 19:07
8
1
NightGuard 来膜拜下大牛,以前都是要用il2cppdumper获得偏移,每次app更新就得更新偏移,这下可以自动支持新版了
https://github.com/HimekoEx/cxk 了解一下?
雪    币: 21
活跃值: (43)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
方块ACE 2021-1-13 03:00
9
1
Il2CppDumper的作者有写过用il2cpp自带的api+反射遍历的方法
https://github.com/Perfare/Riru-Il2CppDumper/blob/master/module/src/main/cpp/il2cpp.cpp#L316
雪    币: 1446
活跃值: (5305)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
唱过阡陌 1 2021-1-13 08:57
10
0
 

第一版是参考他的代码写出来的

第二版使用的 frida-il2cpp-bridge 做底层

最后于 2022-9-2 18:24 被唱过阡陌编辑 ,原因:
雪    币: 2504
活跃值: (2873)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
genliese 1 2022-7-19 15:16
11
0
支持一下
雪    币: 17
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_H_535 2022-7-20 23:30
12
0

这是啥问题 

雪    币: 1446
活跃值: (5305)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
唱过阡陌 1 2022-7-21 09:47
13
0
wx_H_535 这是啥问题&nbsp;
line: 1566  @ c52c986608694cf460161da111979476be135afe  

var MethodInfoOffset = 0x0
改成:
var MethodInfoOffset = 0x1
再试试
2021.2.7f1c1 版本 MethodInfo  的结构体有变化
雪    币: 253
活跃值: (1908)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
wx_嗨 2022-8-30 16:34
14
0
大佬,请教下frida要怎么读取和构造il2cpp里面的string类型的字符串哈
雪    币: 1446
活跃值: (5305)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
唱过阡陌 1 2022-8-30 20:58
15
0
wx_嗨 大佬,请教下frida要怎么读取和构造il2cpp里面的string类型的字符串哈

构造使用:il2cpp的导出函数 il2cpp_string_new
读取使用: mPtr.add(p_size * 2 + 4).readUtf16String()

参考:https://github.com/FateHack/MonoString


https://github.com/axhlzy/Il2CppHookScripts 中的 readU16() 以及 allocUStr()

最后于 2022-8-30 21:01 被唱过阡陌编辑 ,原因:
雪    币: 253
活跃值: (1908)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
wx_嗨 2022-8-31 09:01
16
0
唱过阡陌 wx_嗨 大佬,请教下frida要怎么读取和构造il2cpp里面的string类型的字符串哈 构造使用:il2cpp的导出函数&nbsp ...
好嘞,谢谢,在代码里找到看明白了
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Splite_1130 2022-11-30 22:07
17
0
前来膜拜
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_水山 2022-12-1 15:05
18
0
大佬,某个方法必须由实例去调用,我用 FindObjectsOfType  找到对象了,但是该如何让这个对象调用方法呢
雪    币: 1446
活跃值: (5305)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
唱过阡陌 1 2022-12-2 14:47
19
0
wx_水山 大佬,某个方法必须由实例去调用,我用 FindObjectsOfType 找到对象了,但是该如何让这个对象调用方法呢
callfunction
静态方法 第一个参数就是你的第一个参数
实例方法 第一个参数是对象实例,第二个参数是你的真实第一个参数
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
水丶山 2023-2-1 16:30
20
0
大佬,加固后的游戏,findModuleByName("libil2cpp.so")  返回的是null,我看了mmaps里面,有多个libil2cpp.so。这种情况应该怎么办呢
雪    币: 1446
活跃值: (5305)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
唱过阡陌 1 2023-2-1 17:43
21
0
水丶山 大佬,加固后的游戏,findModuleByName("libil2cpp.so") 返回的是null,我看了mmaps里面,有多个libil2cpp.so。这种情况应该怎么办呢
demo 瞅瞅 ?
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
水丶山 2023-2-1 18:39
22
0
https://ddchess-dl.dragonest.com/apks/2.16.2/autochess_ly_2162.apk    


运行之后,资源还要下载3个G,建议不要登录去下。就在的登录界面看看就行。
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
水丶山 2023-2-1 18:42
23
0
对了   这个游戏还有反ptrace  调试,直接附加会失败,需要用这个评论里面的方式去启动 frida 
   
https://bbs.kanxue.com/thread-263701.htm
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
水丶山 2023-2-3 09:49
24
0
唱过阡陌 demo 瞅瞅 ?
大佬,有戏么
雪    币: 140
活跃值: (446)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_zdkihfht 2023-6-16 11:55
25
0
膜拜大佬   求大佬带我飞
游客
登录 | 注册 方可回帖
返回