00003 不思议迷宫.0003:玩家数据真的就不能改了吗?



00003 不思议迷宫.0003:玩家数据真的就不能改了吗?

玩家数据保存在服务器的数据库里,这使得我们没办法直接修改它。那间接的呢?

在以前,iOS上有一些内购插件,据说可以破解App内购。其实现的原理,在于iOS内购的不完善,或者说bug:用户在手机上点击购买后,会向AppStore发送购买消息;AppStore收到消息,执行扣费,然后返回给手机;手机收到AppStore返回的消息,再向游戏服务器发送购买消息。这里面就有一个问题,我购买1元钱的A物品,但通过破解,向服务发送购买10元钱的B物品。服务器无从辨别我到底买的是A还是B,所以只能当真了。然后,我还能再进一步,直接向服务器发送购买消息,而不再向AppStroe真实地购买物品。这就是内购破解。现在,据说苹果做了修正,已经可以直接通知游戏服务器。如此,内购相关的用户数据,我们已经无法破解。

修改网络游戏玩家数据的最大障碍,在于数据是在服务器端计算的。比如钻石+10,它并不是在客户端(也就是手机上)计算出结果,再将计算结果发给服务器,服务器收到结果后进行持久化。比如玩家当前有100个钻石,打怪掉落10个钻石,客户端就计算出结果110个钻石,然后将110个钻石发给服务器,服务器收到,保存110个钻石。完全不是这样,而是相反的:打怪掉落10个钻石,杂玩家捡取时,向服务器发送捡取10个钻石的消息,服务器计算并保存结果,然后返回结果给手机(有些游戏确实是这样做的,可以确保数据同步。但大多数游戏在保存结果后,返回的不是计算结果,而仅仅是一个是否捡取成功的标志。客户端根据这个标志做出相应的处理。如果收到某些因素的影响,导致标志被更改,这只会让客户端显示出错误的数据。在玩家下次登录游戏时,会接收服务器端的正确数据)。如果向服务器发送的是“捡取10个钻石” 的消息,那我们就又可以动点脑筋了,比如,改成“捡取100000个钻石”怎么样?好吧,这招也行不通,因为真正发送给服务器的是“玩家xx捡取了掉落yy”。服务器收到消息,会检测“掉落yy”所对应的怪是不是“玩家xx”打死的。如果是,再从服务器的数据库中读取“掉落yy”的内容,将之奖励给“玩家xx”,再反馈给客户端。

现在要弄明白的问题是,《不思议迷宫》中有没有漏洞:在客户端进行的计算、类似“捡取10个钻石”的消息。就是这个思路,但我个人精力有限,不可能一一验证。

在我将主线玩到第二关(或是第三关?人老了记忆都不好了,忘记了……)的时候,地牢中出现“奇怪的地板”,用铁锹挖开后会奖励一些东西。关键是这些东西是随机的,可能是金币,也可能是钻石,还可能是物品。我就在想,能改成每次都随机到钻石吗?

计算奖励的代码文件为src/game/formula/CALC_ODD_FLOOR_BONUS.luac,内容如下:

returnfunction(user)

    local dungeonId = DungeonM.getDungeonId();

    local layer = DungeonM.currentLayer();

    local bonus = {};

 

    if DungeonAreaM.isEndlessArea(dungeonId)then

        local rand = DungeonM.getRandSeed("ODD_FLOOR");

        local rand1 =DungeonM.getRandSeed("ODD_FLOOR");

        local classId = FormulaM.invoke("FETCH_BY_RAND",{ 1102, 1103 }, rand1);

        local arr;

        local arr1;

 

        ifBuildingBonusM.getFieldStat("odd_floor_gem") < 10 then

            arr = { {["bonus"] = { 2, "money", 300, }, ["ratio"] = 40,},

                    {["bonus"] = { 1, classId, 3, }, ["ratio"] = 40, },

                    {["bonus"] = { 2, "gem", 1, }, ["ratio"] = 20,},};

        else

            arr = { {["bonus"] = { 2, "money", 300, }, ["ratio"] = 40,},

                    {["bonus"] = { 1, classId, 3, }, ["ratio"] = 40, },};

        end

 

        arr1 = fetchElemBySeed(arr, rand);

        arr = arr1["bonus"];

 

        -- 当前累计的地板奖励钻石

        -- 添加统计

        if arr[2] == "gem" then

          BuildingBonusM.addFieldStat("odd_floor_gem", arr[3]);

        end

        bonus = { arr, };

    else

        -- 小关卡

        if layer == 3 or layer == 12 then

            bonus = { 2, "money", 500};

        else

            -- 18F层奖励:5钻石(再次获得,则为【金币】×500

            ifNewbieDungeonM.isFirstNewbieBonus("odd_floor") then

                bonus = { 2, "gem", 5};

               NewbieDungeonM.statNewbieFirstBonus("odd_floor");

            else

                bonus = { 2, "money",500 };

            end

        end

    end

 

    return { ["bonus"] = bonus, };

end

从上面的代码中完全看不出是如何与服务器通信的。这个暂时先放放,我们先把奖励的数值改了,看看效果。因为我已进入无尽模式,因此就改了无尽模式中的相应奖励,即上面中的红字部分。嗯,就在奖励数值后头加两个0吧:

        ifBuildingBonusM.getFieldStat("odd_floor_gem") < 10 then

            arr = { { ["bonus"] = {2, "money", 30000, }, ["ratio"] = 40, },

                    { ["bonus"] = {1, classId, 300, }, ["ratio"] = 40, },

                    {["bonus"] = { 2, "gem", 100, }, ["ratio"] = 20, },};

        else

            arr = { { ["bonus"] = {2, "money", 30000, }, ["ratio"] = 40, },

                    { ["bonus"] = { 1,classId, 300, }, ["ratio"] = 40, },};

        end

保存后安装到手机,进入游戏后,发现客户端所用数值正是刚刚修改的:挖一次奇怪的地板,如果奖励是金币,就+3万;如果是物品,就+300;如果是钻石,就+100。这让我很高兴,因为它证明了这个文件确实被客户端执行了,而它里面包含了好几个分支逻辑。这些分支逻辑是如何和服务器同步的呢?难道每一个有关玩家信息的函数调用都是同步调用?如果不是,那就应当是使用了某种缓存,我们是不是有什么可以下手的地方?

先不管那么多,改成每次都随机到钻石,这只要在“arr1 = fetchElemBySeed(arr, rand);”这条语句之前添加一个循环即可:

        while true do

            if fetchElemBySeed(arr,rand)["bonus"][2] == "gem" then

                break

            end

            rand =DungeonM.getRandSeed("ODD_FLOOR");

        end

进游戏,开挖,然后,游戏失去响应玩完了。回头再仔细看看代码,发现原来是:

if BuildingBonusM.getFieldStat("odd_floor_gem") < 10then

这个判断的问题。奇怪的地板只能挖取10个钻石,再多就没有了。所以while进入了死循环。既然如此,那我们就改掉吧,来个最简单的:

returnfunction(user)

    return { ["bonus"] = { 2,"gem", 100, }, ["ratio"] = 20, } };

end

这个有啥效果我没有进行验证。——由于手机未越狱,而《不思议迷宫》全是零碎的小文件,安装一次太耗时了;我又是个正处失业状态的穷吊死,舍不得手机被翻来覆去的折腾;最关键的是,我不认为它有啥用。

没验证就没有发言权,所以这个问题暂时保留。——正常情况下应当是在进行同步时报“数据异常”的错误。

如果没有生效,那我就会想,是不是改得该简单了,也许原函数中的某个函数调用中包含了至关重要的和服务器端的“同步调用”。当然,具体的同步机制我们在下面会进行研究。但是在现在,我只想做点小更改看看效果——万一行了呢,不就省了一大堆的代码阅读理解了吗?

这次我们作最小更改:

returnfunction(user)

    ……

 

    if DungeonAreaM.isEndlessArea(dungeonId)then

        ……

 

        arr1 = fetchElemBySeed(arr, rand);

        arr1 =arr[1];

        arr = arr1["bonus"];

 

        ……

    else

        ……

end

只是添加了一条语句,将奖励改为金币而不会出现物品。至于效果嘛,大家自己验证吧,反正我估计是不会成功的。

有人可能就说了,这个你不验证,那个你也不验证,那还写啥啊,洗洗睡吧。我呢,当然有需要验证的东西。个人觉得成功率不大或者没啥帮助的东西,就会忽略过去。我之所以觉得上面的最小修改会没有效果,是因为我跟踪过DungeonM.getRandSeed函数。

-- 获取随机种子,循环使用。这个接口不能随便乱用,一定要确保和服务器严格同步

function getRandSeed(desc)

    if not descthen

       error("desc不能为空")

    end

 

    if notdungeonContainer then

        return0;

    end

 

    -- 根据游标

    local index= RandomFactoryM.fetchRandCursor(desc);

    local seeds= dungeonContainer.rand_seed;

    local num =#seeds;

    local seed =seeds[index % num + 1];

 

    -- 游标往下移动

   RandomFactoryM.useRandCursor(desc);

 

    localRandomType = RandomFactoryM.getRandomType(desc);

 

    local msg =string.format("[%s]获取随机数: %d, cursor: %d",RandomType, seed, index);

 

   DungeonLogM.addLog(msg);

   DungeonDebugM.addCursor(desc, RandomType, index);

    return seed;

end

先不用看代码,就看注释吧,“这个接口不能随便乱用,一定要确保和服务器严格同步”。如果注释没有欺骗我们的话,那么上面的最小修改就是和服务器不同步的。当然,不同步的不是DungeonM.getRandSeed及其相关调用,而是arr及最终的奖励。服务器的奖励是按照

        arr1 = fetchElemBySeed(arr, rand);

        arr = arr1["bonus"];

进行计算的。而本地却横插一杠,得出了和服务端不同的结果:

        arr1 = fetchElemBySeed(arr, rand);

        arr1 =arr[1];

        arr = arr1["bonus"];

这就“数据异常”了。

我们要找出一个和服务器端同步的解决办法。回到开头的哪个while

        while true do

            if fetchElemBySeed(arr,rand)["bonus"][2] == "gem" then

                break

            end

            rand =DungeonM.getRandSeed("ODD_FLOOR");

        end

这个就很好。如果DungeonM.getRandSeed中包含了和服务器的同步调用,那我们的最终计算也会是和服务器的同步的。当然了,这次我们要修改的不是钻石,不是金币:

        while true do

            if fetchElemBySeed(arr,rand)["bonus"][2] == "money" then

                break

            end

            rand =DungeonM.getRandSeed("ODD_FLOOR");

        end

那么,如何验证我们的修改到底生效与否?首先,我们要排除干扰的地方,比如炼金工坊中的冈布奥,不要让它采集金币;先前已验证过不成功的+30000金币的修改,恢复为正常的+300。然后进主线第二关“英雄之村”,玩吧。在碰到“奇怪的地板”,在开挖之前,先暂离,记录一下当前的金币数量。然后再回到副本,开挖。挖取后再暂离,看看金币是否加了300。但这还不够准确,最好的办法是挖取后暂离,然后重新登录,再看看金币是否增加。如果确实增加了,也不要高兴的太早,因为这很可能是巧合。我们需要多试几次。

我在试到第二次的时候失败了。我一时找不到其他办法,只能老老实实坐下来研究下代码,就从DungeonM.getRandSeed开始。

函数不大,瞄几眼就能找到重点:

    -- 根据游标

    local index= RandomFactoryM.fetchRandCursor(desc);

    local seeds= dungeonContainer.rand_seed;

    local num =#seeds;

    local seed = seeds[index % num + 1];

 

    -- 游标往下移动

    RandomFactoryM.useRandCursor(desc);

    ……

    return seed;

很明显,返回的随机数受到两个因素的影响,indexnum,也就是RandomFactoryM.fetchRandCursor(desc)dungeonContainer.rand_seed。后者看起来是一个固定的数组,至少它的值在函数DungeonM.getRandSeed中没有被改变,在计算奖励的匿名函数中也没有被改变。——或许有改变,在下面的RandomFactoryM.useRandCursor(desc)中?前者和RandomFactoryM.useRandCursor(desc)都很可疑,那就一起研究研究吧。

-- 获取随机数游标(暂时只有迷宫中用到,以后再说)

function fetchRandCursor(desc)

    local id =0;

    iftype(cursorTable[desc]) == "table" then

        id =cursorTable[desc].id;

    end

    local cursor= ME.user.dbase:query("randomCursor", {});

 

    return cursor[id]or 0;

end

冒出了一个新的东西:ME.user.dbase:query,看名称是从数据库中查询。函数简短,也没什么难理解的地方,就是获得desc对应的id,然后根据这个id返回从数据库中查询到的、与其对应的一个值。没什么特别的,那先放放,看看RandomFactoryM.useRandCursor(desc)

-- 使用游标

function useRandCursor(desc, offset)

    local id =0;

    iftype(cursorTable[desc]) == "table" then

        id =cursorTable[desc].id;

    end

    local cursor= ME.user.dbase:query("randomCursor", {});

   ME.user.dbase:set("randomCursor", cursor);

 

    local value= (cursor[id] or 0) + (offset or 1);

 

    cursor[id] =bit.band(value, 0xffff);

end

这个函数的前面几句和fetchRandCursor相同,需要注意的是后面三句。

第一句:ME.user.dbase:set("randomCursor",cursor);这个只是个简单的set,它的唯一用处是当randomCursor不存在时,初始化为{}(这个还是在上一句ME.user.dbase:query("randomCursor", {})中作为默认值返回的)。

下面两句是重点,因为它更改了一点东西。

DungeonM.getRandSeed中,仅向useRandCursor传递了一个参数。因此,在此处,第二个参数offsetundefined(我也不知道lua中用啥表示,大家凑合着看吧)。local value = (cursor[id] or 0) + (offset or 1);,也就变成了local value = (cursor[id] or 0) + 1;。看到+1我就放心了:它和cursor这个东西对上了,大概是让cursor移动到下一个位置;而cursor[id]中记录着cursor的当前位置。可是第三句又让我迷糊了,为毛不是cursor[id] = value;?为什么要bit.band(value, 0xffff)?经过查找,得知bit.bandlua提供的内置函数,用来执行整数的位and运算。这下明白了,这是将cursor[id]的值域限制在了[0, 0xffff-1]

到此,就不由得叹气了,没发现和服务器有啥关系啊,看来得回头看看刚刚放过的ME.user.dbase:query

你可能感兴趣的:(游戏破解技术研究,不思议迷宫)