内存解析Il2cpp函数地址
目的
使用函数名拿到U3D非导出函数地址
起因
- 相对u3d游戏进行hook基本上离不开 Il2CppDumper 的使用,有时候想要找个函数什么的就很麻烦,第一步就得使用上面这工具来找到函数地址
- 第二就是每次都要用 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,可以去头文件查看
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;
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)
}
运行一下就可以拿到一下内容
这些玩意其实就是我们用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:这里做的事情其实,就是和做数学一样,把未知量用已知函数去代替,带入到我们能解决即可 )
由此可以写出以下代码进行主动调用
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(
"---------------------------------------------------------"
)
}
结果展示
看到此处的话目的算是勉强达到了,但是还有待优化把,比如一开始的
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中取地址
前两个地址对应的是name和nameNoExt
可以看到第三个指针指向的地址的第一个区域由指回来了,就是咋们的Il2CppImage*
上面这张图不是重点,重点在上上张图第二三个圈起来的部分,也就是
TypeDefinitionIndex typeStart;
uint32_t typeCount;
这两位,分别记录了偏移位置和方法个数,偏移位置的开始在(还是去参考IDA拿到地址,同样也是bss段的一个指针)
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;
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>基本就稳了,就是这意思没猜错
拓展
能拓展的东西就挺多的,这就涉及最初起点了
- 让 Il2CppDumperTool 脱离 Il2CppDumper 以及python脚本更易用
- 搞一些通用的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寻址
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)
}
这里为了实现找到我们需要的参数,主要是通过导出函数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查看了
然后就是挨着列出
也可以解析的更加详细(其实还可以继续深入解析出方法类型返回值类型的,暂时没有继续深入)
用新的方法来解析找函数就不存在解析不到函数的问题了(随意解析这个so里面的任意函数)
实际用处
随便举个例:断点 public extern void SetActive(GameObject obj,bool value);
1.有了gameObject就可以拿到transform,以及他的名字,再遍历拿到父级名字,整个调用链你都清晰了
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可以随意的移动,显示隐藏 。。。。
……