0x0 逆向前的准备

(本文中引用的代码基于UE4.27)

笔者是第一次接触虚幻引擎相关的逆向,所以这里先速览一下基础知识

先快速看一眼UE5的文档,核心的开发模型应该变化不会太大。

UE5文档速览

直接看虚幻引擎术语。这里摘要自认为重要的地方

虚幻引擎术语

对象

Object 是虚幻引擎中最基本的类。虚幻引擎中的几乎所有功能都继承自object(或使用其中的部分功能)。

在C++中,UObject 是所有object的基类,可以实施多种功能,例如垃圾回收、用于将变量提供给虚幻编辑器的元数据(UProperty)支持以及用于加载和保存的序列化。

Actor

Actor 是可以放到关卡中的任何object,例如摄像机、静态网格体或玩家出生点位置。

在C++中,AActor 是所有Actor的基类。

Pawn

Pawn 是Actor的子类,作为游戏内的形象或人像(例如游戏中的角色)。玩家或游戏的AI可以控制Pawn,将其作为非玩家角色(NPC)。

角色

角色(Character) 是计划用作玩家角色的Pawn Actor的子类。角色子类包括碰撞设置、双足运动的输入绑定以及用于玩家控制动作的其他代码。

玩家状态

玩家状态(Player State) 是游戏参与者在游戏中的状态,例如人类玩家或模拟玩家的机器人。非玩家AI作为游戏世界的一部分而存在,没有玩家状态。

玩家状态可能包含的玩家信息示例包括:

  • 名称
  • 当前级别
  • 血量
  • 得分

关联的C++类是 PlayerState

体积

体积(Volumes) 是带有边界的3D空间,根据连接到体积的效果,具有不同的使用方法。例如:

  • 阻挡体积(Blocking Volumes) 是可见的,用于阻止Actor通过它们。
  • 施加伤害体积(Pain Causing Volume) 对与其重叠的任何Actor造成持续伤害。
  • 触发器体积(Trigger Volumes) 的编程方式为,在Actor进入或退出体积时触发事件

Actor的文档

Actor

玩家出生点

玩家出生点(Player Start) 是放置在关卡中的一种Actor,用于指定在玩家开始关卡时,玩家角色在何处生成。

阻挡体积

顾名思义,阻挡体积(Blocking Volumes) 用于防止玩家穿过。例如,你可以使用阻挡体积不让玩家从游戏世界的边缘掉落。

以及命名约定

命名约定

  • 类型名称以额外的大写字母为前缀,以区别于变量名称。例如,FSkin 是类型名称,SkinFSkin 类型的实例。
  • 模板类以 T 为前缀。
  • UObject 继承的类以 U 为前缀。
  • AActor 继承的类以 A 为前缀。
  • SWidget 继承的类以 S 为前缀。
  • 作为抽象接口的类以 I 为前缀。
  • 枚举以 E 为前缀。
  • 布尔变量必须以 b 为前缀。
  • 大多数其他类都以 F 为前缀

也就是说游戏里所有的实体都是一个actor。想实现修改首先需要找到对应的actor。那么该如何找到呢?

UE4内部实现

(以下基于UE4.27)

先编译一下UE4的源码,方便后续分析:https://dev.epicgames.com/documentation/zh-cn/unreal-engine/building-unreal-engine-from-source?application_version=4.27

官方文档中给出了将源码导入到visual studio的方法。导入后可以更直观的分析类之间的关系和计算类成员变量的偏移。注意要把平台设置到android

这里需要先了解一点虚幻引擎的内部实现。UE4的大体框架如下:

image-20250507200957121

(图片来源:https://renyili.org/post/game_cheat2/)

其中:

  • UGameInstance类,对应UE中最顶层的Game概念,在游戏创建时产生,直到游戏实例关闭时才被销毁,它通过WorldContext(图中未表现)管理着游戏中的多个世界(World)。一般来说,全局唯一。
  • UWorld类,对应引擎中的世界(World)概念,它包含了游戏中的所有关卡(Level)。它可以处理关卡流送,还能生成(创建)动态Actor。
  • ULevel类,对应引擎中的**关卡(Level)**概念,它是用户定义的游戏区域。关卡包含了玩家能看到的所有内容,例如几何体、Pawn和Actor。
  • AActor类,对应Actor概念,所有可以放入关卡的对象都是 Actor,无论是实体还是非实体,比如摄像机、静态网格体。
  • USceneComponent类,AActor本身不包含位置信息,UE将其封装到了UScaneComponent中,作为AActorRootComponent成员。
  • APawn类,对应Pawn概念,它是Actor的子类,可以与玩家发生交互的称之为Pawn,它可以由玩家操控,也可以由游戏AI控制并以非玩家角色(NPC)的形式存在于游戏中。
  • AShooterCharacter类,该类是ShooterGame游戏中的类,非UE引擎中定义的类。该类对应**角色(Character)**概念,它是ACharacter的子类(图中未展示),而ACharacter又是APawn的子类。ACharacter旨在表示所有的人形的带骨骼的Pawn。
  • APlayerController类,对应**玩家控制器(Player Controller)**概念,它会获取游戏中玩家的输入信息,然后转换为交互效果,每个游戏中至少有一个玩家控制器。玩家控制器通常会控制一个Pawn或Character,将其作为玩家在游戏中的化身。
  • APlayerState类,存储Player的一些状态信息。

想要做修改,需要先找到对应的Actor。可以通过UWorld→ULevel PersistentLevel→TArray<AActor*> Actors来遍历所有Actor。UE4中实际是用一个全局变量UWorldProxy GWorld; 来指向当前World。其内部结构:

1
2
3
4
5
6
class UWorldProxy
{
...
private:
UWorld* World;
}

也就是说,逆向过程中只要确定了GWorld的地址就可以找到所有Actor 。只需要再计算一下偏移:

首先直接*GWorld,获取到UWorld 对象

UWorld简化结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UWorld
{
...
private:
Level* PersistentLevel;//[Offset: 0x30, Size: 0x8]
NetDriver* NetDriver;//[Offset: 0x38, Size: 0x8]
LineBatchComponent* LineBatcher;//[Offset: 0x40, Size: 0x8]
LineBatchComponent* PersistentLineBatcher;//[Offset: 0x48, Size: 0x8]
LineBatchComponent* ForegroundLineBatcher;//[Offset: 0x50, Size: 0x8]
GameNetworkManager* NetworkManager;//[Offset: 0x58, Size: 0x8]
PhysicsCollisionHandler* PhysicsCollisionHandler;//[Offset: 0x60, Size: 0x8]
...
}

那么*(UWorld+0x30)即可获取到ULevel* PersistentLevel

ULevel实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ULevel : public UObject, public IInterface_AssetUserData, public ITextureStreamingContainer, public IEditorPathObjectInterface
{
GENERATED_BODY()

public:

FURL URL;
TArray<TObjectPtr<AActor>> Actors; //0x98
TArray<TObjectPtr<AActor>> ActorsForGC;
ENGINE_API bool TryAddActorToList(AActor* InActor, bool bAddUnique);
...
}


template<typename InElementType, typename InAllocatorType>
class TArray
{
...
protected:
ElementAllocatorType AllocatorInstance;
SizeType ArrayNum;
SizeType ArrayMax;
...
}

那么到这里我们可以通过ULevel+0x98来获取Actors。即**Actors = *(*GWorld+0x30)+0x98**

获取到所有Actor后如何判断哪个是我们想要的呢?可以通过FName来判断。

FNameUObject的成员变量,用来保存tag的逻辑名称。可以通过观察actor的逻辑名称来判断是不是我们想控制的actor

UObject的成员变量如下:

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
class UObject : public UObjectBaseUtility
{
enum class ENetFields_Private
{
NETFIELD_REP_START = 0,
NETFIELD_REP_END = -1
};

struct FAssetRegistryTag
{
/** Enum specifying the type of this tag */
enum ETagType
{
/** This tag should not be shown in the UI */
TT_Hidden,
/** This tag should be shown, and sorted alphabetically in the UI */
TT_Alphabetical,
/** This tag should be shown, and is a number */
TT_Numerical,
/** This tag should be shown, and is an "x" delimited list of dimensions */
TT_Dimensional,
/** This tag should be shown, and is a timestamp formatted via FDateTime::ToString */
TT_Chronological,
};

/** Flags controlling how this tag should be shown in the UI */
enum ETagDisplay
{
/** No special display */
TD_None = 0,
/** For TT_Chronological, include the date */
TD_Date = 1<<0,
/** For TT_Chronological, include the time */
TD_Time = 1<<1,
/** For TT_Chronological, specifies that the timestamp should be displayed using the invariant timezone (typically for timestamps that are already in local time) */
TD_InvariantTz = 1<<2,
/** For TT_Numerical, specifies that the number is a value in bytes that should be displayed using FText::AsMemory */
TD_Memory = 1<<3,
};

/** Logical name of this tag */
FName Name; //0x30

/** Value string for this tag, may represent any data type */
FString Value;

/** Broad description of kind of data represented in Value */
ETagType Type = TT_Alphabetical;

/** Flags describing more detail for displaying in the UI */
uint32 DisplayFlags = TD_None;
};

}

UObject是几乎所有其他类的基类。所以在其他类中,FName的偏移统一是0x30

实际上FName并不保存字符串,只是保存了字符串在内存池中的位置:

1
2
3
4
5
6
7
8
9
10
11
12
class FName
{
...
private:
/** Index into the Names array (used to find String portion of the string/number pair used for comparison) */
FNameEntryId ComparisonIndex; //0x0
/** Number portion of the string/number pair (stored internally as 1 more than actual, so zero'd memory will be the default, no-instance case) */
uint32 Number; //0x4

/** Index into the Names array (used to find String portion of the string/number pair used for display) */
FNameEntryId DisplayIndex; //0x8
}

实际的字符串内容由FNameEntry来保存

1
2
3
4
5
6
7
8
9
10
11
12
struct FNameEntry
{
private:
FNameEntryHeader Header;
union
{
ANSICHAR AnsiName[NAME_SIZE]; //NAME_SIZE = 1024
WIDECHAR WideName[NAME_SIZE];
FNumberedData NumberedName;
};
...
}

所有的FNameEntry的指针全都保存在FNameEntryAllocator 管理的Blocks中:

1
2
3
4
5
6
7
8
9
10
11
12
class FNameEntryAllocator
{
public:
enum { Stride = alignof(FNameEntry) };
enum { BlockSizeBytes = Stride * FNameBlockOffsets };

private:
mutable FRWLock Lock; //android下是pthread_rwlock_t
uint32 CurrentBlock = 0; //0x38 当前块号
uint32 CurrentByteCursor = 0; //0x3C当前块内偏移
uint8* Blocks[FNameMaxBlocks] = {}; //0x40 FNameMaxBlocks=8192,最大内存块数。每个内存块中有若干FNameEntry指针
}

这里FRWLockandroid下实际是pthread_rwlock_t ,32位下是0x28,64位下是0x38

FNameEntryAllocator封装在FNamePool中。实际上是通过一个全局变量FNamePool GName 来管理。GName保存在alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];。也就是说,找到GWorld后可以找到所有Actor以及ActorFName,找到GName后就能找到所有的FNameEntry

1
2
3
4
5
6
7
8
9
10
11
class FNamePool
{
private:
enum { MaxENames = 512 };

FNameEntryAllocator Entries; //0x0

alignas(PLATFORM_CACHE_LINE_SIZE) FNameEntryId ENameToEntry[(uint32)EName::MaxHardcodedNameIndex] = {};
uint32 LargestEnameUnstableId;
TMap<FNameEntryId, EName, TInlineSetAllocator<MaxENames>> EntryToEName;
};

那么如何将FNameFNameEntry一一对应呢?观察FName中的方法GetComparisonNameEntry

1
2
3
4
5
//FName通过该方法来获取FNameEntry
const FNameEntry* FName::GetComparisonNameEntry() const
{
return ResolveEntryRecursive(GetComparisonIndexInternal());
}

GetComparisonNameEntry调用的GetComparisonIndexInternal直接返回FName的成员变量FNameEntryId ComparisonIndex。其中FNameEntryId 的成员变量为uint32 Value;

1
2
3
4
5
6
7
8
9
10
11
12
FORCEINLINE FNameEntryId GetComparisonIndexInternal() const
{
return ComparisonIndex;
}

struct FNameEntryId
{
...
private:
uint32 Value;
...
};

继续跟进ResolveEntryRecursive

1
2
3
4
5
const FNameEntry* FName::ResolveEntryRecursive(FNameEntryId LookupId)
{
const FNameEntry* Entry = ResolveEntry(LookupId);
return Entry;
}

继续跟进ResolveEntry。这里它调用Resolve的过程做了一次类型转换,将FNameEntryId转换成了FNameEntryHandle。将uint32 Value 的高16位和低16位分别赋给了BlockOffset ,然后根据这两个值从Blocks中获取到对应的FNameEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const FNameEntry* FName::ResolveEntry(FNameEntryId LookupId)
{
return &GetNamePool().Resolve(LookupId);
}

FNameEntryHandle(FNameEntryId Id)
: Block(Id.ToUnstableInt() >> FNameBlockOffsetBits) //FNameBlockOffsetBits = 16;
, Offset(Id.ToUnstableInt() & (FNameBlockOffsets - 1)) //FNameBlockOffsets = 1 << FNameBlockOffsetBits;
{
}


FNameEntry& Resolve(FNameEntryHandle Handle) const
{
// Lock not needed
return *reinterpret_cast<FNameEntry*>(Blocks[Handle.Block] + Stride * Handle.Offset); //Stride = alignof(FNameEntry)
}

也就是说,当我们获取到了ActorFName后,就可以根据它的ComparisonIndex来找到相应的FNameEntry来找到对应的字符串。

0x1 开始逆向

确定UE4版本,定位GUObjectArray GWorld GName

ida打开libUE4,搜索可以看到版本是4.27

GUObjectArray

接下来定位GUObjectArray 。在源码中可以发现以下模式:

image-20250507201039205

跟踪调用发现:

image-20250507201051746

所以在ida中搜索到Max UObject count is invalid. It must be a number that is greater than 0. 所在的函数的第一个参数就是GUObjectArray 。最终偏移是0xB1B5F98

GName

ida搜索ByteProperty ,查找引用可以找到GName的初始化函数

image-20250507201101765

查找引用。调用这个函数传的参数就是GName。偏移是0xB171CC0

image-20250507201111069

GWorld

观察world.cpp源码,可以发现以下模式

image-20250507201122230

image-20250507201127531

image-20250507201135969

ida搜索SeamlessTravel FlushLevelStreaming,找到

image-20250507201144385

往上翻

image-20250507201152063

再往上翻

image-20250507201158821

所以GWorld的地址是0xB32D8A8

有了这三个的地址就可以确定到需要的actor了。接下来用ue4dumper来dump出sdk方便后续操作。

下载ue4dumper上传到手机上:https://github.com/kp7742/UE4Dumper/tree/master

1
2
3
4
5
6
7
8
# dump sdk:
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --sdkw --gworld 0xB32D8A8 --gname 0xB171CC0 --output /data/local/tmp/dumpSDK

# dump objs:
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --objs --gname 0xB171CC0 --guobj 0xB1B5F98 --output /data/local/tmp/dumpSDK

# dump actors
./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --actors --gworld 0xB32D8A8 --gname 0xB171CC0 > /data/local/tmp/dumpSDK/actors.txt

0x2 解题

section0

先把偏移和找actor相关的方法写了

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
class UE4Reverse {
constructor(GNameOffset, GWorldOffset) {
this.GNameOffset = GNameOffset;
this.GWorldOffset = GWorldOffset;
this.moduleName = "libUE4.so";
this.moduleBase = Module.findBaseAddress(this.moduleName);
if (!this.moduleBase) {
console.error(`[!] 模块 ${this.moduleName} 未加载`);
return;
}

this.GWorld = this.moduleBase.add(GWorldOffset).readPointer();
console.log("moduleBase: " + this.moduleBase.toString(16));
console.log("GWorld Offset: " + GWorldOffset.toString(16));
console.log("GWorld: " + this.GWorld.toString(16));
this.GName = this.moduleBase.add(GNameOffset);
this.FNameEntryAllocator = this.GName.add(0x30);
this.Blocks = this.FNameEntryAllocator.add(0x10);

this.GLevel = this.GWorld.add(0x30).readPointer(); //UWorld to Level
this.Actors = this.GLevel.add(0x98).readPointer(); //ULevel -> Actors
this.ActorNum = this.GLevel.add(0x98).add(0x8).readU32(); //ULevel -> Num
}

getNameById(ComparisonIndex) {
var BlockId = ComparisonIndex >> 16;
var Offset = ComparisonIndex & 65535;

var currentBlock = this.Blocks.add(8 * BlockId).readPointer();
var FNameEntry = currentBlock.add(2 * Offset);

var FNameEntryHeader = FNameEntry.readU16();
var str_length = FNameEntryHeader >> 6; // 计算字符串长度
var wide = FNameEntryHeader & 1;
var str_addr = FNameEntry.add(0x2);
if (wide) {
return "wide_string";
}
if (str_length > 0 && str_length < 250) {
var str = str_addr.readUtf8String(str_length);
return str;
0xadf07c0;
} else {
return "None"; // 长度不合理,返回 "None"
}
}

getActorByName(actorName) {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();

var ComparisonIndex = Actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
if (Name == actorName) {
console.log("return Actor " + actorName);
return Actor;
}
}
console.log("can't find Actor:" + actorName);
return "None";
}

getNameByActor(actor) {
var ComparisonIndex = actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
return Name;
}

printAllActorName() {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();

var ComparisonIndex = Actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
console.log(`[!] actor ${i} Name: ${Name}`);
}
}

forAllActor(func) {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();
func(Actor);
}
}
}

首先是出门。碰到墙就会重置到原点。推测可能是撞墙后扣血了?

这里想到的方法:

  • 改扣血函数
  • 改血量
  • 改重生地点
  • 改碰撞检测

这里主要尝试了改扣血函数和改血量。

改血量:

观察dump出的sdk中,FirstPersonCharacter_C存在变量float 生命值;//[Offset: 0x510, Size: 0x4]。用frida直接将其改成一个比较大的数字即可。

扣血函数:

观察dump出的sdk可以看到FirstPersonCharacter_C有一个方法void ReceiveHit(...);// 0x5e93094

跟进可以看到它调用了虚表中偏移为0x8b0的函数

image-20250527200435259

用frida来dump出虚表的地址是0xa86e1f0函数的地址是0x5e843d4

1
2
3
4
5
const ue4Reverse = new UE4Reverse(0xb171cc0, 0xb32d8a8);
const user = ue4Reverse.getActorByName("FirstPersonCharacter_C");
const table = user.readPointer()
console.log("table addr:" + (table - ue4Reverse.moduleBase).toString(16));
console.log("receive hit addr:" + (table.add(0x8b0).readPointer() - ue4Reverse.moduleBase).toString(16));

跟进可以看到

image-20250527201221157

image-20250527201337837

把扣血改掉即可。

section1

关于瞬移(主要用于飞到天上看flag):

  • 尝试调用AddInputVector,失败
  • 尝试调用CharacterMovementComponent.addForce 失败
  • 尝试调用Actor.K2_SetActorLocation,成功

测试过程中推测K2_SetActorLocation本质上是将角色平移到对应的坐标。如果两个坐标的连线上有障碍物的话会被拦截。

隐藏的flag:

  • 对所有actorrootComponent调用SetVisibility(1,1),成功
  • 疑问:再次调用SetVisibility(0,0),只是变成了黑色,物块依旧可见

得到第一段flag:8939

section2

让物块变得不可穿透:

主要试了以下几种方案:

  • 对所有actor调用actor -> setActorEnableCollision 没用

  • 对所有actorrootComponent调用PrimitiveComponent -> setSimulatePhysics 。开启后人动不了了。关闭后会出现很奇怪的问题。

  • 对所有actorrootComponent调用PrimitiveComponent -> SetCollisionEnabled 开启后人动不了了。关闭后出现了奇怪的问题。

  • 对所有名字里带cubeactor调用actor -> setActorEnableCollisionPrimitiveComponent -> setSimulatePhysics 也不行

  • 对所有名字里带cubeactor调用PrimitiveComponent -> SetCollisionEnabled,成功。撞击三个箱子后天上出现flag

得到第二段flag:008

section3

提示是藏在小球所在的类里面。

那么第一个问题是如何找到小球所在的类。我这里用的方法是站在小球前面,然后计算了所有actor和我的距离。离我最近的actor就是小球。

最终定位小球的名字actor,对应的类是MyActor

sdk里找到:

1
2
3
Class: MyActor.Actor.Object
bool getlastflag();// 0x6a91fec

跟进看到这里调用了libplay.so中的get_last_flag方法。

image-20250402213945585

继续跟进,可以看到这里用了br混淆,导致ida没法正确识别控制流。

image-20250527163119996

其实这段代码不长,直接把汇编扔给deepseek就能还原,但本着学习的态度尝试一下去混淆。

去混淆思路:

先unidbg trace一下,观察一下模式:

1
2
3
4
5
6
7
8
[libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5
[libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003
[libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1
[libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80
[libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38
[libplay.so 0x013cc] [105a6bf8] 0x120013cc: "ldr x16, [x16, w11, uxtw #3]" x16=0x12003d80 w11=0x1 => x16=0x12001354
[libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800
[libplay.so 0x013d4] [00021fd6] 0x120013d4: "br x16" x16=0x12001354
1
2
3
[libplay.so 0x01438] [ea279f1a] 0x12001438: "cset w10, lo" nzcv: N=1, Z=0, C=0, V=0 => w10=0x1
[libplay.so 0x0143c] [2a596af8] 0x1200143c: "ldr x10, [x9, w10, uxtw #3]" x9=0x12003d60 w10=0x1 => x10=0x12001314
[libplay.so 0x01440] [40011fd6] 0x12001440: "br x10" x10=0x12001314

主要就是这一种模式:

1
2
3
4
5
cset reg0, flag
...
ldr reg1, xxx
...
br reg1

这里的思路是将ldr向下挪动,移动到br上方,变成

1
2
3
4
5
6
cset reg0, flag
ins1
...
...
ldr reg1, xxx
br reg1

再把ldr替换成b.flag addr,将br替换成br addr,即变成

1
2
3
4
5
6
cset reg0, flag
ins1
...
...
b.flag addr1
br addr2

这里使用unidbg来去混淆。具体思路是,通过一个长度为10的队列来保存最近执行的十条指令和上下文。

考虑当遇到br时:

  • 向上搜索最近的cset并记录条件
  • 向上搜索距离最近的ldr
  • 读取ldr对应地址处偏移为0和1的两个地址。1为条件跳转目的地,0为无条件跳转目的地。并记录
  • 将ldr操作和br之间的操作向上移动
  • 将ldr根据cset的条件修改成条件跳转
  • 将br跳转地址固定

例如,如下指令

1
2
3
4
5
6
7
8
[libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5
[libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003
[libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1
[libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80
[libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38
[libplay.so 0x013cc] [105a6bf8] 0x120013cc: "ldr x16, [x16, w11, uxtw #3]" x16=0x12003d80 w11=0x1 => x16=0x12001354
[libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800
[libplay.so 0x013d4] [00021fd6] 0x120013d4: "br x16" x16=0x12001354

最后被修改为

1
2
3
4
5
6
7
8
[libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5
[libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003
[libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1
[libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80
[libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38
[libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800
[libplay.so 0x013cc] [105a6bf8] 0x120013cc: "b.lo addr"
[libplay.so 0x013d4] [00021fd6] 0x120013d4: "br addr" x16=0x12001354

去混淆后再看,逻辑就清楚多了。这其中还有一些间接跳转可以优化。这里主要就是一个异或加密和一个换表的base64。

image-20250527205336719

xor的密码:0A 0C 0E 00 51 16 27 38 49 1A 3B 5C 2D 4E 6F FA FC FE

base64表:ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/

target:UT1fc0gIYDArdz80Z0Xem46J

解密脚本

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
import base64

# 自定义base64解码
custom_table = 'ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/'
custom_dict = {c: i for i, c in enumerate(custom_table)}

def custom_b64decode(s):
# 处理填充字符
s += '=' * ((4 - len(s) % 4) % 4)
binary_str = []
for c in s:
if c == '=':
binary_str.append('000000')
continue
binary_str.append(f"{custom_dict[c]:06b}")

# 拼接二进制并转换为字节
bit_stream = ''.join(binary_str)
bytes_list = [int(bit_stream[i:i+8], 2) for i in range(0, len(bit_stream), 8) if i+8 <= len(bit_stream)]
return bytes(bytes_list)

# 异或解密
def xor_decrypt(data, key):
key_len = len(key)
return bytes([data[i] ^ key[i % key_len] for i in range(len(data))])

if __name__ == "__main__":
encoded_str = "UT1fc0gIYDArdz80Z0Xem46J"
result1 = custom_b64decode(encoded_str)

key = bytes.fromhex("0A0C0E0051162738491A3B5C2D4E6FFAFCFE")
result2 = xor_decrypt(result1, key)
print("最终结果:", result2.decode("utf-8", errors="replace"))

得到第三段flag:_Anti_Cheat_Expert

至此得到最终flag:8939008_Anti_Cheat_Expert

脚本

frida脚本:

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
class UE4Reverse {
constructor(GNameOffset, GWorldOffset) {
this.GNameOffset = GNameOffset;
this.GWorldOffset = GWorldOffset;
this.moduleName = "libUE4.so";
this.moduleBase = Module.findBaseAddress(this.moduleName);
if (!this.moduleBase) {
console.error(`[!] 模块 ${this.moduleName} 未加载`);
return;
}

this.GWorld = this.moduleBase.add(GWorldOffset).readPointer();
console.log("moduleBase: " + this.moduleBase.toString(16));
console.log("GWorld Offset: " + GWorldOffset.toString(16));
console.log("GWorld: " + this.GWorld.toString(16));
this.GName = this.moduleBase.add(GNameOffset);
this.FNameEntryAllocator = this.GName.add(0x30);
this.Blocks = this.FNameEntryAllocator.add(0x10);

this.GLevel = this.GWorld.add(0x30).readPointer(); //UWorld to Level
this.Actors = this.GLevel.add(0x98).readPointer(); //ULevel -> Actors
this.ActorNum = this.GLevel.add(0x98).add(0x8).readU32(); //ULevel -> Num
}

getNameById(ComparisonIndex) {
var BlockId = ComparisonIndex >> 16;
var Offset = ComparisonIndex & 65535;

var currentBlock = this.Blocks.add(8 * BlockId).readPointer();
var FNameEntry = currentBlock.add(2 * Offset);

var FNameEntryHeader = FNameEntry.readU16();
var str_length = FNameEntryHeader >> 6; // 计算字符串长度
var wide = FNameEntryHeader & 1;
var str_addr = FNameEntry.add(0x2);
if (wide) {
return "wide_string";
}
if (str_length > 0 && str_length < 250) {
var str = str_addr.readUtf8String(str_length);
return str;
0xadf07c0;
} else {
return "None"; // 长度不合理,返回 "None"
}
}

getActorByName(actorName) {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();

var ComparisonIndex = Actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
if (Name == actorName) {
console.log("return Actor " + actorName);
return Actor;
}
}
console.log("can't find Actor:" + actorName);
return "None";
}

getNameByActor(actor) {
var ComparisonIndex = actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
return Name;
}

printAllActorName() {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();

var ComparisonIndex = Actor.add(0x18).readU32(); //Actor -> ComparisonIndex
var Name = this.getNameById(ComparisonIndex);
console.log(`[!] actor ${i} Name: ${Name}`);
}
}

forAllActor(func) {
for (var i = 0; i < this.ActorNum; i++) {
var Actor = this.Actors.add(i * 8).readPointer();
func(Actor);
}
}
}

function setHP(user, HP) {
user.add(0x510).writeFloat(HP);
}

function getCharacterMovementComponent(user) {
var characterMovementComponent = user.add(0x288).readPointer();
return characterMovementComponent;
}

function setActorLocation(user, x, y, z) {
// K2_SetActorLocation: 0x8C3181C
// Actor -> __int64 __fastcall K2_SetActorLocation(__int64 a1, char a2, __int64 a3, char a4, float32x2_t a5, float a6, float a7)
const K2_SetActorLocationAddr = ue4Reverse.moduleBase.add(0x8c3181c);
const K2_SetActorLocation = new NativeFunction(
K2_SetActorLocationAddr,
"void",
["pointer", "bool", "pointer", "bool", "float", "float", "float"]
);
const buf = Memory.alloc(0x100);

K2_SetActorLocation(user, 1, buf, 1, x, y, z);
}

function getActorLoaction(user, log=true) {
//Actor -> Vector K2_GetActorLocation();// 0x965ddf8
const getActorLoactionAddr = ue4Reverse.moduleBase.add(0x965ddf8);
const getActorLoaction = new NativeFunction(
getActorLoactionAddr,
"pointer",
["pointer", "pointer", "pointer"]
);
const buf = Memory.alloc(0x100);
getActorLoaction(user, buf, buf);
var x = buf.readFloat();
var y = buf.add(4).readFloat();
var z = buf.add(8).readFloat();
if(log){
console.log("x: " + x + " y: " + y + " z: " + z);
}
return { x: x, y: y, z: z };
}

function SetVisibility(actor) {
//SceneComponent -> void SetVisibility(bool bNewVisibility, bool bPropagateToChildren);// 0x9910794
const setVisibilityAddr = ue4Reverse.moduleBase.add(0x8e619bc);
const setVisibility = new NativeFunction(setVisibilityAddr, "void", [
"pointer",
"bool",
"bool",
]);

const rootComponent = actor.add(0x130).readPointer();

console.log("rootComponent: " + rootComponent.toString(16));
if (rootComponent != 0) {
setVisibility(rootComponent, 1, 1);
}
}

function setActorEnableCollision(actor) {
//Actor -> void SetActorEnableCollision(bool bNewActorEnableCollision);
const name = ue4Reverse.getNameByActor(actor);
if(!name.includes("Cube")){
return
}
const setActorEnableCollisionAddr = ue4Reverse.moduleBase.add(0x8c21320);
const setActorEnableCollision = new NativeFunction(setActorEnableCollisionAddr, "void", ["pointer", "bool"]);

setActorEnableCollision(actor, 1);

const rootComponent = actor.add(0x130).readPointer();

//PrimitiveComponent -> void SetSimulatePhysics(bool bSimulate);// 0x98f0830
// console.log("rootComponent: " + rootComponent.toString(16))0
if (rootComponent != 0) {
if (rootComponent.readPointer().add(0x5d0) != 0) {

var setSimulatePhysicsAddr = rootComponent.readPointer().add(0x5d0).readPointer();
var setSimulatePhysics = new NativeFunction(setSimulatePhysicsAddr, "void", ["pointer", "bool"]);

try {
console.log("setSimulatePhysics");
setSimulatePhysics(rootComponent, 0);
} catch (e) {
console.error("error: " + e);
}
}
}
}

function setCollisionEnabled(actor) {
//SceneComponent -> void SetVisibility(bool bNewVisibility, bool bPropagateToChildren);// 0x9910794
const name = ue4Reverse.getNameByActor(actor);
if(!name.includes("Cube")){
return
}
console.log("name: " + name);
const rootComponent = actor.add(0x130).readPointer();

console.log("rootComponent: " + rootComponent.toString(16));
if (rootComponent != 0) {
const setCollisionEnabledAddr = rootComponent
.readPointer()
.add(0x660)
.readPointer();
const setCollisionEnabled = new NativeFunction(
setCollisionEnabledAddr,
"void",
["pointer", "bool"]
);
try {
setCollisionEnabled(rootComponent, 3);
} catch (e) {
console.error("error: " + e);
}
}
}

function printDistanceWithActor(actor) {
var location1 = getActorLoaction(user, false)
var location2 = getActorLoaction(actor,false)
const actorName = ue4Reverse.getNameByActor(actor)
var distance = Math.sqrt(Math.pow(location1.x - location2.x, 2) + Math.pow(location1.y - location2.y, 2))
console.log(`actorName: ${actorName} distance: ${distance}`)
}


const ue4Reverse = new UE4Reverse(0xb171cc0, 0xb32d8a8);
const user = ue4Reverse.getActorByName("FirstPersonCharacter_C");

setHP(user, 1000000);


// setActorLocation(user, 292,-1613,9000)
// setActorLocation(user, -900, 1647, 9000)
// getActorLoaction(user)

ue4Reverse.forAllActor(SetVisibility)

// ue4Reverse.forAllActor(setActorEnableCollision)

// const triggerBox = ue4Reverse.getActorByName("TriggerBox")
// getActorLoaction(triggerBox)
// setActorLocation(user, -749, 109, 9000)

ue4Reverse.forAllActor(setCollisionEnabled);
const actor = ue4Reverse.getActorByName("Actor")

unidbg脚本

ACE2024.java:

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
package com.ACE2024;

import capstone.Capstone;
import capstone.api.Instruction;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import unicorn.Arm64Const;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;


public class ACE2024 {
public final AndroidEmulator emulator;
public final VM vm;
public final Memory memory;
public final Module module;
private final AsmContextQueue asmQueue;
private final PatchInfoQueue patchQueue;
private final Capstone capstone;
private final Keystone keystone;

private static final String inName = "C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplay.so";
private static final String outName = "C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplayPatch.so";

public ACE2024() {
emulator = AndroidEmulatorBuilder.for64Bit().build();
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
emulator.getSyscallHandler().setEnableThreadDispatcher(true);
vm = emulator.createDalvikVM();
new AndroidModule(emulator,vm).register(memory);
DalvikModule dalvikModule = vm.loadLibrary(new File("C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplay.so"), true);
module = dalvikModule.getModule();
asmQueue = new AsmContextQueue(); //用于缓存最近执行的10条指令
patchQueue = new PatchInfoQueue(); //用于缓存patch队列
capstone = new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);
keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
}

public static void main(String[] args){
ACE2024 mainActivity = new ACE2024();
mainActivity.debugger();
}
public void debugger(){
// emulator.traceCode(0x40000000,0x40010000);
deBR();
// emulator.attach(DebuggerType.CONSOLE).addBreakPoint(module.base+ 0x000000001458);
module.callFunction(emulator,0x0000000000001458 ,"_Anti_Cheat_Expert");
module.callFunction(emulator,0x000000000000EEC ,"aaaa",1);
doPatch();
}
public void deBR(){
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {

Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);
byte[] bytes = backend.mem_read(address, 4);
Instruction disasm = capstone.disasm(bytes, address)[0];
AsmContext asmContext = new AsmContext();
asmContext.setAddress(address);
asmContext.setIns(disasm);
asmContext.setRegs(saveRegs(backend));
asmQueue.add(asmContext);

if(disasm.getMnemonic().equals("br")){
doProcessBR(backend, disasm);
}
}

@Override
public void onAttach(UnHook unHook) {

}
@Override
public void detach() {

}
},module.base, module.base + module.size, null);
}

public void doProcessBR(Backend backend, Instruction disasm){

long brAddr = disasm.getAddress() - module.base;
long condBrAddr = brAddr -4;

long ldrAddr = 0;

long brTargetAddr = 0;
long condBrTargetAddr = 0;
String jcc = new String();
AsmContext csetContext = null;
AsmContext ldrContext = null;

//遍历保存的指令,寻找cset和ldr
for(int i = 0; i < asmQueue.getSize() - 1; i++){
AsmContext asmContext = asmQueue.get(i);
Instruction ins = asmContext.getIns();
String insMnemonic = ins.getMnemonic();

if(insMnemonic.equals("cset")){
csetContext = asmContext;
}

else if (insMnemonic.equals("ldr")){
//可能会有多个ldr,这里只保留离br最近的ldr
ldrContext = asmContext;
}

}


if(ldrContext == null){
System.err.println("unrecognize br");
}


if(csetContext != null){
// 处理CSET
// "cset w10, lo"

// "w10, lo"
Instruction csetIns = csetContext.getIns();
String opstr = csetIns.getOpStr();

// "lo"
String flag = opstr.split(", ")[1];

// jcc = "blo"
jcc = "b" + flag;
}

//ldr x10, [x9, w10, uxtw #3]
//ldr x17, [x16, #0xd48]

Instruction ldrIns = ldrContext.getIns();
ldrAddr = ldrIns.getAddress();

String[] opList = ldrIns.getOpStr().split(", ");


if(!jcc.isEmpty()){
//如果有cset,说明是条件跳转

//移动ldr和br之间的内容
for(long addr = ldrAddr; addr < brAddr - 4; addr += 4){
byte[] bytes = backend.mem_read(addr + 4, 4);
Instruction patchAsm = capstone.disasm(bytes, 0)[0];
patchQueue.add(addr, patchAsm.getBytes());
}

String ldrBaseReg = opList[1].replace("[", "");
String ldrOffsetReg = opList[2];


Number tabelBaseAddr = ldrContext.getReg(ldrBaseReg);
Number tabelOffsetAddr = ldrContext.getReg(ldrOffsetReg);

condBrTargetAddr = readInt64(backend, tabelBaseAddr.longValue() + 8) - module.base;
brTargetAddr = readInt64(backend, tabelBaseAddr.longValue()) - module.base;

//br-4 改成条件跳转,br改成直接跳转

KeystoneEncoded condBr = keystone.assemble(jcc+" 0x"+ Integer.toHexString((int)condBrTargetAddr).toString(), (int)condBrAddr );
KeystoneEncoded br = keystone.assemble("b 0x" + Integer.toHexString((int)brTargetAddr), (int) brAddr);

patchQueue.add(condBrAddr, condBr.getMachineCode());
patchQueue.add(brAddr, br.getMachineCode());



}

else{
//没有cset,直接跳转

// ldr x17, [x16, #0xd38] ,调用外部库函数,不做处理
if(opList.length < 4) {
return;
}

String ldrBaseReg = opList[1].replace("[", "");
String ldrOffsetReg = opList[2];
Number tabelBaseAddr = ldrContext.getReg(ldrBaseReg);
Number tabelOffsetAddr = ldrContext.getReg(ldrOffsetReg);

brTargetAddr = readInt64(backend, tabelBaseAddr.longValue() + tabelOffsetAddr.longValue() * 8) - module.base;

// 将br改成直接跳转
KeystoneEncoded br = keystone.assemble("b 0x" + Integer.toHexString((int)brTargetAddr), (int) brAddr);
patchQueue.add(brAddr, br.getMachineCode());
}
}


public void doPatch() {
try {
File f = new File(inName);
FileInputStream fis = new FileInputStream(f);
byte[] data = new byte[(int) f.length()];
fis.read(data);
fis.close();

for(int i = 0; i < patchQueue.size(); i++){
PatchInfo pi = patchQueue.get(i);
for(int j=0; j < 4; j++)
{
data[(int) pi.address+j] = pi.ins[j];
}
}

File fo = new File(outName);
FileOutputStream fos = new FileOutputStream(fo);
fos.write(data);
fos.flush();
fos.close();
System.out.println("finish");
}
catch (Exception e)
{
e.printStackTrace();
}
}

public List<Number> saveRegs(Backend bk) {
List<Number> nb = new ArrayList<>();
for(int i=0;i<29;i++)
//顺序存储x0到x28
{
nb.add(bk.reg_read(i+ Arm64Const.UC_ARM64_REG_X0));
}
nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_FP));
nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_LR));
return nb;
}


public long readInt64(Backend bk,long addr) {
byte[] bytes = bk.mem_read(addr, 4);
long res = 0;
for (int i=0;i<bytes.length;i++)
{
res =((bytes[i]&0xffL) << (8*i)) + res;
}
return res;
}
}

AsmContext.java:

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
package com.ACE2024;

import capstone.api.Instruction;

import java.util.List;

public class AsmContext {
private List<Number> regs;
private Instruction ins;
private long address;

public void setRegs(List<Number> regs) {
this.regs = regs;
}

public void setIns(Instruction ins){
this.ins = ins;
}

public void setAddress(long address) {
this.address = address;
}

public List<Number> getRegs() {
return regs;
}

public Instruction getIns() {
return ins;
}

public long getAddress() {
return address;
}

public Number getReg(String reg) {
String offset = (reg.replaceAll("[^0-9]", ""));
return regs.get(Integer.parseInt(offset));
}
}

AsmContextQueue.java

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
package com.ACE2024;
import java.util.LinkedList;


public class AsmContextQueue {
private static final int MAX_SIZE = 10;
private final LinkedList<AsmContext> queue = new LinkedList<>();

// 线程安全的入队操作
public synchronized void add(AsmContext asmContext) {
if (queue.size() >= MAX_SIZE) {
queue.poll(); // 移除最老的元素
}
queue.add(asmContext);
}

// 获取指定位置的元素
public synchronized AsmContext get(int index) {
if (index < 0 || index >= queue.size() || index >= MAX_SIZE) {
return null; // 返回空表示无效索引
}
return queue.get(index); // 使用 LinkedList 的 get 方法获取元素
}

// 获取当前队列快照
public synchronized LinkedList<AsmContext> getInstructions() {
return new LinkedList<>(queue); // 返回副本避免并发修改
}

// 清空队列
public synchronized void clear() {
queue.clear();
}

// 当前队列大小
public synchronized int getSize() {
return queue.size();
}
}

PatchInfo.java

1
2
3
4
5
6
7
8
9
10
11
package com.ACE2024;

public class PatchInfo {
long address;
byte[] ins;

PatchInfo(long address, byte[] ins) {
this.address = address;
this.ins = ins;
}
}

PatchQueue.java

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
package com.ACE2024;

import java.util.ArrayList;
import java.util.List;



public class PatchInfoQueue {


private final List<PatchInfo> queue;

public PatchInfoQueue() {
this.queue = new ArrayList<>();
}

public void add(long address, byte[] ins) {
// Check if an entry with the same address already exists
for (PatchInfo entry : queue) {
if (entry.address == address) {
// Replace the existing entry
entry.ins = ins;
return;
}
}
// Add a new entry if no matching address is found
queue.add(new PatchInfo(address, ins));
}

public PatchInfo get(int idx) {
if(idx >= queue.size())
return null;
return queue.get(idx); // Return null if no matching address is found
}

public boolean remove(long address) {
return queue.removeIf(entry -> entry.address == address);
}

public int size() {
return queue.size();
}

public void printAll(){
for (PatchInfo patchInfo : queue) {
System.out.println("address=" + patchInfo.address + ", ins=" + patchInfo.ins);
}
}
}

解密脚本

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
import base64

# 自定义base64解码
custom_table = 'ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/'
custom_dict = {c: i for i, c in enumerate(custom_table)}

def custom_b64decode(s):
# 处理填充字符
s += '=' * ((4 - len(s) % 4) % 4)
binary_str = []
for c in s:
if c == '=':
binary_str.append('000000')
continue
binary_str.append(f"{custom_dict[c]:06b}")

# 拼接二进制并转换为字节
bit_stream = ''.join(binary_str)
bytes_list = [int(bit_stream[i:i+8], 2) for i in range(0, len(bit_stream), 8) if i+8 <= len(bit_stream)]
return bytes(bytes_list)

# 异或解密
def xor_decrypt(data, key):
key_len = len(key)
return bytes([data[i] ^ key[i % key_len] for i in range(len(data))])

if __name__ == "__main__":
encoded_str = "UT1fc0gIYDArdz80Z0Xem46J"
result1 = custom_b64decode(encoded_str)

key = bytes.fromhex("0A0C0E0051162738491A3B5C2D4E6FFAFCFE")
result2 = xor_decrypt(result1, key)
print("最终结果:", result2.decode("utf-8", errors="replace"))

参考

https://renyili.org/post/game_cheat2/

https://www.52pojie.cn/thread-1838396-1-1.html

https://www.cnblogs.com/revercc/p/17641855.html

https://oacia.dev/unidbg-anti-br/