UE4逆向简单流程

dump SDK

这个可能是属于UE4逆向首先要做的事情,网上关于这一步有很多教程,这里便不再详说。
看UE4具体版本:

1
2
3
<meta-data
android:name="com.epicgames.ue4.GameActivity.EngineVersion"
android:value="4.xx.xx" />

image.png
版本是4.27
将UE4.so拖入IDA,IDA分析完后获取三个主要结构的偏移。
GWorld:0x0B32D8A8
GName:0x0B171CC0
GUObjectArray:0xB1B5F98

工具:https://github.com/revercc/UE4Dumper?tab=readme-ov-file

1
./Ue4dumper --package com.tencent.ace.match2024 --ptrdec --sdku --gname 0x0B171CC0 --guobj 0x0B1B5F98 --output /data/local/tmp --newue+

拿到dump下来的SDK后应该就可以通过它来锁定实例对一些属性进行一个修改。
不过这块我要怎么实现呢?写frida脚本吗?
是的,写frida脚本,但是这和我之前写的一些小小的勾取脚本不同,这次要锁定类的方法是根据地址。

UE4逆向脚本中一些函数的解析

下文的代码块中基本都是JS代码,实现一些功能的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function set(moduleName){
  //获取libUE4的基地址
  moduleBase=Module.findBaseAddress(moduleName);
 
  //UE4基地址加上在IDA中获取的GName偏移获取GName地址
  GName=moduleBase.add(GName_Offset);
 
  //同上获取GUObjectArray的地址
  GUObjectArray=moduleBase.add(GUObjectArray_Offset);
 
//读取GWorld的指针
  GWorld =moduleBase.add(GWorld_Offset).readPointer();
 
}
function setPlayerHP(hp=1000000){
  //生命值属性的偏移
  getPlayAddr().add(0x510).writeFloat(hp);//写入
}

获取对象ID及通过ID获取对象的符号

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
getNameId: function(obj){
  console.log("obj:${obj}");
  try{
    var nameId=ptr(obj).add(offset_UObject_FNameIndex).readU32();//读取4字节。
    //console.log("nameId:${nameId}");
    return nameId;
  }catch(e){
    console.log("error")
    return 0;
  }
}
 
 
function getFNameFromID(index) {
  // FNamePool相关偏移量和步长
  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节
  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量
  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量
   
  // FNameEntry相关偏移量和位
  var offset_FNameEntry_Info = 0;          // FNameEntry 到 Info 的偏移量
  var FNameEntry_LenBit = 6;               // FNameEntry 长度位
  var offset_FNameEntry_String = 0x2;      // FNameEntry 到字符串部分的偏移量
 
  // 计算块和偏移量
  var Block = index >> 16;                 // 块索引
  var Offset = index & 65535;              // 块内偏移量
 
  // 获取FNamePool的起始地址
  var FNamePool = GName.add(offset_GName_FNamePool);
  // console.log(`FNamePool: ${FNamePool}`);
  // console.log(`Block: ${Block}`);
   
  // 获取特定块的地址
  var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();
  // console.log(`NamePoolChunk: ${NamePoolChunk}`);
   
  // 计算FNameEntry的地址
  var FNameEntry = NamePoolChunk.add(FNameStride * Offset);
  // console.log(`FNameEntry: ${FNameEntry}`);
   
  try {
      // 读取FNameEntry的Header
      if (offset_FNameEntry_Info !== 0) {
          var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();    
      } else {
          var FNameEntryHeader = FNameEntry.readU16();
      }
  } catch(e) {
      // 捕捉读取异常并返回空字符串
      // console.log(e);
      return "";
  }
  // console.log(`FNameEntryHeader: ${FNameEntryHeader}`);
   
  // 获取字符串地址
  var str_addr = FNameEntry.add(offset_FNameEntry_String);
  // console.log(`str_addr: ${str_addr}`);
   
  // 计算字符串长度和宽度
  var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 计算字符串长度
  var wide = FNameEntryHeader & 1;                       // 判断字符串是否为宽字符
   
  // 如果是宽字符,返回 "widestr"
  if (wide) return "widestr";
 
  // 如果字符串长度合理,读取并返回UTF-8字符串
  if (str_length > 0 && str_length < 250) {
      var str = str_addr.readUtf8String(str_length);
      return str;
  } else {
      return "None"; // 长度不合理,返回 "None"
  }
}

获取所有Actor的实例地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getActorsAddr(){
  var Level_Offset=0x30//偏移
  var Actors_Offset=0x98
  var Level=GWorld.add(Level_Offset).readPointer()//读取GWorld的level指针
  var Actors=Level.add(Actors_Offset).readPointer()//读取Actors的指针
  var Actors_Num=Level.add(Actors_Offset).add(8).readU32()//获取Actor的数量
  var actorsAddr={};//空对象,下面的实现类似字典
  for(var index=0;index<Actors_Num;index++)
    {
      var actor_addr=Actors.add(index*8).readPointer()//读取当前索引处的Actor地址
      var actorName=UObject.getName(actor_addr)//通过地址获取字符串名字
      actorsAddr[actorName]=actor_addr;//以字符串名字对应地址值
      //console.log(`actors[${index}]`,actorName);
    }
    return actorsAddr;
}

对题目复现的正式开始

出门

实现篡改血量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getActorAddr(str){//根据对象名字获取对象地址
  var player_addr;
  var actorsAddr=getActorsAddr();
  for(var key in actorsAddr){
    if(key==str){
      //console.log(actorsAddr[key]);
      player_addr=actorsAddr[key];
    }
  }
  if(player_addr==null)
    {
      console.log("null pointer!");
    }
  return player_addr;
}
 
function setPlayerHP(hp=1000000){
  //生命值属性的偏移
  getAcotrAddr(playerName).add(0x510).writeFloat(hp);//通过偏移定位到生命值的变量并写入值
}
1
2
3
//main:
set("libUE4.so")
setPlayerHP();

瞬移

这里有一步操作是我们可以从SDK_dump文件中获取到很多函数及类的偏移。
将K2_GetActorLocation函数的偏移传入构造JavaScript函数,然后将玩家的类地址和移动到的坐标作为参数即可调用我们自定义的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getActorLocation(actor_addr){
  GWorld = moduleBase.add(GWorld_Offset).readPointer();
  actor_addr = ptr(actor_addr)
  var buf = Memory.alloc(0x100);
  var f_addr = moduleBase.add(0x965ddf8);//这个地址可在dump下的SDK文件里找到
    // 将目标函数地址转换为JavaScript函数
  var getLocationFunc = new NativeFunction(f_addr, 'void', ['pointer','pointer','pointer']);
    
  // 调用目标函数并传递内存地址作为参数
  try{
      getLocationFunc(actor_addr,buf,buf);
      dumpVector(buf);
      //info(ptr(actor_addr).add(0x130).readPointer().add(0x14c).readU8()&32 != 0);
  }
  catch (e){
  }
}

将获取到的坐标进行一个搜索
image.png
冻结后实现一个方向无法移动,证实该坐标的正确。

从之前的SDK_dump中获取函数的偏移
image.png
构建setActorLocation函数

1
2
3
4
5
6
7
8
9
10
11
function setActorLocation(actor_addr,x,y,z){
  GWorld = moduleBase.add(GWorld_Ptr_Offset).readPointer();
  actor_addr = ptr(actor_addr)
  var f_addr = moduleBase.add(0x8C3181C);//加上偏移获取目标函数的偏移
  // 将目标函数地址转换为JavaScript函数
  var setLocationFunc = new NativeFunction(f_addr, 'bool', ['pointer','bool','pointer','bool','float','float','float']);
  // 调用目标函数并传递内存地址作为参数
  setLocationFunc(actor_addr,0,ptr(0),0,x,y,z);
  //dumpVector(buf);
 
}

这里只要调用该函数即可实现坐标的变化。
瞬移到此结束。
虽然瞬移到此结束,但是这里我要说明一下这里有关函数类封装性的分析。

小重点:定位正确函数地址

这里拿SetActorLocation来做个例子。
image.png
dump下的SDK中相关的该函数地址为0x965dc3c。转到该地址后
image.png
实际参数只有三个。
这里的话,要关联一些关于SDK的知识。SDK中看到的函数地址是中转函数。
这里的三个参数数量是正确的,分别是类指针,存放参数用来解析的结构体,存放返回值的指针,它是一个中转函数。

在下文的return这里的这个函数才是实际和我们源码看到的相符的。
image.png
而我们再进入这个函数时,可以看到和源码中被return的那个函数相符的部分。
image.png
所以sub_8C3181C这里才是我们实际要调用的函数。

Section1:空中的Flag字段

通过改变玩家高度大概锁定了天上的flag在高度3000左右往上。
这里就是对所有高度大于3000的对象实例尽可能调用SetVisibility函数来使得Actor可见。
第一个参数为Component型
image.png
所以传入Actor对象实例的0x130偏移为指针。

1
2
3
4
5
6
7
8
//显示
//void SetVisibility(bool bNewVisibility, bool bPropagateToChildren);
function SetVisibility(Component,bNewVisibility,bPropagateToChildren)
{
var pSetVisibility=moduleBase.add(0x8E619BC);
var callSetVisibility=new NativeFunction(pSetVisibility,"void",['pointer','int','int']);
  callSetVisibility(ptr(Component).add(0x130).readPointer(),bNewVisibility,bPropagateToChildren);
}

Section2:使得方块不可碰撞

构建有关碰撞的函数

1
2
3
4
5
6
//碰撞
function setActorEnableCollision(actor_addr,bNewActorEnableCollision=1){
  var f_addr = moduleBase.add(0x8C21320);
  let CallFunc = new NativeFunction(f_addr, 'void', ['pointer','char']);
  CallFunc(ptr(actor_addr),bNewActorEnableCollision);
}

然后试图对所有对象进行调用。
这里发现问题似乎在于函数的使用,这里使用类本身的setStaticMeshActorCollisionEnabled()可以实现立方体可碰撞,但是我选用上面的更为全局的碰撞函数setActorEnableCollision()时却无法实现。这里不太清楚为什么,把section3搞完再去搜一下。

解读一下第二种碰撞函数:对actor加上0x220的偏移,定位到StaticMeshComponent* StaticMeshComponent,读取指针定位到StaticMeshComponent对象,再读取一次指针定位到StaticMeshComponent对象的虚函数表。然后加上0x660的偏移读取SetCollisionEnable虚函数地址。

1
2
3
4
5
6
7
8
function setStaticMeshActorCollisionEnabled(actor_addr,NewType=3){
  
  actor_addr = ptr(actor_addr)
  var f_addr = actor_addr.add(0x220).readPointer().readPointer().add(0x660).readPointer();
  var getActorCollisionEnabled = new NativeFunction(f_addr, 'char', ['pointer','char']);
  let ret = getActorCollisionEnabled(actor_addr.add(0x220).readPointer(),NewType);
  info(ret);
}

碰撞了所有的三个立方体后天上又出现flag的一部分。
第三个是直接找到了SetCollisionEnabled的地址然后构建函数。

1
2
3
4
5
function SetCollisionEnable(actor_addr,dr=3) {
  var f_addr=moduleBase.add(0x933b300);
  let CallFunc=new NativeFunction(f_addr,'void',['pointer','int']);
  CallFunc(ptr(actor_addr).add(0x130).readPointer(),dr);
}

第一个无法实现,第二个和第三个本质上是一样的,可以实现功能。

Section3:黄色球体

这个类的方法直接猜了下和getflag这种字符串有关,直接在sdkdump中搜到了。但是正解我还需要再学习一下。
找到该函数后直接到目标地址。
image.png
dlopen是一个UE4.so中专门的函数。
调用了libplay中的get_last_flag函数。
再次进入后发现一堆异或。
image.png

这段异或尝试后发现是base64的码表。
打算通过动调理清这段加密流程。
在手机端启动idaserver。
然后以该命令以调试状态启动目标app

1
adb shell am start -D -n com.tencent.ace.match2024/com.epicgames.ue4.SplashActivity

可以Attach到进程,但是F9之后没有任何反应,手机显示wait for debug,电脑直接跑飞。
看来动调并不太行,于是转而去读汇编。
image.png

E4C这一段下面的BR会根据一些比较来决定跳转到下面的EA8还是ED8,这个让gpt就可以分析出来。
大致分析EA8是主要加密段,进行一个异或操作,下面的ED8类似RET,进行一个结束处理。(分析不一定准确,因为不会ARM汇编,只能结合AI)

再分析下面的函数,进入后发现EEC
image.png
这个数组异或完后猜测是base64变表,那这个函数大抵就是base64了。
再往下的部分大概就是比较了。
image.png
汇编喂给GPT后分析出大致是24个字节的比较。
这里就能推断出上文其中一个进行异或的数组是密文
异或后:
image.png

image.png
这段数组应该就是作为EA8段异或操作的key数组。
根据这些写出脚本:
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
    char encode[] = { 0x55,0x4d,0x60,0x74,0x38,0x49,0x64,0x50,0x2c,0x7b,0x4f,0x03,0x68,0x36,0x1f,0x9f,0x8e,0x8a,0,0};
    char key[] = { 0x0A, 0x0C, 0x0E, 0x00, 0x51, 0x16, 0x27, 0x38, 0x49, 0x1A, 0x3B, 0x5C, 0x2D, 0x4E, 0x6F, 0xFA,
    0xFC, 0xFE};
    for (int i = 0; i < 18; i++)
    {
        encode[i] ^= key[i ];
        printf("%c", encode[i]);
    }
     
}

拿到最后一部分flag:_Anti_Cheat_Expertimage.png

最终结果

flag:FLAG{8939008_Anti_Cheat_Expert}
在复习的过程中还是学了不少东西的,也多亏了有两位师傅的无私解疑和指导,写的文章还很粗糙,需要时间打磨。
最后在这里贴上frida的脚本供于参考。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
var GWorld_Offset=0x0B32D8A8
var GName_Offset=0x0B171CC0
var GUObjectArray_Offset=0xB1B5F98
var playerName="FirstPersonCharacter_C"
var moduleBase
var GWorld
var GName
var GUObjectArray
 
//Class: UObject
//对象的内部索引,用于唯一标识对象。
 var offset_UObject_InternalIndex = 0xC;
 
 //指向描述对象类的 UClass 对象
 var offset_UObject_ClassPrivate = 0x10;
  
 //对象名称在 FName 表中的索引
 var offset_UObject_FNameIndex = 0x18;
 
 //指向包含该对象的外部对象,表示层次关系。
 var offset_UObject_OuterPrivate = 0x20;
 var UObject={
  getClass: function(obj){
    var classPrivate=ptr(obj).add(offset_UObject_ClassPrivate).readPointer();//读取指针
    //console.log(`classPrivate: ${classPrivate}`);
    return classPrivate;
  },
getNameId: function(obj){
  //console.log(`obj:${obj}`);
  try{
    var nameId=ptr(obj).add(offset_UObject_FNameIndex).readU32();//读取4字节。
    //console.log(`nameId:${nameId}`);
    return nameId;
  }catch(e){
    console.log("error")
    return 0;
  }
},
getName: function(obj) {
  if (this.isValid(obj)){
      return getFNameFromID(this.getNameId(obj));
  } else {
      return "None";
  }
},
getClassName: function(obj) {
  if (this.isValid(obj)) {
      var classPrivate = this.getClass(obj);
      return this.getName(classPrivate);
  } else {
      return "None";
  }
},
isValid: function(obj) {
  var isValid = (ptr(obj) > 0 && this.getNameId(obj) > 0 && this.getClass(obj) > 0);
  // console.log(`isValid: ${isValid}`);
  return isValid;
}
 
 }
 function getFNameFromID(index) {
  // FNamePool相关偏移量和步长
  var FNameStride = 0x2;                   // FNameEntry 的步长,每个FNameEntry占用2字节
  var offset_GName_FNamePool = 0x30;       // GName 到 FNamePool 的偏移量
  var offset_FNamePool_Blocks = 0x10;      // FNamePool 到 Blocks 的偏移量
   
  // FNameEntry相关偏移量和位
  var offset_FNameEntry_Info = 0;          // FNameEntry 到 Info 的偏移量
  var FNameEntry_LenBit = 6;               // FNameEntry 长度位
  var offset_FNameEntry_String = 0x2;      // FNameEntry 到字符串部分的偏移量
 
  // 计算块和偏移量
  var Block = index >> 16;                 // 块索引
  var Offset = index & 65535;              // 块内偏移量
 
  // 获取FNamePool的起始地址
  var FNamePool = GName.add(offset_GName_FNamePool);
  // console.log(`FNamePool: ${FNamePool}`);
  // console.log(`Block: ${Block}`);
   
  // 获取特定块的地址
  var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();
  // console.log(`NamePoolChunk: ${NamePoolChunk}`);
   
  // 计算FNameEntry的地址
  var FNameEntry = NamePoolChunk.add(FNameStride * Offset);
  // console.log(`FNameEntry: ${FNameEntry}`);
   
  try {
      // 读取FNameEntry的Header
      if (offset_FNameEntry_Info !== 0) {
          var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();    
      } else {
          var FNameEntryHeader = FNameEntry.readU16();
      }
  } catch(e) {
      // 捕捉读取异常并返回空字符串
      // console.log(e);
      return "";
  }
  // console.log(`FNameEntryHeader: ${FNameEntryHeader}`);
   
  // 获取字符串地址
  var str_addr = FNameEntry.add(offset_FNameEntry_String);
  // console.log(`str_addr: ${str_addr}`);
   
  // 计算字符串长度和宽度
  var str_length = FNameEntryHeader >> FNameEntry_LenBit; // 计算字符串长度
  var wide = FNameEntryHeader & 1;                       // 判断字符串是否为宽字符
   
  // 如果是宽字符,返回 "widestr"
  if (wide) return "widestr";
 
  // 如果字符串长度合理,读取并返回UTF-8字符串
  if (str_length > 0 && str_length < 250) {
      var str = str_addr.readUtf8String(str_length);
      return str;
  } else {
      return "None"; // 长度不合理,返回 "None"
  }
}
 
//获取对象实例
function getActorAddr(str){
  var player_addr;
  var actorsAddr=getActorsAddr();
  for(var key in actorsAddr){
    if(key==str){
      console.log(actorsAddr[key]);
      player_addr=actorsAddr[key];
    }
  }
  if(player_addr==null)
    {
      console.log("null pointer!");
    }
  return player_addr;
}
function getActorsAddr(){
  var Level_Offset=0x30//偏移
  var Actors_Offset=0x98
  var Level=GWorld.add(Level_Offset).readPointer()//读取GWorld的level指针
  var Actors=Level.add(Actors_Offset).readPointer()//读取Actors的指针
  var Actors_Num=Level.add(Actors_Offset).add(8).readU32()//获取Actor的数量
  var actorsAddr={};//空对象,下面的实现类似字典
  for(var index=0;index<Actors_Num;index++)
    {
      var actor_addr=Actors.add(index*8).readPointer()//读取当前索引处的Actor地址
      var actorName=UObject.getName(actor_addr)//通过地址获取字符串名字
      actorsAddr[actorName]=actor_addr;//以字符串名字对应地址值
      //console.log(`actors[${index}]`,actorName);
    }
    return actorsAddr;
}
function set(moduleName){
  //获取libUE4的基地址
  moduleBase=Module.findBaseAddress(moduleName);
 
  //UE4基地址加上在IDA中获取的GName偏移获取GName地址
  GName=moduleBase.add(GName_Offset);
 
  //同上获取GUObjectArray的地址
  GUObjectArray=moduleBase.add(GUObjectArray_Offset);
 
//读取GWorld的指针
  GWorld =moduleBase.add(GWorld_Offset).readPointer();
 
}
function setPlayerHP(hp=1000000){
  //生命值属性的偏移
  getActorAddr(playerName).add(0x510).writeFloat(hp);//写入
}
 
 
//瞬移////////////////////////////////////
class Vector {//设置向量对象
  constructor(x, y, z) {
      this.x = x;
      this.y = y;
      this.z = z;
  }
  // 将向量转换为字符串
  toString() {
      return `(${this.x}, ${this.y}, ${this.z})`;
  }
}
function dumpVector(addr){
  // dumpAddr('firstPersion_RootComponent',firstPersion_RootComponent_ptr,0x152)
  // 从地址空间中读取三个浮点数
  const values = Memory.readByteArray(addr, 3 * 4);  // 3float共占12个字节
  // 解析浮点数并初始化 Vector 对象
  const vec = new Vector(
      new Float32Array(values, 0, 1)[0],  // 读取第一个浮点数
      new Float32Array(values, 4, 1)[0],  // 读取第二个浮点数
      new Float32Array(values, 8, 1)[0]   // 读取第三个浮点数
  );
  console.log('[+] 坐标:',vec);//打出坐标。
}
 
function getActorLocation(actor_addr){
  GWorld = moduleBase.add(GWorld_Offset).readPointer();
  actor_addr = ptr(actor_addr)
  var buf = Memory.alloc(0x100);
  var f_addr = moduleBase.add(0x965ddf8);
    // 将目标函数地址转换为JavaScript函数
  var getLocationFunc = new NativeFunction(f_addr, 'void', ['pointer','pointer','pointer']);
    
  // 调用目标函数并传递内存地址作为参数
  try{
      getLocationFunc(actor_addr,buf,buf);
      dumpVector(buf);
      return buf;
      //info(ptr(actor_addr).add(0x130).readPointer().add(0x14c).readU8()&32 != 0);
  }
  catch (e){
  }
}
// 965dc3c
function setActorLocation(actor_addr,x,y,z){
  GWorld = moduleBase.add(GWorld_Offset ).readPointer();
  actor_addr = ptr(actor_addr)
  var f_addr = moduleBase.add(0x8C3181C);//加上偏移获取目标函数的偏移
    // 将目标函数地址转换为JavaScript函数
  var setLocationFunc = new NativeFunction(f_addr, 'bool', ['pointer','bool','pointer','bool','float','float','float']);
  // 调用目标函数并传递内存地址作为参数
  setLocationFunc(actor_addr,0,ptr(0),0,x,y,z);
  //dumpVector(buf);
    
}
 
 
////显示
//void SetVisibility(bool bNewVisibility, bool bPropagateToChildren);
function SetVisibility(Component,bNewVisibility,bPropagateToChildren)
{
var pSetVisibility=moduleBase.add(0x8E619BC);
var callSetVisibility=new NativeFunction(pSetVisibility,"void",['pointer','int','int']);
  callSetVisibility(ptr(Component).add(0x130).readPointer(),bNewVisibility,bPropagateToChildren);
}
 
////碰撞函数
function setActorEnableCollision(actor_addr,bNewActorEnableCollision=1){
  var f_addr = moduleBase.add(0x8C21320);
  let CallFunc = new NativeFunction(f_addr, 'void', ['pointer','char']);
  CallFunc(ptr(actor_addr),bNewActorEnableCollision);
}
function setStaticMeshActorCollisionEnabled(actor_addr,NewType=3){
  
  actor_addr = ptr(actor_addr)
  var f_addr = actor_addr.add(0x220).readPointer().readPointer().add(0x660).readPointer();//这段似乎进行了一个读取虚函数表的操作
  var getActorCollisionEnabled = new NativeFunction(f_addr, 'char', ['pointer','char']);
  let ret = getActorCollisionEnabled(actor_addr.add(0x220).readPointer(),NewType);
  console.log("未知函数:",ret);
}
function SetCollisionEnable(actor_addr,dr=3) {
  var f_addr=moduleBase.add(0x933b300);
  let CallFunc=new NativeFunction(f_addr,'void',['pointer','int']);
  CallFunc(ptr(actor_addr).add(0x130).readPointer(),dr);
}
 
 
//获取虚表指针
async function GetActorVrtualAdress() {
  try {
    moduleBase=Module.findBaseAddress("libUE4.so");
      console.log("[+] moduleBase:", moduleBase);
      console.log("[+] GWorld_Offset:", GWorld_Offset);
      GWorld = moduleBase.add(GWorld_Offset).readPointer();
      console.log("[+] GWorld:", GWorld);
      var Level_Offset = 0x30;
      var Actors_Offset = 0x98;
      var Level = GWorld.add(Level_Offset).readPointer();
 
      console.log("[+] Level Address:", Level);
 
      var Actors = Level.add(Actors_Offset).readPointer();
      console.log("[+] Actors Address:", Actors);
      var Actors_Num1 = Level.add(Actors_Offset).add(8).readU32();
      console.log("[+] ActorNum:", ptr(Actors_Num1));
 
      // 使用循环遍历每个 Actor
      for (var index = 0; index <Actors_Num1; index++) {
         try{ console.log("[+] Number:", index);       
          var actor_addr = Actors.add(index * 8).readPointer();
          if (actor_addr.isNull()) {
              console.error(`Error: Actor address at index ${index} is null`);
              continue;
          }
          var ActorClass = UObject.getClassName(actor_addr);
          var vtable_addr = actor_addr.readPointer();
 
          console.log(`[+] Actor Class: ${ActorClass}`);
          console.log(`[+] Actor Address: ${actor_addr}`);
          console.log(`[+] Virtual Address: 0x${(vtable_addr.sub(moduleBase)).toString(16)}`);
      }catch(h){
        console.error("Error occurred:1", h);
      }
    }
  } catch (e) {
      console.error("Error occurred:2", e);
  }
}
(async () => {
  await GetActorVrtualAdress();
})();
 
 
//主要实现作弊的hanshu
function dumpActorInstances(){
  GWorld = moduleBase.add(GWorld_Offset).readPointer();//找到GWorld这个类并储存它的地址作为指针
  var Level_Offset = 0x30
  var Actors_Offset = 0x98
  var Level = GWorld.add(Level_Offset).readPointer()//关卡的指针
  var Actors = Level.add(Actors_Offset).readPointer()//Actor的指针
  var Actors_Num = Level.add(Actors_Offset).add(8).readU32()//Actors的数量
  var actorsInstances = {};
  for(var index =0; index < Actors_Num; index++){
      var actor_addr = Actors.add(index * 8).readPointer();
      var ActorName = UObject.getName(actor_addr)
      actorsInstances[index] = ActorName;
      //console.log(`actors[${index}]:${actor_addr}`,actorName);
     
      if(ActorName.includes("Cube"))
     { //设置为可碰撞
      try{
        console.log('[+] ActorName:', ActorName);
        //setStaticMeshActorCollisionEnabled(actor_addr,1);
        setStaticMeshActorCollisionEnabled(actor_addr,3); 
        //SetCollisionEnable(actor_addr,3);
      }
        catch(gg){}
          //getActorLocation(actor_addr);
    
        var ccc=getActorLocation(actor_addr)
      const values = Memory.readByteArray(ccc, 3 * 4);
      var z=new Float32Array(values, 8, 1)[0];//高度,Section1
      if(z>3000)//高度大于3000则执行
    {             
      try{
          //setActorHidden(actor_addr)
          //setActorCollisionEnabled(actor_addr,1)
          SetVisibility(actor_addr,1,0);
      }
      catch(e){}
    }
  }
}
 
 
set("libUE4.so")
setPlayerHP();
//GetActorVrtualAdress();
var b=getActorAddr(playerName);
//getActorLocation(b);
//getActorLocation(b);
dumpActorInstances();
//(-1569.0198974609375, -415.2847595214844, 268.3680725097656)
//setActorLocation(b,-1500,-400,3000);
 
  //下面这一句代码是指定要Hook的so文件名和要Hook的函数名,函数名就是上面IDA导出表中显示的那个函数

以及我学习中有看的一些文章
不错文章:http://www.yxfzedu.com/article/670
不错文章:https://iosre.com/t/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1%E8%99%9A%E5%B9%BB4ue4%E6%89%8B%E6%B8%B8%E9%80%86%E5%90%91/19808
好文章:http://www.yxfzedu.com/article/10613