diff --git a/README.md b/README.md index e7976dd..ef43f5a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ A utility for obtaining the hardcoded secrets within the Transformice client. To build, you should use the [asconfig.json](https://github.com/friedkeenan/tfm-secrets-leaker/blob/main/asconfig.json) file to compile the `TFMSecretsLeaker.swf` file. This can be done with [vscode-as3mxml](https://github.com/BowlerHatLLC/vscode-as3mxml) or [asconfigc](https://www.npmjs.com/package/asconfigc). +You will also need to place the SWC files for the following libraries under a `lib` folder at the same level as the `asconfig.json` file: + +- [as3commons-bytecode-1.1.1](https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/as3-commons/as3commons-bytecode-1.1.1.swc) +- [as3commons-lang-0.3.7](https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/as3-commons/as3commons-lang-0.3.7.swc) +- [as3commons-reflect-1.6.4](https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/as3-commons/as3commons-reflect-1.6.4.swc) + If you wish to save yourself the hassle, then there is also a pre-built SWF in the [releases](https://github.com/friedkeenan/tfm-secrets-leaker/releases) of this repo. ## Usage diff --git a/asconfig.json b/asconfig.json index 26d35b8..a7ce818 100644 --- a/asconfig.json +++ b/asconfig.json @@ -11,7 +11,13 @@ "default-size": { "width": 800, "height": 600 - } + }, + + "library-path": [ + "lib/as3commons-bytecode-1.1.1.swc", + "lib/as3commons-lang-0.3.7.swc", + "lib/as3commons-reflect-1.6.4.swc" + ] }, "mainClass": "TFMSecretsLeaker" diff --git a/src/ServerboundLeakerSocket.as b/src/ServerboundLeakerSocket.as.reference similarity index 90% rename from src/ServerboundLeakerSocket.as rename to src/ServerboundLeakerSocket.as.reference index 6ae7e92..219c6fb 100644 --- a/src/ServerboundLeakerSocket.as +++ b/src/ServerboundLeakerSocket.as.reference @@ -3,6 +3,11 @@ package { import flash.utils.ByteArray; public class ServerboundLeakerSocket extends Socket { + /* + NOTE: This class serves as a reference for what + the generated leaker socket class looks like. + */ + private var flush_callback: Function; private var written_bytes: ByteArray = new ByteArray(); diff --git a/src/leakers/DeadMazeLeaker.as b/src/leakers/DeadMazeLeaker.as index 2409165..6d2a52a 100644 --- a/src/leakers/DeadMazeLeaker.as +++ b/src/leakers/DeadMazeLeaker.as @@ -1,7 +1,92 @@ package leakers { + import flash.system.ApplicationDomain; + import flash.utils.describeType; + import flash.net.Socket; + public class DeadMazeLeaker extends Leaker { + private var socket_dict_name: String; + public function DeadMazeLeaker() { super("http://www.deadmaze.com/alpha/deadmeat.swf", true); } + + private function get_socket_method_name(description: XML) : String { + for each (var method: * in description.elements("method")) { + if (method.attribute("returnType") == "flash.net::Socket") { + return method.attribute("name"); + } + } + + return null; + } + + private function get_socket_prop_name(description: XML) : void { + for each (var variable: * in description.elements("variable")) { + if (variable.attribute("type") == "flash.net::Socket") { + this.socket_prop_name = variable.attribute("name"); + + return; + } + } + } + + protected override function process_socket_info(domain: ApplicationDomain, _: XML) : void { + var document: * = this.document(); + var description: * = describeType(document); + + /* Load a socket into the dictionary. */ + document[this.get_socket_method_name(description)](-1); + + for each (var variable: * in description.elements("variable")) { + if (variable.attribute("type") != "flash.utils::Dictionary") { + continue; + } + + var dictionary: * = document[variable.attribute("name")]; + + if (dictionary == null) { + continue; + } + + var maybe_socket: * = dictionary[-1]; + if (maybe_socket == null) { + continue; + } + + if (maybe_socket is Socket) { + delete dictionary[-1]; + + this.socket_dict_name = variable.attribute("name"); + + this.get_socket_prop_name(describeType(maybe_socket)); + + this.build_leaker_socket(domain, "flash.net::Socket"); + + return; + } + } + } + + protected override function get_connection_socket(instance: *) : Socket { + for each (var socket: * in this.document()[this.socket_dict_name]) { + return socket[this.socket_prop_name]; + } + + return null; + } + + protected override function set_connection_socket(instance: *, socket: Socket) : void { + var dictionary: * = this.document()[this.socket_dict_name]; + + for (var key: * in dictionary) { + dictionary[key][this.socket_prop_name] = socket; + + return; + } + } + + protected override function auth_key_return() : String { + return "*"; + } } } diff --git a/src/leakers/Leaker.as b/src/leakers/Leaker.as index 00f56b2..2baa55a 100644 --- a/src/leakers/Leaker.as +++ b/src/leakers/Leaker.as @@ -12,6 +12,21 @@ package leakers { import flash.utils.ByteArray; import flash.system.Capabilities; import flash.system.ApplicationDomain; + import org.as3commons.bytecode.emit.impl.AbcBuilder; + import org.as3commons.bytecode.emit.IClassBuilder; + import org.as3commons.bytecode.emit.IAbcBuilder; + import org.as3commons.bytecode.abc.enum.Opcode; + import org.as3commons.bytecode.abc.QualifiedName; + import org.as3commons.bytecode.abc.LNamespace; + import org.as3commons.bytecode.abc.enum.NamespaceKind; + import org.as3commons.bytecode.emit.ICtorBuilder; + import org.as3commons.bytecode.emit.IAccessorBuilder; + import org.as3commons.reflect.AccessorAccess; + import org.as3commons.bytecode.emit.IMethodBuilder; + import org.as3commons.bytecode.emit.IPackageBuilder; + import org.as3commons.bytecode.emit.event.AccessorBuilderEvent; + import org.as3commons.bytecode.emit.impl.MethodBuilder; + import org.as3commons.bytecode.emit.enum.MemberVisibility; public class Leaker extends Sprite { /* @@ -41,6 +56,8 @@ package leakers { private var logging_class_info: *; + private var leaker_socket_class: Class = null; + protected var socket_prop_name: String; private var connection_class_info: *; @@ -218,11 +235,122 @@ package leakers { return false; } - protected function process_socket_info(description: XML) : void { + protected function build_leaker_socket(domain: ApplicationDomain, parent_name: String) : void { + var abc: IAbcBuilder = new AbcBuilder(); + var pkg: IPackageBuilder = abc.definePackage(""); + + var cls: IClassBuilder = pkg.defineClass("ServerboundLeakerSocket", parent_name); + + cls.defineProperty("flush_callback", "Function"); + cls.defineProperty("written_bytes", "flash.utils::ByteArray"); + + var blank_namespace: * = new LNamespace(NamespaceKind.PACKAGE_NAMESPACE, ""); + + var written_bytes: * = new QualifiedName("written_bytes", blank_namespace); + var flush_callback: * = new QualifiedName("flush_callback", blank_namespace); + + var bytearray: * = new QualifiedName("ByteArray", LNamespace.FLASH_UTILS); + var bytearray_clear: * = new QualifiedName("clear", blank_namespace); + var bytearray_writeBytes: * = new QualifiedName("writeBytes", blank_namespace); + var bytearray_position: * = new QualifiedName("position", blank_namespace); + + var constructor: ICtorBuilder = cls.defineConstructor(); + + constructor.defineArgument("Function"); + + /* Construct 'written_bytes' and assign 'flush_callback'. */ + constructor + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.pushscope) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.constructsuper, [0]) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.findpropstrict, [bytearray]) + .addOpcode(Opcode.constructprop, [bytearray, 0]) + .addOpcode(Opcode.setproperty, [written_bytes]) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getlocal_1) + .addOpcode(Opcode.setproperty, [flush_callback]) + .addOpcode(Opcode.returnvoid); + + var connected: IAccessorBuilder = cls.defineAccessor("connected", "Boolean"); + + connected.access = AccessorAccess.READ_ONLY; + connected.createPrivateProperty = false; + + connected.addEventListener(AccessorBuilderEvent.BUILD_GETTER, function (event: AccessorBuilderEvent) : void { + var method: IMethodBuilder = new MethodBuilder("connected"); + + method.isOverride = true; + method.visibility = MemberVisibility.PUBLIC; + method.returnType = "Boolean"; + + /* Always return true for 'connected'. */ + method + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.pushscope) + .addOpcode(Opcode.pushtrue) + .addOpcode(Opcode.returnvalue); + + event.builder = method; + }); + + var writeBytes: IMethodBuilder = cls.defineMethod("writeBytes"); + + writeBytes.isOverride = true; + + writeBytes.defineArgument("flash.utils::ByteArray"); + writeBytes.defineArgument("uint", true, 0); + writeBytes.defineArgument("uint", true, 0); + + /* Clear 'written_bytes' then forward onto its 'writeBytes' method then reset its position. */ + writeBytes + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.pushscope) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getproperty, [written_bytes]) + .addOpcode(Opcode.callpropvoid, [bytearray_clear, 0]) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getproperty, [written_bytes]) + .addOpcode(Opcode.getlocal_1) + .addOpcode(Opcode.getlocal_2) + .addOpcode(Opcode.getlocal_3) + .addOpcode(Opcode.callpropvoid, [bytearray_writeBytes, 3]) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getproperty, [written_bytes]) + .addOpcode(Opcode.pushbyte, [0]) + .addOpcode(Opcode.setproperty, [bytearray_position]) + .addOpcode(Opcode.returnvoid); + + var flush: IMethodBuilder = cls.defineMethod("flush"); + + flush.isOverride = true; + + /* Call 'flush_callback' with 'written_bytes'. */ + flush + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.pushscope) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getlocal_0) + .addOpcode(Opcode.getproperty, [written_bytes]) + .addOpcode(Opcode.callpropvoid, [flush_callback, 1]) + .addOpcode(Opcode.returnvoid); + + abc.addEventListener(Event.COMPLETE, this.loaded_leaker_socket); + abc.buildAndLoad(domain, domain); + } + + private function loaded_leaker_socket(event: Event) : void { + this.leaker_socket_class = this.game_domain().getDefinition("ServerboundLeakerSocket") as Class; + } + + protected function process_socket_info(domain: ApplicationDomain, description: XML) : void { for each (var variable: * in description.elements("factory").elements("variable")) { if (variable.attribute("type") == "flash.net::Socket") { this.socket_prop_name = variable.attribute("name"); + this.build_leaker_socket(domain, "flash.net::Socket"); + return; } } @@ -312,7 +440,7 @@ package leakers { continue; } - this.process_socket_info(description); + this.process_socket_info(domain, description); var address_prop_name: * = get_address_prop_name(description); var possible_ports_prop_names: * = get_possible_ports_properties(description); @@ -340,6 +468,10 @@ package leakers { } private function try_replace_socket(event: Event) : void { + if (this.leaker_socket_class == null) { + return; + } + var klass: Class = this.connection_class_info.klass; var instance: * = klass[this.connection_class_info.instance_name]; @@ -394,7 +526,7 @@ package leakers { Replace the connection's socket with our own socket which will keep track of the sent packets for us. */ - this.set_connection_socket(instance, new ServerboundLeakerSocket(this.on_sent_packet)); + this.set_connection_socket(instance, new this.leaker_socket_class(this.on_sent_packet)); /* Dispatch fake connection event to trigger handshake packet. */ socket.dispatchEvent(new Event(Event.CONNECT)); diff --git a/src/leakers/TransformiceLeaker.as b/src/leakers/TransformiceLeaker.as index 76108c3..2bceb85 100644 --- a/src/leakers/TransformiceLeaker.as +++ b/src/leakers/TransformiceLeaker.as @@ -2,6 +2,7 @@ package leakers { import flash.utils.describeType; import flash.net.Socket; import flash.system.ApplicationDomain; + import flash.utils.getQualifiedClassName; public class TransformiceLeaker extends Leaker { private var socket_dict_name: String; @@ -10,19 +11,33 @@ package leakers { super("http://www.transformice.com/Transformice.swf", true); } - private function get_socket_method_name(description: XML) : String { + private function get_socket_method_name(domain: ApplicationDomain, description: XML) : String { for each (var method: * in description.elements("method")) { - if (method.attribute("returnType") == "flash.net::Socket") { - return method.attribute("name"); + var parameters: * = method.elements("parameter"); + if (parameters.length() != 1) { + continue; } + + if (parameters[0].attribute("type") != "Number") { + continue; + } + + var return_type: * = method.attribute("returnType"); + if (return_type == "void" || return_type == "*") { + continue; + } + + this.build_leaker_socket(domain, return_type); + + return method.attribute("name"); } return null; } - private function get_socket_prop_name(description: XML) : void { + private function get_socket_prop_name(description: XML, type_name: String) : void { for each (var variable: * in description.elements("variable")) { - if (variable.attribute("type") == "flash.net::Socket") { + if (variable.attribute("type") == type_name) { this.socket_prop_name = variable.attribute("name"); return; @@ -30,12 +45,12 @@ package leakers { } } - protected override function process_socket_info(_: XML) : void { + protected override function process_socket_info(domain: ApplicationDomain, _: XML) : void { var document: * = this.document(); var description: * = describeType(document); /* Load a socket into the dictionary. */ - document[this.get_socket_method_name(description)](-1); + var real_socket: * = document[this.get_socket_method_name(domain, description)](-1); for each (var variable: * in description.elements("variable")) { if (variable.attribute("type") != "flash.utils::Dictionary") { @@ -58,7 +73,7 @@ package leakers { this.socket_dict_name = variable.attribute("name"); - this.get_socket_prop_name(describeType(maybe_socket)); + this.get_socket_prop_name(describeType(maybe_socket), getQualifiedClassName(real_socket)); return; }