一、问题现象
玩家使用鉴定卷轴时,客户端提交了非法的目标背包位置,服务端没有验证目标物品是否存在,直接进入鉴定逻辑。
原始代码:
return SendUseItemResult(pGamePlayer, pHuman, bagIndex,
(USEITEM_RESULT)pHuman->UseIdentScroll(
pPacket->GetBagIndex(),
pPacket->GetTargetItem()));
UseIdentScroll() 内部继续读取目标物品,最终可能对空指针执行物品操作,导致:
SEG exception
Server crash
二、修复位置
修改文件:
Server/Server/GameServer/Server/Packets/CGUseItemHandler.cpp
Server/Server/GameServer/Server/Obj/Obj_Human.cpp
三、封包处理入口增加检查
在 CGUseItemHandler::Execute() 的鉴定卷轴分支中,先验证目标背包位置和目标物品。
if(IsIdentScroll(pBagItem))
{
const BYTE targetItemIndex = pPacket->GetTargetItem();
// 鉴定目标来自客户端,必须先检查背包位置。
if(targetItemIndex >= MAX_BAG_SIZE)
{
CacheLog(LOG_FILE_1,
"ERROR CGUseItemHandler Invalid ident target "
"GUID=%X BagIndex=%d TargetItem=%d",
pHuman->GetGUID(),
bagIndex,
targetItemIndex);
return SendUseItemResult(
pGamePlayer,
pHuman,
bagIndex,
USEITEM_IDENT_TARGET_TYPE_FAIL);
}
Item* pTargetItem =
HumanItemLogic::GetItem(pHuman, targetItemIndex);
// 目标位置为空时,直接拒绝本次鉴定。
if(pTargetItem == NULL || pTargetItem->IsEmpty())
{
return SendUseItemResult(
pGamePlayer,
pHuman,
bagIndex,
USEITEM_IDENT_TARGET_TYPE_FAIL);
}
return SendUseItemResult(
pGamePlayer,
pHuman,
bagIndex,
(USEITEM_RESULT)pHuman->UseIdentScroll(
pPacket->GetBagIndex(),
targetItemIndex));
}
这里必须使用服务端重新获取的 pTargetItem,不能只相信客户端传入的背包索引。
四、鉴定函数内部增加二次保护
修改:
Obj_Human.cpp
在 Obj_Human::UseIdentScroll() 开始位置增加检查:
INT Obj_Human::UseIdentScroll(
UINT BagIndex,
UINT TargetBagIndex)
{
__ENTER_FUNCTION
// 鉴定位置来自客户端,先限制背包范围。
if(BagIndex >= MAX_BAG_SIZE)
{
return USEITEM_IDENT_TYPE_FAIL;
}
if(TargetBagIndex >= MAX_BAG_SIZE)
{
return USEITEM_IDENT_TARGET_TYPE_FAIL;
}
Item* pScrollItem =
HumanItemLogic::GetItem(this, BagIndex);
Item* pTargetItem =
HumanItemLogic::GetItem(this, TargetBagIndex);
Assert(pScrollItem != NULL && pTargetItem != NULL);
// 源物品或目标物品不存在时,拒绝操作。
if(pScrollItem == NULL || pScrollItem->IsEmpty())
{
return USEITEM_IDENT_TYPE_FAIL;
}
if(pTargetItem == NULL || pTargetItem->IsEmpty())
{
return USEITEM_IDENT_TARGET_TYPE_FAIL;
}
if(pScrollItem->GetItemClass() != ICLASS_COMITEM ||
pScrollItem->GetItemQual() != COMITEM_QUAL_MIS)
{
return USEITEM_IDENT_TYPE_FAIL;
}
// 后续保留原有鉴定逻辑。
入口和业务函数都要检查,是为了避免其他服务器内部调用绕过封包入口后再次访问无效物品。
五、修复原则
鉴定流程必须按照以下顺序执行:
检查源背包索引
↓
检查目标背包索引
↓
读取源物品
↓
读取目标物品
↓
检查物品是否为空
↓
检查源物品是否为鉴定卷轴
↓
执行原有鉴定逻辑
不能直接使用客户端传入的目标位置访问物品,也不能只依赖:
Assert(pTargetItem);
Assert 只适合调试阶段,正式运行时仍然必须使用明确的空指针判断和失败返回。
日志中可以看到错误记录,但不会再出现空指针访问和服务端崩溃。