UE4逆向初探-OverWatch

可以看这个线上培训 -先知社区

赛后下面那个偏移问题(主要是被网上某篇瞎写的博客和IDA字符串加载给暗算了)解决了自己做了下直接出了一段flag,后一段flag没找到只能看wp了

好的!我知道了尴尬了,我还是太着急了,刚刚写这个wp的时候又打开来了(之前保存的i64),搜了一下seamless,发现直接出现了,原来是IDA加载太慢了,我太急了,我就不该上课 T.T

image-20251013222036823

image-20251013221924088

dump工具:Spuckwaffel/UEDumper: The most powerful Unreal Engine Dumper and Editor for UE 4.19 - 5.3

image-20251012162204075

可以看这个视频

https://www.youtube.com/watch?v=M7VLd1xrVoM

image-20251012172315439

说一下找三件套吧

Fname找ByteObject

image-20251014102225239

交叉引用这个方法,是最上面那个

image-20251014102258624

image-20251014102334465

第一个偏移:0x4869C80

ps:可以看那个印度佬的视频,挺好的,先去rebase里把机制定位0

找UWorld时,我是IDA没加载出来这个”Seamless”字符串

关于如何对照源码虚幻4 UE4 逆向 寻找 世界地址 UWORLD地址 教程_哔哩哔哩_bilibili

可以先去github把对应版本的源码下载下来,需要加入epicgame组织先,

所以我对照了源码最终找到一个

image-20251014102621791

image-20251014102704430

当然常规的方法更简单一点

搜索: SeamlessTravel FlushLevelStreaming

往上直接找到:

image-20251014103045269

第二个uworld有了:49ee370

第三个gobject,我也是死在这了…

我找的偏移是0x48A5FC0

看了wp发现是:0x48A5FD0

….

就差0x10,可能是数据结构哪里搞错了应该

当时找了

image-20251014104421969

image-20251014104434870

当时字符串没加载出来,简单的把48A5FC0作为Gobject肯定不对,都跟上面不太像其实

实际上被误导了?

ue5游戏逆向之寻找GWorld,GName和GUObjectArray - 怎么可以吃突突 - 博客园

这里虽然是ue5的方法,但是道理应该差不多,为什么偏移不对?

官解搜索的是:NewObject with….

可是我看了下引用:

image-20251014105547046

这找个damn…

不知道是不是我IDA的原因

运气足够好,第二个就是

image-20251014105748059

但是我并没有找到源码中对应的寻找方式,可能是源码中把NewObject字符串包装了,得搜索引用这个函数的才能去找GUObject

推荐搜索Failed to load Enginee class,跟刚才的NewObject With到达的是一个地方

image-20251014110256024

48A5FD0

offset.h里填好

image-20251014110520421

dump成功如下

image-20251014110946452

后面就是游戏逆向,猜测flag在墙外,因而只有几种常规方法:透视(墙不见) 穿墙 飞天遁地

在merged_AudioMixer_Engine_UMG_MovieScene_MovieSceneTracks.h种有

这个文件不是引擎原生的源码文件,而是 自动合并生成的头文件

目的是把多个模块导出的类、枚举、结构体合并在一个文件中方便分析。

这个枚举定义在 UE 原版引擎中是 角色移动组件(Character Movement Component) 的核心枚举

模式 场景举例 行为逻辑
MOVE_None 不可移动(如被眩晕、冻结) 停止更新物理
MOVE_Walking 在地面上走 使用地面摩擦力、速度计算
MOVE_NavWalking AI 路径导航行走 使用 NavMesh
MOVE_Falling 从高处坠落 使用重力
MOVE_Swimming 在水中游动 使用流体阻力、浮力
MOVE_Flying 飞行类角色(如幽灵、飞行器) 关闭重力,使用自由三维移动
MOVE_Custom 自定义移动,如“攀爬”、“滑行” 游戏开发者自己扩展逻辑

image-20251014193847028

我们能发现这里还有

image-20251014201041665

ACharacter是什么?

ACharacter:UE4 自带的行走类Actor,继承自 APawn。含网格体、胶囊体、UCharacterMovementComponent 等,负责角色移动、跳跃等行为。你操作飞行/穿墙时的目标对象就是本地玩家的 ACharacter 实例

这些是 编译时静态断言(static_assert),用于验证 类成员变量的内存偏移是否正确。

UCharacterMovementComponent::MovementModePendingLaunchVelocity这些字段属于 UCharacterMovementComponent(角色移动组件),控制角色的物理状态。

我们除了这些关键属性之外还需要知道世界链路

在 UE 逆向中,找到世界链路 是理解游戏对象体系的关键。

世界结构简图:

1
2
3
4
5
6
7
8
9
GWorld → UWorld
├─ PersistentLevel (ULevel)
│ ├─ AActor[0] = DefaultPawn
│ ├─ AActor[1] = PlayerCharacter
│ └─ ...
├─ GameInstance
├─ GameMode
├─ PlayerController
└─ etc.
名称 类型 含义
GWorld UWorld* 全局指针 当前正在运行的世界(全局变量)
UWorld 类对象 世界实例本身,包含关卡、玩家、Actor 列表等

GWorld 就是指向当前 UWorld 的全局变量。

在内存调试中,通常会通过 GWorld 找到整个世界的根:

1
2
3
UWorld* World = GWorld;
ULevel* Level = World->PersistentLevel;
TArray<AActor*> Actors = Level->Actors;

这样就能遍历世界中所有的角色对象。

元素 含义 逆向用途
offsetof 成员偏移 定位内存字段、直接读写对象成员
CharacterMovement 角色移动组件指针 控制角色物理行为(走、飞、跳)
CapsuleComponent 碰撞体组件 检测碰撞、修改 hitbox 尺寸
MovementMode 当前移动模式 判断或强制移动状态
PendingLaunchVelocity 等待应用的速度 修改跳跃或击飞效果
GWorld → UWorld 世界根节点 遍历所有 Actor,找到玩家对象

在游戏中找到了玩家对象地址 PlayerCharacter

1
UCharacterMovementComponent* MoveComp =  *(UCharacterMovementComponent**)(PlayerCharacter + 0x288);

接下来:

1
2
MoveComp->MovementMode = EMovementMode::MOVE_Flying;
MoveComp->PendingLaunchVelocity = FVector(0, 0, 3000);

角色立刻能在空中飞行或超高跳。

  • UWorld::OwningGameInstance:指向当前世界所属的 UGameInstance。GameInstance 持有全局状态,如本地玩家列表、子系统等,是沿 GWorld 找到你这边玩家控制器的入口。
  • APawn:可被玩家或 AI 控制的 Actor 基类。ACharacter 就是 APawn 的一个扩展版本,加入了骨骼网格和 CharacterMovement。
  • APlayerController::AcknowledgedPawn:玩家控制器当前“正式控制”的 Pawn 指针。正常游戏里它就是你的角色 Pawn(如 ACharacter),读取后才能继续修改移动组件/碰撞。
  • APlayerController:表示本地或远端的玩家控制器,处理输入、相机、HUD 等。我们从 UGameInstance::LocalPlayers 取得的 ULocalPlayer->PlayerController 就是本地玩家的控制器,顺着它的 AcknowledgedPawn 拿到角色后才能进行后续 hack。

世界链路

  • GWorld(基址 base + 0x49EE370,与setOffsets() 中 OFFSET_GWORLD 相符)指向当前关卡对应的 UWorld。
  • UWorld + 0x180(OwningGameInstance)拿到 UGameInstance,这是全局封装玩家列表的对象。
  • UGameInstance + 0x38(LocalPlayers 的 TArray)提供本地玩家数组,下标 0 通常是本地玩家。
  • ULocalPlayer->PlayerController(UPlayer::PlayerController 在 …:8986 给出 0x30)接到 APlayerController,再用 AcknowledgedPawn 偏移 0x2A0 取到实际 Pawn。
  • Pawn + 0x288(CharacterMovement)就能定位 UCharacterMovementComponent;后续通过 MovementMode、PendingLaunchVelocity 等偏移修改为飞行或冲刺,或者抓取 CapsuleComponent(0x290) 调 SetCollisionEnabled 实现穿墙。

image-20251014220645122

在编写代码时我们可以用reinterpret_cast

reinterpret_cast(expr) 是 C++ 提供的强制类型转换之一:

1
2
在没有类型信息/类定义不足的情况下,把某个指针或整数当成别的类型的指针来访问。
与UE这类内存操作结合时,我们经常只有偏移值,所以先把基址转成 uint8_t*,加偏移后再 reinterpret_cast<目标类型*>,这样就能把那块内存看成某个字段或结构。

FVector 在 BasicType.h 里被定义成三个 float 分量(X/Y/Z)。C++ 允许对这种简单结构做聚合初始化,{a, b, c} 就会依次填入 X/Y/Z,所以 {0.f, 0.f, 800.f} 会写成 (0,0,800)。

数值 800/600 只是示例:PendingLaunchVelocity 相当于给角色一个即将施加的冲量,LastUpdateVelocity 是当前速度。这两个字段只要写入任何 FVector,UE4 就会按这些分量处理运动;如果想要更慢或更快的上升,可以自己改成别的值。

exp:

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
#include "pch.h"

#include <Windows.h>
#include <atomic>
#include <thread>
#include <chrono>
#include "BasicType.h" // UEDumper 导出的基础类型,提供 TArray 等模板

// --- 简易前向声明 -----------------------------------------------------------
struct UWorld;
struct UGameInstance;
struct ULocalPlayer;
struct APlayerController;
struct APawn;
struct ACharacter;
struct UCharacterMovementComponent;

struct FVector { float X, Y, Z; };

// 之前我们发现的状态
enum class EMovementMode : uint8_t {
MOVE_None = 0,
MOVE_Walking = 1,
MOVE_NavWalking,
MOVE_Falling,
MOVE_Swimming,
MOVE_Flying,
MOVE_Custom
};

// --- 偏移常量(来自 merged_AudioMixer_Engine_UMG_MovieScene_MovieSceneTracks.h) ---
constexpr uintptr_t OFFSET_GWORLD = 0x49EE370;
constexpr size_t OFFSET_UWORLD_OWNING_GI = 0x180;
constexpr size_t OFFSET_UGAMEINSTANCE_LOCALPLAYERS = 0x38;
constexpr size_t OFFSET_ULOCALPLAYER_PLAYERCONTROLLER = 0x30;
constexpr size_t OFFSET_APLAYERCONTROLLER_ACKPAWN = 0x2A0;
constexpr size_t OFFSET_ACHARACTER_CHARACTERMOVEMENT = 0x288; // 角色移动

constexpr size_t OFFSET_UCHARMOVEMENT_MOVEMENTMODE = 0x168;
constexpr size_t OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE = 0x384;
constexpr size_t OFFSET_UCHARMOVEMENT_PENDINGLAUNCHVELOC = 0x3C0;
constexpr size_t OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC = 0x25C; // LastUpdateVelocity

// --- 全局状态 ---------------------------------------------------------------
static std::atomic<bool> g_running{ true };
static std::atomic<bool> g_flyEnabled{ false };

// --- 便捷访问函数 -----------------------------------------------------------
inline uintptr_t GetModuleBase()
{
static uintptr_t base = reinterpret_cast<uintptr_t>(::GetModuleHandleW(nullptr));
return base;
}

// 根据偏移找GWorld
inline UWorld* GetWorld()
{
return *reinterpret_cast<UWorld**>(GetModuleBase() + OFFSET_GWORLD);
}

// 获取实例,拿本地玩家列表,我们要拿[0]
inline UGameInstance* GetGameInstance(UWorld* world)
{
if (!world) return nullptr;
return *reinterpret_cast<UGameInstance**>(reinterpret_cast<uint8_t*>(world) + OFFSET_UWORLD_OWNING_GI);
}

inline TArray<ULocalPlayer*>& GetLocalPlayers(UGameInstance* gi)
{
return *reinterpret_cast<TArray<ULocalPlayer*>*>(reinterpret_cast<uint8_t*>(gi) + OFFSET_UGAMEINSTANCE_LOCALPLAYERS);
}

inline APlayerController* GetPlayerController(ULocalPlayer* lp)
{
return *reinterpret_cast<APlayerController**>(reinterpret_cast<uint8_t*>(lp) + OFFSET_ULOCALPLAYER_PLAYERCONTROLLER);
}

// 这一帧实际被控制的 Pawn,拿到这个指针就能访问 ACharacter
inline ACharacter* GetAcknowledgedCharacter(APlayerController* pc)
{
return *reinterpret_cast<ACharacter**>(reinterpret_cast<uint8_t*>(pc) + OFFSET_APLAYERCONTROLLER_ACKPAWN);
}

inline UCharacterMovementComponent* GetCharacterMovement(ACharacter* character)
{
return *reinterpret_cast<UCharacterMovementComponent**>(
reinterpret_cast<uint8_t*>(character) + OFFSET_ACHARACTER_CHARACTERMOVEMENT);
}

// --- 写飞行相关字段 ---------------------------------------------------------
void ApplyFlyState(UCharacterMovementComponent* movement, bool enable)
{
if (!movement) return;

uint8_t* base = reinterpret_cast<uint8_t*>(movement);

auto& movementMode = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE);
auto& defaultLandMode = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE);
auto& pendingLaunchVelocity = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_PENDINGLAUNCHVELOC);
auto& lastUpdateVelocity = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC);

if (enable) {
movementMode = EMovementMode::MOVE_Flying;
defaultLandMode = EMovementMode::MOVE_Flying;
pendingLaunchVelocity = { 0.f, 0.f, 800.f };
lastUpdateVelocity = { 0.f, 0.f, 600.f };
}
else {
movementMode = EMovementMode::MOVE_Walking;
defaultLandMode = EMovementMode::MOVE_Walking;
pendingLaunchVelocity = { 0.f, 0.f, 0.f };
lastUpdateVelocity = { 0.f, 0.f, 0.f };
}
}

// 持续维持飞行状态,防止游戏自动还原
void SustainFly(UCharacterMovementComponent* movement)
{
if (!movement) return;

uint8_t* base = reinterpret_cast<uint8_t*>(movement);
auto& movementMode = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE);
auto& lastUpdateVelocity = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELOC);

// movementMode 本质是 *(EMovementMode*)(base + 0x168) 的别名,base 指向 UCharacterMovementComponent 的起始地址. base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE 跳到 MovementMode 字段; reinterpret_cast<EMovementMode*> 把那块内存视为 EMovementMode * ;
// 通过引用赋值 movementMode = EMovementMode::MOVE_Flying; 就是把那 1 字节的内存直接写成飞行枚举。
movementMode = EMovementMode::MOVE_Flying;
if (lastUpdateVelocity.Z < 300.f) {
lastUpdateVelocity.Z = 400.f; // 给一点上升速度
}
}

// --- 热键线程 ---------------------------------------------------------------
DWORD WINAPI FlyThread(LPVOID)
{
while (g_running) {
if (::GetAsyncKeyState(VK_F6) & 1) {
UWorld* world = GetWorld();
auto gi = GetGameInstance(world);
if (!gi) continue;

auto& players = GetLocalPlayers(gi);
if (!players.IsValidIndex(0) || !players[0]) continue;

auto pc = GetPlayerController(players[0]);
if (!pc) continue;

auto character = GetAcknowledgedCharacter(pc);
auto movement = GetCharacterMovement(character);
g_flyEnabled = !g_flyEnabled.load();
ApplyFlyState(movement, g_flyEnabled);
}
/*
玩家切关、死亡重生、切换 Pawn 时,GWorld、GameInstance、LocalPlayers 乃至 AcknowledgedPawn 都可能变;如果只在按热键那一刻取一次指针,等角色重建后就指向旧对象,易崩溃或写不到新角色。
循环里每次重新取 GWorld→GameInstance→LocalPlayer→PlayerController→Pawn→Movement,能在状态变化时自动跟上,确保对当前角色生效,也避免解引用野指针。
用 TArray::IsValidIndex 防御空指针,配合持续刷新 MovementMode 就能稳定维持飞行。
*/
if (g_flyEnabled) {
UWorld* world = GetWorld();
auto gi = GetGameInstance(world);
if (gi) {
auto& players = GetLocalPlayers(gi);
if (players.IsValidIndex(0) && players[0]) {
auto pc = GetPlayerController(players[0]);
if (pc) {
auto character = GetAcknowledgedCharacter(pc);
auto movement = GetCharacterMovement(character);
SustainFly(movement);
}
}
}
}

std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
return 0;
}

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH) {
::DisableThreadLibraryCalls(module);

::MessageBoxW(nullptr,
L"TSCTF DLL 注入成功\nF6 切换飞行模式",
L"TSCTF Helper",
MB_OK | MB_ICONINFORMATION);

::CreateThread(nullptr, 0, FlyThread, nullptr, 0, nullptr);
}
else if (reason == DLL_PROCESS_DETACH) {
g_running = false;
}
return TRUE;
}

visual studio新建dll项目

image-20251014221722121

注意引如BasicType.h

然后生成项目

relase x64

然后找工具注入

如:

DarthTon/Xenos: Windows dll injector

image-20251014211218244

注入即可

按F6起飞

image-20251014211105377

这里再添加一个脚本兼容穿墙和起飞

image-20251014222324466

image-20251014223115885

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
#include "pch.h"

#include <Windows.h>
#include <atomic>
#include <thread>
#include <chrono>
#include "BasicType.h"

struct UWorld;
struct UGameInstance;
struct ULocalPlayer;
struct APlayerController;
struct APawn;
struct ACharacter;
struct UCharacterMovementComponent;
struct UCapsuleComponent;

struct FVector { float X, Y, Z; };

enum class EMovementMode : uint8_t {
MOVE_None = 0,
MOVE_Walking = 1,
MOVE_NavWalking,
MOVE_Falling,
MOVE_Swimming,
MOVE_Flying,
MOVE_Custom
};

enum class ECollisionEnabled : uint8_t {
NoCollision = 0,
QueryOnly = 1,
PhysicsOnly = 2,
QueryAndPhysics = 3
};

constexpr uintptr_t OFFSET_GWORLD = 0x49EE370;
constexpr size_t OFFSET_UWORLD_OWNING_GI = 0x180;
constexpr size_t OFFSET_UGAMEINSTANCE_LOCALPLAYERS = 0x38;
constexpr size_t OFFSET_ULOCALPLAYER_PLAYERCONTROLLER = 0x30;
constexpr size_t OFFSET_APLAYERCONTROLLER_ACKPAWN = 0x2A0;
constexpr size_t OFFSET_ACHARACTER_MOVEMENT = 0x288;
constexpr size_t OFFSET_ACHARACTER_CAPSULE = 0x290;

constexpr size_t OFFSET_UCHARMOVEMENT_MOVEMENTMODE = 0x168;
constexpr size_t OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE = 0x384;
constexpr size_t OFFSET_UCHARMOVEMENT_PENDINGLAUNCH = 0x3C0;
constexpr size_t OFFSET_UCHARMOVEMENT_LASTUPDATEVELO = 0x25C;

constexpr size_t OFFSET_UPRIMITIVE_BODYINSTANCE = 0x2C8;
constexpr size_t OFFSET_FBODYINSTANCE_COLLISIONENABLED = 0x20;

static std::atomic<bool> g_running{ true };
static std::atomic<bool> g_userFly{ false };
static std::atomic<bool> g_autoFlyFromCollision{ false };
static std::atomic<bool> g_noCollision{ false };

inline uintptr_t GetModuleBase()
{
static uintptr_t base = reinterpret_cast<uintptr_t>(::GetModuleHandleW(nullptr));
return base;
}

inline UWorld* GetWorld()
{
return *reinterpret_cast<UWorld**>(GetModuleBase() + OFFSET_GWORLD);
}

inline UGameInstance* GetGameInstance(UWorld* world)
{
if (!world) return nullptr;
return *reinterpret_cast<UGameInstance**>(reinterpret_cast<uint8_t*>(world) + OFFSET_UWORLD_OWNING_GI);
}

inline TArray<ULocalPlayer*>& GetLocalPlayers(UGameInstance* gi)
{
return *reinterpret_cast<TArray<ULocalPlayer*>*>(reinterpret_cast<uint8_t*>(gi) + OFFSET_UGAMEINSTANCE_LOCALPLAYERS);
}

inline APlayerController* GetPlayerController(ULocalPlayer* lp)
{
return *reinterpret_cast<APlayerController**>(reinterpret_cast<uint8_t*>(lp) + OFFSET_ULOCALPLAYER_PLAYERCONTROLLER);
}

inline ACharacter* GetAcknowledgedCharacter(APlayerController* pc)
{
return *reinterpret_cast<ACharacter**>(reinterpret_cast<uint8_t*>(pc) + OFFSET_APLAYERCONTROLLER_ACKPAWN);
}

inline UCharacterMovementComponent* GetCharacterMovement(ACharacter* character)
{
return *reinterpret_cast<UCharacterMovementComponent**>(reinterpret_cast<uint8_t*>(character) + OFFSET_ACHARACTER_MOVEMENT);
}

inline UCapsuleComponent* GetCapsuleComponent(ACharacter* character)
{
return *reinterpret_cast<UCapsuleComponent**>(reinterpret_cast<uint8_t*>(character) + OFFSET_ACHARACTER_CAPSULE);
}

void ApplyFlyState(UCharacterMovementComponent* movement, bool enable, bool giveBoost)
{
if (!movement) return;

uint8_t* base = reinterpret_cast<uint8_t*>(movement);
auto& movementMode = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE);
auto& defaultLand = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_DEFAULTLANDMODE);
auto& pendingLaunch = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_PENDINGLAUNCH);
auto& lastUpdateVel = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELO);

if (enable) {
movementMode = EMovementMode::MOVE_Flying;
defaultLand = EMovementMode::MOVE_Flying;
if (giveBoost) {
pendingLaunch = { 0.f, 0.f, 800.f };
lastUpdateVel = { 0.f, 0.f, 600.f };
}
else {
pendingLaunch = { 0.f, 0.f, 0.f };
lastUpdateVel = { 0.f, 0.f, 0.f };
}
}
else {
movementMode = EMovementMode::MOVE_Walking;
defaultLand = EMovementMode::MOVE_Walking;
pendingLaunch = { 0.f, 0.f, 0.f };
lastUpdateVel = { 0.f, 0.f, 0.f };
}
}

void SustainFly(UCharacterMovementComponent* movement, bool giveBoost)
{
if (!movement) return;

uint8_t* base = reinterpret_cast<uint8_t*>(movement);
auto& movementMode = *reinterpret_cast<EMovementMode*>(base + OFFSET_UCHARMOVEMENT_MOVEMENTMODE);
auto& lastUpdateVel = *reinterpret_cast<FVector*>(base + OFFSET_UCHARMOVEMENT_LASTUPDATEVELO);

movementMode = EMovementMode::MOVE_Flying;
if (giveBoost && lastUpdateVel.Z < 300.f) {
lastUpdateVel.Z = 400.f;
}
}

void ApplyCollisionState(UCapsuleComponent* capsule, bool noCollision)
{
if (!capsule) return;

uint8_t* primitive = reinterpret_cast<uint8_t*>(capsule);
auto& collisionEnabled = *reinterpret_cast<ECollisionEnabled*>(
primitive + OFFSET_UPRIMITIVE_BODYINSTANCE + OFFSET_FBODYINSTANCE_COLLISIONENABLED);

collisionEnabled = noCollision ? ECollisionEnabled::NoCollision
: ECollisionEnabled::QueryAndPhysics;
}

bool ShouldFly()
{
return g_userFly.load() || g_autoFlyFromCollision.load();
}

DWORD WINAPI FlyThread(LPVOID)
{
while (g_running) {
if (::GetAsyncKeyState(VK_F6) & 1) {
UWorld* world = GetWorld();
auto gi = GetGameInstance(world);
if (!gi) continue;

auto& players = GetLocalPlayers(gi);
if (!players.IsValidIndex(0) || !players[0]) continue;

auto pc = GetPlayerController(players[0]);
if (!pc) continue;

auto character = GetAcknowledgedCharacter(pc);
auto movement = GetCharacterMovement(character);
if (!movement) continue;

g_userFly = !g_userFly.load();
ApplyFlyState(movement, ShouldFly(), g_userFly.load());
}

if (::GetAsyncKeyState(VK_F5) & 1) {
UWorld* world = GetWorld();
auto gi = GetGameInstance(world);
if (!gi) continue;

auto& players = GetLocalPlayers(gi);
if (!players.IsValidIndex(0) || !players[0]) continue;

auto pc = GetPlayerController(players[0]);
if (!pc) continue;

auto character = GetAcknowledgedCharacter(pc);
auto movement = GetCharacterMovement(character);
auto capsule = GetCapsuleComponent(character);
if (!movement || !capsule) continue;

bool newState = !g_noCollision.load();
g_noCollision = newState;
g_autoFlyFromCollision = newState;

ApplyCollisionState(capsule, newState);
ApplyFlyState(movement, ShouldFly(), g_userFly.load());
}

/*
玩家切关、死亡重生、切换 Pawn 时,GWorld、GameInstance、LocalPlayers 乃至 AcknowledgedPawn 都可能变;
如果只在按热键那一刻取一次指针,等角色重建后就指向旧对象,易崩溃或写不到新角色。
循环里每次重新取 GWorld→GameInstance→LocalPlayer→PlayerController→Pawn→Movement/ Capsule,
能在状态变化时自动跟上,确保对当前角色生效,也避免解引用野指针。
*/
if (ShouldFly() || g_noCollision.load()) {
UWorld* world = GetWorld();
auto gi = GetGameInstance(world);
if (gi) {
auto& players = GetLocalPlayers(gi);
if (players.IsValidIndex(0) && players[0]) {
auto pc = GetPlayerController(players[0]);
if (pc) {
auto character = GetAcknowledgedCharacter(pc);
if (auto movement = GetCharacterMovement(character)) {
SustainFly(movement, g_userFly.load());
}
if (auto capsule = GetCapsuleComponent(character); g_noCollision.load()) {
ApplyCollisionState(capsule, true);
}
}
}
}
}

std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
return 0;
}

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH) {
::DisableThreadLibraryCalls(module);

::MessageBoxW(nullptr,
L"TSCTF DLL 注入成功\nF6 切换飞行模式\nF5 切换穿墙模式",
L"TSCTF Helper",
MB_OK | MB_ICONINFORMATION);

::CreateThread(nullptr, 0, FlyThread, nullptr, 0, nullptr);
}
else if (reason == DLL_PROCESS_DETACH) {
g_running = false;
}
return TRUE;
}

Flag:TSCTF-J{u0real_or_R1AL?!

还有一段flag找不到

只能看wp了

看了题解是藏在前面了_and_here}

TSCTF-J{u0real_or_R1AL?!_and_here}