首页
论坛
课程
招聘
[原创] Il2cpp数据类型解析の暴打ObscuredTypes
2021-5-7 11:30 7987

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

2021-5-7 11:30
7987

前言

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

 

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

环境 & 工具

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

康康 DamageInfo 类型

DamageInfo

 

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

1
2
3
成员变量自身大小 = 下一个成员变量的偏移 - 自身偏移
Nullable<ObscuredFloat>size: 0x34 - 0x18 = 0x1C
Nullable<ElementalType>size: 0xE4 - 0xDC = 0x8

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

细品 Nullable<> 类型

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

 

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

Struct & Class 区别

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

 

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

 

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

康康 ObscuredFloat 类型

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

内存分析 & 猜想验证

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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);
}

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

 

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

内存对照说明

0x0 - 0x8

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

 

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

0x8 - 0x10

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

0x10 - 0x18

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

0x18 - 0x34

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

0x34 - 0x50

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

 

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

1
2
3
4
5
6
7
8
9
10
struct Nullable<ObscuredFloat>
{
    int currentCryptoKey;
    int hiddenValue;
    ACTkByte4 hiddenValueOldByte4; // 可以当做int32_t
    bool inited;
    float fakeValue;
    bool fakeValueActive;
    bool has_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

使用C++实现Nullable<>

1
2
3
4
5
6
7
8
9
10
11
// 可空类型
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; }
};

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

解密 ObscuredFloat 类型

定位解密函数

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

 

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

1
2
3
public float GetDecrypted();                             // 0x17868F4
private float InternalDecrypt();                         // 0x1786904
public static float op_Implicit(ObscuredFloat value);    // 0x21605BC

IDA 查看一下
GetDecrypted
InternalDecrypt
op_Implicit

 

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

测试解密

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

 

先进行定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// struct 定义直接照搬就行了, 类型占用大小需要注意一下
struct ACTkByte4
{
    char b1; // 0x0
    char b2; // 0x1
    char b3; // 0x2
    char b4; // 0x3
};
struct ObscuredFloat
{
    int32_t currentCryptoKey; // 0x0
    int32_t hiddenValue; // 0x4
    ACTkByte4 hiddenValueOldByte4; // 0x8
    bool inited; // 0xC
    float fakeValue; // 0x10
    bool fakeValueActive; // 0x14
};
// 原型定义
float (*_ObscuredFloat_Decrypted)(ObscuredFloat*) = nullptr;
float ObscuredFloat_Decrypted(ObscuredFloat* self)
{
    return _ObscuredFloat_Decrypted(self);
}

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

1
2
DobbyHook((void*)(Il2cppBaseAddr + 0x2161A10),
          (void*)ObscuredFloat_Decrypted, (void**)&_ObscuredFloat_Decrypted);

调用解密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//内存偏移
template<typename R>
inline R MemoryOff(void* addr, ulong off)
{
    return reinterpret_cast<R>((static_cast<char*>(addr) + off));
}
 
void (* _xxxDamages)(Il2CppObject*, void*) = nullptr;
void xxxDamages(Il2CppObject* behaviour, void* damageInfo)
{
    auto _damage = MemoryOff<Nullable<ObscuredFloat>*>(damageInfo, 0x34);
    if(_damage->HasValue())
    {
        auto value = ObscuredFloat_Decrypted(_damage->GetValue());
        LOGE("_damage: %0.3f", value);
    }
    return _xxxDamages(behaviour, damageInfo);
}

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

用优雅一点的方式Hook

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

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
// 提取B指令的偏移
inline ulong BxxExtract(void* symbol)
{
#if defined(__arm__)
    return static_cast<ulong>((*static_cast<int32_t*>(symbol) << 0x8 >> 0x6));
#elif defined(__arm64__) || defined(__aarch64__)
    return static_cast<ulong>((*static_cast<int32_t*>(symbol) << 0x6 >> 0x4));
#else
#error ABI Error
#endif
}
 
// 修正B指令转跳
template<typename R>
inline R Amend_Bxx(R symbol, ulong off = 0ul)
{
    symbol = MemoryOff<R>(reinterpret_cast<void*>(symbol), off);
#if defined(__arm__)
    return MemoryOff<R>(reinterpret_cast<void*>(symbol),
                BxxExtract(reinterpret_cast<void*>(symbol)) + 0x8);
#elif defined(__arm64__) || defined(__aarch64__)
    return MemoryOff<R>(reinterpret_cast<void*>(symbol),
                BxxExtract(reinterpret_cast<void*>(symbol)));
#else
#error ABI Error
#endif
}
 
// 地址可通过 主动式Hook 或 被动式Hook 动态获取
DobbyHook((void*)Amend_Bxx(Il2cppBaseAddr + 0x17868F4, 0x4),
          (void*)ObscuredFloat_Decrypted, (void**)&_ObscuredFloat_Decrypted);

加密 & 修改 & 其他类型

NTM犯法了你知道吗

 

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

 

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


[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

收藏
点赞4
打赏
分享
最新回复 (6)
雪    币: 257
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_uuhxxerx 活跃值 2021-5-7 12:27
2
1
太棒了,学到了许多。
雪    币: 5420
活跃值: 活跃值 (4249)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
GitRoy 活跃值 3 2021-5-8 08:17
3
0
666,学习了
雪    币: 6
活跃值: 活跃值 (504)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
路易 活跃值 2021-5-10 10:14
4
0
赞一个吧。
unity早就被撸出血了
雪    币: 19
活跃值: 活跃值 (2452)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不吃早饭 活跃值 2021-5-26 13:33
5
0

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

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