赛尔号通信数据的逆向分析与还原(思路篇)
0x01 前言
flash游戏即将淘汰,被称为最强的as加密方式Alchemy被应用于一些主流的页游,很多游戏都转战h5或者手游端。今天闲来没事,就上手一款童年游戏。"赛尔号",应该是全网首发吧
0x02 准备工具
FFDEC(强大的免费开源swf反编译工具) 、 Chrome浏览器 、 在线Hex编辑
0x03分析过程
使用 浏览器加载游戏并 通过 开发者工具抓取到数据包
可以发现一个dll.xml的配置文件
此游戏的习惯将核心数据存放于==TaomeeLibraryDLL.swf==文件当中。
此时直接将swf文件拖入FFDec会发现,读取失败。
需要拖入Hex编辑器搞一下
==CWS== 是标识码 具体可以参考 Swf文件头信息,前面则是一些无关紧要的数据。
可以看到在Client.swf里对于swf文件的加载是这样处理的
private function onComplete(e:Event) : void
{
var info:DLLInfo = this._dllList[0];
var byteArray:ByteArray = new ByteArray();
if(this._isRelease)
{
this._stream.readBytes(new ByteArray(),0,7);//相当于设置了位置 前面可以看出到,标识码前面有7个字节是无用的数据 >> this._stream.potion=7
}
this._stream.readBytes(byteArray);//
if(this._isRelease)
{
byteArray.uncompress();//再进行Zlib解压处理
}
this._stream.close();//关闭字节流
//然后就加载
this._loader.loadBytes(byteArray,new LoaderContext(false,ApplicationDomain.currentDomain));
}
思路清晰,可以干代码,这里使用了易语言
导出文件
可以,再将其导入FFdec看看效果。
解压后,开始找与通讯的类。一般搜索flash.net.Socke即可
==send(param1, param2) #4CAF50==发送数据包的函数
public function send(param1:uint, param2:Array) : uint
{
var loc3_:* = null;
var loc4_:* = null;
if(this.connected)
{
loc3_ = this.pack(this.userID,param1,param2);
loc3_.position = 0;
loc4_ = MessageEncrypt.encrypt(loc3_);//这里就开始加密了
writeBytes(loc4_);//整个数据丢给
flush();//这里就是发送数据出去了
this.sendDataError(param1);
DebugTrace.show(">>Socket[" + this.ip + ":" + this.port.toString() + "][cmdID:" + param1 + "]",getCmdLabel(param1),"[data length:" + this._sendBodyLen + "]");
//这里可以清晰的看出 param1是数据包的命令码,param2为数据的整体
return this._result;
}
return 0;
}
public function pack(param1:uint, param2:uint, param3:Array) : ByteArray
{
var loc4_:ByteArray = new ByteArray();
this.serializeBinary(loc4_,param3);//这里是将数组序列化
this._sendBodyLen = loc4_.length;
var loc5_:ByteArray = this.packHead(param1,param2,loc4_);
var loc6_:ByteArray = new ByteArray();
loc6_.writeBytes(loc5_);
loc6_.writeBytes(loc4_);
return loc6_;
}
private function packHead(param1:uint, param2:uint, param3:ByteArray) : ByteArray
{
var loc6_:* = 0;
var loc7_:int = 0;
var loc4_:ByteArray = new ByteArray();
var loc5_:uint = param3.length + HEAD_LENGTH;
loc4_.writeUnsignedInt(loc5_);//写入数据包的长度
loc4_.writeUTFBytes(VERSION);//写入通信的协议号
loc4_.writeUnsignedInt(param2);//写入CmdId
loc4_.writeUnsignedInt(param1);//写入UserID
if(param2 > 1000)
{
loc7_ = 0;
while(loc7_ < param3.length)
{
loc6_ = uint(loc6_ ^ param3[loc7_] & 255);
loc7_++;
}
this._result = MSerial(this._result,param3.length + HEAD_LENGTH,loc6_,param2);//计算序列号
loc4_.writeInt(this._result);//写入序列号
}
else
{
loc4_.writeInt(0);
}
return loc4_;
}
-
这里有1个知识点,记一下
public class SocketEncryptImpl extends Socket 这个类是继承了 Socket,所以在发送数据包的时候writeBytes(loc4_);和flush(); 其实是省略了 Socket.writeBytes和Socket.flush()
private static var NO_ENCRYPT_LEN:int = 4;
public function MessageEncrypt()
{
super();
}
public static function encrypt(param1:ByteArray) : ByteArray
{
var loc2_:int = param1.readUnsignedInt() - NO_ENCRYPT_LEN;
var loc3_:ByteArray = new ByteArray();
loc3_.writeUnsignedInt(0);
MEncrypt(param1,loc2_,loc3_);
loc3_.position = 0;
loc3_.writeUnsignedInt(loc3_.length);
trace("outData.length:" + loc3_.length);
loc3_.position = 0;
return loc3_;
}
public static function decrypt(param1:ByteArray) : ByteArray
{
var loc2_:int = param1.readUnsignedInt() - NO_ENCRYPT_LEN;
var loc3_:ByteArray = new ByteArray();
loc3_.writeUnsignedInt(0);
MDecrypt(param1,loc2_,loc3_);
loc3_.position = 0;
loc3_.writeUnsignedInt(loc3_.length);
loc3_.position = 0;
return loc3_;
}
根据分析可得,MEncrypt(param1,loc2,loc3);
传入的参数是 原始数据param1 、加密的长度、加密后的数据loc3_
继续找 MEncrypt函数内容
public function MEncrypt(_arg_1:ByteArray, _arg_2:int, _arg_3:ByteArray):void
{
var _local_16:*;
var _local_15:int;
var _local_7:int;
var _local_10:int;
var _local_11:int;
var _local_13:int;
var _local_14:int;
var _local_6:int;
var _local_9:int;
var _local_4:int = ESP;
_local_15 = _local_4;
_local_4 = (_local_4 - 16);
ESP = (_local_4 & 0xFFFFFFF0);
//获取尺寸,可忽略
var _local_5:int = getDefinitionByName("org.taomee.net.SocketEncryptImpl").size;
if (_local_5 == 51706)
{
ESP = (_local_4 & 0xFFFFFFF0);
_local_6 = _arg_2;
_local_4 = (_local_4 - 16);
si32(_local_6, _local_4);
ESP = _local_4;
F_malloc();
_local_4 = (_local_4 + 16);
_local_7 = eax;
ESP = (_local_4 & 0xFFFFFFF0);
CModule.writeBytes(_local_7, _local_6, _arg_1);
_local_9 = 0;
si32(_local_9, (_local_15 - 4));
ESP = (_local_4 & 0xFFFFFFF0);
ESP = (_local_4 & 0xFFFFFFF0);
//_local_10此时是取解密的密钥Key
_local_10 = CModule.mallocString(getDefinitionByName("com.robot.core.net.SocketConnection").key);
_local_11 = (_local_10 & 0xFFFFFFFC);
var _local_12:* = li32(_local_11);
var _temp_1:* = (_local_12 + -16843009);
_local_12 = (_local_12 & 0x80808080);
_local_12 = (_local_12 ^ 0x80808080);
_local_5 = (_local_12 & _temp_1);
outer_block:
{
if (_local_5 != 0)
{
_local_13 = (_local_11 + 4);
while ((_local_14 = (_local_10 + _local_9)) < _local_13)
{
_local_5 = li8(_local_14);
if (_local_5 == 0) break outer_block;
_local_9 = (_local_9 + 1);
};
};
_local_13 = (_local_11 + 4);
do
{
_local_14 = li32(_local_13);
_local_5 = (_local_14 + -16843009);
_local_12 = (_local_14 & 0x80808080);
_local_12 = (_local_12 ^ 0x80808080);
_local_5 = (_local_12 & _local_5);
if (_local_5 != 0)
{
_local_5 = (_local_14 & 0xFF);
if (_local_5 == 0)
{
_local_9 = (_local_13 - _local_10);
break;
};
_local_5 = li8(_local_13 + 1);
if (_local_5 == 0)
{
_local_5 = (1 - _local_10);
_local_9 = (_local_5 + _local_13);
break;
};
_local_5 = li8(_local_13 + 2);
if (_local_5 == 0)
{
_local_5 = (2 - _local_10);
_local_9 = (_local_5 + _local_13);
break;
};
_local_5 = li8(_local_13 + 3);
if (_local_5 == 0)
{
_local_5 = (3 - _local_10);
_local_9 = (_local_5 + _local_13);
break;
};
};
_local_13 = (_local_13 + 4);
} while (true);
}//outer_block
_local_4 = (_local_4 - 32);
_local_5 = (_local_15 - 4);
si32(_local_5, (_local_4 + 16));
si32(_local_9, (_local_4 + 12));
si32(_local_10, (_local_4 + 8));
si32(_local_6, (_local_4 + 4));
si32(_local_7, _local_4);
ESP = _local_4;
F__Z15MEncrypt_x86_32PKhiS0_iPi();
_local_4 = (_local_4 + 32);
_local_6 = eax;
if (_local_7 != 0)
{
_local_4 = (_local_4 - 16);
si32(_local_7, _local_4);
ESP = _local_4;
F_idalloc();
_local_4 = (_local_4 + 16);
};
_local_5 = li32(_local_15 - 4);
ESP = (_local_4 & 0xFFFFFFF0);
CModule.readBytes(_local_6, _local_5, _arg_3);
if (_local_6 != 0)
{
_local_4 = (_local_4 - 16);
si32(_local_6, _local_4);
ESP = _local_4;
F_idalloc();
_local_4 = (_local_4 + 16);
};
if (_local_10 != 0)
{
_local_4 = (_local_4 - 16);
si32(_local_10, _local_4);
ESP = _local_4;
F_idalloc();
_local_4 = (_local_4 + 16);
};
};
_local_4 = _local_15;
ESP = _local_4;
return (_local_16);
}
public function F__Z15MEncrypt_x86_32PKhiS0_iPi():void
{
var _local_17:int;
var _local_2:int;
var _local_9:int;
var _local_7:int;
var _local_5:int;
var _local_3:int;
var _local_12:int;
var _local_6:int;
var _local_14:int;
var _local_11:int;
var _local_15:int;
var _local_16:int;
var _local_4:int;
var _local_10:int;
var _local_1:int = ESP;
_local_17 = _local_1;
_local_2 = li32(_local_17 + 4);
_local_3 = (_local_2 + 1);
_local_5 = li32(_local_17 + 16);
si32(_local_3, _local_5);
_local_1 = (_local_1 - 16);
si32(_local_3, _local_1);
ESP = _local_1;
F_malloc();
_local_7 = li32(_local_17 + 12);
_local_9 = li32(_local_17 + 8);
_local_11 = li32(_local_17);
_local_1 = (_local_1 + 16);
_local_12 = eax;
_local_14 = _local_12;
_local_15 = _local_2;
_local_16 = 0;
if (_local_2 >= 1)
{
do
{
_local_6 = li8(_local_11);
_local_4 = 0;
_local_10 = _local_9;
if (_local_16 != _local_7)
{
_local_10 = (_local_9 + _local_16);
_local_4 = (_local_16 + 1);
};
var _local_8:* = li8(_local_10);
_local_8 = (_local_8 ^ _local_6);
si8(_local_8, _local_14);
_local_14 = (_local_14 + 1);
_local_11 = (_local_11 + 1);
_local_15 = (_local_15 + -1);
_local_16 = _local_4;
} while (_local_15 != 0);
};
_local_14 = (_local_12 + _local_2);
si8(0, _local_14);
_local_8 = (_local_2 + -1);
if (_local_8 >= 0)
{
_local_11 = (0 - _local_2);
if (_local_11 <= -1)
{
_local_11 = -1;
};
_local_8 = (_local_2 + _local_11);
_local_11 = (_local_8 + 1);
do
{
_local_8 = li8(_local_14);
var _local_13:* = li8(_local_14 - 1);
_local_13 = (_local_13 >>> 3);
_local_8 = (_local_13 | _local_8);
si8(_local_8, _local_14);
_local_8 = li8(_local_14 - 1);
_local_8 = (_local_8 << 5);
si8(_local_8, (_local_14 - 1));
_local_14 = (_local_14 + -1);
_local_11 = (_local_11 + -1);
} while (_local_11 != 0);
};
_local_8 = li8(_local_12);
_local_8 = (_local_8 | 0x03);
si8(_local_8, _local_12);
_local_8 = (_local_2 % _local_7);
_local_8 = (_local_9 + _local_8);
_local_8 = li8(_local_8);
_local_8 = (_local_8 * 13);
_local_9 = (_local_8 % _local_3);
if (_local_9 != 0)
{
_local_1 = (_local_1 - 16);
si32(__ZZ15MEncrypt_x86_32PKhiS0_iPiE10temp_bytes, _local_1);
_local_8 = (_local_12 + _local_9);
si32(_local_8, (_local_1 + 4));
_local_8 = (_local_3 - _local_9);
si32(_local_8, (_local_1 + 8));
ESP = _local_1;
Fmemcpy();
_local_1 = (_local_1 + 16);
_local_1 = (_local_1 - 16);
si32(_local_9, (_local_1 + 8));
si32(_local_12, (_local_1 + 4));
_local_8 = (__ZZ15MEncrypt_x86_32PKhiS0_iPiE10temp_bytes + _local_8);
si32(_local_8, _local_1);
ESP = _local_1;
Fmemcpy();
_local_1 = (_local_1 + 16);
var _temp_1:* = li32(_local_5);
_local_1 = (_local_1 - 16);
si32(_temp_1, (_local_1 + 8));
si32(__ZZ15MEncrypt_x86_32PKhiS0_iPiE10temp_bytes, (_local_1 + 4));
si32(_local_12, _local_1);
ESP = _local_1;
Fmemcpy();
_local_1 = (_local_1 + 16);
};
eax = _local_12;
_local_1 = _local_17;
ESP = _local_1;
}
我们来看这个Key是如何获取的 ==com.robot.core.net.SocketConnectio== 找到此类
在 ==RobotCoreDLL.swf==此dll上面可以找出
public static function setEncryptKeyStringArr(param1:Array) : void
{
_encryptKeyStringArr = param1;
}
public static function get key() : String
{
var loc2_:int = 0;
var loc3_:* = null;
var loc1_:String = "";
if(_encryptKeyStringArr == null)
{
loc1_ = "!crAckmE4nOthIng:-)";
}
else
{
loc2_ = 0;
while(loc2_ < _encryptKeyStringArr.length)
{
loc3_ = StringUtil.replace(_encryptKeyStringArr[loc2_],"*","");
loc1_ = loc1_ + loc3_;
loc2_++;
}
}
return loc1_;
}
这位程序员也是挺可爱的,默认的密钥为 ==crack me Nothing :-)==
然后找出设置 ==setEncryptKeyStringArr== 的地方,一般设置密钥如果不是固定的,则可能是服务器返回的数据。如登录的时候,返回密钥。所以我们可以找登录的函数
private static function onLogin(param1:SocketEvent) : void
{
if(MainManager.isNewUser)
{
StatManager.sendStat2014("_newtrans_","fOnlineSucc","");
}
SocketConnection.removeCmdListener(CommandID.LOGIN_IN,onLogin);
EventManager.addEventListener(RobotEvent.CREATED_ACTOR,onCreatedActor);
MainManager.setup(param1.data);
var loc2_:ByteArray = param1.data as ByteArray;
var loc3_:int = loc2_.readUnsignedInt();
initKey(loc3_);
MapConfig.setup();
MapSeatPointConfig.setup();
sendSystemInfo();
SocketConnection.send(1022,86066824);
}
private static function initKey(param1:int) : void
{
var loc2_:String = "c&o&m.--rob-ot.c--o-r-e.&n-et.S-oc-ke-t&C-on-n-e-c-t-i-on";
var loc3_:* = "s*e*tE&&&n*c";
loc3_ = loc3_ + "r*yp*t&&&&Ke*yS*tr*i&n&&g*Arr";
loc2_ = StringUtil.replace(loc2_,"-","");
loc2_ = StringUtil.replace(loc2_,"&","");
loc3_ = StringUtil.replace(loc3_,"*","");
loc3_ = StringUtil.replace(loc3_,"&","");
param1 = param1 ^ MainManager.actorInfo.userID;
var loc4_:String = MD5.hash(param1 + "");
var loc5_:* = MainManager.actorInfo.userID + "";
var loc6_:* = [];
var loc7_:int = 0;
while(loc7_ < 10)
{
loc6_[loc7_] = "*" + loc4_.charAt(loc7_) + "*";
loc7_++;
}
getDefinitionByName(loc2_)[loc3_](loc6_);
}
在 ==RobotAppDLL.swf==此dll中,可以找到登陆包返回处理的函数。里面包含初始化密钥的处理方式,而且是只取前10位
当然直接搜索 ==setEncryptKeyStringArr== 是无效的,因为是==getDefinitionByName==
综合以上就基本上完成数据的解密工作了。
我使用的是易语言来进行操作
Key获取
0x04 写在最后
tm的加解密方式是通用的,只是密钥不同。
如赛尔号的是从返回包截取的
摩尔庄园 “^FStx,wl6NquAVRF@f%6”
赛尔号2 “taomee_seer2k~#”
小花仙 “v#93ta0mee+fl0WErd0t2eatMee&#$+@n0”
从09年到现在,tm的游戏从序列号自加1到封包的加密,也是历经了一段历史了吧,如今各种技术层出不穷。写这篇文章也纯属爱好和对技术的热爱。以前期盼着每周五的更新,到现在也是对过去的一种致敬吧。