WPE|52wpe|我爱WPE

 找回密码
 注册会员
搜索
  • 4518查看
  • 29回复

主题

好友

154

积分

注册会员

发表于 2010-6-20 18:37:51 |显示全部楼层
下载地址:code

1、简单说明wow的包格式

client(客户端,即wow.exe)->server(本例中的trinity core)通用包结构:包长度(2字节)、opcode(4字节)、参数(变长,根据opcode确定)
附包头结构:
struct ClientPktHeader
{
    uint16 size;
    uint32 cmd;
};

server->client通用包结构:包长(2或3字节)、opcode(2字节)、参数(变长,根据opcode确定)

c/s交互的数据包,包头部分(包长、opcode)均指定rc4算法加密后传输,key为通过srp6算法对client产生的随机数运算的结果,基本禁止了中间环节修改的可能。包参数部分是不加密的,这也是wpe之类的工具可以欺骗服务器的基础

2、学技能流程
opcode = 0x01B2 // "CMSG_TRAINER_BUY_SPELL"
handler = WorldSession::HandleTrainerBuySpellOpcode

void WorldSession::HandleTrainerBuySpellOpcode( WorldPacket & recv_data )
{
    uint64 guid;
    uint32 spellId = 0;

    // 可见包参数:8字节npc的id、教会所需技能的spell的id,这也就是为何spellid在15 16位置的原因了(包头6字节,npc id 8字节)
    recv_data >> guid >> spellId;
    sLog.outDebug( "WORLD: Received CMSG_TRAINER_BUY_SPELL NpcGUID=%u, learn spell id is: %u",uint32(GUID_LOPART(guid)), spellId );

    Creature *unit = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_TRAINER);
    // 检查npc是否存在
    if (!unit)
    {
        sLog.outDebug( "WORLD: HandleTrainerBuySpellOpcode - Unit (GUID: %u) not found or you can't interact with him.", uint32(GUID_LOPART(guid)) );
        return;
    }

    // remove fake death
    if(GetPlayer()->hasUnitState(UNIT_STAT_DIED))
        GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);

    // 检查玩家是否可以在此npc处学技能
    if(!unit->isCanTrainingOf(_player,true))
        return;

    // check present spell in trainer spell list
    TrainerSpellData const* trainer_spells = unit->GetTrainerSpells();
    // 检查npc是否是技能训练师,即是否有可教授的技能
    if(!trainer_spells)
        return;

    // not found, cheat?
    TrainerSpell const* trainer_spell = trainer_spells->Find(spellId);
    // 检查这个npc是否可以教这个技能
    if(!trainer_spell)
        return;

    // can't be learn, cheat? Or double learn with lags...
    // 检查玩家学习技能的条件是否足够,函数摘录见下
    if(_player->GetTrainerSpellState(trainer_spell) != TRAINER_SPELL_GREEN)
        return;

    // apply reputation discount
    uint32 nSpellCost = uint32(floor(trainer_spell->spellCost * _player->GetReputationPriceDiscount(unit)));

    // check money requirement
    // 检查玩家是否有足够的钱学技能
    if(_player->GetMoney() < nSpellCost )
        return;

    _player->ModifyMoney( -int32(nSpellCost) );

    // 到这里检查完毕,开始学习
    // 发个包通知客户端放个学习的动画(npc手一挥)
    WorldPacket data(SMSG_PLAY_SPELL_VISUAL, 12);           // visual effect on trainer
    data << uint64(guid) << uint32(0xB3);
    SendPacket(&data);

    // 发个包通知客户端放个学习的动画(玩家浑身金光一闪,做个敬礼的动作)
    data.Initialize(SMSG_PLAY_SPELL_IMPACT, 12);            // visual effect on player
    data << uint64(_player->GetGUID()) << uint32(0x016A);
    SendPacket(&data);

    // 开始学习
    // learn explicitly or cast explicitly
    if(trainer_spell->IsCastable ())
        //FIXME: prof. spell entry in trainer list not marked gray until list re-open.
        _player->CastSpell(_player,trainer_spell->spell,true);
    else
        _player->learnSpell(spellId,false);

    // 学习成功,通知客户端在log区提示相关信息
    data.Initialize(SMSG_TRAINER_BUY_SUCCEEDED, 12);
    data << uint64(guid) << uint32(trainer_spell->spell);
    SendPacket(&data);
}

TrainerSpellState Player::GetTrainerSpellState(TrainerSpell const* trainer_spell) const
{
    // 没这spell,不能学
    if (!trainer_spell)
        return TRAINER_SPELL_RED;

    // 这个spell不是教spell的spell,不能学
    if (!trainer_spell->learnedSpell)
        return TRAINER_SPELL_RED;

    // 玩家已经学会这个spell,不能再学了
    // known spell
    if(HasSpell(trainer_spell->learnedSpell))
        return TRAINER_SPELL_GRAY;

    // 被教授的技能spell有种族、直接需求,不能学
    // check race/class requirement
    if(!IsSpellFitByClassAndRace(trainer_spell->learnedSpell))
        return TRAINER_SPELL_RED;

    // 没达等级要求,不能学
    // check level requirement
    if(getLevel() < trainer_spell->reqLevel)
        return TRAINER_SPELL_RED;

    // 链式技能(12345级之类的),没学会前面的不能直接学后面的
    if(SpellChainNode const* spell_chain = spellmgr.GetSpellChainNode(trainer_spell->learnedSpell))
    {
        // check prev.rank requirement
        if(spell_chain->prev && !HasSpell(spell_chain->prev))
            return TRAINER_SPELL_RED;
    }

    // 学习技能有前置技能且玩家不会,不能学
    if(uint32 spell_req = spellmgr.GetSpellRequired(trainer_spell->spell))
    {
        // check additional spell requirement
        if(!HasSpell(spell_req))
            return TRAINER_SPELL_RED;
    }

    // check skill requirement
    // 未达技能熟练度需求(例如商业技能之类的,直接学宗师级,是不行的),不能学
    if(trainer_spell->reqSkill && GetBaseSkillValue(trainer_spell->reqSkill) < trainer_spell->reqSkillValue)
        return TRAINER_SPELL_RED;

    // exist, already checked at loading
    SpellEntry const* spell = sSpellStore.LookupEntry(trainer_spell->learnedSpell);

    // secondary prof. or not prof. spell
    uint32 skill = spell->EffectMiscValue[1];

    if(spell->Effect[1] != SPELL_EFFECT_SKILL || !IsPrimaryProfessionSkill(skill))
        return TRAINER_SPELL_GREEN;

    // check primary prof. limit
    if(spellmgr.IsPrimaryProfessionFirstRankSpell(spell->Id) && GetFreePrimaryProfessionPoints() == 0)
        return TRAINER_SPELL_GREEN_DISABLED;

    return TRAINER_SPELL_GREEN;
}

主题

好友

154

积分

注册会员

发表于 2010-6-20 18:40:11 |显示全部楼层
3、释放spell流程
opcode = 0x012E // "CMSG_CAST_SPELL"
handler = WorldSession::HandleCastSpellOpcode

void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket)
{
    uint32 spellId;
    uint8  cast_count, unk_flags;
    // 可见参数结构:cast_count(1字节)、spellid(4字节)、unk_flags(1字节,这个一般用来指示target)
    recvPacket >> cast_count;
    recvPacket >> spellId;
    recvPacket >> unk_flags;                                // flags (if 0x02 - some additional data are received)

    // ignore for remote control state (for player case)
    Unit* mover = _player->m_mover;
    if(mover != _player && mover->GetTypeId() == TYPEID_PLAYER)
    {
        recvPacket.rpos(recvPacket.wpos());                 // prevent spam at ignore packet
        return;
    }

    sLog.outDebug("WORLD: got cast spell packet, spellId - %u, cast_count: %u, unk_flags %u, data length = %i",
        spellId, cast_count, unk_flags, (uint32)recvPacket.size());

    SpellEntry const *spellInfo = sSpellStore.LookupEntry(spellId );

    // spell不存在
    if(!spellInfo)
    {
        sLog.outError("WORLD: unknown spell id %u", spellId);
        recvPacket.rpos(recvPacket.wpos());                 // prevent spam at ignore packet
        return;
    }

    // 释放不会的spell或者passive spell(被动技能)是不行的
    if(mover->GetTypeId() == TYPEID_PLAYER)
    {
        // not have spell in spellbook or spell passive and not casted by client
        if (!((Player*)mover)->HasActiveSpell (spellId) || IsPassiveSpell(spellId) )
        {
            //cheater? kick? ban?
            recvPacket.rpos(recvPacket.wpos());                 // prevent spam at ignore packet
            return;
        }
    }
    else
    {
        // not have spell in spellbook or spell passive and not casted by client
        if (!((Creature*)mover)->HasSpell(spellId) || IsPassiveSpell(spellId) )
        {
            //cheater? kick? ban?
            recvPacket.rpos(recvPacket.wpos());                 // prevent spam at ignore packet
            return;
        }
    }

    // Client is resending autoshot cast opcode when other spell is casted during shoot rotation
    // Skip it to prevent "interrupt" message
    // 自动释放的技能直接忽略
    if (IsAutoRepeatRangedSpell(spellInfo) && _player->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL)
        && _player->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL)->m_spellInfo == spellInfo)
        return;

    // can't use our own spells when we're in possession of another unit,
    // 被控制时不能放技能
    if(_player->isPossessing())
        return;

    // client provided targets
    SpellCastTargets targets;
    if(!targets.read(&recvPacket,mover))
    {
        recvPacket.rpos(recvPacket.wpos());                 // prevent spam at ignore packet
        return;
    }

    // some spell cast packet including more data (for projectiles?)
    if (unk_flags & 0x02)
    {
        //recvPacket.read_skip<float>();                      // unk1, coords?
        //recvPacket.read_skip<float>();                      // unk1, coords?
        recvPacket.read_skip<uint8>();                      // >> 1
        recvPacket.read_skip<uint32>();                     // >> MSG_MOVE_STOP
        MovementInfo movementInfo;
        ReadMovementInfo(recvPacket, &movementInfo);
    }

    // auto-selection buff level base at target level (in spellInfo)
    if(targets.getUnitTarget())
    {
        SpellEntry const *actualSpellInfo = spellmgr.SelectAuraRankForPlayerLevel(spellInfo,targets.getUnitTarget()->getLevel());

        // if rank not found then function return NULL but in explicit cast case original spell can be casted and later failed with appropriate error message
        if(actualSpellInfo)
            spellInfo = actualSpellInfo;
    }

    // 准备释放
    Spell *spell = new Spell(mover, spellInfo, false);
    spell->m_cast_count = cast_count;                       // set count of casts
    spell->prepare(&targets);
}
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-20 18:42:06 |显示全部楼层
4、学天赋流程
opcode = 0x0251 // "CMSG_LEARN_TALENT"
handler = WorldSession::HandleLearnTalentOpcode

void WorldSession::HandleLearnTalentOpcode( WorldPacket & recv_data )
{
    uint32 talent_id, requested_rank;
    // 包参数:talent_id(4字节),学习的等级(4字节)
    recv_data >> talent_id >> requested_rank;

    // 学,函数摘录见下
    _player->LearnTalent(talent_id, requested_rank);
    _player->SendTalentsInfoData(false);
}

void Player::LearnTalent(uint32 talentId, uint32 talentRank)
{
    // 得到剩余的天赋点
    uint32 CurTalentPoints = GetFreeTalentPoints();

    // 剩余天赋点等于0,不能学
    if(CurTalentPoints == 0)
        return;

    // 想学的天赋等级 >= MAX_TALENT_RANK(5),不能学
    if (talentRank >= MAX_TALENT_RANK)
        return;

    TalentEntry const *talentInfo = sTalentStore.LookupEntry( talentId );

    // 天赋id不存在,不能学
    if(!talentInfo)
        return;

    TalentTabEntry const *talentTabInfo = sTalentTabStore.LookupEntry( talentInfo->TalentTab );

    // 天赋数据不存在,不能学
    if(!talentTabInfo)
        return;

    // prevent learn talent for different class (cheating)
    // 想学非本职业天赋,不能学
    if( (getClassMask() & talentTabInfo->ClassMask) == 0 )
        return;

    // find current max talent rank (0~5)
    uint8 curtalent_maxrank = 0; // 0 = not learned any rank
    for (int8 rank = MAX_TALENT_RANK-1; rank >= 0; --rank)
    {
        if(talentInfo->RankID[rank] && HasSpell(talentInfo->RankID[rank]))
        {
            curtalent_maxrank = (rank + 1);
            break;
        }
    }

    // we already have same or higher talent rank learned
    // 已学会高级的rank想学低级的(例如5级->4级->3级这样学),不能学
    if(curtalent_maxrank >= (talentRank + 1))
        return;

    // check if we have enough talent points
    // 检查是否有足够点数学习
    if(CurTalentPoints < (talentRank - curtalent_maxrank + 1))
        return;

    // Check if it requires another talent
    // 检查依赖其他天赋的天赋是否已达到要求的条件
    if (talentInfo->DependsOn > 0)
    {
        if(TalentEntry const *depTalentInfo = sTalentStore.LookupEntry(talentInfo->DependsOn))
        {
            bool hasEnoughRank = false;
            for (uint8 rank = talentInfo->DependsOnRank; rank < MAX_TALENT_RANK; rank++)
            {
                if (depTalentInfo->RankID[rank] != 0)
                    if (HasSpell(depTalentInfo->RankID[rank]))
                        hasEnoughRank = true;
            }
            if (!hasEnoughRank)
                return;
        }
    }

    // Find out how many points we have in this field
    uint32 spentPoints = 0;

    uint32 tTab = talentInfo->TalentTab;
    if (talentInfo->Row > 0)
    {
        uint32 numRows = sTalentStore.GetNumRows();
        for (uint32 i = 0; i < numRows; i++)          // Loop through all talents.
        {
            // Someday, someone needs to revamp
            const TalentEntry *tmpTalent = sTalentStore.LookupEntry(i);
            if (tmpTalent)                                  // the way talents are tracked
            {
                if (tmpTalent->TalentTab == tTab)
                {
                    for (uint8 rank = 0; rank < MAX_TALENT_RANK; rank++)
                    {
                        if (tmpTalent->RankID[rank] != 0)
                        {
                            if (HasSpell(tmpTalent->RankID[rank]))
                            {
                                spentPoints += (rank + 1);
                            }
                        }
                    }
                }
            }
        }
    }

    // not have required min points spent in talent tree
    // 越行学习(例如直接学第二行天赋),不能学
    if(spentPoints < (talentInfo->Row * MAX_TALENT_RANK))
        return;

    // spell not set in talent.dbc
    uint32 spellid = talentInfo->RankID[talentRank];
    if( spellid == 0 )
    {
        sLog.outError("Talent.dbc have for talent: %u Rank: %u spell id = 0", talentId, talentRank);
        return;
    }

    // already known
    // 已学会的天赋,不能学
    if(HasSpell(spellid))
        return;

    // learn! (other talent ranks will unlearned at learning)
    // 开始学习
    learnSpell(spellid, false);
    AddTalent(spellid, m_activeSpec, true);

    sLog.outDetail("TalentID: %u Rank: %u Spell: %u Spec: %u\n", talentId, talentRank, spellid, m_activeSpec);

    // update free talent points
    SetFreeTalentPoints(CurTalentPoints - (talentRank - curtalent_maxrank + 1));
}

综上我们看到,client送到server的数据是很少的,服务器的检查也是很多的。修改本地文件,无论是包在mpq(一种打包文件格式,可以理解为zip之类的,只是打包算法不同)的dbc(简单database文件)文件还是cache文件,或者通过ce修改内存中的数据(只是client受骗而已),最后总得通过某个opcode及其参数送到服务器才可能产生实际的效果,因此可以论断,修改数据包、修改本地文件、修改内存临时数据三者在对于服务器欺骗能力方面是基本等效的(严格说修改包的方式能力更强一些),也即,一者不能的其他二者也必不能
回复

使用道具 举报

主题

好友

142

积分

注册会员

发表于 2010-6-20 19:00:45 |显示全部楼层
不懂  但是还是谢谢诶
回复

使用道具 举报

主题

好友

64

积分

注册会员

发表于 2010-6-20 19:09:38 |显示全部楼层
楼主很强大 但是有些不懂 坐下慢慢研究下
回复

使用道具 举报

主题

好友

876

积分

高级会员

发表于 2010-6-20 19:15:34 |显示全部楼层
不懂,不过很有意思
回复

使用道具 举报

主题

好友

566

积分

高级会员

发表于 2010-6-20 20:15:52 |显示全部楼层
楼主讲的是官服
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-20 20:24:12 |显示全部楼层
说的就是[url=http://www.52wpe.net]私服[/url],trinity,当然,其他版本mangos、arcemu也类似
回复

使用道具 举报

主题

好友

104

积分

注册会员

发表于 2010-6-20 20:42:19 |显示全部楼层
有点意思。。。。。。。。。。。。
回复

使用道具 举报

主题

好友

748

积分

高级会员

发表于 2010-6-20 20:46:29 |显示全部楼层
不懂呀。。高手
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-20 20:48:44 |显示全部楼层
看蓝字结论就行了,如果想深入,自行研究服务器代码
回复

使用道具 举报

主题

好友

136

积分

注册会员

发表于 2010-6-21 07:23:36 |显示全部楼层
  一点都没有看明白呢
回复

使用道具 举报

主题

好友

354

积分

中级会员

发表于 2010-6-21 13:44:49 |显示全部楼层
难道加密过的包 要用WPE修改然后传到服务器 是不可能的 ?


  即使是[url=http://www.52wpe.net]私服[/url]也不行吗?
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-21 14:01:44 |显示全部楼层
@电动小黄瓜

blizzard官方wow的数据包上述已说明,只有头部,也就是“包长度”、“opcode”这两个域,是被加密的,由于rc4算法是公开算法,只要知道密钥,当然可以解密,修改,并重新加密伪装传输了,这没问题

此外,blizzard的wow加密数据包的密钥通过srp6算法运算客户端产生的随机数得到,srp6的特点是,仅凭客户端和服务器交互的信息,无法(几乎无法,运算成本极高,已达不实际程度)反运算出足够的数据来产生密钥,因此基本杜绝了网络监听后伪装或窃取相关信息的可能性,当然,运行在本地的wow.exe完全可能被第三方软件获取到key,修改包头也是可能的

其实完全可以不理会加密部分,99%的需求都不会涉及到修改包头,当然,类似于XD之类,没有合适的替代瞬发月火的技能的情况,修改包头,为参数添加target域固然是一个需求
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-21 14:13:27 |显示全部楼层
此文写了这么长,想表达的意思也很多,没有阐述清楚,以至于大家难以理解

其实并非想就一些技术细节问题与大家探讨,只是想简单的说明一个道理,也是蓝字部分提到了,有些看似神奇的修改,实际上在大多数服务器上,都是不切实际不可能做到的,给出那么多代码只是因为代码是最有力的论据之一了,胜过千言万语

网络游戏的漏洞说白了就是网络游戏服务器程序设计的漏洞,考虑不周所致,大体上分两类,一类是接口层面的,就是和客户端交互的数据方面的检查不周,二类是内部的,即便通过正常游戏也能产生的

对于第一类,在[url=http://www.52wpe.net]私服[/url]兴起的初期,这类bug非常多,例如学其他职业天赋、技能、复制物品、交易绑定物品、卡天赋等等,这都是服务器对于客户端充分信任(如果wow.exe确实可以信任),没有对送过来的数据加以详细检查导致出现一些非正常的状态,这些情况,如今的[url=http://www.52wpe.net]私服[/url]程序,已经非常完善,服务端将送来的数据做了充分的检查,在各种运算时刻都反复核实相关的状态是否正确,这点从代码字里行间就能看得出,因此,类似wpe这类利用接口漏洞欺骗服务器的应用将越来越少,由于上面提到,ce、修改缓存、修改mpq,原则上和wpe是等效的,也即,这种方式修改能有神奇效果的可能性也越来越小了,没有必要作为研究方向,这也是我写此文的最重要目的了

第二类bug,通过正常游戏也可以复现的问题,诸如众所周知的缴械溢出bug,这类bug还将持续存在,这个才是bug研究的正确方向,如何研究?要么在游戏里研究,反复思考试验,要么看代码

就写这么多
回复

使用道具 举报

主题

好友

148

积分

注册会员

发表于 2010-6-21 14:40:38 |显示全部楼层
LZ说的很实在,鉴定完毕
回复

使用道具 举报

主题

好友

288

积分

中级会员

发表于 2010-6-21 19:07:00 |显示全部楼层
朦朦胧胧学习下。。。很难消化。。。。。
回复

使用道具 举报

主题

好友

1122

积分

金牌会员

发表于 2010-6-22 13:26:49 |显示全部楼层
[url=http://www.52wpe.net]私服[/url]与官服,完全两个概念。LS那是GF的流程,SF现在用的都是模拟器,就完善程度以及稳定来说和GF的官方端的差别就好象电脑和小霸王一样....
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-22 18:03:44 |显示全部楼层
再说一下,我摘录的就是现在最流行的[url=http://www.52wpe.net]私服[/url]版本trinity的代码,并非官服,我要能搞到官服代码能公开说出来么?
回复

使用道具 举报

主题

好友

154

积分

注册会员

发表于 2010-6-22 18:05:10 |显示全部楼层
版本b6552,适合客户端313,现在大家玩的313 trinity端,都和这个版本能力类似
回复

使用道具 举报

快速发帖

您需要登录后才可以回帖 登录 | 注册会员

手机版|Archiver|WPE|52wpe|我爱WPE ( 闽ICP备15009081号 )

GMT+8, 2024-4-28 14:34 , Processed in 0.062463 second(s), 16 queries .

返回顶部