Erstellung eines MMORPG-Bots durch das Nachbauens des Clients. Autor: Easysurfer

Erstellung eines MMORPG-Bots durch das Nachbauens des Clients Autor: Easysurfer 0x00 Intro Schon jetzt seit fast zwei Monaten analysiere und programmi...
2 downloads 1 Views 867KB Size
Erstellung eines MMORPG-Bots durch das Nachbauens des Clients Autor: Easysurfer 0x00 Intro Schon jetzt seit fast zwei Monaten analysiere und programmiere ich an einem Client für ein MMORPG. In diesem Paper werde ich auf die einzelnen Schritte, die dazu nötig waren eingehen und aufzeigen wie man so etwas realisieren kann. Das ganze wird etwas trocken werden, aber ich hoffe das stört euch nicht =) Das Spiel ist ein Flash basiertes MMORPG. Actionscript? Warum das? Weil der Source so grottig ist? Das mag vielleicht auch stimmen, aber die beiden Hauptgründe sind andere: -

Flash lässt sich sehr gut decompilen und so fast vollkommen in den Sourcecode zurückwandeln Das Ganze ist relativ "simple" gehalten und so können wir uns aufs wesentliche konzentrieren. Die Verbindung zum Server, welche für uns interessant ist, läuft über die sog. "SmartFox"Netzwerkengine, welche oft eingesetzt wird und neben Clients in Flash auch Libaries für Java, C# und weitere Sprachen bereitstellt.

Natürlich hat so ein decompilter Source auch einen kleinen Nachteil: -

Der Source hat keine Namen der Variablen in Parametern und lokal erzeugten Variablen. Stattdessen wird „param*n+“ und „_loc_*n+“ verwendet.

Aber wir wollen es ja nicht zu einfach haben und noch ein bisschen daran knobeln. Und daher ist es auch als „Einsteigerprojekt“ gut geeignet, auch weil die Kommunikation, zumindest eingehend, unverschlüsselt abläuft. 0x01 Teil 1: Sniffing und Smartfox Nun ja, wie fängt man sowas an? Am besten mit ein bisschen Sniffen, ohne groß sich den Source anzuschauen.

Das ganze sieht man eigentlich relativ deutlich: Die ganzen Daten werden, zumindest zuerst, über eine XML Struktur an den Server übertragen. Dann kommt irgendwas mit Zonen und Räumen, dann folgen alle User in dem Raum und schließlich folgen base64 codierte Daten. Ich werde erst kurz ein

paar Worte über diese Zonen und Räume verlieren. Infos darüber hab ich übrigens auf der offiziellen Homepage gefunden, welche sich in solchen Fällen immer gut zum Nachschlagen eignet. [URL1: http://www.smartfoxserver.com/ - SmartFoxServer: massive multiplayer game server for Flash, Unity 3D, Silverlight, iPhone games, MMO, virtual worlds and communities)]. Um es kurz zu machen: Ein Serverbeitritt reicht nicht aus. Man muss ebenfalls einer sog. Zone joinen, was sich mit einer Serverwelt vergleichen lässt. Diese Zone beinhaltet nun mehrere Räume, in denen sich die User befinden. Sobald man in diesen Raum kommt, bekommt man eine Liste mit Usern die ebenfalls in diesem Raum sind und ab da folgen Spieldaten. Übrigens: Wenn man sich das genauer anschauen will, so kann man sich den Smartfox Server lokal Aufsetzen und sich so die ganze Raum/Zone Logik verdeutlichen ;-) Das ist sowieso empfehlenswert wenn man auf Libaries von Smartfox verzichten will und sich selbst eine Client aus einem TCP-Stream basteln will. Aber zurück zu dem kleinen Sniffauszug: Sobald der Login und das Betreten des Raumes abgeschlossen ist, wird man mit Base64 codierten Daten richtig vollgefloodet. Diese Daten werden wir uns später noch genauer anschauen, aber gerade eben reicht es erst mal zu wissen, dass wir diese einfach so decodieren können. Sonst fällt auf, dass nichts gesendet wird solang man nichts "macht". Dieses wird später noch Bedeutung haben. Das war’s erst mal für den ersten Teil. Fazit: Wir wissen jetzt wie der Client zum Server connectet, der Client einer Zone joint und anschließend dem Raum betritt und ab dann der Austausch von Gamedaten base64 codiert stattfindet. 0x02 Noch mehr Smartfox Leider reicht unser Wissen über den SmartfoxClient noch nicht aus. Wie wollen wir einen Client emulieren, wenn wir keine Ahnung haben wie die Netzwerkengine dazu aussieht? Hierzu empfehle ich euch einfach mal die (echt leicht verständlichen) Flash Einleitungstutorials anzuschauen, zu finden unter http://www.smartfoxserver.com/docs/. Vor allem das Tutorial 5.2 (Simple Chat p1) bietet nochmals eine schöne Grafik zum Verdeutlichen der Zone-Room Logik:

Wenn ihr euch ein bisschen eingelesen habt kann’s weiter zu was interessanterem gehen: Dem Versenden von Objekten und Nachrichten. In unserem MMO Fall sind das sog. XT-Objekte (XT = Extension). Diese werden entweder über JSon, XML oder STR (also als String) versendet. Und jetzt ratet mal was bei dem MMO verwendet wird. STR, da es Base64 codierte Strings sind. Das macht das Ganze für uns natürlich leichter, da wir uns nicht mit JSon oder XML Objekten umherschlagen müssen. Die Funktion des originalen Clients hat folgende Syntax: sendXtMessage (xtName:String, cmd:String, paramObj:*, type:String = "xml", roomId:int = -1) : void

Schauen wir uns das ein weniger genauer an. Denn genau diese Funktion werden wir dann nachher verwenden ;) xtName [String]: Hier einfach der "Name" der Nachricht, also das der Server weis als was er diese Nachricht interpretieren muss. Bei unserem MMO ist dieser ein statisches „F“ (Siehe konstanten in der Game.as in KAPITEL 0x03) cmd [String]: Dieses hier ist der Name des Befehls. In unserem MMO kann das z.B. "mv" (move) sein etc. Dieser Param gibt also an, was für ein Befehl das ist. paramObj [Object]: Anhängig von dem cmd-Parameter folgen hier die Befehlsparameter, meistens in Form eines Arrays. Diese Daten werden dann vom Server geparsed und weiterverarbeitet. Unser „mv“-Befehl erwartet z.B. eine Array List mit 4 Integer.

type [String]: Der Standartwert hier ist XML, also wird die ganze Nachricht standardmäßig XMLCodiert abgesendet. Bei uns wird der Typ immer STR (String) sein roomID [int]: Hier kann man eine RoomID angeben, bei -1 wird der aktuelle Raum genommen. Da wir jetzt soweit mit dem Versenden von Objekten vertraut sind, können wir weitermachen… Vielleicht ist die Frage aufgekommen: "Und wo erklärt er uns jetzt wie der String, der Base64 codiert wird, zusammengesetzt wird?". Auf dieses werde ich eingehen wenn ich den Source des originalen Clients behandle, also in kleinen Häppchen. Behaltet halt erst mal diese Funktion im Hinterkopf, dann passt das. Bevor es zu Teil 3 weitergeht nochmal ein kleines Fazit: In diesem Teil wurde gelernt, dass der SmartfoxClient sog. XT-Objekte versendet, welche von dem Server geparst werden. Der Type davon ist in unserem Fall immer STR. Des Weiterem rate ich euch, ein weiteres Mal die Sourcebeispiele in der SMS-Dokumentation anzuschauen um die folgenden Schritte nachvollziehen zu können ;-) 0x03 Eine erste Sourceanalyse Ok, genug Theoriegeschwafel, werden wir etwas konkreter. Fangen wir mit der Game.as an. Diese statische Klasse ist das "Herzstück" des Spiels, bietet die gesamte Config und auch Funktionen zum Senden von Nachrichten und Objekten. Zudem fugiert es als globale Managerklasse für alle globale Spielelemente und Klassen, also ist eine Art Singletonklasse. Schauen wir uns die interessanten Variablen an:

/* ANDERE IMPORTS */ import it.gotoandplay.smartfoxserver.*; public class Game extends MovieClip { public static var Debug:Boolean = false; public static var m_this:Game; public static const URL:String = "http://www.**********.com/"; public static const FacebookURL:String = "http://apps.facebook.com/********/"; public static const GameName:String = "*******"; public static const CharLoaderURL:String = URL + "rest/getChar.php?f="; public static const IconBaseURL:String = URL + "images/"; public static const FBIconBaseURL:String = URL + "fb/images/"; public static const MainMapURL:String = URL + "rest/getMap.php";

So, was sehen wir hier?

Erst mal neben anderen Imports ein Verweis auf den Smartfoxserver. Der "Pfad" zu diesem wird später noch wichtig. 4/5 der Klasse bestehen aus Konstanten, welche uns mehr oder weniger nützlich sind. Auf dieser werde ich nicht alle eingehen, da sehr viele davon auch Spielintern sind (Layer, Bildgrößen, Bitmaps, Klasseninstanzen etc.). Dennoch werden später ein paar weitere dieser Konstanten besprochen und zwar dann, wenn es ans Senden von Daten geht. Ach ja, ich habe die Links zensiert. Warum? Dieses Tutorial ist zum Lernen, nicht zum C&P‘n. Ok, ich will jetzt keine langen Moralreden schwingen, denn wer das Spiel finden will WIRD das auch. Aber so möchte ich wenigstens ein bisschen verhindern dass das dieses hier als Anleitung zur Zerstörung des Spiels gebraucht werden kann. Das klingt jetzt ein bisschen krass, aber ich will einfach echt kein Risiko eingehen… Aber weiter geht’s ;-) Denn weiter unten haben wir noch ein paar tolle Konstanten:

public public public public public public

static static static static static static

var m_zoneName:String = null; const m_ip:String = "1**.**.***.**"; const m_port:int = 443; const m_extJavaName:String = "F"; const m_roomName:String = "An******"; var m_sfs:SmartFoxClient;

So, das sieht doch auch schon sehr cool aus. Hier wird die Server-IP, der Port sowie die vorhin angesprochene statische Variable extJavaName, was unserem "XtName" aus SendXTMessage entspricht. Der Roomname ist ebenfalls nützlich, sowie eine Variable namens zoneName. Diese ist, nicht wie erwartet, der Zone Name vom SmartfoxClient, sondern vom Spiel intern!

0x04 Messages Trotzdem gibt uns diese Zone-Variable eine geschickte Überleitung zu den Messages. Sobald man das ganze Projekt mal nach "Game.m_zoneName = " durchsucht, findet man sofort einen Ordner namens xMsgs (xMessages). Dort findet man dann in der Datei ExtraMsgs.as. Und in dieser finden wir u.a. folgende Codezeilen: public static function parseEnterZone(param1:String, param2:Array) { var _loc_3:* = String(param2[1]).split(GameNetwork.DL); Game.m_zoneName = _loc_3[0];

Viele Neuerungen: Diese Funktion parsed, wie der Name schon sagt, das Betreten einer internen "Spielzone". Eine Zone zeigt also den Abschnitt in der großen Spielewelt an, z.B. "Noob Island" ist solche eine Zone. Beim Betreten bekommt man vom Server also gesagt, in welcher Zone man sich

befindet und diese Funktion parsed dieses. Dabei folgt das immer einem gleichbleibenden Funktionsaufbau: public static function XXX(param1:String, param2:Array). Wie die Parameter genau zusammen kommen, wird gleich erläutert. Aber erst mal soweit: In dem param2-Array befinden sich alle zu parsenden Infos, die in dieser Funktion geparst werden. Die Größe des Arrays ist also jeweils individuell. Und wie genau dieses zusammenkommt, finden wir in der Klasse MessageRouter. public class MsgRouter extends Object { private static var m_msgs:Dictionary = new Dictionary(); private static var m_omsgs:Dictionary = new Dictionary();

Flash hat in diesem Fall ein relativ geschicktes Prinzip (welches es in anderen Sprachen natürlich auch gibt): 1. Es muss bei einem Array/List/Dictionary kein Typ übergeben werden, sondern das ganze wird dynamisch verwaltet und 2. Funktionszeiger können einfach in eine Liste eingefügt werden (sofern diese statisch sind). Das ganze macht sich der MsgRouter jetzt zunutze, um in der Init-Funktion die ganzen Funktionszeiger zu setzen. public static function Init() : void { m_msgs["uj"] = UserMessages.parseUserJoin; // Wenn ein User joint, bekommt man so seine Position, Name, Level m_msgs["us"] = UserMessages.parseUserStatUpdate; // Ein User ändert seine Werte (Schwert, Kleidung) m_msgs["uad"] = UserMessages.parseUserHealthUpdate; // Ein User bekommt +X Leben (Autoregeneration) m_msgs["mvs"] = UserMessages.parseMoveStart; // Ein User hat angefangen sich auf XY zu bewegen m_msgs["mve"] = UserMessages.parseMoveEnd; // Ein User ist fertig mit dem bewegen m_msgs["ded"] = UserMessages.parseDead; // Ein User ist Tod... Soll vorkommen :((( m_msgs["pd"] = UserMessages.parseUserData; // Empfängt die angeforderten Userdaten (Level, Aussehen, Werte) m_msgs["sma"] = UserMessages.parseStartMobAttack; // Ein User greift ein Monster (Mob) an m_msgs["lvu"] = UserMessages.parseLevelUpdate; // Ein User kriegt ein Levelupdate m_omsgs["pcm"] = ChatMessages.parsePrivateChat; // Ein User chattet mit jemandem Privat m_msgs["invt"] = InvMsgs.parseInventory; // Der Spieler bekommt sein Inventarinhalt zugeschickt m_omsgs["ops"] = InvMsgs.parseOpenShop; // Der Spieler öffnet den Shop

m_omsgs["lt"] = InvMsgs.parseLoot; // Ein User empfängt die angeforderten Beutedaten m_omsgs["yif"] = InvMsgs.parseInventoryFull; // Ein User kann keine Sachen mehr nehmen m_msgs["npc"] = NPCMsgs.parseNPCSpawn; // Ein NPC wird gespawned -> Daten (am Anfang werden ALLE gespawned). m_omsgs["qdn"] = NPCMsgs.parseQuestDone; // Eine Quest wurde erfolgreich abgeschlossen m_msgs["gkc"] = NPCMsgs.parseGuildKicked; // Man wurde aus einer Gilde gekickt :((( m_msgs["ms"] = MobMsgs.parseMobSpawn; // Ein Monster wurde gespawned -> Daten (Pos, Leben, Typ). Wie NPCSpawn m_msgs["mms"] = MobMsgs.parseMobMoveStart; // Ein Monster bewegt sich m_msgs["mme"] = MobMsgs.parseMobMoveEnd; // und hört wieder auf m_msgs["md"] = MobMsgs.parseMobData; // Parst die angeforderten Monsterdaten m_msgs["mad"] = MobMsgs.parseMonsterAttackData; // Im Kampf werden hier die Hits geparst (Treffer, Schaden) m_msgs["mc"] = MobMsgs.parseCorpseSpawn; // Ein Monster ist Tod :((( m_msgs["cst"] = CastingMsgs.parseCast; // User führt ein Zauber aus. Zauber = auch Nahkampf m_omsgs["mhh"] = MailMsgs.parseMailHeaders; // EmailHeader werden geparset m_omsgs["ez"] = ExtraMsgs.parseEnterZone; // Schon angesprochen. Parst in welcher "Zone" man ist m_msgs["mnm"] = MarketMsgs.parseMarketItemBought; // Man hat ein Item gekauft

Dieses ist ein kleiner Auszug aus etwa 10-fach so vielen Messages die geparsed werden müssen. Dabei wird der jeweilige Message Type mit einem Funktionszeigers in das Dictionary eingetragen. Es fällt auf, dass einmal m_msgs und einmal m_omsgs verwendet wird. In Grunde genommen sind die m_omsgs nur "Single" Nachrichten, während die Parserfunktion in m_msgs auch mehrere Daten (mehrere NPCSpawns etc.) aufnehmen können. Und das bringt uns auch schon zu der Handlerfunktion. Bei dieser Handlerfunktion hab ich der Verständlichkeit wegen die _loc_[n] durch wirkliche Namen ersetzt. Aber ob man damit klarkommt ist was anderes :D

public static function HandleMessages(event:SFSEvent) : void { var sBase64Decode:String = null; var sMessageSplit:Array = null; var sMessageName:String = null; var FunctionPointer:Function = null; var arMessageParams:Array = null; var iLoopCounter:* = undefined; var sSingleMessageParam:String = null; var arMultiMessageParams:Array = null; var sMessageType:* = event.params.type; var sMessageBase64Encoded:* = event.params.dataObj; if (sMessageType == SmartFoxClient.XTMSG_TYPE_STR) { sBase64Decode = sMessageBase64Encoded; sBase64Decode = Base64.decode(sBase64Decode); sMessageSplit = sBase64Decode.split("="); sMessageName = sMessageSplit[0]; FunctionPointer = m_msgs[sMessageName]; if (FunctionPointer != null) { sBase64Decode = sMessageSplit[1]; arMessageParams = sBase64Decode.split(GameNetwork.ML); iLoopCounter = 0 while (iLoopCounter < arMessageParams.length) { sSingleMessageParam = arMessageParams[iLoopCounter]; if (sSingleMessageParam == "") { } else { arMultiMessageParams = sSingleMessageParam.split(GameNetwork.DL); MsgRouter.FunctionPointer(sBase64Decode, arMultiMessageParams); } iLoopCounter = iLoopCounter + 1; } } else { FunctionPointer = m_omsgs[sMessageName]; if (FunctionPointer != null) { MsgRouter.FunctionPointer(sBase64Decode, sMessageSplit); ; } } } return; }// end function

So, das hier erfordert jetzt ein wenig mehr Aufmerksamkeit, da wir diese Funktion später genauso nachschreiben müssen. Doch um das alles auflösen zu können fehlen uns noch 2 Variablen, GameNetwork.DL und GameNetwork.ML. Diese finden wir in der dazugehörigen statischen Klasse GameNetwork. Behaltet diese Konstanten einfach mal im Hinterkopf. public class GameNetwork extends Object { public static const DL:String = "|"; public static const ML:String = "^";

Aber zurück zu dem Handler-Source. Fangen wir mit dem Parameter an: param1:SFSEvent. Die Dokumentation von SmartFoxServer sagt uns, dass diese Klasse zwei wichtige Variablen hat. Einmal den Message Type, sowie einmal das Objekt dataObj. Nun wird überprüft ob der Message Type STR entspricht, also diese Nachricht ein String ist. Wenn ja, wird der Inhalt in dataObj base64 decoded. Jetzt wird das ganze bei dem = gesplittet und die Variable MessageName bekommt den Inhalt vor dem =. Nun wird in dem Dictionary m_msgs (Multi also) geschaut, ob ein Eintrag (und somit eine Parserfunktion) für diesen Message Typ bekannt ist. Wenn ja, wird fortgefahren, dass das ganze nochmal jeweils bei dem | in einzelne Gruppen von Funktionsparamter gesplittet wird. Nun wird einfach ein einer While-Schleife alle gesplitteten MessageInfos abgearbeitet, indem jeweils der Funktionspointer aus dem Dictionary aufgerufen wird. Da diese alle denselben Aufbau haben ist das auch kein Problem. Nur die Größe des übergebenen Arrays variiert. Sollte der Typ kein "Multi-Message" sein, so wird er einfach normal durch die entsprechende „Single“ Parserfunktion gejagt. Es ist relativ wichtig, dass man diesen Schritt verstanden hat. Schaut euch lieber den Source oben nochmal genau an bevor ihr Fortfahrt. Und wieder ein Fazit für Teil 3+4: Hier wurden ein paar Variablen der Config erklärt und es wurde eine Überleitung auf die MessageParser-Funktionen gemacht. Diese befinden sich wiederum in statischen Klassen. Über Funktionszeiger wird nun in der MessageRouter Klasse alle Message Typen den Parserfunktionen zugeordnet. Dabei wird zwischen "Multi-Message"-Nachrichten und "Single"Nachrichten unterschieden. Eine MessageHandler-Funktion parst nun die ankommenden Nachrichten und ruft die jeweilige Parserfunktion über den Funktionszeiger auf, wobei die Parameter je nach Single oder Multi-Message Typ verschieden gesplittet werden. 0x05 Messages Entschlüsselt Mit diesem Wissen schauen wir uns jetzt mal ein paar Datenpakete der Übertragung an: %xt%dWo9NDUzODExfHRoZXVzZXIxMzM3fDEzNDV8Njk2MHxib2R5LTZfZXllcy1leWVzaGFkb3c tYmx1ZV9jbG90aGluZy1sZWdzMF9jbG90aGluZy1ib2R5MF9oYWlyLW1hbGUtcmVkX3dlYXBvbn Mtc2hpZWxkMH4wX3dlYXBvbnMtc3dvcmQwfjB8NHw=%-1%.

So wie das ganze aussieht, ist das % ein Trennzeichen. %xt% sollte bekannt vorkommen. Das sagt, dass dieses ein XT-Objekt ist welches von dem Server versendet wurde. %base64% schauen wir uns gleich weiter an, aber erst mal %-1%. Erinnert ihr euch an die RoomD = -1? Das bedeutet einfach, dass dieses Objekt an den aktuellen Raum gesendet wurde. Aber ok, jetzt das Base64-Codierte: uj=453811|theuser1337|1345|6960|body-6_eyes-eyeshadow-blue_clothinglegs0_clothing-body0_hair-male-red_weapons-shield0~0_weapons-sword0~0|4| Und, fallen euch ein paar Sachen auf? Es sollte ;-) In der MessageHandler-Funktion wird das ganze erst Base64 decrypted und dann bei dem = getrennt. Also ist uj der Name der Parserfunktion und die andre Hälfe wird weiterverarbeitet. In dieser Weiterverarbeitung wird dieser Teil an allen „|“ gesplittet und in ein Array gepackt. Dieses ist unserer vorhin angesprochener Param2. Schauen wir doch mal nach was „uj“ für eine Parserfunktion ist.: UserMessages.parseUserJoin YEAH! Also sagt uns diese Nachricht, dass ein neuer User gejoint ist. Und das sind wir selbst! Wir schauen uns die Parserfunktion gleich im Detail an, aber schauen wir doch mal was für Daten wir „erraten“ können. Das erste ist offensichtlich eine UserID, die in vielen Messages als erster Parameter verwendet wird. Das zweite ist der Username, das 3te und 4te die Position des spawns. Das 5te ist wohl irgendwas mit dem Equipment, der Kleidung und dem Aussehen. Nur 6te könnte alles sein, daher werden wir das jetzt klären und die anderen Sachen überprüfen. public static function parseUserJoin(param1:String, param2:Array) { var _loc_7:String = null; var _loc_8:Character = null; var _loc_9:* = undefined; var _loc_3:* = param2[0]; var _loc_4:* = param2[1]; var _loc_5:* = param2[2]; var _loc_6:* = param2[3]; _loc_7 = param2[4]; if (Game.m_characters[_loc_3] != null) { } else { if (Game.m_socialWindow != null) { Game.m_socialWindow.m_friendsTab.buddyOnline(_loc_4, true); } _loc_8 = ObjectPool.getObject(Character); _loc_8.construct(0, _loc_3, _loc_4, param2[5], _loc_5, _loc_6); if (Game.m_character == null) { Game.m_character = _loc_8; Game.m_chat.toggleChat(_loc_8); _loc_8.x = Game.CanvasWidthD2;

_loc_8.y = Game.CanvasHeightD2; if (_loc_7 != null) { if (_loc_7.indexOf("null") == -1) { CharDisplay.loadDisplay(_loc_8, _loc_7); GameInit.InitView(); CharUtils.ReloadCharacterDisplay(); } } _loc_8.visible = true; Game.m_characterLayer.addChild(_loc_8); _loc_9 = 0; while (_loc_9 < _loc_8.m_numDamageLabels) {

Game.m_effectsLayer.addChild(_loc_8.m_damageLabels[_loc_9]); _loc_9 = _loc_9 + 1; } GameKongregate.UpdateLevel(); GameFacebook.UpdateFriends(); } else { _loc_8.x = _loc_8.m_originX Game.m_character.m_originX + Game.CanvasWidthD2; _loc_8.y = _loc_8.m_originY Game.m_character.m_originY + Game.CanvasHeightD2; if (_loc_7 != null) { if (_loc_7.indexOf("null") == -1) { _loc_8.m_display = _loc_7; } } } Game.m_characters[_loc_3] = _loc_8; Game.m_charByName[_loc_4] = _loc_8; } return; }// end function

Wow, eine lange Funktion. Doch das wenigste ist wirklich für uns interessant, daher wird ich auch nur das wichtigste besprechen. Als erstes wird geprüft, ob der Charakter mit dieser ID schon eingetragen ist. Wenn nicht, so wird ein neuer Charakter erzeugt. Und genau diese Zeile gilt es genauer zu untersuchen: construct(0, _loc_3, _loc_4, param2[5], _loc_5, _loc_6); _loc_3 = UserID _loc_4 = Username

param2[5] = Unsere Unbekannte mit der Zahl 4 _loc_5 = X Position _loc_6 = Y Position Diese “construct”-Funktion hier werden wir uns ebenfalls etwas genauer anschauen. Zu finden in der xCharacter/Character.as: public function construct(param1:int, param2:String, param3:String, param4:int, param5:int, param6:int, param7:int = 0) { TweenLite.killTweensOf(this); this.m_type = param1; this.m_subType = param7; this.m_id = param2; this.m_name = param3; this.m_level = param4; this.m_npcState = null; this.m_npcStateNum = -1; this.m_originX = param5; this.m_originY = param6; this.m_castBar.visible = false; this.m_castingEmitter.stop(); this.m_renderer.alpha = 1; var _loc_8:int = 0; Das ganze bringt doch schon einiges Licht ins Dunkel. Unser großer unbekannter Parameter gibt also das Level des Charakters an! Zudem will ich an dieser Stelle noch auf den Parameter m_type eingehen. Ohne jetzt groß ein Beispiel diesbezüglich zu bringen sei noch angemerkt, dass NPCs sowie Monster ebenfalls „nur“ Charaktere sind, welche mit diesen Attributen ausgefüllt sind. Also wird bei Monstern und NPCs auch diese Construct-Methode verwendet, nur das hier der Type anders ist. Und hier noch eine weitere Message zum anschauen: ez=Noob Island|0|6144|null| Ez = Enter Zone, den Anfang der Funktion findet man weiter oben und man sollte jetzt eigentlich kein Problem mehr mit dieser Funktion haben.

Das Fazit zu diesem Abschnitt: In diesem Teil haben wir uns mit Messages beschäftigt. Wir haben erfahren in welcher Form diese vom Server versendet werden, wie wir sie zu interpretieren haben und schließlich zu parsen. Es wurde bewiesen dass das erarbeitete über die MessageRouter Klasse soweit stimmt und am Ende wurde eine Parserfunktion „verfolgt“ um eine unbekannte Variable heraus zu bekommen. 0x06 Ein weiteres Mal SmartfoxClient Analyse

So leid es mir auch tut, aber es wird nochmal etwas zäh. Und zwar wollen wir herausfinden, wie der SmartfoxClient die Daten ordnet, codiert, verschlüsselt und schließlich versendet. Und Verschlüsselungen sind (zumindest für mich) immer relativ zäh… Wie vorhin bereits erwähnt bietet die Game-Klasse Funktionen zum Versenden von Objekten und Daten. Also setzen wir dort mal an:

public static function sendMessage(param1:String, param2:Array) : void { GameNetwork.sendMessage(param1, param2); return; }// end function public static function sendMsg(param1:String, ... args) : void { args = new Array(); var _loc_4:int = 0; while (_loc_4 < args.length) { args.push(args[_loc_4]); _loc_4++; } sendMessage(param1, args); return; }// end function Was haben wir da? Param1 ist ein String, also ist dies vermutlich der „Name“ der Nachricht, z.B. „mv“ für Move. In der letzteren Funktion werden nun einfach die Parameter in ein Array geschrieben und schließlich die SendMessage-Funktion aufgerufen, welche die Nachricht einfach an GameNetwork.sendMessage() weiterreicht. Also hier mal schauen… public static function sendMessage(param1:String, param2:Array) { Game.m_sfs.sendXtMessage(Game.m_extJavaName, param1, param2, SmartFoxClient.XTMSG_TYPE_STR); return; }// end function

Dies sieht doch auch schon bekannt aus. Es wird einfach in dem SmartfoxClient m_sfs die vorhin besprochene sendXtMessage aufgerufen (es schadet nicht nochmal nach oben zu scrollen :P). Es passiert hier sonst nichts weiter Interessantes. Da wir es ja gewohnt sind, dass beim XTMSG_TYPE_STR einfach die Daten Base64-codiert werden, sollten wir auch in der Lage sein diese uns anzuzeigen… %xt%F%HoPv1wd19nmjhimS/cUnx6HM7s2X6I6AdAqG1pRSbl3bEIvoQx7JX050u1qJn+5ABeykH JzoGUHlDD1v4V4jbw==%1% Sieht aus wie eine Nachricht vom Server an den Client. %xt%[das angesprochene] F%base64%roomID%. Doch leider bekommen wir beim base64-decoden das:

-ƒï×uöy£†)’ýÅ'Ç¡Ìî͗莀tÖ”Rn]Û‹èC-É_Nt“Z‰Ÿî@줜èAåoá^#o Hmm, da hat uns wohl einer einen Strich durch die Rechnung gemacht… Und das bedeutet auch, dass dieser Jemand an dem Source des Smartfoxclientes rumgepfuscht hat und diesen modifiziert hat. Also hier nachschauen. Da wir uns ja von vorhin so brav den Pfad gemerkt haben, wissen wir auch sofort wo wir dieses finden können :P (import it.gotoandplay.smartfoxserver.*;) public static const MODMSG_TO_ZONE:String = "z"; public static const XTMSG_TYPE_XML:String = "xml"; public static const XTMSG_TYPE_STR:String = "str"; public static const XTMSG_TYPE_JSON:String = "json"; public static const CONNECTION_MODE_DISCONNECTED:String = "disconnected"; public static const CONNECTION_MODE_SOCKET:String = "socket"; public static const CONNECTION_MODE_HTTP:String = "http"; private static var rsa:RSAKey = RSAKey.parsePublicKey("77438812a8fadc9bce6ecf4ca6263c536de600d040def1da4ac7 a03a93bc34cd258b998dce008112231c5fa8edfeaefd9e6ea57ce903de29d54adf4002a953a 7", "10001"); private static var rsa2:RSAKey = RSAKey.parsePrivateKey("77438812a8fadc9bce6ecf4ca6263c536de600d040def1da4ac 7a03a93bc34cd258b998dce008112231c5fa8edfeaefd9e6ea57ce903de29d54adf4002a953 a7", "10001", "6478fcea9376fb613b10b90d1eaff98463822fcf41c75042915b098e2fa58f68d566c2a593 1e9fa64b2c415fb924887ae3613bce26c839d98c6f3da18ec900b9", "cf70b8322874b3514fffac6db0fac849242b2f61f3c1ecb6c9e3cee92a782a65", "932ea7e5d2b76b0aaba438817cc9baed6ae9208f5b4156635449ebe78f763f1b", "cacc01e75751afb4c0a6cda5772dd4fc5c55b3bee9151f1c1c079052b6e83f59", "4367b4a0fe55de01ef05b0932cd13e7888e3e7737ff97654db3f57789f2d2a4d", "c6bf805cb7118ab462518c8038c5b36a75d0698c60080bde88676d58523dbbf2"); Und natürlich haben wir rechtbehalten. Das ganze sieht normal aus, bis auf rsa:RSAKey und rsa2:RSAKey. Wir haben es also mit RSA zu tun -.- Ich habe keine Ahnung in dem Krypto-Bereich, aber für RSA gibt es einmal einen „public key“ und einen „private key“. Den public Key braucht man zum Verschlüsseln, den private Key zum Entschlüsseln. Und genau an diesem Punkt haben die Entwickler einen (riesen) Fehler gemacht. Sie haben den private Key ebenfalls mitgeliefert, obwohl dieser an KEINER STELLE gebraucht wird und nur auf dem Server von Nöten ist. Pech für sie, glück für uns. Wir sind durch diesen „Bonus“ in der Lage, auch ausgehende Nachrichten zu entschlüsseln um diese nicht nur durch Sourceanalyse nachzubauen. Dazu später mehr. Aber hier mal den Source von SendXTMessage (etwas gekürzt, um Platz zu sparen. XML + JSon wurde entfernt, da unmodifiziert): public function param4:String = { var var var var var var

sendXtMessage(param1:String, param2:String, param3, "xml", param5:int = -1) : void _loc_6:Object = null; _loc_7:Object = null; _loc_8:String = null; _loc_9:String = null; _loc_10:Number = NaN; _loc_11:ByteArray = null;

var _loc_12:ByteArray = null; var _loc_13:String = null; var _loc_14:Object = null; var _loc_15:Object = null; if (!this.checkRoomList()) { return; } if (param5 == -1) { param5 = this.activeRoomId; } if (param4 == XTMSG_TYPE_STR) { _loc_9 = param2; _loc_10 = 0; while (_loc_10 < param3.length) { _loc_9 = _loc_9 + (MSG_STR + param3[_loc_10].toString()); _loc_10 = _loc_10 + 1; } _loc_11 = new ByteArray(); _loc_11.writeUTFBytes(_loc_9); _loc_12 = new ByteArray(); rsa.encrypt(_loc_11, _loc_12, _loc_11.length); _loc_9 = Base64.encodeByteArray(_loc_12); _loc_13 = MSG_STR + "xt" + MSG_STR + param1 + MSG_STR + _loc_9 + MSG_STR + param5 + MSG_STR; this.sendString(_loc_13); } Okay, was passiert hier schon wieder? Als erstes wird, wenn -1 als Raum übergeben, der aktuelle Raum genommen. Jetzt werden alle übergeben Message Objekte in einen String konvertiert und an den „finalen“ String angehängt. Dabei wird jeweils als Trennzeichen ebenfalls % verwendet. Dieser String wird nun in ein Byte-Array gewandelt und durch die RSA-Encryption gejagt. Dieses Byte-Array wird Base64-Encoded und schließlich in dem uns bekannten Schema verpackt und versendet. Schließlich schrieb ich ein Programm, das mir die RSA-Decryption mit den oben angegeben Werten ausführt. Dann bekam ich Werte wie diese. %xt%F%mv%3360%3370%20%29%1% Aber jetzt fragt mich nicht, wie der Server zwischen den Parametern unterscheiden konnte, ich denk das läuft über die Anzahl. Sollte einer der Entwickler dass je lesen so sollte er mir doch bitte sagen warum nicht einfach ein weiteres Trennzeichen verwendet wurde. Kleines Fazit: Wir haben gelernt wie die Nachrichten von der Game-Klasse an die Unterklasse GameNetwork weitergereicht werden und von dort ihren Weg in den modifizierten SmartfoxClient finden. Wir haben gelernt dass eine RSA Verschlüsselung verwendet wird und wir durch großes Glück

in der Lage sind, Nachrichten auch zu decrypten. Und schließlich wissen wir jetzt, wie die Nachrichten zusammengesetzt und versendet werden! 0x07 Versenden von eigenen Nachrichten Decrypten ist schön und gut, aber ich will auch was senden, sonst bringt mir das schöne Analysieren für ein Bot NICHTS. Doch wo finden wir jetzt wo, wann und vor allem welche Parameter versendet werden? Den ersten Teil finden wir bei unseren schönen Konstanten in der Game.as (ebenfalls nur ein Auszug): public static const MonsterData:String = "md"; public static const CastSpell:String = "sp"; public static const Move:String = "mv"; Ich werde bei dem Move-Befehl bleiben, um das alles zu erklären. Die Suchsyntax hierfür ist überraschend einfach. Suchen wir doch einfach mal: Game.sendMsg(Game.Move Und prompt werden wir in der Map.as fündig: // Im Click-Handler, vieles gekürzt var _loc_2:* = Game.m_character.m_originX; var _loc_3:* = Game.m_character.m_originY; var _loc_4:* = event.stageX * Game.m_LoaderScale Game.m_character.x; var _loc_5:* = event.stageY * Game.m_LoaderScale Game.m_character.y; if (Math.sqrt(_loc_4 * _loc_4 + _loc_5 * _loc_5) < Game.Spacing) { return; } if (this.checkCollision(_loc_2, _loc_3, _loc_4, _loc_5)) { return; } Game.sendMsg(Game.Move, int(_loc_2), int(_loc_3), int(_loc_4), int(_loc_5)); Und wiedermal: “Das sieht schön aus”^^ Es wird erst in _loc_2 und 3 die Aktuelle Position gesetzt, dann werden die Koordinaten des geklickten berechnet, skaliert und von der Größe des Charakters abgezogen. Jetzt wird geprüft, ob diese „Klickreichweite“ außerhalb des Bildschirms reicht und wenn nicht wird geprüft, ob auf dem Weg eine Kollision mit einem Haus, Wasser oder einem Zaun etc. stattfinden würde. Wenn dieses alles nicht vorkommt, so werden die Daten an den Server übertragen und die Bewegung findet statt. Für das Programmieren eines Onlinegames gibt es eigentlich eine große Regel: „Never trust userinput“, was auch einschließt, dass zumindest die Kollisionsprüfung auf dem Server nochmal

nachgerechnet werden sollte. Aber das ist hier NICHT der Fall. Heißt, wenn ich einfach ein paar Koordinaten angebe, zu denen ich will, so läuft mein Charakter einfach dort hin und lässt sich nicht von Häusern oder metertiefe Schluchten aufhalten. Und es gibt eine Message die nennt sich „QuestComplete“. Habe ich bisher noch nicht ausprobiert, aber die Vermutung liegt nahe… 0x08 Abschluss Und so sind wir auch „schon“ am Ende dieses Artikels angekommen. Als ich damit anfing hätte ich nie gedacht, dass ich schließlich 4.400 Wörter getippt und 18 volle Seiten haben würde. Nun ja, was sollte man zum Abschluss noch groß sagen... Ich hoffe ich konnte euch einen kleinen Einblick in mein Projekt „MMO Client nachschreiben“ geben, obwohl ich keine einzige Zeile meines Sources gepostet habe. Denn vieles davon ist einfach Code lesen, verstehen und umsetzen. Jetzt wisst ihr, wie ihr bei ähnlichen Projekten vorgehen könnt und wenn ihr Glück habt, werden diese ebenfalls Smartfox verwenden. (kleiner Tipp am Rande: Unter Showcase sind ein paar weitere Spiele aufgelistet :P) Greez und viel Erfolg Easysurfer Blogeintrag zu diesem Thema: http://easysurfer.back2hack.cc/wordpress/?p=25

Suggest Documents