记一次UE逆向--远光84
huarui记一次UE逆向–远光84
关于
本文章仅供学习交流,请勿用于非法用途
很难想象这是2025年的游戏
反调试
拿到游戏上手,CE xdbg启动,结果啥都搜不到。。全是打问号的数据。
尝试scylla将游戏内存dump下来,没有入口点,IAT表找不到,dump下来的内存全是打问号的数据。


这种情况很奇怪,如果是藤子游戏的反作弊也不至于等你ce或者xdbg附加了才发现。你一启动就被特征提示警告了。。这款游戏的反作弊更像是一种进程保护。
静态分析这一块还是蛮重要的,这个问题不解决没法干活
所以我花了大半天的时间去寻找能过这种奇怪的反调试的工具,没找到。。(程序快写完的时候确实找到了)
就在我万念俱灰的时候,我把游戏重启了一下,CE突然就找到了内容,然后没过几秒钟就变成?????
啊?
这下搞懂了
这游戏进程保护在刚开游戏的时候没加载,大概过了三十秒才加载
所以打开游戏的前三十秒是无敌时间,只需要抢在这三十秒做逆向分析(或者挂起进程),就可以正常分析游戏内容。
结果真尼玛成功了。IAT表,程序入口点都找到了。。。dump下来的内容完全正常。。。

开搞
众所周知UE的逆向全是公式化
Gworld:
搜SeamlessTravel Flushlevelstreaming找Uworld赋值Gworld的地方

得到偏移0x09F379B0
Gname:
搜ByteProperty找交叉引用

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

黄标文字就是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:

绘制方框:

后记
还记得开始的反调试部分,我用一种特殊的方式过掉了反调试。。但是这种保护机制究竟是什么
我随便浏览了一下文件,突然看到了一个熟悉的身影,,,
卧槽是你啊

(本文完)