Hi everybody,
I don't know if anyone is reading this thread, but I will try to make updates on my progression in reversing packets in The Chronicles of Spellborn.
What I reversed so far:
- connect/disconnect packet
- login packet (client/server) : can validate login or send error (bad login/password or wrong client version)
- universes list packet (client/server) (send universe list with name, population etc)
- a good part of the universe selected packet (sending ip/port etc of the game server)
- How to get packet ID
- What is the header of every packets
- Low level packet writing / reading in d_mmos.dll
- Some parts of the write/read functions of the packets mentionned previously in SBPacket.dll
- I also start to have an understanding of the packet management architecture that allows me to find quickly basics information and place to investigate when starting to reverse a packet.
The process is still very slow and "hand crafted". I will try to document what I did when I have the time.
I will edit this post with much details later.
Edit: Beginning of a doc
General
Sb_client.exe arguments
--show_console: show the console displaying the log (you can also take control of the console and use some functions/load packages)
--packet_log: enable logging of the packets sended et received
--world: not really sure what this option means... seems like bypassing universe selection but does not seem to work
--uc_debugger: (check syntax) attach unreal engine debugger to the process. Sadly scripts files are not accessible.
--unreal_log: seems to do nothing. Test
Encoding
TCoS client encodes its strings in UTF16-LE.
Network
d_mmos.dll
Contains low level functions for sending packets.
Interesting places:
- d_message class with read (void* dataOut, uint dataSize) and write (const void* dataIn, uint dataSize)
- d_address class: represent IP+Port
-
d_message class
d_message::read (void* dataOut, uint dataSize)
void* dataOut is a pointer to a memory location where the value read will be stored. Sometimes this memory is in stack, sometime in heap. It totally depends of the caller. For instance if the client tries to read a string, dataOut will be a pointer to the std::string::data () (or equivalent).
uint dataSize is the size in bytes of the value you want to read. For instance if you want to read a DWORD (double word = 4 bytes) its value will be 4.
The d_message object keeps track of the data you've read in it. So each time you call its
read method, it updates an internal value with the size of data already read (it simply adds
dataSize to its current value). This is also an offset allowing it to know
where to begin the reading of the data in the message. Each time you call read, you actually ask
to read the next data in the d_message object.
If there is no more data, or not enough (there is still 2 bytes and you ask to read 4 bytes), the method will raise an exception.
d_message::write (const void* dataIn, uint dataSize)
Work the same way than
read but write
dataSize bytes of data pointed by
dataIn inside the d_message. Same mechanism checking that not too much data is written.
SBPacket.dll
Contains packet high level stuff. Use templates a lot, which means that the window 'Names' in IDA won't show you all the methods because the same actual method can be exported with multiples names (not apparing in 'Names' but only in 'Exports'). So, look in 'Exports' instead.
The interesting class is d_serializable<struct PACKET> where 'struct PACKET' is the template parameter. Interesting methods of d_serializable for now:
- GetID ()
- GetName () (but actually the name is exactly the same as the template parameter name)
- ReadMessage (const struct d_message&)
- WriteMessage (struct d_message&)
-
/!\ /!\ /!\ ==>WriteMessage and ReadMessage are very interesting to study for each message because it's there you can understand what is put inside the packet.<== /!\ /!\ /!\
This is how I try to reverse packets, by studying these two methods and their context (who calls them, what structure do they fill etc).
To find quickly which message has a specific ID, in OllyDBG use "find command" and type
move ax, ID where ID is a short number. It will point you the good GetID () method. The template parameter gives you the message name.
Note: packet's names follow a simple convention. They have a prefix specifying if its a packet sended by the client to the server or by the server to the client.
C2L: Client to Login server
L2C: Login server to Client
C2S: Client to Server
S2C: Server to Client
There are other prefix but currently I did not find what they mean.
Packets layout
Every packet I have seen in the login process (login+ select universe) have this header:
struct PacketHeader (32 bits)
{
WORD PacketID;
WORD PacketSize;
}
If the PacketSize attribute does not match the actual packet size, the client will not trigger the ReadMessage or will trigger an error. If the client wants to read more data than you give to him, it will sayin the logs: "
PACKET_NAME READ BUFFER OVERFLOW".
Note: For what I've seen so far, the client does not check the size in its read algorithm to verify if its is consistent with the type of packet you send.
The client will just try to read everything it has to read and if you send not enough data, triggers the
BUFFER OVERFLOW error seen above. This behavior is due to the d_mmos.dll::d_message::read (const void* outData, uint dataSize) method which check the message size everytime you ask it to read a value in the packet. If the size alreday read + the size of the data you ask to read is greater than the message size: error.
Note: The following sizes does not take the packet's header into account, because the size contained in the packet's header do the same.
struct C2L_USER_LOGIN_PACKET (various size)
{
struct PacketHeader header;
DWORD zeroDword;//unknown data, = 0
DWORD clientVersion;//not sure but can't be anything else
DWORD loginNumCharacters;
char[loginNumCharacters*2] loginString;//in UTF-16LE
DWORD passwordNumCharacters;
char[passwordNumCharacters * 2] passwordString;//in UTF16-LE
};
struct L2C_USER_LOGIN_ACK_PACKET (2 DWORDS = 8 bytes)
{
struct PacketHeader header;
DWORD zeroDword;//unknown data, its value does nothing
DWORD statusCode;
};
statusCode possible values:
- 0: Login OK
- 1: Wrong client version
- 2: Bad login or password
- 3: Bad login or password (yes, the same)
- above 3: unknown error during login process
struct C2L_QUERY_UNIVERSE_LIST_PACKET (empty)
{
struct PacketHeader header;
};
struct Universe (various size)
{
DWORD universeID;
DWORD universeNameNumChars;
char[universeNameNumChars*2] universeName; //UTF16-LE
DWORD universeLanguageNumChars;
char[universeLanguageNumChars*2] universeLanguage; //UTF16-LE
DWORD universeTypeNumChars;
char[universeTypeNumChars*2] universeType; //UTF16-LE
DWORD universePopulationNumChars;
char[universePopulationNumChars*2] universePopulation; //UTF16-LE
};
struct L2C_QUERY_UNIVERSE_LIST_ACK (various size)
{
struct PacketHeader header;
DWORD unknownDWORD; // MUST BE 0 to work, integrity check?
DWORD universesNumber; //number of universes available
struct Universe[universesNumber] universesList; // universesNumber times the attributes in the struct Universe
};
struct C2L_UNIVERSE_SELECTED_PACKET (size = 1 DWORD)
{
struct PacketHeader header;
DWORD universeID; //id of the selected universe
};
Protocol
Here is the dialog between client and server, don't know if it's very useful. For detailed info about the packets structure, see the section above.
Client send
CONNECT (no data)
Client send
C2L_USER_LOGIN (login + password)
Server answer
L2C_USER_LOGIN_ACK (status code)
Client send
C2L_QUERY_UNIVERSE_LIST (no data)
Server answer
L2C_QUERY_UNIVERSE_LIST_ACK (status code?, universes list with infos)
Client send
C2L_UNIVERSE_SELECTED (universe selected id)
Server answer
L2C_UNIVERSE_SELECTED_ACK (universe package's name, universe IP + port, and unknown data)
List of Packets ID
CONNECT = 0xFFFD;
DISCONNECT = 0xFFFE;
Client packets ID
C2L_USER_LOGIN = 0;
C2S_WORLD_PRE_LOGIN_ACK = 2;
C2L_QUERY_UNIVERSE_LIST = 2;
C2S_WORLD_LOGIN_ACK = 4;
C2L_UNIVERSE_SELECTED = 4;
Server packets ID
L2C_USER_LOGIN_ACK = 1;
S2C_WORLD_LOGIN = 3;
L2C_QUERY_UNIVERSE_LIST_ACK = 3;
L2C_UNIVERSE_SELECTED_ACK = 5;
To be continued