-
-
腾讯游戏安全技术竞赛2024初赛 安卓方向复盘
-
发表于: 2024-5-8 22:23 3178
-
腾讯游戏安全技术竞赛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.Object
的SetCollisionEnabled
方法,设置可以碰撞后,用玩家去撞即可。撞了后生成第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); } } |
总结经验
- 首先按照这比赛惯例,其实完全可以押题UE4提前做准备的。但是直到比赛开始都完全对UE4的正向和逆向完全没有了解。
- 比赛时最开始用ue4dumper64,总之有点问题拿不出SDK,后来换frida-ue4dump才拿出来符号。
- 由于某些文章的误导,直接改RelativeRotation,但是这没法瞬移,并且移动后就会变成正常值。为了追踪值是哪里传播来点,上了stackplz去调试,虽然最后确实通过在某个时机hook这个地方的读写操作,但这就是条歪路。
- 最大的错误:比赛时完全想象着通过简单读写内存的方式修改游戏逻辑,没有去用UE4的api。