FiveM

FXAP 二进制结构与只读解析

根据当前工具代码整理 FXAP 外层封装、控制文件、内层偏移和资源 ID,并提供不触碰密钥的安全检查器。

.fxap 在实际资源目录中有两种含义。第一种是资源根目录下名为 .fxap 的控制文件,第二种是仍保留 .lua、.ydr、.ytd 等原扩展名,但文件内容以 FXAP 开头的受保护文件。

本文根据 CK 当前工具中的 decryptor-raw.js、escrow-decryptor.js 和 decrypt-eup-stream.js 整理可重复观察到的结构。它不是 Cfx.re 官方格式规范,也不包含密钥、解密常量、授权获取或绕过步骤。

先区分文件名与文件头

不能只根据文件名判断:

  • 根目录的 .fxap 通常承担资源控制和资源 ID 定位。
  • 被封装的内容文件可以继续叫 client.lua、model.ydr 或 texture.ytd。
  • 当前代码统一读取前 4 字节,只有 ASCII FXAP 才进入 FXAP 分支。
  • 普通 RSC7、RSC8 或明文文件不会因为位于同一目录就自动被认定为 FXAP。

verifyFxap() 和 isFxapFile() 都只做 4 字节 magic 检查。因此它们是快速分类器,不是完整性验证器。

外层封装

当前代码稳定使用下面的外层偏移:

十进制偏移 十六进制偏移 长度 当前解释
0 0x00 4 ASCII FXAP magic
4 0x04 70 工具未命名的不透明头部区域
74 0x4A 12 外层 IV 区域
86 0x56 到文件末尾 外层负载

因此要成为包含非空负载的完整候选,文件至少需要 87 字节。当前代码没有把 4 到 73 字节拆成稳定字段,也没有在这里读取版本号、总长度或校验和。公开解析器不应给这 70 字节编造字段名称。

外层处理后的内层布局

在合法授权环境已经得到内容文件的内层 buffer 后,代码优先尝试一个长度驱动布局:

内层偏移 长度 当前解释
0 4 未命名的内层前缀
4 2 nameLength,UInt16 Little Endian
6 nameLength 文件名或名称字段
6 + nameLength 12 内层 IV 区域
18 + nameLength 到末尾 内层负载

代码用 readUInt16LE(4) 读取名称长度,再计算后续两个边界。它并没有先验证名称编码,也没有确认每种资源都遵循相同版本。因此安全解析器必须做长度检查,不能直接相信 nameLength。控制 .fxap 的 decoded 元数据只可靠用于读取资源 ID,不应自动套用这张内容文件布局表。

旧分支中还存在固定偏移和按文件扩展名寻找边界的启发式回退。那些路径用于兼容样本差异,不是稳定格式字段,本文不会把它们写成正式规范。

控制文件中的资源 ID

根目录 .fxap 控制文件经过合法外层处理后,当前代码从内层的 74 到 77 字节读取资源 ID:

offset 0x4A
length 4 bytes
byte order big endian

源码使用 buffer.slice(74, 78).toString('hex') 再交给 parseInt(..., 16),等价于把这 4 个字节按大端无符号整数理解。

这个位置也被部分 stream-only 处理路径用作候选资源 ID。它是当前样本和代码路径验证过的约定,但仍不是官方承诺。解析结果为 0、buffer 少于 78 字节,或对应授权元数据不存在时,工具都会把它视为无法定位。

一个不解密的外层检查器

下面的 Node.js 示例只读取文件大小、magic 和结构边界。它不会读取密钥,也不会处理受保护负载:

const fs = require('node:fs');

function inspectFxapEnvelope(buffer) {
  if (!Buffer.isBuffer(buffer)) {
    throw new TypeError('input must be a Buffer');
  }
  if (buffer.length <= 86) {
    throw new Error('file is too short for the observed FXAP envelope');
  }

  const magic = buffer.subarray(0, 4).toString('ascii');
  if (magic !== 'FXAP') {
    throw new Error('not an FXAP envelope');
  }

  return {
    magic,
    fileBytes: buffer.length,
    opaqueHeader: { offset: 4, length: 70 },
    outerIv: { offset: 74, length: 12 },
    outerPayload: { offset: 86, length: buffer.length - 86 },
  };
}

const file = process.argv[2];
if (!file) throw new Error('usage: node inspect-fxap.js <file>');
console.log(JSON.stringify(inspectFxapEnvelope(fs.readFileSync(file)), null, 2));

这个检查器能回答“它是否符合当前观察到的外层框架”,不能回答“文件是否拥有有效授权”或“负载是否能正确加载”。

解析已经取得的内层 buffer

如果调用方已经在合法授权流程中取得内层 buffer,可以只解析名称边界和控制文件资源 ID:

function inspectDecodedInner(buffer) {
  if (!Buffer.isBuffer(buffer) || buffer.length < 18) {
    throw new Error('decoded buffer is too short');
  }

  const nameLength = buffer.readUInt16LE(4);
  const nameOffset = 6;
  const innerIvOffset = nameOffset + nameLength;
  const payloadOffset = innerIvOffset + 12;

  if (payloadOffset >= buffer.length) {
    throw new Error('nameLength points outside the decoded buffer');
  }

  return {
    nameLength,
    nameOffset,
    innerIv: { offset: innerIvOffset, length: 12 },
    payload: { offset: payloadOffset, length: buffer.length - payloadOffset },
  };
}

function readDecodedControlResourceId(buffer) {
  if (!Buffer.isBuffer(buffer) || buffer.length < 78) {
    throw new Error('decoded control buffer is too short');
  }
  return buffer.readUInt32BE(74);
}

示例故意不把名称字段直接当 UTF-8 输出。二进制解析器应先确认版本、编码和可打印范围,再决定如何展示文本。

解析顺序

一个稳妥的只读分析流程是:

  1. 根据目录位置区分根控制文件和普通资源文件。
  2. 检查文件长度至少为 86 字节。
  3. 检查前 4 字节是否为 FXAP。
  4. 记录外层头部、IV 和负载的边界,不解释未知字段。
  5. 只有在已有合法授权结果时,才解析内层 nameLength 和资源 ID。
  6. 对所有加法做越界检查,拒绝异常长度。
  7. 把 Lua、RSC7、RSC8 magic 检查看作 sanity check,而不是完整验证。

当前代码中最容易误解的地方

FXAP magic 不等于文件有效

任意文件都可以伪造前 4 字节。只检查 magic 无法证明头部长度、负载、授权或内容完整。

.fxap 控制文件不等于所有内容文件

控制文件帮助定位资源级元数据。真正的内容文件通常保留原扩展名,需要逐个检查文件头。

资源 ID 不等于密钥

资源 ID 只是授权数据中的索引。读出 ID 不会产生授权,也不能据此还原受保护负载。

文件签名只是一层快速检查

当前后处理把 Lua bytecode、RSC7 和 RSC8 的 4 字节头作为成功候选。它无法证明模型页表、纹理、碰撞、音频或 Lua 语义完整。

与 RPF 的关系

FXAP 和 RPF 解决不同层次的问题:

  • FXAP 是资源文件或控制元数据的受保护封装标识。
  • resource.rpf 是资源归档,内部有目录、条目和名称表。
  • RPF 展开后仍可能得到以 FXAP 开头的文件。
  • 判断 RPF 成功展开,不能替代对每个 FXAP 文件的边界检查。

这份结构图适合做检测、分类和故障定位。若未来样本出现不同头长、不同字段版本或官方格式更新,应先更新解析器的边界验证,再调整字段解释,不能依靠固定偏移静默读取。