Announcement

Collapse
No announcement yet.

Tibia Packets and Proxy Setup

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • Tibia Packets and Proxy Setup

    Okay, so in this article, we will learn all basics, and somewhat specific stuff about Tibia's way of communication from their servers and the client of the end-user.

    The current approach I'm using is a regular network proxy between Tibia's communication layer and my computer.

    The next tutorial has based as of the current Tibia Version (10.97) though it hasn't been modified much, as per the protocol. The packet structure is prone to change as per CipSoft deem necessary.

    Normally, we would need only a few addresses from Tibia's Memory which are: RSA, IP Login Start, Selected Character and that's it. So once we have all addresses needed we should get into coding.

    The first step is replacing Tibia's Static RSA Public Key with our own. (generally we use a RSA OT Public Key) so that we can decrypt the packets.

    So, the core function of our proxy is to listen for all data incoming to our clientSocket and dealing with it accordingly.

    Tibia specifically, uses 2 different protocols when it comes to their network messages
    * Login protocol
    * Game protocol

    Tibias Packet Class constructs all packets very similar, we are going to call this logical header, and it the following.
    PHP Code:
    Logical Header
    [2 bytesWhole message buffer size
    [4 bytesAdlers checksum
    [2 bytesReal Network Msg size (when encrypted with XTea)
    [
    1 byteMessage Type 
    PHP Code:
    Strings:
        [
    2 bytesstring_len
        
    [string_len bytesstring UTF-8 Encoded
    Login protocol routine:

    1. Listen for connection on Tibias Port: 7171 (clientSocket)
    2. clientSocket receives connection and getDataBuffer (LoginRequest)
    - This packet is partially raw. It contains all client versions information along with the XTea key.
    PHP Code:
                The structure is the following:

                [
    2 bytesNetwork Msg Buffer Size
                
    [4 bytesAdlers checksum

                Console
    .WriteLine("Packet Type: " p.read_byte());
                
    Console.WriteLine("OS: " p.read_ushort());
                
    Console.WriteLine("Tibia Version:" p.read_ushort());
                
    Console.WriteLine("Client Version: " p.read_int());
                
    Console.WriteLine("Tibia.Dat Version: " p.read_int());
                
    Console.WriteLine("Tibia.Pic Version: " p.read_int());
                
    Console.WriteLine("Tibia.Pic Version: " p.read_int());
                
    Console.WriteLine("IsPreviewClient:" p.read_byte());

                [
    128 bytesRSA encrypted which contains the XTea Key. (Their RSA encryption routine is literally the same you can find in wikipedia.)

                [
    1 byteRSA Dechipering key MUST BE 0 )
                [
    16 bytesXTea Key uint[4]

                
    Console.WriteLine("Account:" p.read_string());
                
    Console.WriteLine("Password:" p.read_string()); 
    So once we have parsed the LoginRequest Packet and retrieved the XTea Key we will need to encrypt back the packet using Tibias RSA Public Key

    3. Set up another socket (serversocket) to connect to Tibias Server (LoginServers)
    4. Forward the LoginRequest Buffer to the Client. If all information is correct, the next incoming buffer should be the character list packet (LoginResponse)
    5. Now we must parse that packet and replace their login IP servers and store them so that we now where to connect after (see below)
    - This packet is already encrypted with the XTEa key sent during the LoginRequest, so our job is to decrypt it using the same key we obtained.

    PHP Code:
            The character list packet structure is the following:

                [
    2 bytesNetwork Msg Buffer Size
                
    [4 bytesAdlers checksum

                I will now copy
    -paste a code snippet  from my current project showing the character list structure. (for the sake of lazynesslol)

                while (
    p.position p.Length)
                {
                    var 
    packetType p.read_byte();

                    switch (
    packetType)
                    {
                        case 
    0x0AConsole.WriteLine(p.read_string()); break;
                        case 
    0x0B/* Invalid Login  */ break;
                        case 
    0x0C//token succes
                        
    case 0x0D//token error
                            
    Console.WriteLine("Token Error PHP: " p.read_byte());
                            break;
                        case 
    0x11// Update
                            
    Console.WriteLine(p.read_string());
                            break;
                        case 
    0x14Console.WriteLine(p.read_string()); break;
                        case 
    0x1E: case 0x1F: case 0x20Console.WriteLine("Patching"); break;
                        case 
    0x08: break; // FYI
                        
    case 0x28:
                            
    //session key
                            
    p.read_string();
                            break;
                        case 
    129:
                            break;
                        case 
    160// Client Corrupted
                            
    Console.WriteLine(p.read_string());
                            return;
                        case 
    0x64// Character List
                            
    byte serverCount p.read_byte();

                            
    GameServers = new List<GameServer>();

                            
    GameServers = new List<GameServer>();

                            for (
    byte i 0serverCounti++)
                            {
                                
    GameServers.Add(new GameServer() {
                                    
    id p.read_byte(),
                                    
    worldName p.read_string(),
                                    
    ip p.read_string(); /* here we must replace it to localhost */
                                    
    port p.read_ushort(), /* to whichever port you'd like, I personally, leave this unaffected. */
                                    
    isPreview p.read_byte();
                                });
                            }

                            
    byte characterCount p.read_byte();

                            
    CharList = new List<CharacterList>();

                            for (
    byte i 0characterCounti++)
                            {
                                
    byte worldId p.read_byte();

                                
    CharList.Add(new CharacterList()
                                {
                                    
    CharacterName p.read_string(),
                                    
    HostName GameServers[worldId].ip,
                                    
    Port GameServers[worldId].port,
                                    
    World GameServers[worldId].worldName
                                
    });
                            }

                            var 
    premiumDays p.read_ushort();
                            var 
    premium_timestamp p.read_uint();
                            break;
                    }
                } 
    - If you noticed the while loop, you may be wondering - why?
    Well, Tibia network system seem to append packets on the same buffer as others, so we can expect multiple packets in the same buffer.
    And because of that we HAVE to parse all existing packets on their repostery otherwise we may be missing important incoming data.


    6. Since we are now managing Tibias internal connections we should set the connections ourselves, in this case we must setup the clientSocket to listen for new incoming connections to localhost ( When you hit enter to login to whatever character tibia will trigger an event and connect to that characters ip and since we edited it to connect to our proxy, this will trigger our onAcceptClient event in the clientSocket).
    Now that we have established the connection to the client, we have to set up the serverSocket to connect to the character real world ip and once that connection is established you must encrypt the packet back and push it to this stream (serverSocket)
    7. If everything goes as expected, the next incoming buffer should be the GameServerRequest from the Client which contains the new XTea key that it going to server to decrypt all game protocol packets
    PHP Code:
            The structure is the following:
                 
    /* This packet is partially raw and includes a RSA block - no xtea required :) */

                
    Console.WriteLine("Packet Type: " p.read_byte());
                
    Console.WriteLine("Client Type: " p.read_ushort());
                
    Console.WriteLine("protocolVersion: " p.read_ushort());
                
    Console.WriteLine("Client Version: " p.read_uint());
                
    Console.WriteLine("contentRevision: " p.read_ushort());
                
    Console.WriteLine("Preview State: " p.read_byte());

                
    /* The XTEa is about to change, so its encrypted with RSA */
                
    byte[] rsaBlock p.PeekAt(p.position128);
                
    p.Buffer RSADecrypt(rsaBlock);
                var 
    rsa_result p.read_byte();            
                
    xteaKey[0] = p.read_uint();
                
    xteaKey[1] = p.read_uint();
                
    xteaKey[2] = p.read_uint();
                
    xteaKey[3] = p.read_uint();

                
    p.read_byte(); // session token result
                
    p.read_string(); // session token string
                
    p.read_string(); // I THINK this is the account name I'll check it later since I just skipped it in my parser. 

    8. Once we have parsed that last packet, we are going to encrypt it back with RSA and forward it to the server (serverSocket)
    9. We now must set up an inifinite loop for data recv in both ends of the sockets (clientSocket & serverSocket) and parse them accordingly. And from now on you will have to call your method to do the following:
    PHP Code:
             /* 
                server -> serverSocket -> clientSocket
                clientSocket -> serverSocket -> server
             */

             
    private void setup_async_loop()
            {
                var 
    data_client = new byte[2];
                
    clientSocket.BeginReceive(data_client02SocketFlags.Noneincoming_client_datadata_client);
                var 
    data_server = new byte[2];
                
    serverSocket.BeginReceive(data_server02SocketFlags.Noneincoming_server_datadata_server);
                
    tibia = new Tibia(this);
            }

            private 
    void incoming_server_data(IAsyncResult ar) {
                ....
                
    tibia.parse_server_msg(p);
            }

            private 
    void incoming_client_data(IAsyncResult ar) {
                ...
                
    tibia.parse_client_msg(p);
            } 
    The first packet that we should receive if you logged in appropriately is the LoginSuccessPacket (0x17)
    PHP Code:
            Which structure looks like this:

            private 
    void ParseLoginSuccess(Packet p)
            {
                
    this.player_id p.read_uint();
                
    ushort beat_duration p.read_ushort();
                
    double speed_a p.read_double();
                
    double speed_b p.read_double();
                
    double speed_c p.read_double();
                
    bool can_report = (p.read_byte() == 1);
                
    bool can_pvp = (p.read_byte() == 1);
                
    bool is_expert = (p.read_byte() == 1);
                
    string store_uri p.read_string();
                
    p.position += 3// unkown bytes, possibly another packet...
            

    From this point you should be able to read all packets, remember to parse all messages accordingly using the already documented packet structures otherwise your parser will break.
    All packets references can be found by reading the tibia flash client sources or OT sources.

    If something is somewhat misleading, let me know because I wrote this in like an hour or so lol

    Feel free to ask away your doubts!

    Diego.
    Last edited by Diego; 06-09-2016, 12:59 AM.

  • #2
    Nicely written. I'm going to dig on your tutorial soon.

    Since this tutorial will be a reference on this forum for packet related stuff from now on, I would suggest you to include the Tibia version you were using here for future comparison.

    Comment


    • #3
      Originally posted by Blequi View Post
      Nicely written. I'm going to dig on your tutorial soon.

      Since this tutorial will be a reference on this forum for packet related stuff from now on, I would suggest you to include the Tibia version you were using here for future comparison.
      You're right. I've edited the original post, thanks!

      Comment


      • #4
        Omg. I need something like this in Delphi xD

        Comment


        • #5
          You could easily port it out to Delphi as I am just pointing out the steps needed for the proxy setup

          Comment


          • #6
            Can you provide more detail on how to setup the network proxy?

            Comment


            • #7
              Okay,

              I'm tryin' to do something like that without XTEA & Adler - 7.6 protocol.

              I've got troubles in this - probably I miss some lenght or something.

              I found some code from OTServ and personaly used it.

              Code:
              #define NETWORKMESSAGE_MAXSIZE 15360
              
              int32_t m_MsgSize;
              int32_t m_ReadPos;
              uint8_t m_MsgBuf[NETWORKMESSAGE_MAXSIZE];
              
              bool NetworkMessage::readFromSock(SOCKET sock)
              {
              	m_MsgSize = recv(sock, (char*)m_MsgBuf, 2, 0);
              	int datasize = m_MsgBuf[0] | m_MsgBuf[1] << 8;
              	if((m_MsgSize != 2) || (datasize > NETWORKMESSAGE_MAXSIZE - 2))
              	{
              		int errnum = WSAGetLastError();
              		if(errnum == EWOULDBLOCK)
              		{
              			m_MsgSize = 0;
              			return true;
              		}
              		Reset();
              		return false;
              	}
              	
              	m_MsgSize += recv(sock, (char*)m_MsgBuf+2, datasize, 0);
              
              	// we got something unexpected/incomplete
              	if((m_MsgSize <= 2) || ((m_MsgBuf[0] | m_MsgBuf[1] << 8) != m_MsgSize-2))
              	{
              		Reset();
              		return false;
              	}
              	m_ReadPos = 2;
              
              	return true;
              }
              
              bool NetworkMessage::writeToSock(SOCKET sock)
              {
              	if(m_MsgSize == 0)
              		return true;
              
              	m_MsgBuf[2] = (uint8_t)(m_MsgSize);
              	m_MsgBuf[3] = (uint8_t)(m_MsgSize >> 8);
              	bool ret = true;
              	int sendBytes = 0;
              	int start = 0;
              	int flags;
              	u_long mode = 1;
              	ioctlsocket(sock, FIONBIO, &mode);
              	flags = 0;
              	do
              	{
              		int b = send(sock, (char*)m_MsgBuf + sendBytes + start, std::min(m_MsgSize - sendBytes + 2, 1000), flags);
              		if(b <= 0)
              		{
              			ret = false;
              			break;
              		}
              		sendBytes += b;
              	}while(sendBytes < m_MsgSize + 2);
              	mode = 0;
              	ioctlsocket(sock, FIONBIO, &mode);
              	return ret;
              }
              
              void Reset()
              {
              	m_MsgSize = 0;
              	m_ReadPos = 2;
              }
              Then I do:

              Code:
              NetworkMessage nm;
              nm.readFromSock(con.acceptSock);
              nm.writeToSock(tibiaSock);
              nm.readFromSock(tibiaSock);
              closesocket(tibiaSock);
              byte b = nm.GetByte();
              if (b == 0x14) //MOTD
              {	
              	printf(":: >> MOTD received: %s", nm.GetString().c_str());
              }
              else
              {
              	printf(":: >> Unexpected packet recieved. Expected [0x14]. Recieved [0x%x].", b); // I recv 0x21 packet
              	return 0;
              }
              What's wrong? :
              Last edited by Bryl; 05-07-2017, 07:40 PM.

              Comment

              Working...
              X