首页
社区
课程
招聘
[原创] Il2cpp数据类型解析の暴打ObscuredTypes
发表于: 2021-5-7 11:30 12093

[原创] Il2cpp数据类型解析の暴打ObscuredTypes

2021-5-7 11:30
12093

摸鱼了好几个月, 期间是有写过文章的不过咕掉了, 搓文章太累了
呜呜呜

4月初我hxd介绍了款手游给我 <坎****>, 说来让我碰碰
我一看! 嗷! ObscuredFloat 啊, 游戏概述, 没意思, 骡的岛用的就是这个, 太弱鸡了不搞
然后我就试着玩了一下, 意外的有点上头, 然后就研究了一下伤害的解析
刚一碰, 我大E了啊, 没有闪, 一套不定长内存把我打麻瓜了
我说你这小伙子不讲武德, 来骗, 来偷袭, 我这单身的几十年的老同志

Android 10 aarch64
GDTS国际服: 2.13.1
IDA Pro: 7.2
Dobby: gayhub

DamageInfo

映入眼帘的就是 _damage 这个 成员变量
不过类型有点点奇怪, 是 Nullable<T> 类型
再看看偏移, 就更奇怪了 convertAttackTo 这个成员变量的大小跟 _damage 似乎不一样

ん, 确实不一样, 开始头大了

Nullable
好家伙, 就一个 has_value 成员变量是个 bool 类型以外, 其他信息全部木大
至于为何特化出来的类型大小不一, 因为成员变量 value 的类型为特化类型, 主要还是要看具体特化类型的内存占用大小

这时就引出了一个问题, T value 为何不应该以指针存储呢?
或者说, 所有对象都继承于 Il2cppObject 却不以指针方式存储值而导致长度不定?

老司机可能会发现这几个类型的定义并不是传统的 class 而是 struct, 并且首个成员变量的偏移量并不是以 0x10 开始(32位为 0x8, 其实就是Il2cppObject 内部的两个指针), so, struct 的成员变量, 多半没法直接去调用api获取对应的东西
Il2cppObject

还有一个区别就是, class 的成员变量都是以二级指针形式存储, 指向的是另一块内存, 获取时必须 * 取值一下才能得到目标 Object
struct 是直接占用结构体内部, 偏移即可获得目标 Object

这种特性也解释了为何 struct 的成员变量的长度是不同的

ObscuredFloat
ACTkByte4
也是 struct 的, 成员变量并没有很有用的信息
需要注意的是C#里 byte 类型对应C++内是 char (8字节长度), 并且 ACTkByte4 也是 struct 结构体, so hiddenValueOldByte4 这个成员变量可以看做 int32_t 类型

怎么Hook我就不贴出来了, 只贴关键代码

随便找个陷阱撞一下
未处理的内存打印

有点丑, 先按照 类型占用大小去划分一下内存对照吧
内存对照图

可以看得出并不是指向某处的指针, 所以验证了 struct 类型并没有继承 Il2cppObject 的成员变量

其值是 0x80 , 瞄了一下下面的 0x8-0x10, 确认该数据类型是 8字节 长度
对照一下结构体的定义, 第一个成员变量 DamageType type, 而 0x80(128)DamageType::Trap 对应的上
DamageType

很显然是一枚指针, 在64位下指针的长度是 8字节
对应于结构体中第二个成员变量 IFieldObject sender

一枚指针
对应于结构体中第三个成员变量 IFieldObject target

对应于结构体中第四个成员变量 Nullable<ObscuredFloat> _modifer
由于值为空, 没啥好说明的, 直接跳过

对应于结构体中第五个成员变量 Nullable<ObscuredFloat> _damage

由于 Nullable<T> 的第一个成员变量是 T value, 所以特化后展开成员变量样子是这样的

C# 中 int 类型占用 4 字节
0x34 - 0x38 对应 int currentCryptoKey
0x38 - 0x3C 对应 int hiddenValue

ACTkByte4 类型可以当做 int32_t , 也是 4 字节
0x3C - 0x40 对应 ACTkByte4 hiddenValueOldByte4

bool 类型应该占用 1 字节, 但是由于结构体需要 字节对齐, 被拓展成了 4 字节
0x40 - 0x44 对应 bool inited

float 类型占用 4 字节
0x44 - 0x48 对应 float fakeValue

0x48 - 0x4C 对应 bool fakeValueActive

最后的四字节则是 bool has_value

再自行把特化类型的定义抄下来, 就能对Il2cpp中的 Nullable<T> 类型进行操作了
不嫌麻烦的也可以手动展开特化类型的成员变量, 能用就是不够优雅

通过关键词 Decrypt 就能得到几个关键函数, 再过滤一下 形参返回值 , 传入key的那些就可以无视了
ObscuredFloat

看了一圈最可疑的就这三个函数了

IDA 查看一下
GetDecrypted
InternalDecrypt
op_Implicit

均直接调用另一个内部的解密函数
sub_2161A10

直接Hook sub_2161A10 函数并且调用试试看

先进行定义

hook解密函数, map获取动态库地址的源码太多就不贴上来了

调用解密函数

再去找个陷阱撞一下, 正确识别, 收工
游戏截图
logcat
好耶

解析B指令获取目标地址然后进行hook
至于怎么解析B指令emmm, 看arm手册就行了, 简单概述的话就是去掉 指令标志位
以后有机会再讲一下吧, 这玩意也是个雷, 去年那篇 Android10 aarch64 dlopen Hook 的解析写法就是有点问题的, 虽然还没触发到那颗雷

NTM犯法了你知道吗

点到为止! 点到为止!
已经讲了很多了, 再多说会被 的.

搞懂怎么解密后, 加密
其他类型的也是同样的套路
先定义类型, 然后阿吧阿吧阿吧

 
 
成员变量自身大小 = 下一个成员变量的偏移 - 自身偏移
Nullable<ObscuredFloat>size: 0x34 - 0x18 = 0x1C
Nullable<ElementalType>size: 0xE4 - 0xDC = 0x8
成员变量自身大小 = 下一个成员变量的偏移 - 自身偏移
Nullable<ObscuredFloat>size: 0x34 - 0x18 = 0x1C
Nullable<ElementalType>size: 0xE4 - 0xDC = 0x8
 
 
 
#include <cinttypes>
 
//内存读取
template<typename R>
inline R MemoryRead(void* addr, ulong off)
{
    return *reinterpret_cast<R*>((static_cast<char*>(addr) + off));
}
 
//xx伤害 函数定义
void (* _xxxDamages)(Il2CppObject*, void*) = nullptr;
void xxxDamages(Il2CppObject* behaviour, void* damageInfo)
{
    for (auto i = 0; i < 0x50; i += sizeof(void*))
        LOGE("0x%x:\t0x%08" PRIX32 " - 0x%08" PRIX32 " - 0x%016" PRIX64, i,
        MemoryRead<int32_t>(damageInfo, i),
        MemoryRead<int32_t>(damageInfo, i + 0x4),
        MemoryRead<int64_t>(damageInfo, i));
    return _xxxDamages(behaviour, damageInfo);
}
#include <cinttypes>
 
//内存读取
template<typename R>
inline R MemoryRead(void* addr, ulong off)
{
    return *reinterpret_cast<R*>((static_cast<char*>(addr) + off));
}
 
//xx伤害 函数定义
void (* _xxxDamages)(Il2CppObject*, void*) = nullptr;
void xxxDamages(Il2CppObject* behaviour, void* damageInfo)
{
    for (auto i = 0; i < 0x50; i += sizeof(void*))
        LOGE("0x%x:\t0x%08" PRIX32 " - 0x%08" PRIX32 " - 0x%016" PRIX64, i,
        MemoryRead<int32_t>(damageInfo, i),
        MemoryRead<int32_t>(damageInfo, i + 0x4),
        MemoryRead<int64_t>(damageInfo, i));
    return _xxxDamages(behaviour, damageInfo);
}
 
 
 
struct Nullable<ObscuredFloat>
{
    int currentCryptoKey;
    int hiddenValue;
    ACTkByte4 hiddenValueOldByte4; // 可以当做int32_t
    bool inited;
    float fakeValue;
    bool fakeValueActive;
    bool has_value;
};
struct Nullable<ObscuredFloat>
{
    int currentCryptoKey;
    int hiddenValue;
    ACTkByte4 hiddenValueOldByte4; // 可以当做int32_t
    bool inited;
    float fakeValue;
    bool fakeValueActive;
    bool has_value;
};
 
 
 
 
 
// 可空类型
template<typename T>
struct Nullable
{
    T value;
    bool has_value;
 
    bool HasValue() const { return has_value; }
    const T& GetValue() const { return value; }
    void SetValue(T& newValue) { value = newValue; }
};
// 可空类型
template<typename T>
struct Nullable
{
    T value;
    bool has_value;
 
    bool HasValue() const { return has_value; }

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 6
支持
分享
最新回复 (6)
雪    币: 293
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
太棒了,学到了许多。
2021-5-7 12:27
1
雪    币: 5330
活跃值: (5464)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
666,学习了
2021-5-8 08:17
0
雪    币: 6
活跃值: (856)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
赞一个吧。
unity早就被撸出血了
2021-5-10 10:14
0
雪    币: 29
活跃值: (5637)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5

Nullable<T>是泛型结构体。unity的结构体作为类成员时是值类型,其头部的class指针以及monitor都会被移除,同时在定义类中直接展开。因此这个Nullable<T>的大小由T是否为结构体来决定,如果是结构体,则还要看一下T在Nullable<T>中展开后的大小

最后于 2021-5-26 13:34 被不吃早饭编辑 ,原因:
2021-5-26 13:33
0
雪    币: 29
活跃值: (5637)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
不过如果这个T是一个interface,那么不管实现类是结构体还是类,都会按照引用类型来进行处理
2021-5-26 13:36
0
雪    币: 29
活跃值: (5637)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
其实arm架构下还有个thumb指令的问题,通过函数地址最后一位是不是0来判断是arm指令还是thumb指令。
2021-5-27 22:56
0
游客
登录 | 注册 方可回帖
返回
//