AzerothCore 反作弊与数据安全体系:从反外挂机制到完整性验证 原创

温馨提示:
本文最后更新于 2026-09-10,已超过 0 天没有更新。 若文章内的图片失效(无法正常加载),请留言反馈或直接 联系我

AzerothCore 反作弊与数据安全体系:从反外挂机制到完整性验证

面向有 AzerothCore 基础的开发者,深入剖析 WoW 模拟器的攻防博弈。


一、WoW 模拟器面临的常见作弊手段

在深入 AzerothCore 的防御机制之前,我们需要先理解”对手”是谁。WoW 私服生态中,作弊手段大致分为四类:

1.1 飞行与加速(Speed/Fly Hack)

客户端通过修改本地内存中的移动速度字段,或注入代码钩住 GetMoveSpeed() 函数,使角色移动速度超出服务端允许的上限。常见工具如 Cheat Engine 可以直接扫描并改写内存中的浮点速度值。

攻击表现:角色在短时间内跨越异常距离,或在不满足飞行条件时悬浮/飞行。

1.2 瞬移(Teleport Hack)

利用修改后的客户端或自定义工具,直接向服务端发送 MSG_MOVE_TELEPORT_ACK 或伪造 MOVEMENTFLAG 标志位,将角色坐标设为任意值。高级变种会”碎片化”瞬移——每帧移动一小段距离,避免触发简单的距离阈值检测。

1.3 发包伪造(Packet Forgery)

使用 WPE Pro 或自制代理拦截/篡改网络数据包。攻击者可以:

  • 重放(Replay)合法的数据包以触发服务端逻辑
  • 修改 opcode 参数(如将灰品质物品的 GUID 替换为紫装 GUID)
  • 绕过客户端校验直接发送服务端不该收到的操作请求

1.4 内存修改(Memory Modification)

通过 DLL 注入修改客户端运行时数据——从简单的速度/坐标修改,到 hook FrameScript::Execute 执行任意 Lua,再到修改客户端校验逻辑让非法操作通过本地检查。

关键认知:服务端永远不能信任客户端。任何只依赖客户端校验的防线都是纸糊的。真正的防线必须在服务端。


二、AzerothCore 服务端反作弊机制

2.1 核心防线:WorldSession 包处理

AzerothCore 的所有客户端交互都经过 WorldSession 的 opcode 分发器。每个客户端发来的包,都会经过以下管线:

// 简化示意:WorldSession 包处理管线
bool WorldSession::Update(uint32 diff, PacketFilter& filter)
{
    // 1. 从 recv 队列取出数据包
    WorldPacket* packet = _recvQueue.Pop();
    if (!packet)
        return false;

    // 2. 查找 opcode 处理器
    auto handler = _opcodeTable.find(packet->GetOpcode());
    if (handler == _opcodeTable.end()) {
        sLog->outError("Received unknown opcode 0x%04X", packet->GetOpcode());
        return false;
    }

    // 3. 检查会话状态(未登录时拒绝大多数操作)
    if (handler->second.status >= STATUS_LOGGEDIN && !IsLoggedIn())
        return false;

    // 4. 调用具体 handler
    (this->*(handler->second.handler))(*packet);
    return true;
}

这个管线是第一道门——但仅仅是”门”,还谈不上”检测”。真正的反作弊逻辑发生在 handler 内部。

2.2 Anticheat Module 的工作原理

AzerothCore 社区维护着一个独立的 Anticheat Module(通常作为 server 模块加载)。其核心设计思路:

  1. Hook 移动处理链:在 Player::HandleMovement 路径上插入校验逻辑
  2. 累积统计:不是一次异常就封号,而是累积”嫌疑分数”
  3. 多维度检测:速度、高度、距离、频率、地形,多角度交叉验证
  4. 延迟响应:检测到异常后,服务端可以强制回滚角色位置(rubberband),而不是直接踢线

模块的核心类结构:

// anticheat module 核心结构(简化)
class PlayerAnticheat
{
public:
    PlayerAnticheat(Player* player) : _player(player) {}

    // 每次收到移动包时调用
    void HandleMovement(MovementInfo& moveInfo);

    // 定期检查(受 World::Update 驱动)
    void Update(uint32 diff);

    // 速度检测
    bool CheckSpeed(MovementInfo& moveInfo);

    // 高度/飞行检测
    bool CheckHeight(MovementInfo& moveInfo);

    // 瞬移检测
    bool CheckTeleport(MovementInfo& moveInfo);

    // 地形碰撞检测
    bool CheckCollision(MovementInfo& moveInfo);

private:
    Player* _player;
    float _lastSpeedRate = 1.0f;
    uint32 _violationCount = 0;
    uint32 _reportTimer = 0;
    float _accumulatedDistance = 0.0f;
    Position _lastValidPos;
    time_t _lastViolationTime = 0;
};

关键设计:每个 Player 对象持有一个 PlayerAnticheat 实例,在移动包处理链中被调用。检测到异常后,调用 Player::TeleportToCacheClean 将角色回滚到上一个合法位置。


三、移动校验系统

3.1 MovementInfo 结构

WoW 客户端每个移动包都携带 MovementInfo,这是服务端判断移动合法性的核心数据:

// MovementInfo 核心字段(3.3.5a 协议)
struct MovementInfo
{
    uint32 flags;            // MOVEMENTFLAG_* 位掩码
    uint16 flags2;           // 额外标志(3.3.5a 新增)
    uint32 time;             // 客户端时间戳(ms)
    Position pos;            // 当前坐标 (x, y, z, orientation)
    uint32 t_guid;           // 传送目标 GUID(如果 flag 包含 TRANSPORT)
    Position t_pos;          // 传送上的坐标
    uint8 t_seat;            // 座位
    uint32 t_time2;          // 传送时间
    uint32 t_time3;          // 传送时间2
    float pitch;              // 俯仰角(飞行/游泳时)
    float fallTime;          // 下落时间
};

flags 是最关键的字段——它声明了角色的移动状态。常见的 flag 值包括:

Flag 含义
MOVEFLAG_FORWARD 0x00000001 向前移动
MOVEFLAG_BACKWARD 0x00000002 向后移动
MOVEFLAG_STRAFE_LEFT 0x00000004 左侧移动
MOVEFLAG_STRAFE_RIGHT 0x00000008 右侧移动
MOVEFLAG_TURN_LEFT 0x00000010 左转
MOVEFLAG_TURN_RIGHT 0x00000020 右转
MOVEFLAG_FLYING 0x00000200 飞行中
MOVEFLAG_SWIMMING 0x00200000 游泳中
MOVEFLAG_ROOT 0x00400000 定身
MOVEFLAG_FALLING 0x00000080 下落中

3.2 速度检测

速度检测的核心思路:客户端报告的位移速度,不能超过服务端计算的理论最大速度

bool PlayerAnticheat::CheckSpeed(MovementInfo& moveInfo)
{
    // 计算两次移动包之间的位移距离
    float dx = moveInfo.pos.m_positionX - _lastValidPos.m_positionX;
    float dy = moveInfo.pos.m_positionY - _lastValidPos.m_positionY;
    float dz = moveInfo.pos.m_positionZ - _lastValidPos.m_positionZ;
    float distance2D = std::sqrt(dx * dx + dy * dy);

    // 客户端报告的时间差
    uint32 timeDiff = moveInfo.time - _lastMoveTime;
    if (timeDiff == 0)
        timeDiff = 1;

    // 实际速度(单位: 码/秒)
    float actualSpeed = distance2D * 1000.0f / timeDiff;

    // 服务端理论最大速度
    // baseSpeed = 7.0 (跑步), 可被 aura/buff 提升
    float maxSpeed = _player->GetSpeedRate(MOVE_RUN);
    // 额外容忍 10% 误差(网络延迟/浮点精度)
    maxSpeed *= 1.1f;

    // 如果速度超标,记录违规
    if (actualSpeed > maxSpeed && distance2D > 1.0f)
    {
        _violationCount++;
        sLog->outChar("Anticheat: %s speed violation: %.2f > %.2f (dist=%.2f, tdiff=%u)",
            _player->GetName().c_str(), actualSpeed, maxSpeed, distance2D, timeDiff);
        return false; // 拒绝这次移动
    }

    _lastValidPos = moveInfo.pos;
    _lastMoveTime = moveInfo.time;
    return true;
}

为什么需要 10% 容忍? 因为网络延迟、客户端帧率和浮点精度误差会导致实际速度略高于理论值。容忍阈值太低会误杀正常玩家;太高则放行小幅加速。

3.3 高度与飞行检测

飞行检测的逻辑:如果 flags 不包含 MOVEFLAG_FLYING,但角色的 Z 坐标持续上升或保持在高空,则判定为非法飞行。

bool PlayerAnticheat::CheckHeight(MovementInfo& moveInfo)
{
    // 如果客户端声明在飞行但角色没有飞行权限
    if (moveInfo.flags & MOVEFLAG_FLYING)
    {
        if (!_player->HasAuraType(SPELL_AURA_FLY) &&
            !_player->GetGMFly() &&
            !_player->IsInFlight())
        {
            _violationCount++;
            sLog->outChar("Anticheat: %s flying without aura - possible fly hack",
                _player->GetName().c_str());
            return false;
        }
    }

    // 地面模式下的高度异常检测
    float dz = moveInfo.pos.m_positionZ - _lastValidPos.m_positionZ;
    float groundZ = _player->GetMap()->GetHeight(
        _player->GetPhaseMask(),
        moveInfo.pos.m_positionX,
        moveInfo.pos.m_positionY,
        moveInfo.pos.m_positionZ);

    // 角色悬停在地面以上不合理的高度且未下落
    if (!(moveInfo.flags & MOVEFLAG_FALLING) &&
        !(moveInfo.flags & MOVEFLAG_FLYING) &&
        !(moveInfo.flags & MOVEFLAG_SWIMMING))
    {
        float heightDiff = moveInfo.pos.m_positionZ - groundZ;
        if (heightDiff > 3.0f && std::abs(dz) < 0.1f)
        {
            // 悬空检测:不在下落、不在飞行,但悬浮在空中
            _violationCount++;
            sLog->outChar("Anticheat: %s hovering at %.2f above ground",
                _player->GetName().c_str(), heightDiff);
            return false;
        }
    }

    return true;
}

3.4 瞬移检测与位置回滚

瞬移检测是防范”碎片化瞬移”的关键。攻击者可能每次移动跳跃 10 码来规避简单的距离检查。

bool PlayerAnticheat::CheckTeleport(MovementInfo& moveInfo)
{
    float distance2D = _player->GetExactDist2d(
        moveInfo.pos.m_positionX,
        moveInfo.pos.m_positionY);

    // 正常移动的连续阈值
    // 服务端每帧之间最大允许位移取决于速度和时间差
    uint32 timeDiff = getMSTimeDiff(_lastMoveTime, moveInfo.time);
    float maxAllowed = _player->GetSpeedRate(MOVE_RUN) * (timeDiff / 1000.0f) * 1.5f;

    // 单次位移超过阈值
    if (distance2D > maxAllowed && distance2D > 5.0f)
    {
        // 可能是正常的传送(法师传送门、飞行路径等)
        if (_player->IsBeingTeleported() ||
            _player->IsInFlight())
            return true;

        _violationCount++;
        sLog->outChar("Anticheat: %s teleport detected: %.2f yards (max %.2f)",
            _player->GetName().c_str(), distance2D, maxAllowed);

        // 关键:回滚到上一个合法位置
        _player->TeleportToCacheClean(
            _player->GetMapId(),
            _lastValidPos.m_positionX,
            _lastValidPos.m_positionY,
            _lastValidPos.m_positionZ,
            _lastValidPos.m_orientation);

        return false;
    }

    return true;
}

位置回滚(Rubberbanding) 是 AzerothCore 反作弊的核心策略——不让攻击者获得任何非法位移的收益。服务端在拒绝移动包后,主动发送 SMSG_MOVE_TELEPORT_ACK 将客户端角色拉回合法位置。


四、数据完整性验证

4.1 数据库一致性检查

作弊检测不只限于移动。数据库层面同样需要保护。以下是针对常见数据完整性问题的检查方案:

-- 检查1:物品来源追踪 - 查找可疑的高价值物品
-- 角色拥有的物品中,品质 >= 4(史诗/传说)但无合法来源记录的
SELECT
    c.name AS character_name,
    i.entry AS item_entry,
    i.count AS stack_count,
    c.account AS account_id
FROM character_inventory ci
JOIN item_instance i ON ci.item = i.guid
JOIN characters c ON ci.guid = c.guid
WHERE
    -- 查询物品模板获取品质
    i.entry IN (
        SELECT entry FROM item_template WHERE Quality >= 4
    )
    AND ci.bag = 0  -- 仅检查装备栏和背包
    AND c.level < 40  -- 低等级角色持有高价值物品
ORDER BY c.account, c.name;
-- 检查2:金币异常 - 查找金币来源不合理的角色
-- 计算每个角色从所有来源获得金币 vs 当前持有金币
SELECT
    c.name,
    c.money AS current_gold,
    COALESCE(SUM(CASE WHEN cl.event_type = 3 THEN cl.amount ELSE 0 END), 0) AS loot_gold,
    COALESCE(SUM(CASE WHEN cl.event_type = 6 THEN cl.amount ELSE 0 END), 0) AS vendor_gold,
    COALESCE(SUM(CASE WHEN cl.event_type = 12 THEN cl.amount ELSE 0 END), 0) AS quest_gold,
    ABS(c.money - COALESCE(SUM(cl.amount), 0)) AS unaccounted_gold
FROM characters c
LEFT JOIN character_log cl ON c.guid = cl.guid
WHERE c.money > 10000000  -- 超过1000金币(3.3.5a经济体系)
GROUP BY c.guid, c.name, c.money
HAVING unaccounted_gold > 100000  -- 100金币以上无法追溯
ORDER BY unaccounted_gold DESC;
-- 检查3:物品数量异常 - 单角色持有超过合理数量的同ID物品
SELECT
    c.name AS character_name,
    i.entry AS item_entry,
    it.name AS item_name,
    SUM(i.count) AS total_count,
    it.stackable AS max_stack
FROM character_inventory ci
JOIN item_instance i ON ci.item = i.guid
JOIN item_template it ON i.entry = it.entry
JOIN characters c ON ci.guid = c.guid
GROUP BY c.name, i.entry, it.name, it.stackable
HAVING total_count > it.stackable * 20  -- 超过堆叠上限的20倍
ORDER BY total_count DESC;

4.2 物品来源追踪

AzerothCore 本身的日志系统有限。要实现完整的物品追踪链,我们需要扩展 Player::AddItem 的日志记录:

// 扩展物品日志:在 Player::AddItem 中注入追踪记录
// 对应 src/server/game/Entities/Player/Player.cpp

uint32 Player::AddItem(uint32 itemEntry, uint32 count, Item* pItem)
{
    // ... 原有逻辑 ...

    if (result != EQUIP_ERR_OK)
        return result;

    // 记录物品来源
    // source: 0=loot, 1=vendor, 2=craft, 3=quest, 4=trade, 5=mail, 6=gm
    CharacterDatabase.PExecute(
        "INSERT INTO character_item_log "
        "(guid, item_entry, count, source, timestamp, source_guid) "
        "VALUES (%u, %u, %u, %u, %u, %u)",
        GetGUIDLow(), itemEntry, count,
        GetItemSource(),           // 来源类型
        uint32(time(nullptr)),     // 时间戳
        GetItemSourceGuid()        // 来源 GUID(NPC/玩家)
    );

    return result;
}
-- 创建物品来源追踪表
CREATE TABLE IF NOT EXISTS character_item_log (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    guid        INT UNSIGNED NOT NULL COMMENT '角色GUID',
    item_entry  INT UNSIGNED NOT NULL COMMENT '物品模板entry',
    count       INT UNSIGNED NOT NULL DEFAULT 1,
    source      TINYINT UNSIGNED NOT NULL COMMENT '0=loot 1=vendor 2=craft 3=quest 4=trade 5=mail 6=gm',
    timestamp   INT UNSIGNED NOT NULL,
    source_guid INT UNSIGNED DEFAULT 0 COMMENT '来源GUID (NPC/玩家)',
    INDEX idx_guid (guid),
    INDEX idx_entry (item_entry),
    INDEX idx_source (source),
    INDEX idx_time (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.3 金币流动审计

-- 金币流动审计:追踪玩家间金币转移
CREATE TABLE IF NOT EXISTS character_gold_audit (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    from_guid    INT UNSIGNED NOT NULL COMMENT '转出角色',
    to_guid      INT UNSIGNED NOT NULL COMMENT '转入角色',
    amount       INT UNSIGNED NOT NULL COMMENT '金额(铜币)',
    trade_type   TINYINT UNSIGNED NOT NULL COMMENT '0=trade 1=mail 2=vendor_sell 3=gm_command',
    timestamp    INT UNSIGNED NOT NULL,
    ip_from      VARCHAR(64) DEFAULT NULL COMMENT '转出方IP',
    ip_to        VARCHAR(64) DEFAULT NULL COMMENT '转入方IP',
    INDEX idx_from (from_guid),
    INDEX idx_to (to_guid),
    INDEX idx_amount (amount),
    INDEX idx_time (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 查找可疑的金币转移模式(RMT特征)
SELECT
    from_guid, to_guid, amount, trade_type,
    FROM_UNIXTIME(timestamp) AS transfer_time
FROM character_gold_audit
WHERE
    amount > 1000000  -- 超过100金币
    AND trade_type IN (0, 1)  -- 交易或邮件
    AND from_guid IN (
        SELECT guid FROM characters
        WHERE online = 0 AND deleteDate IS NOT NULL  -- 已删除角色
    )
ORDER BY timestamp DESC
LIMIT 100;

五、实战:配置和定制反作弊模块

5.1 模块安装与基础配置

社区版 Anticheat Module 通常放在 modules/ 目录下,通过 CMake 编译时自动链接:

# 拉取 anticheat 模块(社区维护版本)
cd /path/to/azerothcore/modules
git clone https://github.com/azerothcore/mod-anticheat.git

# 重新编译
cd /path/to/azerothcore
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)

5.2 核心配置项

worldserver.conf 中添加模块配置:

########################################
#  Anticheat Module Configuration
########################################

# 启用反作弊模块
Anticheat.Enable = 1

# 速度检测容忍系数(1.0 = 精确,1.1 = 允许10%误差)
Anticheat.Speed.Tolerance = 1.1

# 单次瞬移检测阈值(码)
Anticheat.Teleport.MaxDistance = 5.0

# 飞行检测启用
Anticheat.FlyCheck.Enable = 1

# 悬空检测高度阈值(码)
Anticheat.HoverCheck.MaxHeight = 3.0

# 违规次数阈值 - 达到此值后执行惩罚
Anticheat.Violation.Threshold = 5

# 惩罚动作:0=仅记录 1=回滚位置 2=踢线 3=封禁
Anticheat.Punish.Action = 1

# 封禁时长(小时,仅 Action=3 时生效)
Anticheat.Punish.BanDuration = 24

# GM 免疫(GM 等级 >= 此值不受检测)
Anticheat.GMImmunity = 3

# 实时告警到 GM 在线频道
Anticheat.NotifyGMs = 1

5.3 自定义检测规则

假设我们需要添加一个自定义检测:防止在战斗中使用炉石等传送物品(正常客户端会阻止,但发包伪造可以绕过)。

// 在 anticheat 模块中注册自定义 hook
// mod-anticheat/src/AnticheatHooks.cpp

#include "ScriptMgr.h"
#include "Player.h"
#include "SpellMgr.h"
#include "AnticheatMgr.h"

class AnticheatSpellCastingHook : public ModuleScript
{
public:
    void OnBeforeSpellCast(Player* player, uint32 spellId,
                           Unit* /*target*/, Item* /*item*/) override
    {
        // 炉石类传送法术
        if (spellId == 8690 ||    // Hearthstone
            spellId == 556 ||     // Cold Weather Flying teleport
            spellId == 39937)     // Dimensional Ripper
        {
            if (player->IsInCombat())
            {
                // 战斗中使用传送法术 - 标记违规
                if (auto* ac = player->GetAnticheat())
                {
                    ac->AddViolation("combat_teleport",
                        "Attempted teleport spell in combat");
                }
            }
        }
    }
};

void AddSC_AnticheatHooks()
{
    new AnticheatSpellCastingHook();
}

然后在模块的 ScriptLoader 中注册:

// mod-anticheat/src/Loader.cpp
void AddAnticheatScripts()
{
    AddSC_AnticheatMgr();
    AddSC_AnticheatHooks();  // 新增的自定义 hook
}

5.4 扩展违规处理链

默认的违规处理只是位置回滚。我们可以扩展为多级响应:

// 分级惩罚策略
void PlayerAnticheat::HandleViolation(const std::string& type, const std::string& detail)
{
    _violationCount++;
    _lastViolationTime = time(nullptr);

    // 记录到数据库
    CharacterDatabase.PExecute(
        "INSERT INTO anticheat_violations "
        "(guid, player_name, violation_type, detail, count, timestamp) "
        "VALUES (%u, '%s', '%s', '%s', %u, %u) "
        "ON DUPLICATE KEY UPDATE count = count + 1, timestamp = %u",
        _player->GetGUIDLow(),
        _player->GetName().c_str(),
        type.c_str(),
        detail.c_str(),
        _violationCount,
        uint32(time(nullptr)),
        uint32(time(nullptr))
    );

    // 分级响应
    switch (_violationCount)
    {
        case 1:  // 第1次:静默回滚
            RubberbandPlayer();
            break;

        case 3:  // 第3次:警告
            _player->GetSession()->SendNotification(
                "检测到异常移动,位置已回滚。");
            RubberbandPlayer();
            break;

        case 5:  // 第5次:踢线
            sLog->outChar("Anticheat: Kicking %s for repeated violations",
                _player->GetName().c_str());
            _player->GetSession()->KickPlayer();
            break;

        case 10: // 第10次:封禁
            sLog->outChar("Anticheat: Banning %s for excessive cheating",
                _player->GetName().c_str());
            // 调用封禁 API
            sWorld->BanCharacter(
                _player->GetName(),
                "24h",
                "Anticheat: excessive movement violations"
            );
            break;
    }

    // 通知在线 GM
    if (sWorld->getBoolConfig(CONFIG_ANTICHEAT_NOTIFY_GMS))
    {
        sWorld->SendGMText(
            "Anticheat: %s 触发 %s (第%d次)", _player->GetName().c_str(),
            type.c_str(), _violationCount);
    }
}

六、服务端日志与告警体系搭建

6.1 违规日志表设计

-- anticheat 违规记录表
CREATE TABLE IF NOT EXISTS anticheat_violations (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    guid            INT UNSIGNED NOT NULL,
    player_name     VARCHAR(32) NOT NULL,
    violation_type  VARCHAR(64) NOT NULL COMMENT 'speed/teleport/fly/packet/etc',
    detail          TEXT DEFAULT NULL,
    count           INT UNSIGNED DEFAULT 1,
    timestamp       INT UNSIGNED NOT NULL,
    x               FLOAT DEFAULT NULL COMMENT '违规时坐标',
    y               FLOAT DEFAULT NULL,
    z               FLOAT DEFAULT NULL,
    map_id          INT UNSIGNED DEFAULT NULL,
    ip_address      VARCHAR(64) DEFAULT NULL,
    INDEX idx_guid (guid),
    INDEX idx_type (violation_type),
    INDEX idx_time (timestamp),
    INDEX idx_ip (ip_address)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6.2 日志查询视图

-- 视图:违规汇总看板
CREATE OR REPLACE VIEW v_anticheat_summary AS
SELECT
    player_name,
    ip_address,
    COUNT(*) AS total_violations,
    SUM(CASE WHEN violation_type = 'speed' THEN 1 ELSE 0 END) AS speed_violations,
    SUM(CASE WHEN violation_type = 'teleport' THEN 1 ELSE 0 END) AS teleport_violations,
    SUM(CASE WHEN violation_type = 'fly' THEN 1 ELSE 0 END) AS fly_violations,
    MAX(timestamp) AS last_violation_time,
    FROM_UNIXTIME(MAX(timestamp)) AS last_violation_readable
FROM anticheat_violations
GROUP BY guid, player_name, ip_address
ORDER BY total_violations DESC;

-- 查询:过去24小时内的活跃作弊者
SELECT * FROM v_anticheat_summary
WHERE last_violation_time > UNIX_TIMESTAMP() - 86400
ORDER BY total_violations DESC;

6.3 Discord/Telegram 告警集成

对于运营中的服务器,实时告警至关重要。通过 Lua 脚本或 C++ 模块将告警推送到即时通讯渠道:

// 告警推送:通过 HTTP 请求发送到 Discord Webhook
void AnticheatMgr::SendDiscordAlert(Player* player, const std::string& violationType,
                                     const std::string& detail)
{
    // 构建 JSON payload
    std::ostringstream payload;
    payload << "{"
            << "  \"embeds\": [{"
            << "    \"title\": \"Anticheat Alert\","
            << "    \"color\": 16711680,"
            << "    \"fields\": ["
            << "      {\"name\": \"Player\", \"value\": \"" << player->GetName() << "\", \"inline\": true},"
            << "      {\"name\": \"Type\", \"value\": \"" << violationType << "\", \"inline\": true},"
            << "      {\"name\": \"Detail\", \"value\": \"" << detail << "\", \"inline\": false},"
            << "      {\"name\": \"Map\", \"value\": \"" << player->GetMapId() << "\", \"inline\": true},"
            << "      {\"name\": \"IP\", \"value\": \"" << player->GetSession()->GetRemoteAddress() << "\", \"inline\": true}"
            << "    ],"
            << "    \"timestamp\": \"" << GetISO8601Timestamp() << "\""
            << "  }]"
            << "}";

    // 异步发送 HTTP POST(使用 boost::asio 或 std::thread)
    std::thread([payloadStr = payload.str()]() {
        CurlPost(
            "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_TOKEN",
            payloadStr,
            "Content-Type: application/json"
        );
    }).detach();
}

6.4 日志轮转与归档

#!/bin/bash
# /etc/logrotate.d/azerothcore-anticheat
# anticheat 日志轮转配置

/var/log/azerothcore/anticheat.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0644 azerothcore azerothcore
    postrotate
        # 发送 SIGHUP 让 worldserver 重新打开日志文件
        kill -HUP $(cat /run/azerothcore-worldserver.pid 2>/dev/null) 2>/dev/null || true
    endscript
}

对于数据库层面的数据归档,可以设置定期任务将超过 90 天的违规记录移到归档表:

-- 定时归档任务(配合 MySQL Event Scheduler)
DELIMITER //
CREATE EVENT IF NOT EXISTS archive_anticheat_violations
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_TIMESTAMP
DO
BEGIN
    -- 移动90天前的记录到归档表
    INSERT INTO anticheat_violations_archive
    SELECT * FROM anticheat_violations
    WHERE timestamp < UNIX_TIMESTAMP() - (90 * 86400);

    -- 删除已归档的记录
    DELETE FROM anticheat_violations
    WHERE timestamp < UNIX_TIMESTAMP() - (90 * 86400);

    -- 优化表空间
    OPTIMIZE TABLE anticheat_violations;
END //
DELIMITER ;

七、最佳实践与安全加固清单

7.1 服务端加固要点

除了反作弊模块本身,整体安全加固同样重要:

// 1. 限制每秒包频率(防发包洪水)
// 在 WorldSession 中增加包频率检测
bool WorldSession::HandlePacketRateCheck(WorldPacket const* packet)
{
    static constexpr uint32 MAX_PACKETS_PER_SEC = 100;
    static constexpr uint32 WARN_PACKETS_PER_SEC = 50;

    uint32 now = getMSTime();
    uint32 windowStart = now - 1000; // 1秒窗口

    // 清理过期记录
    while (!_packetTimes.empty() && _packetTimes.front() < windowStart)
        _packetTimes.pop();

    _packetTimes.push(now);

    uint32 count = _packetTimes.size();
    if (count > MAX_PACKETS_PER_SEC)
    {
        // 超过频率上限,踢线
        sLog->outChar("Packet flood from %s: %u packets/sec",
            GetRemoteAddress().c_str(), count);
        KickPlayer();
        return false;
    }
    else if (count > WARN_PACKETS_PER_SEC)
    {
        // 警告但不踢线(可能是正常的高频操作)
        sLog->outDebug("High packet rate from %s: %u/sec",
            GetRemoteAddress().c_str(), count);
    }

    return true;
}

// 2. 限制同一账号的并发会话
bool AccountMgr::CheckConcurrentSessions(uint32 accountId, uint32 maxAllowed)
{
    uint32 activeSessions = 0;
    SessionMap::const_iterator itr;
    for (itr = sWorld->GetAllSessions().begin();
         itr != sWorld->GetAllSessions().end();
         ++itr)
    {
        if (itr->second->GetAccountId() == accountId)
            activeSessions++;
    }

    return activeSessions < maxAllowed;
}

7.2 网络层加固

# 防火墙规则:限制游戏端口连接频率(防 DDoS)
iptables -A INPUT -p tcp --dport 8085 -m connlimit \
    --connlimit-above 3 --connlimit-mask 32 -j DROP

# 限制每 IP 的连接数
iptables -A INPUT -p tcp --dport 8085 -m conntrack \
    --ctstate NEW -m recent --set
iptables -A INPUT -p tcp --dport 8085 -m conntrack \
    --ctstate NEW -m recent --update --seconds 60 --hitcount 10 -j DROP

# 白名单 GM IP
iptables -A INPUT -p tcp --dport 8085 -s 192.168.1.0/24 -j ACCEPT

7.3 定期安全审计

-- 检查管理员账号是否有异常登录
SELECT
    a.id AS account_id,
    a.username,
    r.rl.abank AS gm_rank,
    COUNT(*) AS login_count,
    MAX(FROM_UNIXTIME(ll.loginTime)) AS last_login,
    GROUP_CONCAT(DISTINCT ll.ip) AS login_ips
FROM account a
JOIN account_access r ON a.id = r.accountId
LEFT JOIN account_logintime ll ON a.id = ll.accountId
WHERE r.rl.abank >= 2  -- GM等级 >= 2
GROUP BY a.id, a.username, r.rl.abank
ORDER BY login_count DESC;

-- 检查非工作时间段的服务端操作日志
SELECT * FROM command_log
WHERE
    command LIKE '%add item%'
    OR command LIKE '%set money%'
    OR command LIKE '%ban%'
    AND HOUR(FROM_UNIXTIME(timestamp)) NOT BETWEEN 9 AND 22
ORDER BY timestamp DESC;

安全加固清单

分类 项目 风险等级 建议措施
移动检测 速度检测(Speed Hack) 🔴 高 启用 Anticheat.SpeedCheck,容忍系数 ≤ 1.15,超阈值回滚位置
瞬移检测(Teleport Hack) 🔴 高 设置 MaxDistance 阈值,实现碎片化瞬移检测(连续小跳跃累积)
飞行检测(Fly Hack) 🔴 高 校验 MOVEFLAG_FLYING 与角色 aura 状态,检测无 aura 悬空
包检测 包频率限制 🟡 中 单 IP 每秒包数上限 ≤ 100,超过踢线并记录
Opcode 白名单 🟡 中 未登录会话拒绝除 AUTH 之外的所有 opcode
包重放检测 🟡 中 记录最近 N 个包的序列号,拒绝重复或倒序的包
数据完整性 物品来源追踪 🔴 高 扩展 Player::AddItem 日志,记录物品来源类型和来源 GUID
金币流动审计 🔴 高 记录玩家间金币转移,设置单笔转移上限告警阈值
数据库一致性检查 🟡 中 每日 cron 执行异常物品/金币查询,结果推送给管理员
定期数据快照 🟢 低 每周全量备份 character DB,保留 4 周滚动快照
权限管理 GM 权限最小化 🔴 高 GM 等级分级,日常管理用 rank 2,仅运维用 rank 3
GM 命令审计 🟡 中 记录所有 GM 命令到 command_log 表,每周审查
管理后台 IP 白名单 🟡 中 SOAP/RA 端口仅允许管理网段访问,防火墙 iptables 限制
网络防护 DDoS 防护 🔴 高 connlimit 限制每 IP 并发连接数,考虑前置 CDN/反向代理
游戏端口隔离 🟡 中 世界服务器、SOAP、数据库端口分离,最小暴露面
SSL/TLS 加密 🟢 低 SOAP 管理接口启用 HTTPS,客户端连接可选加密(AuthCrypt 已有)
告警体系 实时 Discord/Telegram 告警 🟡 中 高危违规(speed/teleport/fly)触发 Webhook 推送
每日安全报告 🟢 低 Cron 任务汇总每日违规统计、异常物品/金币变动,邮件发送

结语

反作弊不是一劳永逸的工程,而是一场持续的攻防博弈。攻击者会不断研究服务端检测逻辑,寻找新的绕过方式。有效的防御策略是:

  1. 分层防御:不依赖单一检测点,从网络层 → 包处理 → 移动校验 → 数据审计,多层级交叉验证
  2. 持续迭代:关注社区安全公告,及时更新检测规则,对新型作弊手段快速响应
  3. 数据驱动:用日志和统计发现问题,而不是靠 GM 人工巡查
  4. 最小信任:服务端不信任任何客户端数据,所有关键操作服务端二次验证

AzerothCore 的模块化架构给了我们充分的扩展空间——从 anticheat 模块的移动检测,到自定义的物品追踪和金币审计,每一层都可以独立加固。记住:最好的防线不是一道墙,而是让攻击者的每一次尝试都付出高于收益的代价。


本文基于 AzerothCore 3.3.5a (WotLK) 分支的架构分析。不同版本的核心实现可能有所差异,请以实际代码为准。