2024TencentGameAndroid初赛复现
0x0 引言
通过查看这两位师傅的文章代码学习,进行复现
https://www.52pojie.cn/forum.php?mod=viewthread&tid=1930752
http://www.yxfzedu.com/article/10613
0x1 dump ue4 SDK
搜索ue4+
得到ue4.27版本
https://github.com/hackcatml/frida-ue4dump
下载dump 工具
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
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
0x2 关卡内容
总共4关
0x3 section0_逃出房间
尝试走出房间,会重生回房间,多试几次观察细节发现是生命值被清空了
找到人物血量内存位置 修改血量
SDK搜索FirstPersonCharacter 找到人物属性
对照两份看,发现 float widestr 是生命值 偏移0x510
编写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();
}
成功走出房间
0x4 section1_空中flag
出来抬头可以看见空中有flag{}
获取自己位置
//坐标
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被隐藏了
搜索找到设置隐藏的api
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全部显示
0x5 section2_让立方体变得不可穿透
使物品有碰撞属性
Class: StaticMeshComponent.MeshComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object
调用它基类PrimitiveComponent.SceneComponent.ActorComponent.Object
的SetCollisionEnabled
方法
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);
}
0x6 section3_查找黄色小球的类
SDK中搜索flag字样
发现一个getlastflag函数
调用了libplay.so中的get_last_flag
异或出来发现base表
回到开头,这里应该是密文和一个数组
sub_192c进去
经过计算可能会跳转到ea8 或 ed8 都进入查看
a8是异或
d8是返回
那我们将上面的数据base64变表解密再异或解密尝试
最终得到 _Anti_Cheat_Expert
flag
FLAG{8939008_Anti_Cheat_Expert}