#随笔-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,才能找到
1
看到是4.27版本


开找GWorld,嘟嘟嘟嘟
3
翻翻源码
5
4
6
可以看到这个GWorld参数在源码中先被赋值为空指针,然后被赋值为一个载入的地图,在经过这些操作后,代码进行了一个字符串相关操作——SeamlessTravel FlushLevelStreaming

所以找这个GWorld参数的思路就是搜索这个字符串,然后查看字符串操作上方的赋值操作

搜索Seamlesstravel FlushLevelStreaming字符串,然后通过交叉引用定位到GWorld的引用函数
2
7
8
可以看到这个qword_B32D8A8和源码中的GWorld操作相同,位置符合
即GWorld的地址为B32D8A8


接下来找GNames

同样方法,先看源码,UnrealNames.cpp

这次要寻找NamePoolData
9
10
直接搜字符串Duplicate
11
13
交叉引用过去发现是FNamePool变量池的构造函数,所以继续交叉引用查找FNamePool调用的过程
12
哎这里被坑了,看到好几个跳转,没敢直接分析,后面发现每一个构造函数的调用都使用了同一个指针,所以直接随便挑一个进入即可
14

得到地址是B171CC0


最后一个UObjectArray同理
找UObjectArray.cpp源代码
15
然后在ida中找,这里很坑,需要手动修复字符串至utf16
16
17
找到一个初始化函数,继续交叉引用回退
18
19
找到地址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;
        }

瞬移

22
进到ida里面看这个函数,函数末尾调用三参,很明显是xyz坐标,所以实际上要hook的是sub_8C31C98函数,即偏移0x8C31C98
23
直接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
24
实际上,SDK中的都是中转函数,所以要通过return的函数获得具体函数
25
所以要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
26

  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
27
拿到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()
28

SetCollisionEnabled设置碰撞

29

这个函数是一个虚表函数,实际调用不是调用当全部地址的函数,继续观察,这个函数属于PrimitiveComponent这个类,而这个类经过了下面的继承:
Class: InstancedStaticMeshComponent.StaticMeshComponent.MeshComponent.PrimitiveComponent.SceneComponent.ActorComponent.Object
30

这是要调用的物体对应的类,在这个类中调用了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”

32
34

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+/

丢赛博厨子能解
33


拼起来得到flag
flag{8939008_Anti_Cheat_Expert}