3647 字
18 分钟
The Outlast Trials 赛季7.1更新后 MOD 失效问题逆向分析报告
2026-09-09
加载中...
WARNING

您的浏览器窗口宽度不足,我们建议您使用屏幕宽度大于1600px的设备来获取更好的浏览体验。

  • 报告日期:2026-09-09
  • 分析对象:游戏客户端 09-08 更新(构建时间 2026-09-04)
  • 样本TOTClient-Win64-Shipping.exe(旧版,PE 2026-07-25)/ TOTClient-Win64-Shipping.exe(PE 2026-09-04)
  • 总体结论:本次更新在 pak 挂载路径前新增了一道文件名硬编码白名单,只放行 OPP-WindowsClient.pak 这一个名字;所有第三方 *_P.pak(MOD)一律被拒并记录日志。改文件名无法绕过(白名单是编译期写死的精确全等比较)。

报告分两个独立部分,可按身份选读:

  • 第一部分(面向玩家):不需要技术背景,说明”为什么 MOD 没法继续用了”。
  • 第二部分(技术分析):完整判断链路、硬编码来源的证据推导,供复查与二次开发。

为什么 MOD 没法用了#

1. MOD 在《The Outlast Trials》里是怎么工作的#

这个游戏使用 Unreal Engine 开发,游戏本体内容是一个大型数据包 Paks/OPP-WindowsClient.pak

MOD 的常见做法是:把自制内容打包成 .pak 文件放进同一个 Paks 目录,文件名带 _P 后缀(如 Public_P.pak)。游戏启动时,引擎除了挂载官方包,还会把这些包一起挂载进去,用 MOD 里的资源覆盖或补充官方内容。

在 2026-09-08 更新之前,这个流程是正常的——旧版客户端对 Paks 目录里的 pak 文件来者不拒,MOD 作者和玩家都依赖这一点。

2. 9 月 8 日更新后发生了什么#

更新后的客户端在挂载每个 pak 之前,先做了一道**“实名核对”**:

只允许挂载文件名恰好为 OPP-WindowsClient.pak 的文件;其它任何名字,打印一条警告日志并跳过挂载

你的游戏日志(OPP.log)里的记录就是这道核对在逐一点名拒绝:

LogPakFile: Warning: Refusing to mount unexpected pak file: ...Public_P.pak
LogPakFile: Warning: Refusing to mount unexpected pak file: ...CustomUI_Plus_Hawk_P.pak
LogPakFile: Warning: Refusing to mount unexpected pak file: ...Quiet Psychosis_P.pak
LogPakFile: Warning: Refusing to mount unexpected pak file: ...Anti MotionSickness+Psychosis_P.pak

注意前缀是 LogPakFile —— 这是游戏引擎自己的日志,不是反作弊(EasyAntiCheat)在拦你。游戏本身正常运行,只是多了个”门卫”。

打个比方:以前进小区不看名字;现在门口保安拿着一张只写了一个名字的名单,只有叫 OPP-WindowsClient.pak 的才能进,其它一律挡在门外并记一笔。

3. 为什么”改文件名”绕不过去#

这是你问过、也是很多人第一反应会想到的办法,结论是:行不通名单不是”看规律”,是”背死名字”。保安(程序)不是检查”名字像不像官方”(比如是不是 OPP- 开头),而是拿你的完整文件名和一个程序里写死的名字做逐字对比,必须一模一样。你的 MOD 改成 OPP-Test.pakOPP-ABC.pak 都不在名单上,照样被拒。

所以结论是:只要不修改游戏程序本身,MOD 就进不去。这与 MOD 内容好坏、版本新旧完全无关——门卫换人了,不是你的东西坏了。

4. 想继续用 MOD,目前只有一条路#

把”门卫”换掉:程序里那道核对只有 4 个字节,我们已经定位并制作了补丁版客户端,打上后跳过这道核对,MOD 恢复加载。

但使用前必须清楚三条限制:

  1. 只建议在”免 EasyAntiCheat(EAC)“的方式下使用。EAC 会校验游戏程序本身,改动过的 exe 走在线联机(Steam → start_protected_game.exe)可能无法启动,且绕过反作弊有账号风险。你之前用旧版能打 MOD 的启动方式(如果是直接运行 exe、离线自娱),继续用那种方式即可。
  2. 每次游戏更新后需要重新打补丁。官方一更新,exe 会被还原/替换,需要把补丁重放一遍。

5. 常见误区#

想法实际情况
”等 MOD 作者更新 MOD 版本”没用。MOD 本身没坏,是游戏程序换了门卫,MOD 作者也绕不过
”删掉官方 pak 就能让 MOD 顶上”不行。官方包是游戏全部内容,删了游戏就没东西可玩了
”是不是我被封了 / EAC 在搞我”不是。这是游戏引擎日志,与 EAC 无关,单机行为

技术分析#

样本与方法#

旧版样本TOTClient-Win64-Shipping.exe,PE 时间戳 2026-07-25
新版样本TOTClient-Win64-Shipping.exe,PE 时间戳 2026-09-04
分析环境Ghidra 12.0.2 与 capstone 5.0.7 自建反汇编/扫描脚本完成
映像基址0x140000000
文件偏移换算(.text)RVA = fileoff + 0xC00(fileoff 0x400 ↔ RVA 0x1000)

核对基准(新版 exe 全文引用地址均为 RVA;补丁字节用 fileoff):

  • 原版 SHA256:c8f7a4558bd509f7bf53b54f36d9cbad3fd1a4f71c2877f03b198b747689e6b0

结论#

更新在 pak 挂载入口(函数 RVA 0x261B410)新增”期望文件名核对”:用格式串常量构造唯一期望绝对路径 <Root>/Paks/OPP-WindowsClient.pak,与待挂载 pak 的完整路径做 FString 精确全等比较0x9F7D50);不相等则打印 Refusing to mount unexpected pak file: %s(Warning 级)并拒绝挂载。期望名两段常量(OPPWindowsClient)均为编译期字面量,全镜像不存在第二个期望模板,故任何改名都不能命中白名单。修改比较结果标志(2 处指令共 4 字节,RVA 0x261B56D/0x261B56F)可使任意 pak 绕过核对进入挂载路径。

判断链路#

链路总览#

日志行 "Refusing to mount unexpected pak file: %s"
  │ ①

RVA 0x66A833A(拒绝串,新版独有)
  │ ②

RVA 0x1732560(日志桩:fmt=0x66A833A, verbosity=3=Warning, call 0x31D420)
  │ ③

RVA 0x261C00F(位于函数 0x261B410 内"拒绝打印块")
  │ ④

RVA 0x261B56D test al,al / 0x261B56F jne ──(不等)──► 拒绝块 0x261BD10 → 日志桩 → bl=0 → 返回失败
  │                                              (相等 → 放行 bl=1)
  │ ⑤

比较右操作数(期望路径)来自:
  RVA 0x261B4EE lea rdx → 0x673A1F2 "%sPaks/%s-%s.pak"(新版独有)
  RVA 0x261B4F5 lea r9  → 0x6C9FFB0 "OPP"(UTF-16 字面量)
  RVA 0x261B4C1 rbx ← [obj+0x108] ← "WindowsClient"(ANSI 常量池 0x6487A90)
  经 0x1A250D0 (Printf) + 0x5D0890 (路径解析) → 唯一期望名
  │ ⑥

⑦ 上游调用关系确认闸门地位:0x550EE2 / 0x47A7C27 / 0x47A84F3 / 0x47A96A8
   (0x47A7B10 遍历 0x38 字节条目数组 = 枚举候选 pak;0x47A8480 wrapper 按返回值登记)

⑧ 最小语义补丁点:bl 标志 → 4 字节等长替换(RVA 0x261B56D / 0x261B56F)

步骤 ①#

对新旧两个 exe 做全量字符串提取与差异比对:

字符串
Refusing to mount unexpected pak file: %s(RVA 0x66A833A / fileoff 0x66A773A)
%sPaks/%s-%s.pak(RVA 0x673A1F2)
Pak chunk signing mismatch 等签名错误串有(非新增)

→ 拒绝逻辑整体是 7/25→9/4 之间源码新增,旧版不存在这道核对。

步骤 ②#

全镜像 xref(字节模式 48 8D 05 xx xx xx xx + 位移反算)命中唯一引用点,位于 RVA 0x1732560(fileoff 0x1731960)。该区域是典型 UE_LOG 编译产物——冷路径日志桩

0x1732560  sub  rsp, 0x38
           mov  [rsp+0x28], rcx          ; 参数 1:被拒 pak 路径
           lea  rax, [0x66A833A]         ; fmt = "Refusing to mount unexpected pak file: %s"
           mov  [rsp+0x20], rax
           lea  r8, [LogCategory]        ; LogPakFile
           xor  ecx, ecx                 ; 无文件名行号信息
           xor  edx, edx
           mov  r9b, 3                   ; verbosity = Warning
           call 0x31D420                 ; 统一日志输出
           ret

mov r9b, 3(Warning)与日志前缀 LogPakFile: Warning: 完全对应。该类桩是 UE_LOG 宏在 Shipping 构建下将”低概率分支”冷分离的结果。

步骤 ③#

唯一调用点 RVA 0x261C00Fcall 0x1732560),位于函数 0x261B410 内的拒绝打印块(0x261BD10–0x261C014)。桩调用前后无其它路径可达 → 该函数即本次新增核对的宿主。

步骤 ④#

关键区段反汇编(地址为 RVA):

0x261B4C1  mov   rbx, [rsi+0x108]        ; 取 "WindowsClient" 常量数据指针(见 §4)
0x261B4D0  call  0x93CBF0                ; 取根目录字符串 → [rsp+0x70](0x715F50 取全局)
0x261B4D5  cmp   [rsp+0x78], 0           ; 根目录为空?
0x261B4E2  lea   r8, [0x69BAC10]         ; 空 → 用空串回退常量
0x261B4EE  lea   rdx, [0x673A1F2]        ; fmt = "%sPaks/%s-%s.pak"
0x261B4F5  lea   r9,  [0x6C9FFB0]        ; 常量 "OPP"
0x261B504  call  0x1A250D0               ; FString::Printf → 期望串(相对形式)
0x261B514  call  0x5D0890                ; 结合全局根 → 规范化为绝对路径 → [rsp+0x80]

0x261B546  lea   rcx, [rsp+0xA0]
0x261B556  call  0x8EEE0                 ; 拷贝构造:实际 pak 路径 → [rsp+0xA0]
0x261B566  call  0x9F7D50                ; FString 全等比较(实际, 期望) → eax
0x261B56B  mov   ebx, eax                ; bl = 比较结果(== 的“放行标志”)
0x261B56D  test  al, al                  ; ┐
0x261B56F  jne   0x261B57E               ; ┴ 相等 → 直接跳汇合点(放行, bl=1)
0x261B571  cmp   [verbosity 全局], 3     ; 不相等时:
0x261B578  jae   0x261BD10               ;   LogPakFile 启用 Warning 级 → 打印拒绝日志
0x261B57E  ; —— 汇合点(不放行时 bl 保持 0)——
           mov   rcx, [rsp+0xA0]         ; 释放 [rsp+0xA0] 拷贝
           mov   rcx, [rsp+0x80]         ; 释放期望串
           mov   rcx, [rsp+0x60]         ; 释放入参路径
0x261B5B1  test  bl, bl
0x261B5B3  je    0x261BADF               ; bl=0 → 失败出口(函数返回 0,拒绝)
           ; —— bl=1:真实挂载路径 ——
0x261B5B9  ...  call [rax+0xC0]           ; 执行挂载(分配 0x18/0x298 对象,
0x261B629  call 0x261C400                ;  调用构造 0x261C400,检查 [rbx+0x249]==1)

拒绝打印块(冷块,落在函数末尾):

0x261BD10  cmp  [rsp+0xA8], 0            ; 实际路径串长度是否为 0
0x261BD18  je   0x261C008                ; 空 → rcx = 空串常量 0x69BAC10
0x261BD1E  mov  rcx, [rsp+0xA0]          ; 否则 rcx = 被拒 pak 路径
0x261C00F  call 0x1732560                ; 日志桩(唯一调用者)
0x261C014  jmp  0x261B57E                ; 汇合 → bl=0 → 拒绝返回

语义结论

  1. 该函数对每个候选 pak 调用一次,先构造”期望路径”再全等比较;
  2. 相等 → 放行进入真实挂载路径;不相等 → (按日志级别可选地)打印拒绝行 → 返回失败;
  3. 函数返回值(al)即”是否允许挂载”,由 bl(= 比较结果)一路携带。

步骤 ⑤#

0x9F7D50 完整反汇编(out\full_9f7d50.txt):按 FString 标准布局(+0 数据指针 / +8 长度 / +0xC 容量)以 0x8EEE0(FString 拷贝构造)复制两个操作数后逐 UTF-16 字符比较sete al 返回相等性;无子串、前缀、通配、忽略大小写等逻辑 → 比较是字典级精确全等。这与 %sPaks/%s-%s.pak 构造出的单一期望串配合,构成名符其实的”白名单”而非”命名模式”。

步骤 ⑥#

  • UTF-16 字面量 OPP 全镜像扫描:唯一命中 RVA 0x6C9FFB0(即 Printf 实参),不存在任何 OPP-* 之类的附加模板;
  • 期望模板格式串(%sPaks/%s-%s.pak)全镜像唯一(0x673A1F2);
  • 亦无读配置文件 / 注册表 / 目录枚举拼白名单的迹象(期望路径仅在 §4 一处构造)。

→ 放行名单只有 <Root>/Paks/OPP-WindowsClient.pak 一个名字,且名字三段中两段是编译期常量。改名无法绕过由此获得直接证据:改成任何 OPP-X.pak 都不等于期望串;改成 OPP-WindowsClient.pak 则与官方 59 GB 主包撞名,且撞上的老签名校验。

步骤 ⑦#

0x261B410 共 4 个调用点(均检查返回 al):0x550EE20x47A7C270x47A84F30x47A96A8。反汇编显示:

  • 0x47A7B10 区域存在遍历 0x38 字节/条目的数组循环,0x47A7C27 为该循环内调用点 → 典型的”枚举目录候选 pak → 逐个过闸门”结构;
  • 0x47A8480 为包装函数,按返回值决定是否把结果压入挂载列表(out\full_47a7c.txtfull_47a84.txt)。

→ 与日志现象一致:候选 pak 被逐一核对,命中的(官方包)进列表,未命中的被拒。

排除项#

Pak chunk signing mismatchMaster signature table has changed since initialization!Corrupt pak index detected 在新旧版本中均存在 → chunk 级签名检查是引擎既有机制,不是 9/8 新增。

硬编码来源#

Printf 调用现场(RVA 0x261B4D0–0x261B504)参数布局:

参数来源性质证据
%sPaks/%s-%s.pakRVA 0x673A1F2编译期字面量(新版独有)格式串模板
实参 1(%s,Root)0x93CBF0 构造的全局根目录串;为空时回退 0x69BAC10 空串常量唯一运行期成分0x715F50 读取全局(进程环境相关)
实参 2(%sRVA 0x6C9FFB0 OPP编译期 UTF-16 字面量全镜像唯一 OPP 命中
实参 3(%s0x944E00 装载 ANSI 常量 WindowsClient(RVA 0x6487A90),取其数据指针(对象 +0x108)编译期字面量见 4.2

源码形态#

// FPakPlatformFile 挂载入口(9/8 新增核对段,语义等价还原)
// 期望串 = FString::Printf(TEXT("%sPaks/%s-%s.pak"), *RootDir, TEXT("OPP"), TEXT("WindowsClient"))
FString ExpectedPak = NormalizeToAbsolute(              // 0x5D0890
    FString::Printf(TEXT("%sPaks/%s-%s.pak"),           // fmt = 0x673A1F2
        RootDir.IsEmpty() ? TEXT("") : *RootDir,        // 0x93CBF0 / 回退 0x69BAC10
        TEXT("OPP"),                                    // 0x6C9FFB0
        TEXT("WindowsClient")));                        // 0x6487A90 → 0x944E00

if (CandidatePakPath == ExpectedPak)                    // 0x9F7D50 精确全等
{
    // 真实挂载(0x261B5B9+,对象分配 + 构造 + 状态检查 [..+0x249]==1)
    return true;
}

if (LogPakFile_Verbosity >= Warning)                    // 0x261B571(冷路径)
    UE_LOG(LogPakFile, Warning, TEXT("Refusing to mount unexpected pak file: %s"),
        *CandidatePakPath);                             // fmt=0x66A833A,桩 0x1732560
return false;                                           // 0x261BADF

说明:无符号二进制无法还原到逐字源码,但”构造唯一期望串 → 全等比较 → 拒绝返回”的语义已被控制流与常量证据锁定,等价性无歧义。

修补方案#

bl(来自 mov ebx, eax)在汇合点 0x261B57E 直接决定放行与否(test bl,bl; je 失败出口)。因此只需让”不相等”分支也携带 bl=1 进入汇合点:

位置(fileoff / RVA)原字节原指令新字节新指令
0x261A96D / 0x261B56D84 C0test al, alB3 01mov bl, 1(强制置位放行标志)
0x261A96F / 0x261B56F75 0Djne +0x0DEB 0Djmp +0x0D(无条件进入挂载路径)
  • 等长 4 字节替换,无重定位/位移修正负担;原 cmp [verbosity],3 / jae 两条指令成为不可达死代码(语义上等价于”永不打印拒绝日志”);
  • 成功路径原有状态检查([rbx+0x249]==1、e0–ec 偏移量 or 校验)保留不动——补丁只拆除白名单,不触碰挂载自身的合法性校验(避免引入新的不稳定因素);

使用限制#

  1. 补丁版 exe 改变了文件内容,EAC(EasyAntiCheat)在线保护下可能拒绝启动;仅在免 EAC / 离线自娱场景使用,联机前请换回原版。绕过反作弊用于在线模式违反服务条款并有封号风险。
  2. 官方每次更新后需重放补丁。

局限与未决项#

  • 0x261B410 成功路径上另有 _P.pak 后缀判断(0x717070 = FString::EndsWith,RVA 0x261B6DE,后缀串 0x673A256)与若干子状态分支,属于挂载后的分类/登记逻辑,与白名单拒绝无关,语义未逐一还原——不影响本文结论。
  • EAC 对补丁 exe 的具体行为(拒启/报错形态)未做运行时验证,需由使用者在受控环境确认。

附录:关键地址速查表(新 exe,RVA;.text 段 fileoff = RVA − 0xC00)#

对象RVA(fileoff)说明
拒绝日志字符串0x66A833A(0x66A773A)Refusing to mount unexpected pak file: %s,新版独有
期望格式串0x673A1F2%sPaks/%s-%s.pak,新版独有
常量 OPP0x6C9FFB0UTF-16,全镜像唯一
常量 WindowsClient0x6487A90ANSI,元数据串池
常量 _P.pak0x673A256成功路径 EndsWith 用
日志桩0x1732560(0x1731960)UE_LOG Warning 冷路径,fmt 指针 LEA 于 0x1732569
宿主(闸门)函数0x261B410白名单核对 + 挂载
Printf 期望串0x261B4EE–0x261B5141A250D0 + 5D0890
FString 拷贝/全等0x8EEE0 / 0x9F7D50拷贝构造 / 精确全等比较
拒绝打印块0x261BD10–0x261C014空串回退 0x69BAC10
挂载成功路径0x261B5B9+vtable call [rax+0xC0]、构造 0x261C400、[+0x249]==1
失败出口0x261BADF返回 0
补丁点 10x261B56D(0x261A96D)84 C0B3 01
补丁点 20x261B56F(0x261A96F)75 0DEB 0D
上游调用者0x550EE2、0x47A7C27、0x47A84F3、0x47A96A8均检查返回 al;0x47A7B10 为枚举循环
The Outlast Trials 赛季7.1更新后 MOD 失效问题逆向分析报告
https://eterill.xyz/posts/tots71/
作者
EterIll
发布于
2026-09-09
许可协议
CC BY-NC-SA 4.0