作者 | 孙景卫
责编 | 郭 芮
最近流行玩《魔卡幻想》,作为一名手游测试人员,于是申请了一个号码,取名“大头包子”。闯关、打塔,打了一个多月,只有一张五星黑龙,很是悲催。于是花了40两银子在某宝上买了一个天使开局,附送了一个凤凰号。
所谓天使、凤凰,就是其中两张比较厉害的五星卡牌,如下图所示:
左:天使,我的最爱;右:凤凰
所以,我现在就有了三个号码:一个开局号,一个天使号,一个凤凰号。虽然说,手游本身不太花费时间,但三个号一起玩,就很费时间了,而且和其他手游一样,魔卡也有一个很“变态”的设置,就是有些行动,你必须间隔10分钟或者1个消失才能执行一次,每天中午事儿点和晚上十点还有“魔神入侵”的活动,然后在魔神被打死前,你需要每隔3分钟打一次魔神,才能获得足够的金币奖励,这样你就不能花五分钟玩完游戏,然后放手去做其他事情。也就是说,你必须每隔一段时间上去看看,很是神伤。
另外,魔卡高级卡牌的获取有三种方式,一种是魔卡碎片换取,一种是晶钻兑换,一种是刷塔(反复的对指定的关卡进行通关)。不管你是不是RMB玩家,不管你是大R还是小R,或者是如我之辈的非酋玩家,换取高级卡牌都是很考验人品的,有人说洗漱好之后获得五星卡牌几率高,有人说,晚上六点到六点十分几率高,有人说贴吧爆照第二天会出五星,所以,十分的纠结,魔卡的五星卡牌发放到底是什么样的规则呢?
总结一下,问题有二:
费时间:玩多个号、每天刷塔,耗费时间,而且需要你一直惦记着;
运行机制搞不清楚:高级卡牌靠人品,不免想起,魔卡到底是怎么工作的呢?
遭遇困境,尝试反编译
想搞清楚魔卡到底是怎么工作的,第一个,很自然的想法就是反编译。对Android程序更熟悉,所以,试着反编译Android版本的apk。
我们知道android的安装文件.apk实际上是一个压缩包,所以将你下载下来的魔卡幻想的apk文件后缀改为zip并解压(我放置在D:\works\mokahuanxiang\下面),得到如下文件:
先看第一个文件夹,assets下面有两个文件夹,一个是卡牌的底图,一个是符文的底图,如下:
apk解压后的卡片底图,可以看到天使也在其中
apk解压后的符文图片
其他几个文件对我们用处不大,我们关注的代码都放置在classes.dex里面,游戏逻辑应该都写在里面了,不过classes.dex是经过Google制定的一种可以在android dalvik虚拟机上运行的格式,要查看里面的内容,先用dex2jar转换一下格式,命令行模式下运行:
dex2jar classes.dex
会生成classes_dex2jar.jar,这个jar包就可以利用jdgui来反编译。进入jdgui文件夹双击jd-gui.exe,打开上面生成的jar包classes_dex2jar.jar,即可看到源代码了,代码如下图:
反编译后的Java代码
至此,大功告成!但深入去看的时候,找遍了所有的代码,发现竟然完全没有逻辑代码,比如卡牌强化是如何强化的?魔神是如何的?盗贼是什么情况下出现的?......纠结呀,那么逻辑代码都是写在哪里的呢?
反编AIR,柳暗又花明
可以看到,dex2jar的文件夹下面有一个shanks.flash.ane的类,ane即Adobe AIR Native Extension,因为Adobe AIR是跨平台的,Mac、Windows、Android、iOS都能运行,但也有其局限性,比如在Android上面,AIR就无法调用Android系统级别的API,比如摄像头、系统通知等。
如果想做类似的事情,就需要先用Android写一段本地代码,让AIR和这段代码通信,进而完成类似的工作,这也就是魔卡游戏的总体工作机制。
如下图所示:
魔卡游戏架构图
所以,大胆猜测,大部分游戏逻辑是写在Adobe AIR层的。据百度百科,AIR是针对网络与桌面应用的结合所开发出来的技术,可以不必经由浏览器而对网络上的云端程式做控制,这样的优势,就是跨平台,主要的逻辑代码无论在Android上还是在IOS上,甚至在Windows Phone上都是一致的。在每个平台上,只要实现对应的系统API即可,这也是为什么,魔卡的账号体系在iOS上和Android上是统一的,不像某掌门游戏。
既然确定,游戏逻辑写在AIR层,于是开始找对应的实现文件,终于发现了:
找到逻辑代码所在处
得到了Cardmain.swf文件,在浏览器下直接打开看看,没有任何反应,只是白屏,怎么办呢?再来反编译,使用业界出名的硕思闪客精灵进行反编译试试,安装了一个企业版,打开这个swf文件,经过“漫长”的等待,终于,结果出来了。可以看到,游戏界面主要的形状、图像、音频、视频以及关键的动作脚本都是在这个swf文件里面的。
反编译后的Cardmain.swf文件
点开对应的额动作复选框,查看对应的脚本代码。其中有几个关键的文件和目录,其中一个是URLconfig文件,是记录的所有的客户端命令所对应的服务器指令地址,比如:
这个card.php?do=GetAllCard就是用来获的指定用户的所有的卡片的信息的,后面还有很多:
反编译的UrlConfig文件
核心逻辑代码写在这里:
魔卡核心逻辑代码
以卡牌/符文强化为例,每张卡牌都对应着一定的升级金币和经验点数,每张符文也有对应的经验点数,这是每张卡牌固有的属性。当你升级的时候,是将被升级卡牌的金币和经验乘以一个系数进行累加,然后再计算被升级卡牌、符文的升级情况的。
具体代码片段如下:
可以看到,一星卡牌的升级系数是0.6,二星卡牌是0.7,以此类推,也就是说,如果你吃三星卡牌,那么你将损失20%的经验,如果你吃五星卡牌的话,那么经验点将完全不会损失哦。不过又有几个非大R玩家能富裕到舍得吃五星呢......
当然,里面还有很多其他秘密,待你去发掘。
协议分析,自动打迷宫
到目前为止,我们了解了魔卡的架构,还有一些底层的实现。我们知道,主题的逻辑是通过AIR来和服务器进行交互,所以,大部分命令都是server端完成的,那么,很自然想到分析一下协议,使用大名鼎鼎的wireshark软件抓包。但是,魔卡幻想是个手机游戏,想要抓手机给服务器端发包,怎么做呢?可以用模拟器来做。
大家知道BlueStacks本身就是一个Android模拟器,这个模拟器运行在我的Windows操作系统之上,那么,我在BlueStacks上玩魔卡幻想的时候,所有的命令包都会通过我操作系统的网卡发送出去。所以,利用wireshark抓取Windows上的数据包,也就能够抓取到魔卡幻想给其服务器端的数据包了。
我们先看一下,打一个迷宫大致的数据流如何。我现在在末日之塔,也就是7塔,第四层的最后一个怪,先在BlueStacks上进入这个状态,然后打开wireshark开始抓包,打怪,结束后停止录制,查看wireshark上的信息。
记录的网络通信包如下图所示。因为wireshark记录了所有的网络交互,所以,我使用一个Filter来过滤我们所关注的数据包,filter为:
http && (ip.dst == 116.90.81.139) || (ip.src==116.90.81.139)
这句话的意思是,过滤掉HTTP协议的中目标地址为116.90.81.139或者源地址为116.90.81.139的协议包,其中116.90.81.139为魔卡幻想的web server。可以看到,其实每次打一个怪,相当于给服务器发送了一个POST命令!而POST的数据内容为“manual=1&MapStageId=7&Layer=4&ItemIndex=13”——这句话什么意思呢?Manual=1,代表自动战斗,MapStageID=7,代表我打的是第7个塔,也就是末日之塔,ItemIndex=13是什么意思呢?为什么是13呢?暂时搞不清楚。
再刷几个怪对比一下数据。我们发现,每进入一个楼层的时候,服务器端会返回一个本楼层的一个基本信息。末日之塔第四层的信息如下:
{"status":1,"data":{"Name":"\u672b\u65e5\u4e4b\u5854\u7b2c4\u5c42","BoxNum":2,"MonsterNum":2,"RemainBoxNum":2,"RemainMonsterNum":1,"Layer":4,"TotalLayer":5,"Map":{"IsFinish":false,"WallRows":[0,0,0,1,0,0,0,0,1,1,1,1,0,0,1,0,1,0,1,0,0,0,0,1,1,0,0,0],"WallCols":[0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,1,1,1,0,0,1,0],"Items":[1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,3,1,1,1,1,6,1,4,6,1,1,6,1,1,1]}},"version":{"http":"201302090","stop":"","appversion":"version_1","appurl":"ios:\/\/xxx"}}
其中Name字段是unicode编码,然后用gb2312编码打印出来,"\u672b\u65e5\u4e4b\u5854\u7b2c4\u5c42" 就是“末日之塔第4层”,后面的字段顾名思义,重点说一下Items字段。
我们发现,"Items" : [1,1,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,3,1, 1,1,1, 6, 1 , 4, 6, 1,1,6,1,1,1 ] 共28位,我们调整一下格式:
[1,1,1,1,1,1,1,5,
1,1,1,1,1,1,1,1,
1,3,1,1,1,1,6,1,
4,6,1,1,6,1,1,1]
再和下图对比一下:
你会发现,这个Items字段中,1代表普通方块,5代表是向上的楼梯,4代表向下的楼梯,6代表已经被打掉的宝箱或者怪物,同理可以分析,2代表宝箱,3代表怪物,然后indexitem就是方块的索引!
这样,我们自动打塔的思路就确定了:
先获得每一层的信息,然后分析一下这一层的怪物和宝箱的情况,得到对应的indexitem,然后再POST给对应的server就可以完成了,上代码:
其中maze_info返回的是一个指定地图和楼层的json格式的信息,我使用exec将其转化为一个dict对象,然后获得其中的Items字段,如果这个字段是2或者3的话,我就去打,最后再打去楼上的楼梯口,很简单吧。
具体如何打的呢?请看maze_battle函数:
关键是我们的PostMessage函数,最终都是通过这个函数组成特定的魔卡服务器能识别的HTTP协议包,利用Post命令发送出去。
发送的HTTP协议包的内容为:
POST /maze.php?do=Info&v=9945&phpp=ANDROID&phpl=ZH_CN&pvc=1.2.0&pvb=2013-04-16%209%3A18%3A23 HTTP/1.1
Host: s9.mysticalcard.com
Accept-Encoding: identity
Content-Length: 20
Accept: text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, text/css, image/png, image/jpeg, image/gif;q=0.8, application/x-shockwave-flash, video/mp4;q=0.9, flv-application/octet-stream;q=0.8, video/x-flv;q=0.7, audio/mp4, application/futuresplash, */*;q=0.5
User-Agent: Mozilla/5.0 (Android; U; zh-CN) AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/3.6
Connection: Keep-Alive
Cookie: _sid=qjl7qn8js501756f57lrevet55
Cache-control: no-cache
Content-Type: application/x-www-form-urlencoded
Refer: app:/assets/CardMain.swf
Layer=2&MapStageId=5
使用Python的urllib库来进行HTTP协议发送,另外,由于server端可能返回gzip格式的数据内容,所以需要利用Python的gzip库对gzip格式的返回数据进行解码。
这样,刷塔的程序就大致成型了。
最终,你只需要两行代码就能完成刷塔工作了:
这两句话的意思是,我新建一个魔卡幻想的对象,然后刷第五塔的一二三四层!搞定。
再举一个简单的例子,就是对某个地图进行探索,返回信息。下图为我进行一次探索的运行截图,我对编号为44的关卡进行探索,获得了740点经验,1280个金币,以及一个8号魔幻碎片。
探索地图协议流
探索地图获得奖励
除了自动打地图、自动探索外,你可以利用协议做更多的事情,比如每隔10分钟获取一次盗贼,如果有盗贼,则挑选级别最高的打。再比如每天中午12点魔神入侵的时候,你每隔3分钟打一次魔神,这样会累积比较高的积分,也不用你总是惦记着打魔神这回事了。
登陆破解,大江已东去
上文中,利用HTTP协议进行自动打塔,有一个关键的地方没有说明,就是登陆。魔卡幻想本身具有自动登陆的功能,其实现的原理就是利用Cookie信息,如果你发送HTTP请求的时候,HTTP头携带了Cookie字段,而且Cookie字段和你最近登录一次的设备匹配的话,那么就不需要重新登录,而可以直接进行一些对应操作了。
魔卡对Cookie信息的处理流程大致是,如果你发送过来的Cookie信息在我的server端有记录,而且最近登录设备匹配的话,那么我就返回给你所有你需要的信息。这样,如果你有一个账号,只在一台设备上登录的话,那么你可以一直用这个Cookie,但是如果你有多个账号的话,则需要一个手动登录账号的过程,非常麻烦,能否进行自动登陆呢?
那么我们就来看一下魔卡的登陆是怎么做的吧。
继续抓取登陆的时候的协议,如下图所示,即给login.php?do=PassportLogin发送一个POST命令,POST的消息体内容为Devicetoken=&time=1371305578867&key=7a370df34c5fddd1f1c6ee9d6279d7da&Origin=TTGW&Udid=3D%3ABF%3A4A%3A6A%3AC0%3A7D&UserName=qqqaaagsr&Password=1000616820。其中Devicetoken为空,time为自Epoc以来的秒数,key这个东西,猜测是用来做验证的,比如验证cookie,uid等是否是一致的。Udid是设备的唯一标示信息。UserName是我的账号名,password=1000616820,这个是经过加密之后的密码。
试着直接将UserName改成我另外一个好的用户名,发送POST信息,得到一个错误“passport\u7b7e\u540d\u9a8c\u8bc1\u5931\u8d25!”,意思是:“passport签名验证失败!”。猜测玄机在key上,其应该是用来验证Udid、UserName和Password的一致性的。
魔卡登陆协议包抓取
但是,这个key是怎么生成的呢?猜测是在登陆页面生成的,同样利用反编译,找到其登陆界面的网址:http://pp.fantasytoyou.com/pp/start.do?udid=3D:BF:4A:6A:C0:7D&locale=CHS&gameName=CARD-ANDROID-CHS&client=flash,这样就可以在Chrome中打开了:
在Chrome中打开登陆界面
邮件查看源码,发现其主要的登陆逻辑都是写在“http://d.muhecdn.com/pp/V20130528/js/startHttp.js”这个JS脚本里面的。时间关系,这个登陆的破解暂时还没有搞定,服务器端总是提示Passport验证错误。
经过一段时间探索,自动登陆终于搞定了。魔卡的服务器主要有两个,第一个是业务服务器,host为s9.mysticalcard.com,绝大多数的业务操作、日常事务都是和这个服务器交互完成的,还有一个是登陆验证服务器,host是pp.fantasytoyou.com,是专门进行用户登陆的验证的。
所以,登陆共分为三步:
第一步,先给业务服务器发送一个GET请求,进入登陆界面。包主题内容为:
GET /pp/start.do?udid=3D:BF:4A:6A:C0:7D&locale=CHS&gameName=CARD-ANDROID-CHS&client=flash
返回的即是登陆界面。
第二步,将用户名、密码、设备标识信息发送给验证服务器,获得密钥。包主体内容为POST /pp/httpService.do,POST的内容为:
{"serviceName":"login","callPara":{"userName":"qqqaaagsr","userPassword":"password1","gameName":"CARD-ANDROID-CHS","udid":"3D:BF:4A:6A:C0:7D","clientType":"flash","releaseChannel":"","locale":"chs"}}
返回的信息(密钥)如下:
{"returnCode":"0","returnMsg":"No error.","returnObjs":{"GS_NAME":"server9","GS_IP":"http://s9.mysticalcard.com/","friendCode":"null","GS_PORT":"80","timestamp":"1372814484134","GS_CHAT_PORT":"8000","source":"qqqaaagsr","userName":"qqqaaagsr","GS_DESC":"","U_ID":"1000616820","key":"b1ddb0dd6c2ab73ff1b5cb84b2c9d465","G_TYPE":"1","GS_CHAT_IP":"116.90.81.139"}}
第三步,利用上述返回的信息进行登陆,在业务服务器上的login.php进行登陆,包主体内容为:
POST /login.php?do=PassportLogin&v=3539&phpp=ANDROID&phpl=ZH_CN&pvc=1.2.0&pvb=2013-04-16%2012%3A46%3A54
POST的消息体为:
Devicetoken=&time=1372814484134&key=b1ddb0dd6c2ab73ff1b5cb84b2c9d465&Origin=TTGW&Udid=3D%3ABF%3A4A%3A6A%3AC0%3A7D&UserName=qqqaaagsr&Password=1000616820
登陆成功。
其中,登陆能否成功,上述的三个蓝色字段是关键字段,其中timestamp是个时间戳,验证的时间戳必须和登陆的时间戳为同一时间,key是验证服务器生成的一个类似MD5的数值,要伪造不太可能,只能通过截取转发的方式来记录,而U_ID字段,则是在登陆的时候作为Password这个参数的值传递给server的。
最终的效果为:
这段代码既可完成对我三个账号的第8塔的1~5层进行刷塔。
作者:孙景卫,运营个人公众号老铁开花,IT行业资深从业者,拥有十余年技术实战经历,之前任职于百度、网易等多家上市公司。具备管理大型项目经验,管理上百人团队,目前就职于某互金公司,担任高级技术总监。
声明:本文为CSDN原创投稿,未经允许请勿转载。欢迎通过以下方式联系投稿。
热 文 推 荐
☞ 阿里布局物联网!开源操作系统 AliOS Things 喜提 1 亿芯片出货量
☞没有新芯片,没有大核弹,黄教主这次给大家带来了个PRADA
☞曝光!月薪 5 万的程序员面试题:73% 人都做错,你敢试吗?
System.out.println("点个在看吧!");
console.log("点个在看吧!");
print("点个在看吧!");
printf("点个在看吧!\n");
cout << "点个在看吧!" << endl;
Console.WriteLine("点个在看吧!");
Response.Write("点个在看吧!");
alert("点个在看吧!")
echo "点个在看吧!"
点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。