安全策略和欺诈防御

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

Roblox 使用一个分布式物理系统,其中客户端对控制中的物体的物理模拟拥有权限,通常是玩家角色和距离角色附近的未锚定对象。此外,通过使用第三方软件,漏洞利用者可以在客户端上运行任意 Luau 代码来操纵其客户端的数据模型并反编译和查看在其上运行的代码。

总的来说,这意味着熟练的漏洞利用者可能会执行代码来作弊在你的游戏中,包括:

  • 传送他们自己的角色环绕这个场景。
  • 发射未安全的 RemoteEvents 或调用 RemoteFunctions ,例如授予自己物品而未获得它们。
  • 调整他们角色的 WalkSpeed 使其移动速度很快。

虽然您可以实现有限的设计防御来捕捉常见攻击,但强烈建议您实现更可靠的服务器端缓解策略,因为服务器是任何运行体验的最终权威。

防御性设计策略

基本设计决定可以作为遏止漏洞的“第一步”安全措施。例如,在一款射击游戏中,玩家通过击杀其他玩家获得积分时,漏洞可能会创建一堆机器人,它们可以快速传送到同一个位置,以便被击杀获得积分。考虑到这种潜在漏洞,请考虑两种方法和它们预期的结果:

方法可预测的结果
通过写代码来追踪机器人,以尝试检测它们。
减少或直接移除新生玩家的击杀点数获得。

虽然防御性设计显然不是完美或全面的解决方案,但它可以为更广泛的安全方法提供贡献,以及服务器端缓解

服务器端缓解

尽可能多地, 服务器 应该对什么是“真实”以及世界当前状态做最终判断。客户可以要求服务器进行更改或执行操动作,但服务器应该在结果复制到其他玩家之前验证并批准每一个更改/操作 除了某些物理操作外,客户端上数据模型的更改不会复制到服务器,因此主攻击路径通常是通过你用 RemoteEventsRemoteFunctions 宣布的网络事件来实现的。请记住,在您的客户端运行自己代码的漏洞可以使用任何他们想要的数据来调用这些。

远程运行时类型验证

一个攻击路径是让漏洞利用者使用 RemoteEventsRemoteFunctions 的参数类输入错误。在一些场景中,这可能会导致服务器上的代码在接收这些远程时出错,这对漏洞利用者有利。

当使用远程事件/函数时,您可以通过验证服务器上传递的 类型 来防止这种攻击。模块 “t”,可用于在这种方式进行类型检查。例如,假设模块的代码存在于 ModuleScript 命名为 tReplicatedStorage 内:

入门玩家脚本中的本地脚本

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest")
-- 在调用函数时传递部分颜色和位置
local newPart = remoteFunction:InvokeServer(Color3.fromRGB(200, 0, 50), Vector3.new(0, 25, 0))
if newPart then
print("The server created the requested part:", newPart)
elseif newPart == false then
print("The server denied the request. No part was created.")
end
服务器脚本服务中的脚本

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest")
local t = require(ReplicatedStorage:WaitForChild("t"))
-- 提前创建类型验证器以避免不必要的开销
local createPartTypeValidator = t.tuple(t.instanceIsA("Player"), t.Color3, t.Vector3)
-- 使用传递的属性创建新部分
local function createPart(player, partColor, partPosition)
-- 类型检查通过的参数
if not createPartTypeValidator(player, partColor, partPosition) then
-- 在此处检查类型失败时静默返回“false”
-- 在没有冷却时间的情况下提出错误可能会被滥用来拖慢服务器
-- 提供客户反馈取而代之!
return false
end
print(player.Name .. " requested a new part")
local newPart = Instance.new("Part")
newPart.Color = partColor
newPart.Position = partPosition
newPart.Parent = Workspace
return newPart
end
-- 将“createPart()”绑定到远程函数的回调
remoteFunction.OnServerInvoke = createPart

数据验证

滥用者可能发起的另一种攻击是发送 技术上有效的类型 但使它们极其巨大、长或其他不正确的形式。例如,如果服务器需要对包含长度扩展的字符串执行昂贵操作,漏洞利用者可以发送一个非常大或不正确的字符串,让服务器卡住。

相同地, both infNaNtype() 作为 number ,但两者都可能导致重大问题,如果漏洞发送它们,它们通过函数如关注中/正在关注不正确处理:


local function isNaN(n: number): boolean
-- NaN 永远不会等于自己
return n ~= n
end
local function isInf(n: number): boolean
-- 数字可以是 -inf 或 inf
return math.abs(n) == math.huge
end

滥用者可能使用的另一个常见攻击是在 tables 位置发送 Instance 。复杂的付载可以模仿通常的普通对象引用。

例如,提供了一个 体验商店 系统,其中价格等物品数据存储在 NumberValue 对象中,漏洞利用者可以通过以关注中/正在关注操作避开所有其他检查:

入门玩家脚本中的本地脚本

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local payload = {
Name = "Ultra Blade",
ClassName = "Folder",
Parent = itemDataFolder,
Price = {
Name = "Price",
ClassName = "NumberValue",
Value = 0, -- 也可以使用负值,结果是给予货币而不是接受它!
},
}
-- 向服务器发送恶意载荷(这将被拒绝)
print(buyItemEvent:InvokeServer(payload)) -- 输出“提供了无效物品”
-- 向服务器发送实物(这将通过!)
print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- Outputs "true" and remaining currency if purchase succeeds
服务器脚本服务中的脚本

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- 检查传递的项目是否未被伪造且位于物品数据文件夹
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- 服务器可以继续处理以下示例流程的购买
end
-- 将“buyItem()”绑定到远程函数的回调
buyItemEvent.OnServerInvoke = buyItem

价值验证

除了验证 类型数据 之外,你还应该验证 通过 RemoteEventsRemoteFunctions 传递,确保它们在要求的上下文中是有效且合理的。两个常见例子是经验商店和武器瞄准系统。

经验购物

考虑带用户界面的经验商店系统,例如带“购买”按钮的产品选择菜单。当按钮被按下时,您可以在客户端和服务器之间调用 RemoteFunction 来请求购买。然而,重要的是, 服务器 ,体验最可靠的管理员,必须确认用户有足够的资金来购买物品。

Example purchase flow from client to server through a RemoteEvent
客户端到服务器的例购买流程通过一个 RemoteFunction

武器瞄准

战斗场景需要特别关注验证值,特别是通过瞄准和命中验证。

想象一个游戏,在那里玩家可以向另一名玩家发射激光束。而不是客户告诉服务器 要损坏,它应该告诉服务器射击的起源位置和它认为击中的部件/位置。服务器可以验证以关注中/正在关注内容:

  • 客户端报告的 射击位置 靠近服务器上玩家角色。请注意,由于延迟,服务器和客户端会略有不同,因此需要应用额外的容差。

  • 客户端报告的位置 击中 与服务器上客户端报告击中的 部分 位置相对较近

  • 客户端报告射击的位置与客户端报告射击到的位置之间没有静电障碍。这个检查确保客户端不试图穿墙射击。请注意,这仅检查静态几何图形,以避免由于延迟导致有效射击被拒绝。 另外 ,您可能想要实现以下服务器端验证:

  • 跟踪玩家最后发射武器的时间,并验证以确保他们没有过于频繁地射击。

  • 在服务器上跟踪每个玩家的弹药数量,确认发射玩家拥有足够的弹药来执行武器攻击。

  • 如果你已实现 团队 或一个“玩家对抗机器人”战斗系统,确认命中角色是敌人,而不是队友。

  • 确认命中的玩家还活着。

  • 在服务器上存储武器和玩家状态,确认发射玩家不被当前操作,例如重新装弹或状态像跑步等阻止。

数据存储操作

在使用 DataStoreService 保存玩家数据的体验中,恶意程序可利用无效的 数据 和更加隐秘的方法,防止 DataStore 正常保存。这可能在与物品交易、市场和类似系统的经验中特别滥用,其中物品或货币会离开玩家的道具。

确保任何通过 RemoteEventRemoteFunction 对玩家数据进行客户端输入影响的操作都基于以关注中/正在关注内容进行了清洁:

  • Instance 值无法序列化为 DataStore 并将失败。使用 类型验证 来防止这种情况。
  • DataStores数据限制。应检查并/或限制随机长度的字符串,以避免这种情况,同时确保客户端无法添加无限随机键到表。
  • 表索引不能是 NaNnil 。遍历客户端传递的所有表并验证所有索引都有效。
  • DataStores 只能接受有效的UTF-8字符,因此您应该通过 utf8.len() 对客户端提供的所有字符进行验证,确保它们有效。utf8.len() 将返回字符串的长度,将Unicode字符视为单个字符;如果遇到无效的UTF-8字符,将返回nil和无效字符的位置。请注意,无效的 UTF-8 字符串也可以出现在表中作为键和值。

远程限速

如果客户能使您的服务器完成计算成本高的操作或访问限速服务,例如 DataStoreService 通过 RemoteEvent 访问率限制服务,那么您必须实现 限速限制 以确保操作不会过于频繁地调用。限制速率可以通过跟踪客户端最后调用远程事件的时间并拒绝下一次请求来实现。如果太快地调用,则下一次请求将被拒绝。

运动验证

对于竞争体验,您可能希望在服务器上验证玩家角色移动,以确保它们不会在地图上传送或移动速度超过可接受的水平。

  1. 在 1 秒的增量中,检查角色的新位置与之前缓存的位置进行比较。

    Image showing moving character's position on a straight path in increments of 1 second
  2. 根据角色的 WalkSpeed (每秒钟的点数) 最大值来确定距离上的“可容忍”变化,乘以 ~1.4 以允许对服务器延迟的一些宽容。例如,在默认值 WalkSpeed 的 16 上,可接受的差异是 ~22。

    Image showing tolerable change in distance based on character's walk speed
  3. 将实际距离差与可容差差进行比较,然后按照以下步骤继续:

    • 对于可容忍的差异,在准备下一次增量检查之前,将角色的新位置缓存。
    • 对于意外或不堪承受的差异(潜在速度/传送漏洞):
      1. 为玩家增加一个单独的“犯罪数量”值,而不是因极端服务器延迟或其他非漏洞因素导致的“错误正确”而惩罚他们。
      2. 如果在 30-60 秒期间发生大量违规,那么 Kick() 完全重置经验中的玩家;否则,重置“违规数量”计数。请注意,当踢出玩家欺诈时,记录事件是最佳实践,以便您可以跟踪多少玩家受到影响。