2024TencentGameAndroid初赛复现


2024TencentGameAndroid初赛复现

0x0 引言

通过查看这两位师傅的文章代码学习,进行复现

https://www.52pojie.cn/forum.php?mod=viewthread&tid=1930752

http://www.yxfzedu.com/article/10613

0x1 dump ue4 SDK

搜索ue4+

image-20250118154524831

得到ue4.27版本

https://github.com/hackcatml/frida-ue4dump

下载dump 工具

image-20250118143857478

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

GNames是0xB171CC0

libUE4.so搜索Max UObject count is invalid. It must be a number that is greater than 0.

image-20250118152736459

用交叉引用找到sub_6FE10CC ,再查sub_6FE10CC的交叉引用,看到sub_6FE4C54。这里给sub_6FE10CC的a1传参是&dword_B1B5F98因此GUObject是0xB1B5F98

image-20250118152757295

同理,在虚幻引擎源代码项目文件夹和libUE4.so中搜索SeamlessTravel FlushLevelStreaming

image-20250118154735900

image-20250118154842994

GNames是0xB171CC0
GWorld的是0xB32D8A8
GUObject是0xB1B5F98

修改frida-ue4dump中的script.js片段,手工指定GUObject的偏移即可获取SDK

function findGUObjectArray1(moduleBase) {

        console.log("[*] Using predefined GUObjectArray offset.");

        // 已知的偏移地址
        var offset = ptr(0xB1B5F98); // 直接使用已知的偏移

        // 计算 GUObjectArray 的实际地址
        GUObjectArray = moduleBase.add(offset);
        
        if (GUObjectArray.isNull()) {
            console.log("[!] GUObjectArray is null, please check if the address is correct.");
            return;
        }

        console.log(`[*] Got GUObjectArray at: ${GUObjectArray}`);
    
}

输出内容很多 重定向输出到文本中

frida -U -f com.tencent.ace.match2024 -l .\script.js > sdkdump.txt

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

也可以用ue4dumper来dump

image-20250118164700788

0x2 关卡内容

image-20250118165646863

总共4关

0x3 section0_逃出房间

尝试走出房间,会重生回房间,多试几次观察细节发现是生命值被清空了

image-20250118165758444

找到人物血量内存位置 修改血量

SDK搜索FirstPersonCharacter 找到人物属性

image-20250118165241561

对照两份看,发现 float widestr 是生命值 偏移0x510

image-20250118165257857

编写frida hook脚本

var GName_Offset = 0xB171CC0;
var GWorld_Offset = 0xB32D8A8;
var GUObjectArray_Offset = 0xB1B5F98;
var playerName = "FirstPersonCharacter_C";
var moduleBase;
var GWorld;
var GName;
var GUObjectArray;

// 对象在GUObjectArray中的序号
var offset_UObject_InternalIndex =0xC;
// 对象的类
var offset_UObject_ClassPrivate = 0X10;
// 对象名称在FName表中的索引
var offset_UObject_FNameIndex = 0x18;
// 指向包含该对象的外部对象,表示层次关系
var offset_UObject_OuterPrivate = 0x20;

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 setPlayerHP(hp = 1000000){
    //生命属性偏移
    getActorAddr(playerName).add(0x510).writeFloat(hp);
}


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();
  
  }

成功走出房间

image-20250118191914209

0x4 section1_空中flag

出来抬头可以看见空中有flag{}

image-20250118192445083

获取自己位置

//坐标
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);  // 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]   // 读取第三个浮点数
    );
    console.log('[+] 坐标:',vec);//打出坐标。
  }

//用ue4 api获取坐标
function getActorLocation(actor_addr){

    actor_addr = ptr(actor_addr)
    var buf = Memory.alloc(0x100);
    var f_addr = moduleBase.add(0x965ddf8);//ue4中的api K2_GetActorLocation的偏移
      // 将目标函数地址转换为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){
    }
}
[Pixel 6::com.tencent.ace.match2024 ]-> getActorLocation(getActorAddr(playerName))
0x6e6c4d7120
[+] 坐标: (-1264.872314453125, -347.19818115234375, 268.3692321777344)
"0x71c7474aa0"
function setActorLocation(actor_addr,x,y,z){

    actor_addr = ptr(actor_addr)
    var f_addr = moduleBase.add(0x8C3181C);//加上偏移获取目标函数的偏移 K2_SetActorLocation
      // 将目标函数地址转换为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);
  
}

第一个房间得到提示,天空flag被隐藏了

image-20250118200627992

搜索找到设置隐藏的api

image-20250118193951880

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 dumpActorInstances(){
    GWorld = moduleBase.add(GWorld_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{
            setActorVisibility(actor_addr)
        }
        catch(e){
 
        }
    }
}

遍历全部Actor全部显示

image-20250118200349735

0x5 section2_让立方体变得不可穿透

使物品有碰撞属性

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

调用它基类PrimitiveComponent.SceneComponent.ActorComponent.ObjectSetCollisionEnabled方法

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);
}

image-20250118203507195

0x6 section3_查找黄色小球的类

image-20250118203616054

SDK中搜索flag字样

image-20250118203819938

发现一个getlastflag函数

image-20250118203853896

调用了libplay.so中的get_last_flag

异或出来发现base表

image-20250118204947448

回到开头,这里应该是密文和一个数组

image-20250118205048286

sub_192c进去

image-20250118205720656

经过计算可能会跳转到ea8 或 ed8 都进入查看

image-20250118205804161

a8是异或

image-20250118205815792

d8是返回

那我们将上面的数据base64变表解密再异或解密尝试

最终得到 _Anti_Cheat_Expert

flag

FLAG{8939008_Anti_Cheat_Expert}


文章作者: Blue
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Blue !
评论
  目录