首页
社区
课程
招聘
腾讯游戏安全技术竞赛2024初赛 安卓方向复盘
发表于: 2024-5-8 22:23 3213

腾讯游戏安全技术竞赛2024初赛 安卓方向复盘

2024-5-8 22:23
3213

腾讯游戏安全技术竞赛2024初赛 安卓方向复盘

解题情况

比赛前毫无UE4正向和逆向经验,最后只做了section0,瞬移出房间,完全败北。后面抽空在群友帮助下才做完了。

题目信息

信息搜集

分析apk

UE4板子题,静态无混淆,动态随便注。

了解UE4

c++代码。核心就是一系列复杂的继承关系。摆在游戏画面上的就是一系列actor,各个actor的自身属性与彼此交互构成了画面。(简单的自我理解)

确认UE4版本

游戏包名为com.tencent.ace.match2024,AndroidManifest.xml的UE4版本信息被修改成了0.0

用IDA打开apk中的libUE4.so,搜索++UE4,发现++UE4+Release-4.27-CL-18319896,确认为4.27版本,注册epic账号并关联github获取对应版本的源代码辅助后续分析。

获取SDK

ue4dumper64需要GUObject,GNames,GWorld,frida-ue4dump需要GUObject,GNames。

frida-ue4dump可以直接获得GNames,但是获取GUObject出错了。

GNames是0xB171CC0

libUE4.so搜索Max UObject count is invalid. It must be a number that is greater than 0.,用交叉引用找到sub_6FE10CC ,再查sub_6FE10CC的交叉引用,看到sub_6FE4C54。这里给sub_6FE10CC的a1传参是&dword_B1B5F98因此GUObject是0xB1B5F98

同理,在虚幻引擎源代码项目文件夹和libUE4.so中搜索SeamlessTravel FlushLevelStreaming,简单分析可知存放GWorld的偏移是0xB32D8A8

修改frida-ue4dump中的script.js片段,手工指定GUObject的偏移即可获取SDK,里面有各种类的属性名称,结构体偏移和相关方法。

正式解题

核心思路

通过frida主动调用UE4的API函数,调整actor的属性,从而实习一系列外挂效果。

通过遍历全体actor,获取名称和坐标确认关键的actor。

玩家的名称是FirstPersonCharacter_C

section0

玩家被关在房间里,碰任何东西都会血条清空之后重置世界。

思路:瞬移,加血。

瞬移:调用bool K2_SetActorLocation

加血:dump玩家这个actor所在的内存空间。考虑玩家几乎肯定是继承Character.Pawn.Actor.Object,dump到character的虚表更多些,测试时直接dump了0x600的内存空间。同时猜测血量是个float类型,把看着像float的,并且足够靠后的的浮点数都改大,撞墙后发现不重置世界。再次dump后检查内存空间,确认偏移0x510是血量。

section1

题目要求:天上飘着flag{}字符,根据提示,有部分flag被隐藏了。

思路:直接对全体actor尽可能调用void SetVisibility(bool bNewVisibility, bool bPropagateToChildren); // 0x9910794 [Final|Native|BlueprintCallabl]

void SetHiddenInGame(bool NewHidden, bool bPropagateToChildren); // 0x99105d0 [Final|Native|BlueprintCallabl]试过了,没显示flag)

section2

题目要求:让立方体变得不可穿透

思路:根据分析,那个立方体看着就像UE4文档中直接生成的静态网格体(StaticMeshActor.Actor.Object)。偏移0x220有一个关键属性StaticMeshComponent*,这个决定了碰撞效果。

Class: StaticMeshComponent.MeshComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object

调用它基类PrimitiveComponent.SceneComponent.ActorComponent.ObjectSetCollisionEnabled方法,设置可以碰撞后,用玩家去撞即可。撞了后生成第2个立方体,再次设置再次撞。撞了后又生成第3个立方体,再次设置再次撞。之后就没有新的生成了。转视角看天空方向,出现了的新的3d字符。

SetActorEnableCollision试了,对立方体没用,但是可以对墙有用,这样就可以获得穿墙效果了。

对玩家使用的话会导致玩家无法被手机操作移动,但是还是可以K2_SetActorLocation。配合使用的话可以将玩家锁定在高空,看空中的flag可以方便些。

section3

题目要求:最后的flag在黄色小球所在的类中。

黄色小球的名字是actor,那就先看下获取的SDK中继承actor且不再被继承的类。反正是手工看到了MyActor.Actor.Object,定位到

bool getlastflag(); // 0x6a91fec [Final|Native|BlueprintCallabl]

总之最后就是异或和换表base64。

测试代码

混合gpt与frida-ue4dump,非常垃圾。

cheat()瞬移,穿墙,加血

dumpActorInstances()获取所有actor坐标并显形第1段flag

setStaticMeshActorCollisionEnabled()可以让section2的cube,cube2,cube3能够被碰撞。定位的话用名称或坐标。

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
// frida -F -U -l release.js
var GWorld_Ptr_Offset = 0xB32D8A8
var GName_Offset = 0xB171CC0
var GUObjectArray_Offset = 0xB1B5F98
var playerName = "FirstPersonCharacter_C";
var moduleBase;
var GWorld;
var GName;
var GUObjectArray;
 
//Class: UObject
var offset_UObject_InternalIndex = 0xC;
var offset_UObject_ClassPrivate = 0x10;
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();
            // console.log(`nameId: ${nameId}`);
            return nameId;
        } catch(e) {
            info('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
    var offset_GName_FNamePool = 0x30;
    var offset_FNamePool_Blocks = 0x10;
    // FNameEntry
    var offset_FNameEntry_Info = 0;
    var FNameEntry_LenBit = 6;
    var offset_FNameEntry_String = 0x2;
 
    var Block = index >> 16;
    var Offset = index & 65535;
 
    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}`);
    var FNameEntry = NamePoolChunk.add(FNameStride * Offset);
    // console.log(`FNameEntry: ${FNameEntry}`);
    try {
        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;
    if (wide) return "widestr";
 
    if (str_length > 0 && str_length < 250) {
        var str = str_addr.readUtf8String(str_length);
        return str;
    } else {
        return "None";
    }
}
 
function set(moduleName) {
    moduleBase = Module.findBaseAddress(moduleName);
    GName = moduleBase.add(GName_Offset);
    GUObjectArray = moduleBase.add(GUObjectArray_Offset);
}
 
class Vector {
    constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    // 将向量转换为字符串
    toString() {
        return `(${this.x}, ${this.y}, ${this.z})`;
    }
}
 
function getPlayerAddr(){
    var player_addr;
    var actorsAddr = getActorsAddr();
    for(var key in actorsAddr){
        if(key==playerName){
            // info(actorsAddr[key]);
            player_addr = actorsAddr[key];
        }
    }
    return player_addr;
}
function setVector(addr,x,y,z){
    let vecAddr = addr;
    info('vecAddr',vecAddr);
    // 创建一个 Vector 对象
    let vec = new Vector(x,y,z);
    // 指定要写入的地址
    let address = vecAddr;
    // 创建一个包含三个浮点数的 Float32Array
    let floatArray = new Float32Array([vec.x, vec.y, vec.z]);
    // 将 Float32Array 写入指定地址
    Memory.writeByteArray(address, floatArray.buffer);
    console.log("Vector values written to address:", address.toString());
}
 
function dumpActorInstances(){
    GWorld = moduleBase.add(GWorld_Ptr_Offset).readPointer();
    var Level_Offset = 0x30
    var Actors_Offset = 0x98
 
    var Level = GWorld.add(Level_Offset).readPointer()
    var Actors = Level.add(Actors_Offset).readPointer()
    var Actors_Num = Level.add(Actors_Offset).add(8).readU32()
    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;
        info(`actors[${index}]:${actor_addr}`,actorName);
        getActorLocation(actor_addr);
        try{
            //setActorHidden(actor_addr)
            //setActorCollisionEnabled(actor_addr,1)
            setActorVisibility(actor_addr)
        }
        catch(e){
 
        }
    }
}
 
function getActorsAddr(){
    GWorld = moduleBase.add(GWorld_Ptr_Offset).readPointer();
    var Level_Offset = 0x30
    var Actors_Offset = 0x98
 
    var Level = GWorld.add(Level_Offset).readPointer()
    var Actors = Level.add(Actors_Offset).readPointer()
    var Actors_Num = Level.add(Actors_Offset).add(8).readU32()
    var actorsAddr = {};
    for(var index = 0; index < Actors_Num; index++){
        var actor_addr = Actors.add(index * 8).readPointer()
        var actorName = UObject.getName(actor_addr)
        actorsAddr[actorName] = actor_addr;
        // info(`actors[${index}]`,actorName);
    }
    return actorsAddr;
}
 
function setPlayerLocation(x,y,z){
    setActorLocation(getPlayerAddr(),x,y,z);
}
 
function dumpVector(addr){
    // dumpAddr('firstPersion_RootComponent',firstPersion_RootComponent_ptr,0x152)
    // 从地址空间中读取三个浮点数
    const values = Memory.readByteArray(addr, 3 * 4);  // 3个float共占12个字节
    // 解析浮点数并初始化 Vector 对象
    const vec = new Vector(
        new Float32Array(values, 0, 1)[0],  // 读取第一个浮点数
        new Float32Array(values, 4, 1)[0],  // 读取第二个浮点数
        new Float32Array(values, 8, 1)[0]   // 读取第三个浮点数
    );
    info('location',vec);
}
 
function getActorLocation(actor_addr){
    GWorld = moduleBase.add(GWorld_Ptr_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);
        //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_Ptr_Offset).readPointer();
    actor_addr = ptr(actor_addr)
    var buf = Memory.alloc(0x100);
    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);
     
}
 
function getActorVisibility(actor_addr){
    info(ptr(actor_addr).add(0x130).readPointer().add(0x14d).readU8()&4 != 0);
}
 
function dump(addr,len){
    var buf = ptr(addr).readByteArray(len);
    info(buf);
}
 
function setPlayerHP(hp = 1000000){
    getPlayerAddr().add(0x510).writeFloat(hp);
}
 
function setActorHidden(actor_addr,NewHidden=0,bPropagateToChildren=2){
    var f_addr = moduleBase.add(0x8E61C70);
    let setActorHiddenFunc = new NativeFunction(f_addr, 'void', ['pointer','char','char']);
    setActorHiddenFunc(ptr(actor_addr).add(0x130).readPointer(),NewHidden,bPropagateToChildren);
     
}
 
function setActorVisibility(actor_addr,NewHidden=1,bPropagateToChildren=2){
    var f_addr = moduleBase.add(0x8E619BC);
    let setActorHiddenFunc = new NativeFunction(f_addr, 'void', ['pointer','char','char']);
    setActorHiddenFunc(ptr(actor_addr).add(0x130).readPointer(),NewHidden,bPropagateToChildren);
}
 
function setActorCollisionEnabled(actor_addr,bNewActorEnableCollision=1){
    var f_addr = moduleBase.add(0x8C21320);
    let setActorCollisionEnabledFunc = new NativeFunction(f_addr, 'void', ['pointer','char']);
    setActorCollisionEnabledFunc(ptr(actor_addr),bNewActorEnableCollision);
}
 
function getStaticMeshActorCollisionEnabled(actor_addr){
    actor_addr = ptr(actor_addr)
    var f_addr = actor_addr.add(0x220).readPointer().readPointer().add(0x510).readPointer();
    var getActorCollisionEnabled = new NativeFunction(f_addr, 'char', ['pointer']);
    let ret = getActorCollisionEnabled(actor_addr.add(0x220).readPointer());
    info(ret);
}
 
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);
}
 
 
function cheat(){
    setPlayerLocation(-1000, 100, 270)
    setPlayerHP(10000000)
    var actorsAddr = getActorsAddr();
    for(var key in actorsAddr){
        if(key.includes("Wall")){
            setActorCollisionEnabled(actorsAddr[key],0)
        }
    }
 
}
 
 
set('libUE4.so')
// setPlayerLocation(-1000, 100, 270) section0
// setPlayerLocation(-1700, 700, 270) section1
// setPlayerLocation(-1700, 1500, 270) section2
// setPlayerLocation(-1000, 1500, 270) section3
// setPlayerLocation(800, -1800, 3770)
 
function info(x,y){
    if(y !== undefined && y != null){
      console.log(x+' => '+y);
    }
    else{
      console.log(x);
    }
}
    

总结经验

  1. 首先按照这比赛惯例,其实完全可以押题UE4提前做准备的。但是直到比赛开始都完全对UE4的正向和逆向完全没有了解。
  2. 比赛时最开始用ue4dumper64,总之有点问题拿不出SDK,后来换frida-ue4dump才拿出来符号。
  3. 由于某些文章的误导,直接改RelativeRotation,但是这没法瞬移,并且移动后就会变成正常值。为了追踪值是哪里传播来点,上了stackplz去调试,虽然最后确实通过在某个时机hook这个地方的读写操作,但这就是条歪路。
  4. 最大的错误:比赛时完全想象着通过简单读写内存的方式修改游戏逻辑,没有去用UE4的api。

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

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//