Reversing a packet protocol: The FusionFall protocol
In the event you keep in mind my previous website earlier than I switched to a static website, I wrote a few posts about FusionFall Retro. Sadly, that mission has since been shutdown. A few months in the past within the spirit of FFR (and since it obtained introduced up in dialog) I began to turn into inquisitive about how they really made the server. This kick began my journey into the depths of the FusionFall consumer. On this put up I’m going to clarify how I wrote the bottom for OpenFusion (and JUST the bottom, significantly, I solely imply how OpenFusion accepts connections and sends packets to/from the FF consumer.)
How will we get the consumer?
FusionFall was an previous MMO by Cartoon Community. It ran on a customized Unity Internet Participant engine till it was shutdown in late 2013. Funnily sufficient, their previous CDN is definitely nonetheless as much as this present day! It hosts consumer variations from early 2010 to late 2011. Their CDN format appears like this:
http://ht.cdn.turner.com/ff/huge/{$BUILD}/{$ASSET}
So for instance, to seize the principle.unity3d for the january 4th 2010 construct, we will merely goto http://ht.cdn.turner.com/ff/big/beta-20100104/main.unity3d. Wow, thats handy.
NOTE: Their CDN was taken down mid September of 2021, fortunately although there are nonetheless archives of all the general public builds over at archive.org!
In the event you’ve seen, that foremost.unity3d file is only a unity net participant file. We are able to’t truly run the consumer with out the Internet Plugin put in. There’s tons of how to go about this, for instance utilizing the Pale Moon browser & having FF’s previous UnityWebPlayer put in. However I’m simply going to chop to the chase and offer you an previous electron consumer I made that can set every part up for our foremost.unity3d file to run by an identical Unity Internet Participant model. Which you’ll obtain here.
Configuration
In the event you’ve seen within the electron consumer I’ve offered, there’s a whole lot of non-php recordsdata with the .php extension. It seems the FusionFall consumer truly makes GET requests to search out out the place to hook up with the login server, and the place to search for property (which I simply offered the CDN).
Right here’s an inventory of the necessary requests the beta-20100104 consumer makes:
- /assetInfo.php
- Defines the bottom URL from which property are requested from
- /loginInfo.php
- Defines the ip & port the loginserver is hosted on
Each of those recordsdata may be discovered at assets/app/recordsdata. I already took care of assetInfo.php for you and simply linked the corresponding CDN hyperlink, and loginInfo.php is ready to take a look at 127.0.0.1 on port 8001. Principally the electron consumer (which is simply an previous model of chromium) is pointed to assets/app/recordsdata/, so when the principle.unity3d file makes the GET request by the browser the electron consumer simply reads our recordsdata as an alternative 🙂
Studying the C# assemblies
foremost.unity3d recordsdata have their very own format. Fortunately although there have been many instruments made to extract the property. Actually, after googling “extract UnityWeb property” I discovered QuickBMS, which explains that we will simply use QuickBMS and a corresponding script to extract the property from the file. So after operating:
$ quickbms unity3d_webplayer.bms foremost.unity3d
We’ll find yourself with these recordsdata
Now it’s only a matter of feeding the meeting to DNSpy or in my case ILSpy.
$ ilspycmd Meeting - CSharp.dll > out.cs
Bizarre obfuscation
Let’s check out csSocketManager, it’s answerable for obfuscating packets earlier than sending to the server and de-obfuscating packets from the server. You’ll discover there’s an EKey and an FEKey. I’m solely going to be speaking in regards to the EKey as we speak however the FEKey is used when switching from the login server to the shard server (the precise sport server.) Within the constructor you may see the default key that’s set.
They use Marshal to transform the string into an array of bytes after which into a protracted int (8 bytes lengthy.) We are able to truly see what this secret is used for over in Encrypt_Data()
First, Encrypt_Data() calls Encrypt_xor_64bit() with the default key, the info, and the dimensions of the info. Encrypt_xor_64bit() appears like
It appears like a large number, however principally the default secret is xor’d over the info and the dimensions of the xor’d knowledge is returned. After Encrypt_xor_64bit() known as, Encrypt_Data() calls Encrypt_byte_change_A() which appears like
Encrypt_byte_change_A() swaps some bytes round in an simply reversible manner and returns the quantity of bytes swapped.
If I had been to rewrite the above strategies in C++, it will seem like so
namespace CNSocketObfuscation {
static constexpr const char* defaultKey = "m@rQn~W#";
static const unsigned int keyLength = 8;
// actually C/P from the consumer and transformed to C++ (does some byte swapping /shrug)
int Encrypt_byte_change_A(int ERSize, uint8_t* knowledge, int dimension) {
int num = 0;
int num2 = 0;
int num3 = 0;
whereas (num + ERSize <= dimension) {
int num4 = num + num3;
int num5 = num + (ERSize - 1 - num3);
uint8_t b = knowledge[num4];
knowledge[num4] = knowledge[num5];
knowledge[num5] = b;
num += ERSize;
num3++;
if (num3 > ERSize / 2) {
num3 = 0;
}
}
num2 = ERSize - (num + ERSize - dimension);
return num + num2;
}
int xorData(uint8_t* buffer, uint8_t* key, int dimension) {
// xor each 8 bytes with 8 byte key
for (int i = 0; i < dimension; i++) {
buffer[i] ^= key[i % keyLength];
}
return dimension;
}
int encryptData(uint8_t* buffer, uint8_t* key, int dimension) {
int eRSize = dimension % (keyLength / 2 + 1) * 2 + keyLength; // C/P from consumer
int size2 = xorData(buffer, key, dimension);
return Encrypt_byte_change_A(eRSize, buffer, size2);
}
// we'll get again to those :eyes:
int decryptData(uint8_t* buffer, uint8_t* key, int dimension);
uint64_t createNewKey(uint64_t uTime, int32_t iv1, int32_t iv2);
}
Decrypting knowledge is simply as easy, simply name the strategies in reverse order
int decryptData(uint8_t* buffer, uint8_t* key, int dimension) {
int eRSize = dimension % (keyLength / 2 + 1) * 2 + keyLength; // dimension % of 18????
int size2 = Encrypt_byte_change_A(eRSize, buffer, dimension);
return xorData(buffer, key, size2);
}
Packet construction
So as soon as the info has been de-obfuscated, how is it parsed? Effectively, after checking what calls Decrypt_Data I discover this methodology in cnNetworkManager.
Very first thing this methodology calls is csSocketManager.GetPacket(), which is a reasonably easy methodology
Principally from a queue of ready packets to be processed it’ll return one. This queue is populated by csSocket.Replace() which explains how a part of the packets are laid out.
I’ve taken the freedom of going forward and annotating the decompiled supply of this methodology. Principally this methodology populates csSockManager’s queue of packets.
So earlier than the info is even de-obfuscated a dimension is learn and used to seize the remainder of the packet. Packets begin with a 4 byte lengthy integer in little endian which tells us the dimensions of the remainder of the packet. So going again to cnNetworkManager.Replace(), after the packet is grabbed, it’s de-obfuscated with csSocketManager.Decrypt_Data() and handed to a cnEvent. This cnEvent is outlined as
cnEvents are used internally as a communication between managers. Wanting on the constructor and the SendEvent() methodology we discover
SendEvent() finds the GlobalManager (or creates it if it doesn’t exist) and calls ReceiveEvent() with the required occasion
and looking out into GameManager we discover
I omitted a whole lot of ineffective code right here so you may simply see what’s necessary.
SO, when cnEvent.SendEvent() known as, it calls GameManager.ReceiveEvent() which calls the corresponding outlined supervisor’s operate. We all know that 2 is outlined because the GameFrame.ReceiveEvent(), so taking a look at GameFrame.ReceiveEvent() we discover
Once more, I omitted a whole lot of out of context code so you may simply see what’s necessary
Now, I do know what you’re considering, “wow, thats a whole lot of code simply to name a single operate in one other class.” Sure. I 100% agree, however that is what they did and I didn’t need to simply say “so principally packets are learn right here” as a result of a part of the method is sifting by a LOT of junk code. In any case, when cnEvent.SendEvent() known as with the cnEvent(2, 5) constructor GameFrame.ReceivePacket() known as and the occasion is handed; which appears like
This methodology handles each single incoming packet from the server and sends it to it’s corresponding supervisor class. It is a HUGE methodology.
OK, we’re lastly getting someplace, the occasion is unpacked and csSocketData.GetPacketType() known as and relying on the packet kind returned, every packet is dealt with. So what we actually must know proper now’s how are packet sorts encoded? Effectively we will simply have a look at csSocketData.GetPacketType()
Ah, so after being de-obfuscated theres a 4 byte unsigned int that represents the packet kind id after which a construction proper after.
Receiving from the consumer
So after writing some socket boilerplate code (which you’ll learn in main() on the repository) we will lastly attempt studying some packets.
void readPacket(SOCKET sock, uint32_t id, void *buff) {
std::cout << "[READ] ID: " << id << std::endl;
}
void receivePacket(SOCKET sock) {
uint8_t buff[4096];
// very first thing we do is learn the packet dimension into our tmp buffer
int dimension = learn(sock, (buffer_t*)buff, sizeof(uint32_t));
// now learn the packet dimension (this consists of the kind)
uint32_t packetSize = *((uint32_t*)buff);
// now learn the remainder of the packet && deobfuscate it with the default key
// (we'll additionally overwrite the packetSize within the buffer however thats superb bc we already learn it)
dimension = learn(sock, (buffer_t*)buff, packetSize);
CNSocketObfuscation::decryptData(buff, (uint8_t*)CNSocketObfuscation::defaultKey, packetSize);
// now learn the packet ID and ship the pointer to the struct can be and move it to readPacket()
readPacket(sock, *((uint32_t*)buff), buff + sizeof(uint32_t));
}
After compiling and operating our server, we will run our consumer (with loginInfo.php set to 127.0.0.1:8001) and we’ll see a login display screen. After getting into in our crucial login particulars and clicking login our server will politely say
And Ctrl+F’ing our decompiled supply with the corresponding packet kind ID exhibits us
Neat! So it sends a construction with all of the login knowledge to the server, that construction is outlined as
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4, Size = 468)]
public struct sP_CL2LS_REQ_LOGIN
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
public string szID;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
public string szPassword;
[MarshalAs(UnmanagedType.I4)]
public int iClientVerA;
[MarshalAs(UnmanagedType.I4)]
public int iClientVerB;
[MarshalAs(UnmanagedType.I4)]
public int iClientVerC;
[MarshalAs(UnmanagedType.I4)]
public int iLoginType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] szCookie_TEGid;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 255)]
public byte[] szCookie_authid;
}
Rewriting that struct in C++ (abusing the pack preprocessor) we’ll get
#pragma pack(push)
#pragma pack(4)
struct sP_CL2LS_REQ_LOGIN {
char16_t szID[33];
char16_t szPassword[33];
int32_t iClientVerA;
int32_t iClientVerB;
int32_t iClientVerC;
int32_t iLoginType;
uint8_t szCookie_TEGid[64];
uint8_t szCookie_authid[255];
};
#pragma pack(pop)
To learn these Unicode strings I’ll simply use the deprecated codecvt api.
std::string U16toU8(char16_t* src) {
attempt {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>,char16_t> convert;
return convert.to_bytes(src);
} catch(const std::exception& e) {
return "";
}
}
So updating readPacket() to print out login information shall be
void readPacket(SOCKET sock, uint32_t id, void *buff) {
change (id) {
case 301989889: { // sP_CL2LS_REQ_LOGIN
sP_CL2LS_REQ_LOGIN *loginInfo = (sP_CL2LS_REQ_LOGIN*)buff;
std::cout << "[READ] Acquired login request:" << std::endl
<< "Shopper Ver: " << loginInfo->iClientVerA << "." << loginInfo->iClientVerB << "." << loginInfo->iClientVerC << std::endl
<< "Login kind: " << loginInfo->iLoginType << std::endl
<< "ID: " << U16toU8(loginInfo->szID) << std::endl
<< "Password: " << U16toU8(loginInfo->szPassword) << std::endl;
break;
}
default:
std::cout << "[READ] UNKNOWN ID: " << id << std::endl;
}
}
and our login request is now a bit extra readable
Speaking with the consumer
Now that we will learn packets from the consumer, the very last thing to cowl actually is sending packets to the consumer. To do that, we’ll simply need to do the reverse of what we did when studying packets. Allocate a buffer sufficiently big to carry the dimensions, kind and struct, obfuscate the physique (kind & struct) and ship. This methodology will seem like
void sendPacket(SOCKET sock, uint32_t id, void *buff, size_t dimension) {
size_t fullSize = dimension + (sizeof(uint32_t) * 2); // * 2 for the dimensions & id
size_t bodySize = dimension + sizeof(uint32_t);
uint8_t fullPacket[fullSize]; // allocate sufficient for the struct & the packet kind id
uint8_t *bodyPacket = fullPacket + sizeof(uint32_t); // skips the situation of the place the dimensions is
// set the primary 4 bytes of our precise packet (excluding the dimensions) to our packet kind
memcpy(bodyPacket, (void*)(&id), sizeof(uint32_t));
// set the remainder of the packet to our struct
memcpy(bodyPacket + sizeof(uint32_t), buff, dimension);
// encrypt the physique
CNSocketObfuscation::encryptData((uint8_t*)bodyPacket, (uint8_t*)CNSocketObfuscation::defaultKey, bodySize); // encrypts the physique of the packet
// lastly, set the dimensions & ship to the socket
memcpy(fullPacket, (void*)&bodySize, sizeof(uint32_t));
write(sock, fullPacket, fullSize);
}
After sending the login request packet, the consumer expects a response from the server. For our demo I’ll simply have our server ship a login failed response to each login try. First lets discover out what construction is required for that packet. Simply Ctrl+F’ing for “login_fail” gave us this struct.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4, Size = 72)]
public struct sP_LS2CL_REP_LOGIN_FAIL
{
[MarshalAs(UnmanagedType.I4)]
public int iErrorCode;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)]
public string szID;
}
and in C++ appears like
#pragma pack(4)
struct sP_LS2CL_REP_LOGIN_FAIL {
int32_t iErrorCode;
char16_t szID[33];
};
Searching for references to this struct we’ll additionally discover the packet kind id and what every error code corresponds too.
We’ll ignore the szID area because it’s not necessary. So after we obtain a sP_CL2LS_REQ_LOGIN, we simply want to reply with the sP_LS2CL_REP_LOGIN_FAIL packet. After updating our readPacket() operate, it appears like
void readPacket(SOCKET sock, uint32_t id, void *buff) {
change (id) {
case 301989889: { // sP_CL2LS_REQ_LOGIN
sP_CL2LS_REQ_LOGIN *loginInfo = (sP_CL2LS_REQ_LOGIN*)buff;
std::cout << "[READ] Acquired login request:" << std::endl
<< "Shopper Ver: " << loginInfo->iClientVerA << "." << loginInfo->iClientVerB << "." << loginInfo->iClientVerC << std::endl
<< "Login kind: " << loginInfo->iLoginType << std::endl
<< "ID: " << U16toU8(loginInfo->szID) << std::endl
<< "Password: " << U16toU8(loginInfo->szPassword) << std::endl;
sP_LS2CL_REP_LOGIN_FAIL fail;
memset((void*)&fail, 0, sizeof(sP_LS2CL_REP_LOGIN_FAIL)); // zeros out the info
fail.iErrorCode = 6; // consumer model outdated
sendPacket(sock, 553648130, (void*)&fail, sizeof(sP_LS2CL_REP_LOGIN_FAIL)); // ship the packet!
break;
}
default:
std::cout << "[READ] UNKNOWN ID: " << id << std::endl;
}
}
After compiling and operating once more, as soon as we attempt logging in from our consumer, we’re greeted with
And growth! We’ve efficiently acquired and despatched packets to and from the FusionFall consumer! I purposely ignored the important thing switching and such, documentation on that can most likely be written on the OpenFusion wiki. Till subsequent time!