Skip to content

Commit

Permalink
Report client verification template, add newlines at the end of most …
Browse files Browse the repository at this point in the history
…files
  • Loading branch information
friedkeenan committed Feb 15, 2023
1 parent adc6171 commit 9a28369
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
bin/
.vscode/
.vscode/
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Transformice's networking protocol utilizes several hardcoded, frequently-changi
These secrets include:

- The server address.
- This is the address of the server that the client connects to. This changes and has changed, but infrequently enough that I think it could feasibly be hardcoded and manually rediscovered when it does change. The port the client connects to is a random choice between `11801`, `12801`, `13801`, and `14801` (if the first choice does not result in a successful connection, another is randomly chosen, and so on). I do not think there is a real way for this utility to acquire those ports, though they should not ever change.
- This is the address of the server that the client connects to. This changes and has changed, but infrequently enough that I think it could feasibly be hardcoded and manually rediscovered when it does change. The port the client connects to is a random choice between `11801`, `12801`, `13801`, and `14801` (if the first choice does not result in a successful connection, another is randomly chosen, and so on). I do not think there is a real way for this utility to acquire all the possible ports, though they should not ever change.
- The game version.
- This is what the game displays in the bottom right corner of the login screen, showing text like `1.740`. The game version that this reports is the `740` component of that, and is sent in the handshake packet that the client sends to the server. This does not change as often as the other secrets do.
- The connection token.
Expand All @@ -36,3 +36,7 @@ These secrets include:
- After the client sends the handshake packet to the server, the server then responds with a packet containing an "auth token". This is an integer that is used again when the client sends the login packet. The client XOR's the auth token with the hardcoded "auth key", resulting in a ciphered token, which is then sent to the server in the login packet.
- The packet key sources.
- Certain packets within Transformice's network protocol are encrypted, for example the login packet. The particular cipher varies per packet, but the keys used are derived from an array of integers called the "packet key sources". These integers are combined with a key name, e.g. "identification", to obtain the actual key used to encrypt a packet.
- The client verification template.
- Shortly after the handshake sequence has been completed by the client and server, the server will send a packet to the client to make sure that the client is official and otherwise proper (i.e. not a bot). This packet contains a "verification token" (an integer) which the client will then use in its response. The client will respond with a ciphered packet using the XXTEA algorithm with the verification token converted to a string as the name for the key. The (plaintext) packet data will begin with the verification token, and then some semi-random, hardcoded fields, with the verification token thrown in again in the midst of it. This does not seem to change as often as the other secrets do, but it does change.

What this reports is a hex string representing a string of bytes of the plaintext body of this packet (in Python, something you could use `bytes.fromhex` on). In place of where the verification token should go, `aabbccdd` is used, and should be replaced with the actual packed verification token.
2 changes: 1 addition & 1 deletion asconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
},

"mainClass": "TFMSecretsLeaker"
}
}
2 changes: 1 addition & 1 deletion leak-secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ def main(leaker_path):

sys.exit(1)

main(sys.argv[1])
main(sys.argv[1])
12 changes: 6 additions & 6 deletions src/HandshakeLeakerSocket.as → src/ServerboundLeakerSocket.as
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package {
import flash.net.Socket;
import flash.utils.ByteArray;

public class HandshakeLeakerSocket extends Socket {
public class ServerboundLeakerSocket extends Socket {
private var flush_callback: Function;
private var written_bytes: ByteArray = new ByteArray();

public function HandshakeLeakerSocket(flush_callback: Function) {
public function ServerboundLeakerSocket(flush_callback: Function) {
this.flush_callback = flush_callback;
}

/* NOTE: We only override the methods that we need to in order to get to the handshake packet. */
/* NOTE: We only override the methods that we need to for serverbound packets. */

public override function get connected() : Boolean {
/* Just always report that we're connected. Makes things faster too. */
Expand All @@ -22,8 +22,8 @@ package {
/*
NOTE: We clear the buffer because we don't need the
length and fingerprint data, just the body of the
handshake packet, which is the second (and last)
call to this method.
packet, which is the second (and last) call to this
method before the socket is flushed.
*/
this.written_bytes.clear();
this.written_bytes.writeBytes(bytes, offset, length);
Expand All @@ -35,4 +35,4 @@ package {
this.flush_callback(this.written_bytes);
}
}
}
}
205 changes: 195 additions & 10 deletions src/TFMSecretsLeaker.as
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,36 @@ package {
import flash.utils.ByteArray;

public class TFMSecretsLeaker extends Sprite {
/*
NOTE: We use this value to make it easy to replace
in the client verification data and hard for the
other random data to collide with. In particular
each byte being different makes it impossible for
a single byte to mess with this replacement, for
instance if we used '0xAAAAAAAA' then if just another
'0xAA' showed up next to the verification token, it
would mess the template up.
This token also uses bytes which would just simply
not occur as the length of a string (of which the data
includes multiple). *Theoretically* they could of course,
but practically the strings will never be that long.
Also note that this token is negative, which I think
furthermore helps reduce/negate the possibility of
collision. Furthermore it has fairly large components
both when interpreted as positive and negative so I'm
not sure that its value would ever cause a collision.
*/
private static const VERIFCATION_TOKEN: * = int(0xAABBCCDD);

private var final_loader: Loader;
private var connection_class_info: *;

private var server_address: String;

private var handshake_secrets: * = null;

public function TFMSecretsLeaker() {
super();

Expand Down Expand Up @@ -171,9 +196,9 @@ package {

/*
Replace the connection's socket with our own socket
which will keep track of the handshake packet for us.
which will keep track of the sent packets for us.
*/
instance[socket_prop_name] = new HandshakeLeakerSocket(this.on_handshake);
instance[socket_prop_name] = new ServerboundLeakerSocket(this.on_sent_packet);

/* Dispatch fake connection event to trigger handshake packet. */
socket.dispatchEvent(new Event(Event.CONNECT));
Expand Down Expand Up @@ -268,22 +293,182 @@ package {
return null;
}

private function on_handshake(data: ByteArray) : void {
var handshake_secrets: * = this.get_handshake_secrets(data);
private function get_handle_packet_func() : Function {
var game: * = (this.final_loader.content as DisplayObjectContainer).getChildAt(0) as Loader;

var domain: * = game.contentLoaderInfo.applicationDomain;
for each(var class_name: String in domain.getQualifiedDefinitionNames()) {
/*
The connection class is the only one that only
inherits from 'Object', doesn't implement any
interface, and has a non-static 'Socket' property.
*/

var klass: * = domain.getDefinition(class_name);
if (klass.constructor != Class) {
continue;
}

var description: * = describeType(klass);

/* The packet handler class is the only one with a static const 'Loader' attribute. */
var constants: * = description.elements("constant");
if (constants.length() != 1) {
continue;
}

if (constants[0].attribute("type") != "flash.display::Loader") {
continue;
}

for each (var method: * in description.elements("method")) {
if (method.attribute("returnType") != "void") {
continue;
}

var parameters: * = method.elements("parameter");
if (parameters.length() != 1) {
continue;
}

if (parameters[0].attribute("type") == "flash.utils::ByteArray") {
return klass[method.attribute("name")];
}
}
}

return null;
}

private static const XXTEA_DELTA: uint = 0x9E3779B9;

private static function XXTEA_MX(e: uint, p: uint, y: uint, z: uint, sum: uint, key: Array) : uint {
/* Even though these are all 'uint', we still need to use '>>>'. This language sucks. */
return (((z >>> 5) ^ (y << 2)) + ((y >>> 3) ^ (z << 4))) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z));
}

private static function xxtea_decipher(buffer: ByteArray, key: Array) : ByteArray {
var n: uint = buffer.readUnsignedShort();
if (n == 1) {
return buffer;
}

var blocks: * = new Array();
for (var i: uint = 0; i < n; ++i) {
blocks.push(buffer.readUnsignedInt());
}

--n;

var y: uint = blocks[0];

var cycles: uint = uint(6 + 52 / (n + 1));
var sum: uint = cycles * XXTEA_DELTA;

while (sum > 0) {
var e: uint = (sum >> 2) & 3;

for (var p: uint = n; p > 0; --p) {
var z: uint = blocks[p - 1];

blocks[p] -= XXTEA_MX(e, p, y, z, sum, key);
y = blocks[p];
}

var last_z: uint = blocks[n];

blocks[0] -= XXTEA_MX(e, 0, y, last_z, sum, key);
y = blocks[0];

sum -= XXTEA_DELTA;
}

var deciphered_buffer: * = new ByteArray();

for each (var block: uint in blocks) {
deciphered_buffer.writeUnsignedInt(block);
}

deciphered_buffer.position = 0;

return deciphered_buffer;
}

private static function key_from_name(name: String, packet_key_sources: Array) : Array {
var num: int = 0x1505;

for (var i: uint = 0; i < packet_key_sources.length; ++i) {
var source_num: * = packet_key_sources[i];

num = (num << 5) + num + source_num + name.charCodeAt(i % name.length);
}

var key: * = new Array();

for each (var _: * in packet_key_sources) {
num ^= (num << 13);
num ^= (num >> 17);
num ^= (num << 5);

key.push(num);
}

return key;
}

private function on_sent_packet(data: ByteArray) : void {
if (this.handshake_secrets == null) {
this.handshake_secrets = this.get_handshake_secrets(data);

var handle_packet_func: * = this.get_handle_packet_func();

var client_verification_packet: * = new ByteArray();

client_verification_packet.writeByte(176);
client_verification_packet.writeByte(7);

client_verification_packet.writeInt(VERIFCATION_TOKEN);

handle_packet_func(client_verification_packet);

return;
}

/* At this point 'data' is the serverbound client verification packet. */

var game: * = (this.final_loader.content as DisplayObjectContainer).getChildAt(0) as Loader;
var document: * = game.getChildAt(0);

var auth_key: * = this.get_auth_key(document);
var packet_key_sources: * = this.get_packet_key_sources(document);

trace("Server Address: ", this.server_address);
trace("Game Version: ", handshake_secrets.game_version);
trace("Connection Token: ", handshake_secrets.connection_token);
trace("Auth Key: ", auth_key);
trace("Packet Key Sources:", packet_key_sources);
/* Read the packet ID. Will be (176, 47). */
data.readUnsignedByte();
data.readUnsignedByte();

var key: * = key_from_name(VERIFCATION_TOKEN + "", packet_key_sources);

var deciphered: * = xxtea_decipher(data, key);

var string_data: * = "";
while (deciphered.bytesAvailable) {
var byte: * = deciphered.readUnsignedByte();

if (byte < 0x10) {
string_data += "0";
}

string_data += byte.toString(16);
}

trace("Server Address: ", this.server_address);
trace("Game Version: ", this.handshake_secrets.game_version);
trace("Connection Token: ", this.handshake_secrets.connection_token);
trace("Auth Key: ", auth_key);
trace("Packet Key Sources: ", packet_key_sources);
trace("Client Verification Template:", string_data);

fscommand("quit");
}
}
}
}

0 comments on commit 9a28369

Please sign in to comment.