记一次UE逆向--远光84

记一次UE逆向–远光84


关于


本文章仅供学习交流,请勿用于非法用途

很难想象这是2025年的游戏

反调试


拿到游戏上手,CE xdbg启动,结果啥都搜不到。。全是打问号的数据。

尝试scylla将游戏内存dump下来,没有入口点,IAT表找不到,dump下来的内存全是打问号的数据。

841

842

这种情况很奇怪,如果是藤子游戏的反作弊也不至于等你ce或者xdbg附加了才发现。你一启动就被特征提示警告了。。这款游戏的反作弊更像是一种进程保护。

静态分析这一块还是蛮重要的,这个问题不解决没法干活

所以我花了大半天的时间去寻找能过这种奇怪的反调试的工具,没找到。。(程序快写完的时候确实找到了)

就在我万念俱灰的时候,我把游戏重启了一下,CE突然就找到了内容,然后没过几秒钟就变成?????

啊?

这下搞懂了

这游戏进程保护在刚开游戏的时候没加载,大概过了三十秒才加载

所以打开游戏的前三十秒是无敌时间,只需要抢在这三十秒做逆向分析(或者挂起进程),就可以正常分析游戏内容。

结果真尼玛成功了。IAT表,程序入口点都找到了。。。dump下来的内容完全正常。。。

843

开搞


众所周知UE的逆向全是公式化

Gworld

搜SeamlessTravel Flushlevelstreaming找Uworld赋值Gworld的地方

844

得到偏移0x09F379B0

Gname

搜ByteProperty找交叉引用

845

这里的继续找交叉引用,F5进去看函数

846

黄标文字就是Gname,得到偏移0x09DC01C0

其他的不用多说了,都是直接推的,算法大概就是这样:

Uworld = DR.读写_读长整数 (进程ID, 模块地址 + 166951344)
Gname = 模块地址 + 165413312
Ulevel = DR.读写_读长整数 (进程ID, Uworld + 48)
Actor = DR.读写_读长整数 (进程ID, Ulevel + 168)
Count = DR.读写_读整数型 (进程ID, Ulevel + 176)

GameInstance = DR.读写_读长整数 (进程ID, Uworld + 496)
LocalPlayer = DR.读写_读长整数 (进程ID, DR.读写_读长整数 (进程ID, GameInstance + 56))
PlayerController = DR.读写_读长整数 (进程ID, LocalPlayer + 48)
APawn = DR.读写_读长整数 (进程ID, PlayerController + 880)

算法

这里提供部分参考代码

Gname

这个Gname比较常规,大致算法如下:


E:

1
2
3
4
5
6
7
8
9
10
11
12
GNameTable = Gname
TableLocation = Uchar (右移 (id, 16))
RowLocation = Ulong (id)
pRowLocation = 取字节集数据 (到字节集 (RowLocation), #整数型, )
TableLocationAddress = DR.读写_读长整数 (进程ID, GNameTable + 16 + TableLocation × 8)
TableLocationAddress = TableLocationAddress + 2 × pRowLocation
sLength = DR.读写_读短整数 (进程ID, TableLocationAddress)
sLength = 右移 (sLength, 6)
.如果真 (sLength < 128)
ObjName = 取字节集数据 (DR.读写_读字节集 (进程ID, TableLocationAddress + 2, sLength), #文本型, )
.如果真结束
返回 (ObjName)

Cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string GetGName(int id) {
auto chunk = (UINT)((int)(id) >> 16);
auto name = (USHORT)id;
auto poolChunk = memory.read<UINT64>(memory.MoudleBase + offset::GName + ((chunk + 2) * 8));
auto entryOffset = poolChunk + (ULONG)(2 * name);
auto nameEntry = memory.read<INT16>(entryOffset);
auto nameLength = nameEntry >> 6;
char buff[1028];
if ((DWORD)nameLength && nameLength > 0) {
ReadMemory(memory.PID, (PVOID)(entryOffset + 2), &buff, nameLength);
buff[nameLength] = '\0';
return { buff };
}
else return "";
}

绘制算法

E:

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

绘制文本 (50, 50, “花蕊绘制-开源版”, #绿色, 30)

' 更新数据 ()
Aaary = 全_对象数组
count = 取数组成员数 (Aaary)
移动窗口 (句柄, 窗口矩形.左边, 窗口矩形.顶边 + 15, 窗口矩形.宽度 - 窗口矩形.左边, 窗口矩形.高度 - 窗口矩形.顶边, 假)
取窗口矩形_ (窗口句柄, 窗口矩形)

.计次循环首 (count, i)
Player = Aaary [i]
RootComponent = DR.读写_读长整数 (进程ID, Player + #RootComponent)
Pos = GetPos3D (RootComponent + #Location)

Mesh = DR.读写_读长整数 (进程ID, Player + #Mesh)
对象类名 = GetNamePoolData (DR.读写_读整数型 (进程ID, Player + 24))
调试输出 (“对象类名”, 对象类名)
.如果真 (Mesh = 0)
到循环尾 ()
.如果真结束
.如果真 (WorldTo_BoxRect (Pos, Rect, Dis))
' 绘制矩形 (Rect.x, Rect.y, Rect.w, Rect.h, #绿色, 2)
绘制文本 (Rect.x - 14, Rect.y + Rect.h, 对象类名, #绿色, 15)
.如果真结束

.计次循环尾 ()

cpp的算法只有内部绘制,外部没写过。我这里用的是其他人写的绘制框架,不好公开。可以去BV1MjCHY9ELJ支持一下作者

世界转屏幕

E:

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
屏幕X = (窗口矩形.宽度 - 窗口矩形.左边) ÷ 2
屏幕Y = (窗口矩形.高度 - 窗口矩形.顶边) ÷ 2
' 调试输出 (“屏幕X”, 屏幕X, “屏幕Y”, 屏幕Y)
相机Z = 矩阵数据 [1] [4] × _对象坐标.x + 矩阵数据 [2] [4] × _对象坐标.y + 矩阵数据 [3] [4] × _对象坐标.z + 矩阵数据 [4] [4]

' 调试输出 (“矩阵数据”, 矩阵数据)
' 调试输出 (“相机Z”, 相机Z)

.如果真 (相机Z ≤ 0.01)
返回 (假)
.如果真结束
比例 = 1 ÷ 相机Z

相机X = 屏幕X + (矩阵数据 [1] [1] × _对象坐标.x + 矩阵数据 [2] [1] × _对象坐标.y + 矩阵数据 [3] [1] × _对象坐标.z + 矩阵数据 [4] [1]) × 比例 × 屏幕X
相机Y = 屏幕Y - (矩阵数据 [1] [2] × _对象坐标.x + 矩阵数据 [2] [2] × _对象坐标.y + 矩阵数据 [3] [2] × (_对象坐标.z - 90) + 矩阵数据 [4] [2]) × 比例 × 屏幕Y
相机Y2 = 屏幕Y - (矩阵数据 [1] [2] × _对象坐标.x + 矩阵数据 [2] [2] × _对象坐标.y + 矩阵数据 [3] [2] × (_对象坐标.z + 90) + 矩阵数据 [4] [2]) × 比例 × 屏幕Y
物品Y = 屏幕Y - (矩阵数据 [1] [2] × _对象坐标.x + 矩阵数据 [2] [2] × _对象坐标.y + 矩阵数据 [3] [2] × _对象坐标.z + 矩阵数据 [4] [2]) × 比例 × 屏幕Y

' 调试输出 (“相机X”, 相机X, “相机Y”, 相机Y, “相机Y2”, 相机Y2, “物品Y”, 物品Y)

屏幕坐标.x = 相机X - (相机Y2 - 相机Y) ÷ 4
屏幕坐标.y = 相机Y
屏幕坐标.w = (相机Y2 - 相机Y) ÷ 2
屏幕坐标.h = 相机Y2 - 相机Y

返回 (真)

过滤人物算法

E:

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
.判断循环首 (控制变量)
Uworld = DR.读写_读长整数 (进程ID, 模块地址 + 166951344)
Gname = 模块地址 + 165413312
Ulevel = DR.读写_读长整数 (进程ID, Uworld + 48)
Actor = DR.读写_读长整数 (进程ID, Ulevel + 168)
Count = DR.读写_读整数型 (进程ID, Ulevel + 176)

GameInstance = DR.读写_读长整数 (进程ID, Uworld + 496)
LocalPlayer = DR.读写_读长整数 (进程ID, DR.读写_读长整数 (进程ID, GameInstance + 56))
PlayerController = DR.读写_读长整数 (进程ID, LocalPlayer + 48)
APawn = DR.读写_读长整数 (进程ID, PlayerController + 880)

Matrix = DR.读写_读长整数 (进程ID, DR.读写_读长整数 (进程ID, 模块地址 + 进制_十六到十 (“99C0F50”)) + 进制_十六到十 (“20”)) + 进制_十六到十 (“290”)

.计次循环首 (Count, i)
对象指针 = DR.读写_读长整数 (进程ID, Actor + (i - 1) × 8)
.如果真 (对象指针 > 0)
对象ID = DR.读写_读整数型 (进程ID, 对象指针 + 24)
对象类名 = GetNamePoolData (对象ID)
' 调试输出 (“对象类名”, 对象类名)
.如果真 (对象ID = 0 或 对象类名 = “”)
到循环尾 ()
.如果真结束
.如果真 (对象指针 = APawn)
到循环尾 ()
.如果真结束
.如果真 (取文本左边 (对象类名, 2) ≠ “BP”)
到循环尾 ()
.如果真结束
' 标准输出 (, 十到十六 (Object) + “ ” + Gname, #换行符)
.如果真 (取文本左边 (对象类名, 12) = “BP_Character”)
RootComponent = DR.读写_读长整数 (进程ID, 对象指针 + #RootComponent)
pos = GetPos3D (RootComponent + #Location)
.如果真 (pos.x = 0 或 pos.y = 0 或 pos.z = 0)
到循环尾 ()
.如果真结束
加入成员 (临时数组, 对象指针)
.如果真结束

.如果真结束

.计次循环尾 ()

Cpp这里也略过,原因同上。。。(

绘制展示

遍历ACtor:

847

绘制方框:

848


后记


还记得开始的反调试部分,我用一种特殊的方式过掉了反调试。。但是这种保护机制究竟是什么

我随便浏览了一下文件,突然看到了一个熟悉的身影,,,

卧槽是你啊

849

(本文完)