def parseFirstMessage(orig_msg): """Parse the first (client's) message from the game protocol. Args: orig_msg (NetworkMessage): the network message to be parsed. Returns list """ orig_msg.skipBytes(16) msg_buf = RSA.RSA_decrypt(orig_msg.getRest()[:128]) msg = NetworkMessage(msg_buf) # Extract the XTEA keys from the RSA-decrypted message. xtea_key = [msg.getU32() for _ in range(4)] assert (msg.getByte() == 0) # gamemaster flag account_number = msg.getString() # account character_name = msg.getString() # character name password = msg.getString() # password challenge_pos = msg.getPos() timestamp = msg.getU32() random_number = msg.getByte() return create_handshake_reply(xtea_key=xtea_key, account_number=account_number, password=password, character_name=character_name, timestamp=timestamp, random_number=random_number, challenge_pos=challenge_pos, first_16=orig_msg.getRaw()[:16], decrypted_raw=bytearray(msg_buf))
def parseFirstMessage(msg): """Parse the first (client's) message from the login protocol. Args: msg (NetworkMessage): the network message to be parsed. skip_bytes (int): the offset at which is the RSA-encrypted message. Returns list """ msg.skipBytes(28) msg_buf = RSA.RSA_decrypt(msg.getRest()[:128]) msg = NetworkMessage(msg_buf) # Extract the XTEA keys from the RSA-decrypted message. return [msg.getU32() for _ in range(4)]
def parseFirstMessage(orig_msg): """Parse the first (client's) message from the game protocol. Args: orig_msg (NetworkMessage): the network message to be parsed. Returns list """ orig_msg.skipBytes(16) msg_buf = RSA.RSA_decrypt(orig_msg.getRest()[:128]) msg = NetworkMessage(msg_buf) # Extract the XTEA keys from the RSA-decrypted message. xtea_key = [msg.getU32() for _ in range(4)] assert(msg.getByte() == 0) # gamemaster flag account_number = msg.getString() # account character_name = msg.getString() # character name password = msg.getString() # password challenge_pos = msg.getPos() timestamp = msg.getU32() random_number = msg.getByte() return create_handshake_reply(xtea_key=xtea_key, account_number=account_number, password=password, character_name=character_name, timestamp=timestamp, random_number=random_number, challenge_pos=challenge_pos, first_16=orig_msg.getRaw()[:16], decrypted_raw=bytearray(msg_buf))
def handleLogin(self, conn, msg): """Handles the login communication, passing it to the destination host, modifying the server IPs and returning the modified character list to the user. Args: conn (socket): the already established connection. msg (NetworkMessage): the first message received. Returns None """ xtea_key = LoginProtocol.parseFirstMessage(msg) # Connect to the destination host, send the request and read the reply. dest_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log("Connecting to the destination host...") dest_s.connect((self.destination_login_host, self.destination_login_port)) if not self.real_tibia: dest_s.send(msg.getRaw()) else: reencrypted = RSA.RSA_encrypt(RSA.RSA_decrypt(msg.getRaw()[28:])) unencrypted = msg.getRaw()[6:28] new_buf = "" new_buf += msg.getRaw()[:2] new_buf += struct.pack("<I", adlerChecksum(unencrypted+reencrypted)) new_buf += unencrypted new_buf += reencrypted dest_s.send(new_buf) data = dest_s.recv(1024) if data == '': log("Server disconnected.") conn.close() return msg = NetworkMessage(data) reply = LoginProtocol.parseReply(msg, xtea_key) if reply is None: # The reply doesn't seem to contain character list - just forward # it. log("WARNING: Passing through the login request.") conn.send(data) conn.close() return for character in reply['characters']: self.characters[character['name']] = character # Replace the IP and port with the address to the proxy. client_reply = copy.deepcopy(reply) for world in client_reply['worlds']: world['hostname'] = self.announce_host world['port'] = self.announce_port client_reply_msg = LoginProtocol.prepareReply(client_reply) # Send the message and close the connection. conn.send(client_reply_msg.getEncrypted(xtea_key)) conn.close()
def accept_login_conn(): conn, addr = self.l_s.accept() log("Received a login connection from %s:%s" % addr) data = conn.recv(1024) msg = NetworkMessage(data) if not self.debug: t = threading.Thread(target=self.handleLogin, args=[conn, msg]) t.start() else: self.handleLogin(conn, msg)
def prepareReply(handshake_reply, real_tibia): """Create a handshake reply based on the dictionary from the argument that has a modified challenge response. handshake_reply (dict): the origina handshake reply dictionary real_tibia (bool): whether to reencrypt the message for real Tibia Returns bytearray """ to_encrypt_raw = handshake_reply['decrypted_raw'] to_encrypt_msg = NetworkMessage(to_encrypt_raw) to_encrypt_msg.skipBytes(handshake_reply['challenge_pos']) to_encrypt_msg.replaceU32(handshake_reply['timestamp']) to_encrypt_msg.replaceByte(handshake_reply['random_number']) to_encrypt = to_encrypt_msg.getRaw() first16_wo_headers = handshake_reply['first_16'][6:] if real_tibia: encrypted = RSA.RSA_encrypt(to_encrypt) else: encrypted = RSA.RSA_encrypt(to_encrypt, n=RSA.otserv_n) rest = first16_wo_headers + encrypted checksum = struct.pack("<I", adlerChecksum(rest)) return (handshake_reply['first_16'][:2] + checksum + rest)
def client_send_said(self, player, pos, msg): sendmsg = NetworkMessage() sendmsg.addByte(0xAA) sendmsg.addU32(3) # statement ID sendmsg.addString("1") sendmsg.addU16(1) # level sendmsg.addByte(1) # type: SPEAK_SAY assert(len(pos) == 3) sendmsg.addU16(pos[0]) sendmsg.addU16(pos[1]) sendmsg.addByte(pos[2]) sendmsg.writable = True # FIXME sendmsg.addString(msg) self.conn.send(sendmsg.getEncrypted(self.xtea_key))
def handleGame(self, conn): """Connect to the game server, relay the packets between the the player, the proxy and the game server, reading them and running callbacks based on incoming/outgoing data. TODO: add anything beyond the eval() proof of concept. This function is one giant kludge at the moment. Args: conn (socket): the already established connection. data (str): the raw data sent by the player. Returns None """ # send a bogus challenge = 109, timestamp = 1385139009 conn.send(b'\x0c\x00@\x02!\x07\x06\x00\x1fA\x8b\x8fRm') data = conn.recv(2) size = struct.unpack("<H", data)[0] data += conn.recv(size) # Read the XTEA key from the player, pass on the original packet. msg = NetworkMessage(data) firstmsg_contents = GameProtocol.parseFirstMessage(msg) # Connect to the game server. dest_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) character = self.characters[firstmsg_contents['character_name']] game_host = character['world']['hostname'] game_port = character['world']['port'] log("Connecting to the game server (%s:%s)." % (game_host, game_port)) dest_s.connect((game_host, game_port)) size_raw = dest_s.recv(2) size = struct.unpack("<H", size_raw)[0] checksum = dest_s.recv(4) data = dest_s.recv(size - 4) msg = NetworkMessage(data) challenge_data = GameProtocol.parseChallengeMessage(msg) xtea_key = firstmsg_contents['xtea_key'] firstmsg_contents['timestamp'] = challenge_data['timestamp'] firstmsg_contents['random_number'] = challenge_data['random_number'] dest_s.send(GameProtocol.prepareReply(firstmsg_contents, self.real_tibia)) conn_obj = Connection(conn, xtea_key) received_player = False while True: # Wait until either the player or the server sent some data. has_data, _, _ = select.select([conn, dest_s], [], []) if conn in has_data: data = bytearray() size_raw = conn.recv(2) data += size_raw if size_raw == bytearray(): log("The client disconnected") break size = struct.unpack("<H", size_raw)[0] data += conn.recv(size+4) msg = NetworkMessage(data) msg_size = msg.getU16() msg.getU32() # skip the checksum validation if msg_size != len(data) - 2: log("Strange packet from client: %s" % repr(data)) log("len(data)=%s, msg_size=%s" % (len(data), msg_size)) dest_s.send(data) continue msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) msg.getU16() packet_type = msg.getByte() if packet_type in GameProtocol.client_packet_types: if self.debug: log("C [%s] %s" % (hex(packet_type), GameProtocol.client_packet_types[packet_type])) else: log("Got a packet of type %s from client" % packet_type) should_forward = True if packet_type == 150: # We got a player "say" request. Read what the player # wanted to say, treat it like a Python expression and send # the result back to the user. msg.skipBytes(1) player_said = msg.getString() for plugin in self.plugins: if 'on_client_say' in dir(plugin): plugin_returned = plugin.on_client_say(conn_obj, player_said) if plugin_returned: should_forward = False if should_forward: # Otherwise, just pass the packet to the server. dest_s.send(data) if dest_s in has_data: # Server sent us some data. data = bytearray() size_raw = dest_s.recv(2) data += size_raw if data == bytearray(): conn.close() log("The server disconnected") break size = struct.unpack("<H", size_raw)[0] data += dest_s.recv(size) msg = NetworkMessage(data) msg_size = msg.getU16() msg.getU32() # skip the checksum validation if msg_size != len(data) - 2: log("Strange packet from server: %s" % repr(data)) log("len(data)=%s, msg_size=%s" % (len(data), msg_size)) conn.send(data) continue msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) msg.getU16() while msg.finished(): def getMapDescription(): def getTile(): got_effect = False for stackPos in range(256): if msg.peekU16() >= 0xff00: return msg.getU16() & 0xff if not got_effect: msg.getU16() got_effect = True continue assert(stackPos <= 10) thingId = msg.getU16() log(thingId) assert(thingId != 0) if thingId == 96: log("strange... got a staticText") elif thingId in [97, 98, 99]: # it's a creature pass else: # it's a thing - might require reading 3 # extra bytes pass def getFloorDescription(x, y, z, w, h, offset, skip): for nx in range(w): for ny in range(h): getTile() x, y, z = msg.getCoordinates() w = 18 h = 14 skip = [-1] if z > 7: startz = z - 2 endz = min(16 - 1, z + 2) zstep = 1 else: startz = 7 endz = 0 zstep = -1 nz = startz while nz != endz + zstep: getFloorDescription(x, y, nz, w, h, z - nz, skip) nz += zstep packet_type = msg.getByte() if packet_type in GameProtocol.server_packet_types: if self.debug: log("S [%s] %s" % (hex(packet_type), GameProtocol.server_packet_types[packet_type]) ) else: log("Got a packet of type %s from server" % packet_type) # login successful if packet_type == 0x17: msg.getU32() # player ID msg.getU16() # beat duration msg.getU32() # speed A msg.getU32() # speed B msg.getU32() # speed C msg.getByte() # 1 if can report bugs, 0 otherwise # no idea what's this: assert_equal(msg.getByte(), 0xB6) assert_equal(msg.getByte(), 0x7F) assert_equal(msg.getByte(), 0x00) assert_equal(msg.getByte(), 0x0A) assert_equal(msg.getByte(), 0x0F) assert_equal(msg.getByte(), 0x64) getMapDescription() log("TODO") received_player = True # FYI box if packet_type == 0x15: fyi = msg.getString() log("Got a FYI: %s" % fyi) log("Sending!") conn.send(data)
def parseReply(msg, xtea_key): """Parse the reply from the login server. Args: msg (NetworkMessage): the network message to be parsed. Returns dict or None """ size = msg.getU16() # TODO: someday perhaps I'll have enough time to even check the checksums! msg.skipBytes(4) msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) #assert(len(msg.getWithHeader()) == size) decrypted_size = msg.getU16() #assert(decrypted_size == size - 5) packet_type = msg.getByte() if packet_type != 0x14: # The reply doesn't seem to contain character list. return None motd = msg.getString() assert(msg.getByte() == 0x64) num_worlds = msg.getByte() worlds = [] for _ in range(num_worlds): world_id = msg.getByte() world_name = msg.getString() world_hostname = msg.getString() world_port = msg.getU16() log("Received server address %s:%s" % (world_hostname, world_port)) msg.skipBytes(1) # no idea what's that. worlds += [create_login_world_entry(name=world_name, hostname=world_hostname, port=world_port)] num_chars = msg.getByte() characters = [] for _ in range(num_chars): world_num = msg.getByte() char_world = worlds[world_num] char_name = msg.getString() characters += [create_login_character_entry(name=char_name, world=char_world)] return create_login_reply_info(characters, motd, worlds)
def prepareReply(login_reply): """Prepare the reply based on a LoginReply instance. Args: login_reply (dict): the login_reply structure used to build the response. Returns NetworkMessage """ ret = NetworkMessage() ret.addByte(0x14) ret.addString(login_reply['motd']) ret.addByte(0x64) ret.addByte(len(login_reply['worlds'])) world_id = 0 for world in login_reply['worlds']: ret.addByte(world_id) ret.addString(world['name']) ret.addString(world['hostname']) ret.addU16(world['port']) ret.addByte(0) world_id += 1 ret.addByte(len(login_reply['characters'])) for char in login_reply['characters']: ret.addByte(login_reply['worlds'].index(char['world'])) ret.addString(char['name']) ret.addByte(0x00) ret.addByte(0x00) return ret
def parseReply(msg, xtea_key): """Parse the reply from the login server. Args: msg (NetworkMessage): the network message to be parsed. Returns dict or None """ size = msg.getU16() # TODO: someday perhaps I'll have enough time to even check the checksums! msg.skipBytes(4) msg_buf = XTEA.XTEA_decrypt(msg.getRest(), xtea_key) msg = NetworkMessage(msg_buf) #assert(len(msg.getWithHeader()) == size) decrypted_size = msg.getU16() #assert(decrypted_size == size - 5) packet_type = msg.getByte() if packet_type != 0x14: # The reply doesn't seem to contain character list. return None motd = msg.getString() assert (msg.getByte() == 0x64) num_worlds = msg.getByte() worlds = [] for _ in range(num_worlds): world_id = msg.getByte() world_name = msg.getString() world_hostname = msg.getString() world_port = msg.getU16() log("Received server address %s:%s" % (world_hostname, world_port)) msg.skipBytes(1) # no idea what's that. worlds += [ create_login_world_entry(name=world_name, hostname=world_hostname, port=world_port) ] num_chars = msg.getByte() characters = [] for _ in range(num_chars): world_num = msg.getByte() char_world = worlds[world_num] char_name = msg.getString() characters += [ create_login_character_entry(name=char_name, world=char_world) ] return create_login_reply_info(characters, motd, worlds)