#随笔-try to reverse tencent ACE2024
😤5ea1
首先要配置ue4dumper环境
配置NDK的用户path,编译ue4dumper项目
在UE4Dumper-master目录下,在cmd中使用命令ndk-build进行编译
获得如下目录:
F:.
├─.vscode
├─jni
│ ├─ELF
│ └─ELF64
├─libs
│ ├─arm64-v8a
│ └─armeabi-v7a
└─obj
└─local
├─arm64-v8a
│ └─objs
│ └─ue4dumper64
│ └─ELF64
└─armeabi-v7a
└─objs
└─ue4dumper
└─ELF
要的文件在libs中,直接拿
这个文件需要推至安卓机子上运行
进到对应目录,使用指令adb push ue4dumper64 /data/local/tmp/
然后依次打下面的指令开启权限
adb shell
su
cd /data/local/tmp
chmod +x ue4dumper
打下面的指令运行并拿到电脑上
./ue4dumper64 --lib --output /data/local/tmp --package com.tencent.ace.match2024
adb pull /data/local/tmp/libUE4.so .\
UE4dump到手了,可以逆了
ida好慢的逆。
啊啊啊字符串要加utf-16,才能找到
看到是4.27版本
开找GWorld,嘟嘟嘟嘟
翻翻源码
可以看到这个GWorld参数在源码中先被赋值为空指针,然后被赋值为一个载入的地图,在经过这些操作后,代码进行了一个字符串相关操作——SeamlessTravel FlushLevelStreaming
所以找这个GWorld参数的思路就是搜索这个字符串,然后查看字符串操作上方的赋值操作
搜索Seamlesstravel FlushLevelStreaming字符串,然后通过交叉引用定位到GWorld的引用函数
可以看到这个qword_B32D8A8和源码中的GWorld操作相同,位置符合
即GWorld的地址为B32D8A8
接下来找GNames
同样方法,先看源码,UnrealNames.cpp
这次要寻找NamePoolData
直接搜字符串Duplicate
交叉引用过去发现是FNamePool变量池的构造函数,所以继续交叉引用查找FNamePool调用的过程
哎这里被坑了,看到好几个跳转,没敢直接分析,后面发现每一个构造函数的调用都使用了同一个指针,所以直接随便挑一个进入即可
得到地址是B171CC0
最后一个UObjectArray同理
找UObjectArray.cpp源代码
然后在ida中找,这里很坑,需要手动修复字符串至utf16
找到一个初始化函数,继续交叉引用回退
找到地址B1B5F98
./ue4dumper64 --package com.tencent.ace.match2024 --ptrdec --sdku --gname 0x0B171CC0 --guobj 0x0B1B5F98 --output /data/local/tmp --newue+
开梭
锁血
var player_addr = getActorAddr("FirstPersonCharacter_C"); //获取玩家角色地址
if (player_addr != null) {
console.log("[+] 玩家角色地址:", player_addr); //打印玩家角色地址
player_addr = player_addr.add(0x510); //获取玩家角色的坐标地址
player_addr.writeFloat(1000000.0);
return;
}
瞬移
进到ida里面看这个函数,函数末尾调用三参,很明显是xyz坐标,所以实际上要hook的是sub_8C31C98函数,即偏移0x8C31C98
直接hook这个函数
var targetFunctionOffset = 0x8C31C98;
function setLocation(actorPtr, bParam1, dataPtr, bParam2, x, y, z) {
var sub_8C31C98 = new NativeFunction(
moduleBase.add(targetFunctionOffset),
"bool",
["pointer", "int", "pointer", "int", "float", "float", "float"] // 修正参数类型为 int
);
const result = sub_8C31C98(
actorPtr,
bParam1 ? 1 : 0, // 布尔值转换为 1/0
dataPtr,
bParam2 ? 1 : 0,
x,
y,
z
);
console.log(`函数调用结果: ${result}`);
return result;
}
取消碰撞
SDK中有函数SetActorEnableCollision
实际上,SDK中的都是中转函数,所以要通过return的函数获得具体函数
所以要hook函数sub_8C21320
const targetFunctionOffset_Wall = 0x8C21320;
function setwall(pointerArg, intParam) {
var sub_8C21320 = new NativeFunction(
moduleBase.add(targetFunctionOffset_Wall),
"int",
["pointer", "int"]
);
const result = sub_8C21320(
pointerArg,
intParam
);
console.log(`函数调用结果: ${result}`);
return result;
}
老样子,拿到参数类型直接改
section2
高空可见,使用函数SetVisibility
function SetVisibility(actor_addr, bNewVisibility, bPropagateToChildren) {
var f_addr = moduleBase.add(0x8e619bc);
var setVisibility = new NativeFunction(f_addr, "bool", [
"pointer",
"int",
"int",
]);
setVisibility(
actor_addr.readPointer(),
bNewVisibility,
bPropagateToChildren
);
}
function ChangeVisibility() {
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的数量
for (var index = 0; index < Actors_Num; index++) {
var actor_addr = Actors.add(index * 8).readPointer(); //读取当前索引处的Actor地址
var actorName = UObject.getName(actor_addr); //通过地址获取字符串名字
if(actorName.includes("Flag") ){
SetVisibility(actor_addr.add(0x130), 1, 0);
console.log("[+]显示flag:", actorName);
}
}
}
实现方法几乎一样,去把这个函数hook了,唯一不同的就是这次要用到的对象名称相同,而这个搜索全部actor的函数实现是一个字典,导致同样名字的对象只会被放置一次
所以直接抄上面的轮子里的代码就行了
hook完,天空中出现flag
现在来构筑一下地图的示意图
老样子,先拿到获取坐标的函数GetActorLocation
拿到hook直接爆
function PutAllActorLocation() {
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的数量
for (var index = 0; index < Actors_Num; index++) {
var actor_addr = Actors.add(index * 8).readPointer(); //读取当前索引处的Actor地址
var actorName = UObject.getName(actor_addr); //通过地址获取字符串名字
console.log("[+]物体:", actorName);
GetActorLocation(actor_addr);
}
}
梭出下面的坐标:
[+]物体: WorldInfo
[+] 坐标: (0, 0, 0)
[+]物体: LightmassImportanceVolume
[+] 坐标: (138.776123046875, -0.01416015625, 1277.7109375)
[+]物体: TemplateLabel
[+] 坐标: (597.9150390625, -38.27427673339844, 180.01016235351563)
[+]物体: SkySphereBlueprint
[+] 坐标: (60, 0, 3930)
[+]物体: AtmosphericFog
[+] 坐标: (784.916015625, 541.0174560546875, 1024.0997314453125)
[+]物体: SphereReflectionCapture
[+] 坐标: (-192.63723754882813, 94.50370788574219, 519.3599853515625)
[+]物体: NetworkPlayerStart
[+] 坐标: (326.24322509765625, 3.556586980819702, 232.00067138671875)
[+]物体: LightSource
[+] 坐标: (100, -200, 570)
[+]物体: PostProcessVolume
[+] 坐标: (-316.16876220703125, 6.159666061401367, 460)
[+]物体: SkyLight
[+] 坐标: (30.1624755859375, 7.713703155517578, 20.00006103515625)
[+]物体: EditorCube8
[+] 坐标: (863.9987182617187, -1169.60498046875, 295.2381896972656)
[+]物体: EditorCube9
[+] 坐标: (1464.425537109375, -657.312744140625, 245.24537658691406)
[+]物体: EditorCube10
[+] 坐标: (1464.425537109375, -46.78537368774414, 245.245361328125)
[+]物体: EditorCube11
[+] 坐标: (860.8214721679687, -46.78562545776367, 245.23817443847656)
[+]物体: EditorCube12
[+] 坐标: (1307.8231201171875, 714.8047485351563, 245.24351501464844)
[+]物体: EditorCube13
[+] 坐标: (1310.8233642578125, 874.9714965820312, 245.24354553222656)
[+]物体: EditorCube14
[+] 坐标: (1310.8232421875, 790.3173217773437, 395.2434997558594)
[+]物体: EditorCube15
[+] 坐标: (-896.78076171875, 828.9819946289063, 245.21722412109375)
[+]物体: EditorCube16
[+] 坐标: (-1034.3577880859375, 746.9697265625, 245.215576171875)
[+]物体: EditorCube17
[+] 坐标: (-961.6448364257813, 790.3168334960937, 395.21624755859375)
[+]物体: EditorCube18
[+] 坐标: (-1439.838623046875, -811.4143676757813, 245.2107696533203)
[+]物体: None
然后开脚本继续梭
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# 数据
data = [
("WorldInfo", (0, 0, 0)),
("LightmassImportanceVolume", (138.776123046875, -0.01416015625, 1277.7109375)),
("TemplateLabel", (597.9150390625, -38.27427673339844, 180.01016235351563)),
("SkySphereBlueprint", (60, 0, 3930)),
("AtmosphericFog", (784.916015625, 541.0174560546875, 1024.0997314453125)),
("SphereReflectionCapture", (-192.63723754882813, 94.50370788574219, 519.3599853515625)),
("NetworkPlayerStart", (326.24322509765625, 3.556586980819702, 232.00067138671875)),
("LightSource", (100, -200, 570)),
("PostProcessVolume", (-316.16876220703125, 6.159666061401367, 460)),
("SkyLight", (30.1624755859375, 7.713703155517578, 20.00006103515625)),
("EditorCube8", (863.9987182617187, -1169.60498046875, 295.2381896972656)),
("EditorCube9", (1464.425537109375, -657.312744140625, 245.24537658691406)),
("EditorCube10", (1464.425537109375, -46.78537368774414, 245.245361328125)),
("EditorCube11", (860.8214721679687, -46.78562545776367, 245.23817443847656)),
("EditorCube12", (1307.8231201171875, 714.8047485351563, 245.24351501464844)),
("EditorCube13", (1310.8233642578125, 874.9714965820312, 245.24354553222656)),
("EditorCube14", (1310.8232421875, 790.3173217773437, 395.2434997558594)),
("EditorCube15", (-896.78076171875, 828.9819946289063, 245.21722412109375)),
("EditorCube16", (-1034.3577880859375, 746.9697265625, 245.215576171875)),
("EditorCube17", (-961.6448364257813, 790.3168334960937, 395.21624755859375)),
("EditorCube18", (-1439.838623046875, -811.4143676757813, 245.2107696533203))
]
# 提取数据
names = [item[0] for item in data]
x = [item[1][0] for item in data]
y = [item[1][1] for item in data]
z = [item[1][2] for item in data]
# 创建 3D 图形
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 绘制散点图
ax.scatter(x, y, z)
# 添加标签
for i, name in enumerate(names):
ax.text(x[i], y[i], z[i], name)
# 设置坐标轴标签
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
# 显示图形
plt.show()

SetCollisionEnabled设置碰撞

这个函数是一个虚表函数,实际调用不是调用当全部地址的函数,继续观察,这个函数属于PrimitiveComponent这个类,而这个类经过了下面的继承:
Class: InstancedStaticMeshComponent.StaticMeshComponent.MeshComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object
这是要调用的物体对应的类,在这个类中调用了StaticMeshComponent,而StaticMeshComponent是对上面的虚表函数的类的继承,偏移为0x220
所以我们实际上要hook的函数应该是要修改的actor的0x220偏移处对应的函数
function SetCollisionEnabled()
{
var actors = getActorsAddr();
for (var str in actors) {
if(str.includes("Cube") ){
var actor_addr = actors[str];
var f_addr = actor_addr.add(0x220).readPointer().readPointer().add(0x660).readPointer();
var SetCollision = new NativeFunction(f_addr, "bool", [
"pointer",
"char",
]);
SetCollision(
actor_addr.add(0x220).readPointer(), // actor地址
3
);
console.log("[+]开启碰撞:", str);
}
}
}
黄色小球的Actor名为“Actor”


xor + base64,有BR混淆
z队伟大,无需多言
import idautils
import idc
import ida_bytes
import idaapi
import re
from keystone import *
from collections import deque
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
MAX=15
MAX_PATCH=100
def get_funcaddr(name):
for func_start in idautils.Functions():
func_name = idc.get_func_name(func_start)
if func_name == name:
return func_start
return None;
def get_opcode(disasm):
opcode = None
if disasm.find('LT') != -1:
opcode = 'LT'
elif disasm.find('EQ') != -1:
opcode = 'EQ'
elif disasm.find('CC') != -1:
opcode = 'CC'
elif disasm.find('GT') != -1:
opcode = 'GT'
elif disasm.find('NE') != -1:
opcode = 'NE'
elif disasm.find('GE') != -1:
opcode = 'GE'
elif disasm.find('HI') != -1:
opcode = 'HI'
return opcode
def get_functable(ea,reg,funcaddr):
while True:
disasm=idc.GetDisasm(ea)
if disasm == "RET":
return 0
if disasm.find("ADRP")!=-1 and disasm.find(reg)!=-1:
index=disasm.find("off_")
indexend=disasm.find("@PAGE")
if index!=-1:
return int(disasm[index+4:indexend],base=16)
else:
return 0
ea=idaapi.prev_head(ea,funcaddr)
def get_patch(disasm_queue:deque,jmp_reg:str,funcaddr):
patch_queue = deque(maxlen=MAX)
index=0
patch_addr=disasm_queue[0][0]
func_reg=""
for item in disasm_queue:
if item[1].find("ADRP")!=-1:
index=disasm_queue.index(item)
patch_addr=item[0]
break;
for i in range(index,len(disasm_queue)):
if disasm_queue[i][1].find("LDR ")!=-1 and disasm_queue[i][1].find(jmp_reg)!=-1:
pattern = r'\[([^,]+)' #获取存储跳转函数地址的寄存器
match = re.search(pattern, disasm_queue[i][1])
func_reg= match.group(1)
break
functable=get_functable(disasm_queue[len(disasm_queue)-1][0],func_reg,funcaddr)#获取函数表
#调整指令顺序
for i in range(index,len(disasm_queue)):
if disasm_queue[i][1].find("ADRP")!=-1:
continue
elif disasm_queue[i][1].find("CSET")!=-1:
continue
elif disasm_queue[i][1].find("ADD")!=-1 and disasm_queue[i][1].find("PAGEOFF")!=-1:
continue
elif disasm_queue[i][1].find("LDR ")!=-1:
continue
elif disasm_queue[i][1].find("BR")!=-1:
continue
else:
patch_queue.append(disasm_queue[i])
for i in range(index,len(disasm_queue)):
if disasm_queue[i][1].find("ADRP")!=-1:
patch_queue.append((disasm_queue[i][0],"NOP"))
elif disasm_queue[i][1].find("CSET")!=-1:
cond=get_opcode(disasm_queue[i][1])#获取条件码
patch_queue.append((disasm_queue[i][0],"NOP"))
elif disasm_queue[i][1].find("ADD")!=-1 and disasm_queue[i][1].find("PAGEOFF")!=-1:
patch_queue.append((disasm_queue[i][0],"NOP"))
elif disasm_queue[i][1].find("LDR")!=-1 and disasm_queue[i][1].find(jmp_reg)!=-1:
patch_queue.append((disasm_queue[i][0],"NOP"))
elif disasm_queue[i][1].find("BR")!=-1:
jmp1="B."+cond+" "+"(%d)"%(ida_bytes.get_qword(functable+8)-disasm_queue[i][0]+4)
jmp2="B "+"(%d)"%(ida_bytes.get_qword(functable)-disasm_queue[i][0])
patch_queue.append((disasm_queue[i][0]-4,jmp1))
patch_queue.append((disasm_queue[i][0],jmp2))
else:
continue
ea_arrry = set()
length=len(patch_queue)
i=0
#去除重复地址指令
while i<length:
if patch_queue[i][0] not in ea_arrry:
ea_arrry.add(patch_queue[i][0])
else:
for j in range(len(patch_queue)):
if patch_queue[j][0]==patch_queue[i][0] and patch_queue[j][1]=="NOP":
patch_queue.remove(patch_queue[j])
length=length-1
break
i=i+1
return patch_queue,patch_addr
def set_patch(patch_queue,patch_addr):
#patch数据
for item in patch_queue:
print(hex(item[0]),item[1])
print(hex(patch_addr)+" patching...")
for item in patch_queue:
encode, count = ks.asm(item[1])
for i in range(len(encode)):
ida_bytes.patch_byte(patch_addr+i,encode[i])
patch_addr+=len(encode)
print(hex(patch_addr)+" patched")
def anti_br(funcname):
CSET=False
LDR=False
disasm_queue = deque(maxlen=MAX)
funcaddr=get_funcaddr(funcname)
ea=funcaddr
ret_ea=0
jmp_reg=""
patch_queue = deque(maxlen=MAX_PATCH)
while True:
disasm=idc.GetDisasm(ea)
disasm = disasm.split(';')[0].strip() #去除注释
disasm_queue.append((ea,disasm))
if disasm=="RET":
ret_ea=idc.next_head(ea)
break;
#满足条件CSET/LDR/BR,为br混淆的条件跳转
elif disasm.find("BR")!=-1:
#获取存储跳转地址的寄存器
jmp_reg=disasm[disasm.find("X"):len(disasm)]
for item in disasm_queue:
if item[1].find("CSET")!=-1:
CSET=True
elif item[1].find("LDR ")!=-1 and item[1].find(jmp_reg)!=-1:
LDR=True
if CSET and LDR:
#记录所有需要patch的指令块与地址
patch_queue.append(get_patch(disasm_queue,jmp_reg,funcaddr))
CSET=False
LDR=False
disasm_queue.clear()
ea = idc.next_head(ea)
patch_queue.reverse()
#从高地址像低地址patch
for patch in patch_queue:
set_patch(patch[0],patch[1])
#定义函数
idaapi.del_items(funcaddr, idaapi.DELIT_SIMPLE, ret_ea - funcaddr)
idaapi.create_insn(funcaddr)
idaapi.add_func(funcaddr, ret_ea)
print(funcname+" done")
if __name__ == "__main__":
anti_br("get_last_flag")
anti_br("sub_EEC")
anti_br("sub_E4C")
# 定义新的 xmmword_3E50
xmmword_3E50 = bytes([
0x31, 0xBB, 0x87, 0x09, 0xF8, 0xE4, 0xE7, 0x90, 0xF4, 0x99,
0xCC, 0x69, 0x5F, 0x04, 0x46, 0x89, 0x75, 0x5C, 0xF0, 0xCC,
0xBD, 0x2E, 0xA3, 0x68, 0x0F, 0xD6, 0xDC, 0x4E, 0x7A, 0x4D,
0x63, 0xD0, 0x60, 0x24, 0x2D, 0x75, 0x3C, 0x16, 0xFC, 0x41,
0x1D, 0x6E, 0xDF, 0xA4, 0x0D, 0xD3, 0xA6, 0x9D, 0xB9, 0x58,
0x88, 0xB2, 0xBB, 0x8D, 0x9F, 0x25, 0x1B, 0x11, 0xB0, 0x41,
0x2F, 0xCD, 0x10, 0xB6, 0x84
])
# 定义新的 word_3E60
word_3E60 = 0x9F8D
# 定义新的 byte3e30
byte3e30 = bytes([
0x9D, 0x43, 0xB0, 0xD7, 0xD4, 0x53, 0x1C, 0x7D, 0xB4, 0xB6,
0xF6, 0x37, 0x23, 0x66, 0xDB, 0x92, 0x19, 0xDF, 0xCF, 0xF9,
0x9A, 0x92, 0xF2, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xD8, 0x98, 0x54, 0xC1, 0x64, 0x93, 0x56, 0x84,
0x38, 0x4F, 0x60, 0xBB, 0xA9, 0xA4, 0xCC, 0x88, 0x8D, 0x9F
])
# 假设的加密数据(需要替换为实际数据)
encrypted_xmm = xmmword_3E50
encrypted_byte30 = byte3e30
# byte_3E30的异或密钥(前24字节)
xor_keys_byte30 = [
0xC8, 0x17, 0x81, 0xB1, 0xB7, 0x63, 0x7B, 0x34, 0xED, 0xF2,
0xB7, 0x45, 0x47, 0x1C, 0xE3, 0xA2, 0x43, 0xEF, 0x97, 0x9C,
0xF7, 0xA6, 0xC4, 0x76, 0xD2, 0x94, 0x5A, 0xC1, 0x35, 0x85,
0x71, 0xBC, 0x71, 0x55, 0x5B, 0xE7, 0x84, 0xEA, 0xA3, 0x72,
0x71, 0x61
]
xor_key_base = [
0x70, 0xF8, 0xC2, 0x39, 0xBA, 0xA0, 0xA1, 0xD7, 0xBC, 0xD0,
0x86, 0x22, 0x13, 0x49, 8, 0xC6, 0x25, 0xD, 0xA2, 0x9F,
0xE9, 0x7B, 0xF5, 0x3F, 0x57, 0x8F, 0x86, 0x2F, 0x18, 0x2E,
7, 0xB5, 6, 0x43, 0x45, 0x1C, 0x56, 0x7D, 0x90, 0x2C,
0x73, 1, 0xAF, 0xD5, 0x7F, 0xA0, 0xD2, 0xE8, 0xCF, 0x2F,
0xF0, 0xCB, 0xC1, 0xBC, 0xAD, 0x16, 0x2F, 0x24, 0x86, 0x76,
0x17, 0xF4, 0x3B, 0x99, 0x84
]
# 解密xmm部分
decrypted_xmm = bytes([byte3e30[i+32] ^ xor_keys_byte30[i+24] for i in range(0, 17)])
# 解密byte_3E30部分
decrypted_byte30 = bytes([byte3e30[i] ^ xor_keys_byte30[i] for i in range(0,24)])
decrypted_x = bytes([xmmword_3E50[i] ^ xor_key_base[i] for i in range(0, 64)])
for i in range(0, 17):
print(hex(decrypted_xmm[i]),end=' ')
print()
for i in range(0, 24):
print(chr(decrypted_byte30[i]),end='')
print()
for i in range(0, 64):
print(chr(decrypted_x[i]),end='')
得到
0xa 0xc 0xe 0x0 0x51 0x16 0x27 0x38 0x49 0x1a 0x3b 0x5c 0x2d 0x4e 0x6f 0xfa 0xfc 0xfe
UT1fc0gIYDArdz80Z0Xem46J
ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/
丢赛博厨子能解
拼起来得到flag
flag{8939008_Anti_Cheat_Expert}