找回密码
 register
搜索
查看: 58|回复: 0

洱海月 交易中背包拖拽物品漏洞 欺诈交易 修复说明

[复制链接]
  • 打卡等级:本地老炮
  • 打卡总天数:533
  • 打卡月天数:22
  • 打卡总奖励:530
  • 最近打卡:2026-06-24 01:45:59
Waylee 发表于 2026-4-1 15:17 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?register

×

问题现象

玩家 A 把物品 A 放入交易盒后,如果客户端继续重放背包拖拽交换封包,仍然可以把背包里的 A 和 B 强行换位,或者把 A 拖到空格。

这样会出现一个很危险的结果:

  1. 目标玩家交易界面里看到的还是 A。
  2. 但服务端最终结算时,真实背包里的位置已经被改成了 B。
  3. 最后就变成“界面展示 A,实际收到 B”。

这本质上是:交易展示使用的是“放入交易盒那一刻的对象引用”,而背包真实状态又被后续重放包偷偷改掉了。


最终修复思路

这次修复的正确做法不是“拦截所有锁定物品拖拽”,而是:

  1. 只拦截 正在交易中的物品
  2. 判断依据使用 item:is_in_exchange()
  3. 不使用 item:is_lock() 作为这条漏洞的最终拦截条件。

原因很重要:

  1. is_lock() 在这个项目里既可能表示玩家手动锁,也可能表示交易临时锁。
  2. 如果直接用 is_lock() 拦截,会把普通锁定物品和交易中的物品混为一谈。
  3. 这会导致交易结束后,客户端还可能认为物品不能拖动,或者误伤正常逻辑。

所以最终版修复的核心是:

  1. is_in_exchange() 专门识别“交易态”。
  2. 只封交易态物品的拖拽交换。
  3. 交易结束后,再主动同步背包格子给客户端,确保客户端立刻看到“已解锁”的状态。

涉及文件

本次最终修复涉及 3 个位置:

  1. lualib/item.lua
  2. services/scene/scenecore.lua
  3. lualib/exchange_box.lua

其中:

  1. item.lua 负责保存物品是否正在交易。
  2. scenecore.lua 负责拦截背包拖拽交换请求。
  3. exchange_box.lua 负责交易结束时把客户端背包状态刷新回来。

第一步:给物品增加真正的交易状态

文件:lualib/item.lua

之前交易流程里虽然已经在调用:

item:set_in_exchange(true)
item:set_in_exchange(false)

但最早这个函数本身是空实现,等于没有真正保存状态。

后来补成了真正的字段:

function item:ctor()
    self.guid = guid_cls.new()
    self.item_index = -1
    self.rule = 0
    self.status = 0
    self.in_exchange = false
    ...
end

function item:is_in_exchange()
    return self.in_exchange == true
end

function item:set_in_exchange(in_exchange)
    self.in_exchange = in_exchange and true or false
end

这一步是整个修复能成立的基础。

因为只有这样,服务端才能分清楚:

  1. 这件物品只是锁了。
  2. 还是这件物品正处于交易流程中。

第二步:定位背包拖拽交换入口

客户端消息入口在:

services/msgagent.lua

function request:CGPackage_SwapItem()
    skynet.call(my_scene, "lua", "package_swap_item", my_obj_id, self)
end

真正处理逻辑在:

services/scene/scenecore.lua

function scenecore:package_swap_item(whos, swap)
    local obj = self.objs[whos]
    ...
end

也就是说,所有背包拖拽换位、拖到空格、同类叠加,都会经过这个函数。

所以这里是最佳拦截点。


第三步:原始漏洞为什么会出现

原始代码里有这样一段:

if item_to then 
    if not item_from:can_lay()
    or item_from:is_lock()
    or item_to:is_lock() then
        is_change = true
    else
        -- 尝试叠加
    end
else
    is_change = true
end

后面只要 is_change == true,就直接交换:

bag:set_item(from,item_to)
bag:set_item(to,item_from)

问题就在这里:

  1. 交易中的物品放入交易盒后,会被 lock_item()
  2. package_swap_item() 并没有把“锁定”当成非法操作直接拒绝。
  3. 它反而把“锁定”当成“不能叠加,那就改成交换”。

于是就变成了:

  1. 交易物品因为锁定,反而更容易落入交换分支。
  2. 重放拖拽包后,交易中的 A 还能被换成 B。

第四步:为什么不能直接用 is_lock()

这个问题是本次修复里最容易走偏的地方。

一开始很容易想到:

if item_from:is_lock() then
    reject_swap(...)
    return
end

但这样会出问题,因为这个项目里的 is_lock() 语义不纯。

4.1 is_lock() 实际承载了两种状态

它既可能表示:

  1. 玩家手动给物品加的普通锁。

也可能表示:

  1. 交易、摆摊这类流程中的临时锁。

所以直接用 is_lock() 拦截,就把两类完全不同的场景混到一起了。

4.2 混用 is_lock() 的副作用

如果这条漏洞修复直接按 is_lock() 拦截,会有这些风险:

  1. 普通锁定物品也被当成交易物品处理。
  2. 交易结束后,客户端仍可能残留“不可拖动”的显示。
  3. 以后如果别的系统也复用这个锁位,也会一起被误伤。

所以最终版修复做了语义拆分:

  1. is_lock() 继续保留给系统原有用途。
  2. 交易漏洞拦截只看 is_in_exchange()

这才是正确的收口方式。


第五步:最终版 package_swap_item() 修法

文件:services/scene/scenecore.lua

最终版关键逻辑如下:
***付费内容*** ">[/kkpay]

第六步:关键修改点解释

6.1 先刷新锁状态

obj:refresh_locked_item_unlock_state(false)

虽然最终不是用 is_lock() 做拦截,但这一步仍然保留。

作用是:

  1. 让背包里的锁信息先更新到服务端最新状态。
  2. 避免客户端残留旧锁定显示时,服务端判断还在旧状态。

这一步是状态整洁,不是交易拦截核心。

6.2 只提取交易态

local item_from_in_exchange = item_from.is_in_exchange and item_from:is_in_exchange()
local item_to_in_exchange = item_to and item_to.is_in_exchange and item_to:is_in_exchange()

这一步就是把“交易中的锁”和“普通锁”从语义上拆开。

最终判断只看:

  1. 源格物品是不是交易物品。
  2. 目标格物品是不是交易物品。

6.3 交易态直接拒绝

if item_from_in_exchange then
    reject_swap("交易中的道具无法移动")
    return
end
if item_to_in_exchange then
    reject_swap("交易中的道具无法交换")
    return
end

这样就能保证:

  1. 已放入交易盒的 A 无法再被拖走。
  2. 也不能把别的普通物品拖到 A 所在格形成换位。

6.4 为什么失败时要同步格子

sync_swap_slots()

因为客户端重放拖拽包后,可能本地先把画面改了。

如果服务端只是静默 return

  1. 服务端状态没变。
  2. 客户端画面却可能临时显示成已经换位。

所以必须重新回发两个背包格子的 GCItemInfo,把客户端状态强制拉回正确值。


第七步:为什么交易结束后还要刷新背包格子

这部分对应的是:

文件:lualib/exchange_box.lua
***付费内容***
这行:

self.my_self:send_prop_bag_item_info(bag_index)

是有必要保留的。

7.1 它不是为了服务端正确性

前两步已经完成了服务端状态修改:

item_operator:unlock_item(prop_bag, bag_index)
item:set_in_exchange(false)

从服务端角度看,到这里逻辑已经正确了。

7.2 它是为了客户端立即同步

问题在于客户端不知道你已经解锁了。

如果 reset() 之后不主动发这个格子的 GCItemInfo

  1. 服务端认为物品已不在交易中。
  2. 客户端却可能还显示成交易锁状态。
  3. 于是玩家就会看到“交易结束后还是拖不动”。

所以这里的发包是必须的客户端同步动作。

7.3 为什么只在 reset() 里发还不够

因为交易成功时,物品可能已经被搬到别的格子,甚至给了对方。

所以我在:

文件:services/scene/scenecore.lua

char_exchange_ok_III() 里又补了一层刷新:

  1. 记录交易成功后双方受影响的背包格子。
  2. reset() 完成后,再对这些格子主动调用 send_prop_bag_item_info()

这样就覆盖了两类场景:

  1. 交易取消 / 失败 / 回滚:靠 exchange_box:reset() 内部刷新。
  2. 交易成功:靠 char_exchange_ok_III() 末尾补发受影响格子。

第八步:交易成功后的补充刷新

文件:services/scene/scenecore.lua

char_exchange_ok_III() 里增加了两个刷新表:

local my_refresh_prop_indices = {}
local tar_refresh_prop_indices = {}

在实际搬移物品时记录受影响格子:

local function mark_refresh(refresh_map, bag_index)
    if bag_index ~= nil and bag_index ~= define.INVAILD_ID then
        refresh_map[bag_index] = true
    end
end

例如 A 给了对方时:

mark_refresh(my_refresh_prop_indices, bag_index)
mark_refresh(tar_refresh_prop_indices, empty_index)

最后在交易成功包发完、双方 reset() 之后,再主动刷新:

for bag_index in pairs(my_refresh_prop_indices) do
    obj_me:send_prop_bag_item_info(bag_index)
end
for bag_index in pairs(tar_refresh_prop_indices) do
    obj_tar:send_prop_bag_item_info(bag_index)
end

这样客户端不会残留旧的锁显示。


第九步:这次修复和“普通锁定物品”是什么关系

这次修复 不是 为了统一处理普通锁定物品。

它解决的是:

  1. 交易中的物品被重放拖拽交换。

它不试图解决:

  1. 普通玩家手动锁物品的所有拖拽行为。
  2. 其他系统里复用 is_lock() 的场景。

所以文档里必须明确:

  1. 本次漏洞的拦截条件是 is_in_exchange()
  2. 不是 is_lock()
  3. 否则就会再次把两种语义混掉。

第十步:修复后的行为

修复前

  1. A 放入交易盒。
  2. A 被打上锁,但背包里还在。
  3. 客户端重放拖拽交换包。
  4. 服务端把锁定物品仍然当作可交换对象处理。
  5. 背包中 A/B 实际换位。
  6. 交易界面仍显示 A。
  7. 最终收到的可能是 B。

修复后

  1. A 放入交易盒时,item:set_in_exchange(true)
  2. 客户端重放拖拽交换包。
  3. package_swap_item() 先判断 item_from:is_in_exchange()
  4. 发现源格或目标格在交易中,立即拒绝。
  5. 回发失败包和两个背包格子的真实状态。
  6. 客户端显示被服务端强制纠正。
  7. 交易界面里的 A 和最终收到的物品保持一致。

第十一步:建议测试项

11.1 交易中 A/B 强制换位

步骤:

  1. 把 A 放入交易盒。
  2. 重放背包拖拽封包,把 A 和 B 强制换位。

预期:

  1. 服务端拒绝本次拖拽。
  2. 客户端背包格子被重新同步。
  3. 最终交易仍然只会给出 A。

11.2 交易中 A 拖到空格

步骤:

  1. 把 A 放入交易盒。
  2. 重放拖拽包,把 A 拖到空格。

预期:

  1. 服务端拒绝。
  2. A 原位置不变。
  3. 客户端显示被纠正。

11.3 交易取消后立即拖动物品

步骤:

  1. 把 A 放入交易盒。
  2. 取消交易。
  3. 立刻尝试拖动 A。

预期:

  1. 客户端能立即拖动。
  2. 不会残留“看起来还锁着”的状态。

11.4 交易成功后检查双方背包

步骤:

  1. 正常完成一笔有物品交换的交易。
  2. 交易结束后检查双方受影响的背包格子。

预期:

  1. 客户端显示与服务端一致。
  2. 不残留交易锁定状态。

11.5 普通未交易物品拖拽

步骤:

  1. 使用普通未处于交易中的物品做交换、叠加、拖到空格。

预期:

  1. 原有功能保持不变。

第十二步:总结

这次修复最关键的结论是:

不要用 is_lock() 去直接定义“交易中的物品”。

正确做法是:

  1. is_in_exchange() 只负责表达“当前是否处于交易流程中”。
  2. package_swap_item() 只拦截交易态。
  3. exchange_box:reset()char_exchange_ok_III() 负责在交易结束后把客户端背包格子状态刷新回来。

这样既能修住“交易中背包拖拽换物”的漏洞,又不会再把普通锁定和交易锁状态混淆。

付费看帖
剩余 16% 内容需要支付 66.00 金币 后可完整阅读
支持付费阅读,激励作者创作更好的作品。
已有1人购买阅读
  • 1358
您需要登录后才可以回帖 登录 | register

本版积分规则

QQ|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )|天天打卡

GMT+8, 2026-6-24 05:37 , Processed in 0.060968 second(s), 26 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表