cfx.fivemcn.cc/v1:FiveM 授权协议与保护层全流程
基于已还原的 FiveM Lua 样本,逐步梳理 /v1/verify 与 /v1/version 的请求、响应、状态机、TLS 边界和安全缺口。
这篇文章分析的是一份已获授权的 FiveM 资源样本。样本版本为 1.0.2,服务端 Lua 会访问 cfx.fivemcn.cc/v1/verify 和 cfx.fivemcn.cc/v1/version。本文把资源启动、请求构造、响应处理、失败分支和外围代码保护逐层拆开,并明确哪些结论已经由原 VM 验证,哪些服务端细节仅凭请求端样本无法得知。
先说明最重要的结论:样本里不存在一套名为“cfx.fivemcn.cc/v1 加密算法”的应用层密码协议。请求体是普通 JSON,请求端也预期并按 JSON 解析响应 body;网络上的机密性来自 HTTPS/TLS。Luraph、Base85、LZMA 和虚拟机用于保护落盘 Lua,它们不参与 /v1 请求体加密。
本文不会公开真实授权密钥或完整资产标识,也不讨论绕过授权。示例中的 <REDACTED_KEY> 和 <ASSET_ID> 都是占位符。
一张表看清所有层
| 层次 | 样本中的实际机制 | 准确分类 | 是否属于 /v1 应用层加密 |
|---|---|---|---|
| Lua 落盘保护 | LPH、Base85、raw LZMA1、自定义 VM | 编码、压缩、序列化、虚拟化 | 否 |
| 网络传输 | https://cfx.fivemcn.cc/... |
TLS 传输加密 | 否,由 FiveM 运行时处理 |
| 请求体 | json.encode({ ... }) |
JSON 序列化 | 否 |
| 响应体 | json.decode(body) |
JSON 反序列化 | 否 |
| 授权判断 | HTTP 状态码、error、asset |
业务状态机 | 否 |
| 版本判断 | HTTP 状态码、latest、latest_version |
业务状态机 | 否 |
在恢复出的业务代码中,没有发现 AES、RSA、ChaCha、XXTEA、HMAC、数字签名、nonce、timestamp、IV、认证标签、密钥派生或响应体二次解密。不能因为源码本身经过了 VM 虚拟化,就把随后发出的 JSON 请求也称为“VM 加密协议”。
从资源启动到授权请求
资源的 manifest 按下面的顺序加载服务端文件:
server_scripts {
'sv_config.lua',
'server/bridge.lua',
'server/gamemodes/xzdd.lua',
'server/gamemodes/jpmj.lua',
'server/main.lua'
}
escrow_ignore {
'sv_config.lua',
'shared/config.lua',
'server/bridge.lua'
}
sv_config.lua 先把授权密钥写入全局变量 KEY,server/main.lua 随后读取它。sv_config.lua 位于 escrow_ignore 中,说明服主需要能够编辑这份配置。对资源进程而言,KEY 是普通 Lua 字符串,不是只存在于某个硬件密钥区的秘密。
受保护的 server/main.lua 则先经历本地恢复链:
LPH 文本载荷
-> Base85 解码
-> raw LZMA1 解压
-> VM loader + 二进制 blob
-> Prototype / 指令 / 常量反序列化
-> 创建并运行 Lua VM entry closure
-> 调用 FiveM PerformHttpRequest
前五步不依赖 cfx.fivemcn.cc,也没有从该域名领取“解密密钥”。关于这部分保护层的结构,可以参阅 Luraph VM 虚拟化:从 LPH 双流到可读 FiveM Lua。
完整启动时序可以压缩为:
FXServer
-> 加载 sv_config.lua,取得 KEY
-> 运行受 Luraph 保护的 server/main.lua
-> 创建 authPromise
-> POST /v1/verify
-> 等待回调 resolve 或 reject
-> resolve:继续注册麻将服务端逻辑,并启动版本检查
-> reject:打印失败信息,进入不 yield 的无限循环
这也解释了为什么客户端 Lua 中找不到 cfx.fivemcn.cc:两个 /v1 请求都位于服务端 server/main.lua,不是客户端请求。
/v1/verify 请求格式
还原出的 method、URL、header 和 body 可以写成下面的伪 HTTP 形式;实际协商的 HTTP 版本不由这段 Lua 决定:
POST /v1/verify
Host: cfx.fivemcn.cc
Content-Type: application/json
{
"key": "<REDACTED_KEY>",
"aid": "<ASSET_ID>"
}
对应的 FiveM Lua 是:
PerformHttpRequest(
'https://cfx.fivemcn.cc/v1/verify',
callback,
'POST',
json.encode({
key = KEY,
aid = '<ASSET_ID>'
}),
{ ['Content-Type'] = 'application/json' }
)
这里有几个容易误读的点:
key来自服主可编辑的sv_config.lua。aid是当前构建写死的 43 字符资产标识,不是随机数,也不是一次性挑战。它的外形符合无填充 Base64url 编码 32 字节数据的特征,但样本没有它的生成过程,不能据此断言原数据是哈希还是密文。- 请求体没有显式 IP 字段。
403的提示虽然提到绑定 IP,但服务端如何取得和比较 IP 只能由服务端实现决定。 - Lua 显式传入的 headers 中没有
Authorization、自定义签名头或 Cookie;这不能排除运行时或代理层添加其他头。 - 调用没有传入
options,按当前 FiveM Lua scheduler 的默认值会跟随重定向;具体重定向请求语义由运行时 HTTP 实现决定。 - 回调只使用状态码和 body,忽略响应 headers 与
errorData,也不核对响应Content-Type。 - Lua table 序列化后的 JSON 字段顺序不应被当作协议要求。
- 同一组字段可以由普通 HTTPS 客户端构造,不需要运行 Luraph VM;VM 只是隐藏了这段业务代码。
/v1/verify 响应算法
原 VM 的授权回调已经通过隔离 stub 中的离线差分验证;这不是线上接口实测。其高层逻辑是:
local response = json.decode(body)
if statusCode == 200 then
print(('授权通过 · %s'):format(response and response.asset or ''))
authPromise:resolve(true)
return
end
authPromise:reject(
response and response.error
or AUTH_ERROR_MESSAGES[statusCode]
or ('HTTP ' .. tostring(statusCode))
)
内置状态映射如下:
| HTTP 状态 | 本地回退提示 |
|---|---|
| 401 | 密钥无效 |
| 402 | 授权已过期 |
| 403 | 与绑定的 IP 不符,请联系该资产创作者 |
| 410 | 授权已撤销 |
| 421 | 资产授权不匹配,请重新下载资产 |
| 500 | 服务器内部错误 |
| 其他 | HTTP <status> |
如果非 200 响应中的 response.error 为 Lua truthy 值,服务端返回的文本优先于这张本地映射表;值为 false 或 nil 时会继续使用本地回退。
这里的“通过条件”非常宽:只要 HTTP 状态是 200 且响应体能够被 json.decode 处理,promise 就会 resolve(true)。代码不会检查 success、不会要求 asset 存在、不会核对响应中的 aid,也不会验证响应签名。因此以下几个 200 响应在当前请求端状态机中没有区别:
{ "asset": "Br_Mahjong" }
{}
{ "error": "some error text" }
后两种写法不代表真实服务端会这样返回,只说明请求端没有更严格的语义校验。起始 URL 仍通过 HTTPS/TLS 按运行时信任策略认证对端;由于当前调用默认跟随重定向,最终响应是否经过其他目标和协议跳转还取决于运行时策略。“没有应用层签名”不等于“网络上完全没有服务端身份验证”。
授权失败状态机
回调先执行 json.decode(body),之后才检查 HTTP 状态。这一顺序决定了几个特殊分支:
收到 HTTP 回调
-> json.decode(body) 成功?
否:异常逃出回调,authPromise 可能一直 pending
是:statusCode == 200?
是:resolve(true)
否:reject(error / 本地映射 / HTTP 状态)
Citizen.Await(authPromise)
-> resolve:继续启动
-> reject:pcall 返回 false,打印错误,进入 while true do end
-> pending:继续等待
离线原 VM 与重建代码的十场景差分已经确认:无效 JSON 会逃出回调,promise 不会自动 resolve 或 reject。资源代码本身也没有设置授权超时。
Citizen.Await 外层使用了 pcall。promise 被 reject 后,脚本打印授权失败和“资源已停止运行”,但实际代码不是 return 或正常停止资源,而是:
while true do end
这是一个不 yield 的忙循环。它对不同 FXServer 构建和调度环境的真实影响尚未做实机验证,因此不能仅凭静态代码断言会卡住整个服务器;可以确认的是,这不是一种安全的失败退出方式。
/v1/version 请求格式
只有授权 promise 成功后,代码才会读取 manifest 中的版本:
local currentVersion = GetResourceMetadata(
GetCurrentResourceName(),
'version',
0
)
当前样本得到 1.0.2,随后立即调用一次版本接口。下面仍是伪 HTTP 表示,不指定实际协商的 HTTP 版本:
POST /v1/version
Host: cfx.fivemcn.cc
Content-Type: application/json
{
"key": "<REDACTED_KEY>",
"version": "1.0.2"
}
请求仍然只是 json.encode,并会再次发送同一个 KEY。代码随后创建线程,每等待 3,600,000 毫秒,也就是一小时,再发起一次相同类型的版本请求:
checkVersion()
Citizen.CreateThread(function()
while true do
Citizen.Wait(3600000)
checkVersion()
end
end)
这一小时任务是版本检查,不是重新授权。当前资源只在首次启动时执行 /v1/verify。
/v1/version 响应算法
版本回调与授权回调的处理顺序不同:
if statusCode ~= 200 then
return
end
local response = json.decode(body)
if response and response.latest == false then
print(('发现新版本 %s,当前版本 %s')
:format(response.latest_version or '?', currentVersion))
end
因此:
- 非 200 响应被静默忽略,而且不会先解析 JSON。
- 200 响应才会执行
json.decode。 - 只有
latest严格等于布尔值false才提示更新。 - 新版本号来自
latest_version;字段缺失时显示?。 - 响应没有签名或哈希校验。
- 返回内容只进入日志,不会作为 Lua、JavaScript 或二进制代码执行。
静态扫描没有发现版本包 URL、下载器、自动覆盖文件、loadstring 或“下载后执行”链。Luraph 外层确实使用 load 来加载本地解压出的 VM loader,但那是源码保护的本地启动步骤,不能和版本接口混为一谈。
HTTPS 到底加密了什么
FiveM 的 Lua PerformHttpRequest 会把 URL、method、data、headers 和重定向选项交给运行时的内部 HTTP 实现,再通过内部响应事件把 status、body、headers 和错误信息交回 Lua callback。当前业务脚本只传了前四类数据,没有自行设置 TLS 参数。
从样本中能确认的是:
- URL 使用
https://,所以请求头和 JSON body 会进入 HTTPS/TLS 连接。 KEY在 TLS 建连前已经以 JSON 明文存在于 FXServer 进程内存中。- 对端或中间的 TLS 终止代理解密后,也会看到普通 JSON。
- Lua 没有在 TLS 之外再包一层密文,也没有对 body 做认证签名。
从样本中不能确认的是某次线上连接最终协商的 TLS 版本、密码套件、证书链、反向代理实现或服务端私钥。这些都取决于当时的 FXServer 构建、操作系统、DNS、网络路径和线上服务配置,不能根据一段 Lua 静态代码编造。
所以“完整加密流程”的准确表述应是:
Lua table
-> json.encode
-> UTF-8 JSON 明文
-> FiveM HTTP 运行时
-> HTTPS/TLS 加密传输
-> 服务端或重定向目标终止 TLS
-> 对端取得 JSON 请求体并执行授权判断
-> 对端返回响应 body,请求端预期其为 JSON
-> HTTPS/TLS 加密传输
-> FiveM HTTP 运行时解密
-> Lua json.decode
TLS 内部当然会使用密码学算法,但具体算法是握手协商结果,不是这个资源自定义的 /v1 算法。
从请求端样本能推断什么
状态码和提示文本能证明请求端预期服务端至少区分这些业务结果:密钥无效、过期、IP 不匹配、撤销、资产不匹配和内部错误。它们可以形成服务端实现的候选流程:
读取 key 和 aid
-> 查找授权
-> 检查有效期
-> 检查撤销状态
-> 检查资产绑定
-> 检查来源 IP 或相关绑定
-> 返回 HTTP 状态与 JSON
但这只是依据请求端错误语义整理出的候选顺序。当前样本无法证明服务端实际按什么顺序检查,也无法恢复:
- 授权数据库结构;
- 密钥的生成算法、长度规则和存储形式;
- 服务端保存的是明文 key 还是哈希;
- IP 的取值、代理头信任规则和绑定策略;
- 过期、撤销和资产关联的真实数据模型;
- 成功响应的完整 JSON schema;
- 当前线上接口是否仍与该
1.0.2样本一致。
要回答这些问题,需要服务端源码、服务端日志或经过授权的实时观测。仅凭 Lua 请求端不能“反推出”不存在于样本中的后端实现。
与 FiveGuard、ac 'fg' 和 svadhesive 的边界
同一批分析中还出现过 FiveGuard VM、私有字节 codec、fg:Comm、函数引用桥、.net.dll 和 ac 'fg'。它们与这里的 /v1 授权协议没有静态关联:
/v1的调用点只在麻将资源的服务端 Lua 中。- FiveGuard 的事件名、codec 和函数引用桥没有引用
cfx.fivemcn.cc。 - 标准 FiveM 中,未知 manifest 指令会成为 metadata;仅凭
ac 'fg'不能证明运行时自动注入某个 DLL。 - 本次核查的特定
svadhesive.dll样本只显示下载阶段的合规过滤或原地流处理,没有证据表明它构造了这两个 JSON 请求;这一结论不能自动套用到其他构建。
名称中都出现“cfx”或运行在同一个 FiveM 环境,不足以把这些机制拼成一套加密链。
协议安全性审计
下面只评价已经观察到的请求端设计,不推断线上服务是否另有防护:
| 观察项 | 当前行为 | 影响 |
|---|---|---|
| 授权密钥保存 | 可编辑 Lua 配置中的普通字符串 | TLS 不能保护磁盘、日志或进程内存中的 key |
| 请求认证 | key + aid,无 nonce、时间戳或签名 |
应用层本身没有重放新鲜度证明 |
| 响应认证 | 依赖 HTTPS;无应用层签名 | 请求端信任 TLS 终止点返回的状态和 body |
| 200 成功校验 | 只检查 HTTP 200 | 缺失字段或带 error 的有效 JSON 仍会通过 |
| JSON 异常处理 | 授权回调先裸调用 json.decode |
畸形响应可能让 promise 保持 pending |
| 超时 | 资源层没有显式授权超时 | 网络或回调异常时启动可能长期等待 |
| 失败退出 | while true do end |
存在忙循环和调度风险 |
| 版本检查 | 每小时再次发送 key | 扩大 key 被日志、代理或观测系统接触的次数 |
| 更新行为 | 只打印 latest_version |
当前没有远程代码执行或自动覆盖风险 |
如果重新设计这套授权请求端,更稳妥的方向是:
- 对 JSON 解码使用受控错误处理,并保证每条回调路径都会 resolve 或 reject。
- 增加明确的授权超时和可恢复的失败策略,不使用不 yield 的无限循环。
- 对 200 响应验证固定 schema、业务成功字段、资产标识和有效期。
- 如果威胁模型要求独立于 TLS 的响应真实性,可使用服务端签名的短期授权结果,并把请求挑战值纳入签名;不要自创加密算法。
- 版本检查若不需要完整授权 key,就使用权限更小的令牌或公开版本元数据。
- 保持版本响应只作为数据处理,不把远端字符串直接交给
load、命令行或文件覆盖逻辑。
证据等级与验证边界
本文结论来自四层证据:
- 从原始 Luraph 包装中静态提取 VM loader、数据 blob、Prototype、指令和常量。
- 把授权与版本 Prototype 提升为普通 FiveM Lua,并核对 URL、method、字段、状态分支和调度顺序。
- 在隔离 stub 中对原 VM 与高层重建运行十组授权响应差分,包括成功、拒绝、空对象、多个 HTTP 状态和无效 JSON;已覆盖场景的可观察结果一致。
- 对照 FiveM 开源 Lua scheduler,确认
PerformHttpRequest如何把 Lua 参数交给内部 HTTP 请求并分发回调。
公开运行时证据可以直接在固定版本源码中复核:
这些证据足以说明当前样本的请求端协议和状态机,但没有执行真实授权 key,也没有取得服务端源码。本文因此不声称恢复了后端密钥算法、数据库或当前线上部署配置。
最终结论
cfx.fivemcn.cc/v1 在这份资源中的完整可证流程只有两条:
启动授权:KEY + 固定 aid -> HTTPS POST /v1/verify
-> 按 JSON 解析的响应 body + HTTP 状态
-> resolve 后继续启动 / reject 后失败分支
版本检查:KEY + manifest version -> HTTPS POST /v1/version
-> latest / latest_version
-> 立即一次,之后每小时一次
它没有自定义请求加密、签名、随机挑战或响应解密,也没有从接口下载 Lua 源码。真正的密码学边界是 HTTPS/TLS;真正的代码保护边界是本地 Luraph VM。把这两层分开,才能准确理解这份资源,也能避免把普通 JSON、源码混淆和传输加密误写成同一种“加密算法”。
