Gameserver

05/07/2016 05:56 4D1#1
posted this on another forum a long time ago but it didnt gain any traction so maybe people here will have use

Quote:
I decided to meddle around into guild wars 2 and found that their client/server architecture is roughly the same as the first game so, made for easy reverse engineering work :D

Code:
 template <typename T>
class gw_array
{
protected:
	T* arr_;
	DWORD allocatedsize_;
	DWORD currentsize_;
	DWORD unk_;
public:
	inline T& operator[](DWORD index) { return arr_[index]; }
	inline const DWORD& size() const { return currentsize_; }
};

using CtoSSend_t = void(__fastcall*)(void* thisptr, DWORD size, BYTE* packet);

template <typename packet>
using StoCHandler = bool(__fastcall*)(DWORD unk,packet* pak);

struct StoCPacketbase
{
	WORD header;
};

struct CtoSTableIndex 
{
	DWORD paramcount;
	DWORD* params;
};

template <typename packet>
struct StoCTableIndex
{
	DWORD paramcount;
	DWORD* params;
	DWORD unk;
	StoCHandler<packet> handler;
};


class GameServer {
	GameServer(){}
	GameServer(GameServer&){}
public:

	BYTE pad1[0xC];
	struct {
		BYTE pad1[0x2C];
		gw_array<CtoSTableIndex> ctos_table;
		DWORD unk1;
		gw_array<StoCTableIndex<StoCPacketbase>> stoc_table;
	} *consts;

};

GWCA::Hook ctos_log;
CtoSSend_t ctos_trampoline;
ConsoleEx* con = nullptr;

void __fastcall ctos_detour(GameServer* thisptr, DWORD size, CtoGS::Packet* packet)
{
	CtoGS::P010_UpdatePlayerPosition* spec;
	switch (packet->header){
	default:
		con->printf("<CtoS> Sz: %X Header: %X\n", size, *(WORD*)packet);
		for (DWORD i = 0; i < size; ++i){
			if (i % 16 == 0) con->printf("\n");
			con->printf("%02X ", ((BYTE*)packet)[i]);
		}
		con->printf("\n --------------------- \n");
		break;
	}
	return ctos_trampoline(thisptr, size, (BYTE*)packet);
}

void WINAPI init_consoleex(HMODULE module)
{
	con = ConsoleEx::GenerateLogger(module, "[CtoGS] Packet Log", CONSOLEEX_PARAMETERS(80, 20, 500, true));
	GWCA::PatternScanner scan;
	BYTE* ctos_hook = (BYTE*)scan.FindPattern("\x55\x8B\xEC\x83\xEC\x2C\x53\x56\x57\x8B\xF1\x8B\xFA", "xxxxxxxxxxxxx", 0);
	if (ctos_hook){
		con->printf("ctos = %X\n", ctos_hook);
		ctos_trampoline = (CtoSSend_t)ctos_log.Detour(ctos_hook, (BYTE*)ctos_detour, 6);
	}
	else{
		con->printf("ctos = ERR");
	}

	while (con->isOpen()) Sleep(100);

	ctos_log.Retour();
	delete con;
	FreeLibraryAndExitThread(module, EXIT_SUCCESS);
}

BOOL WINAPI DllMain(HMODULE hDllHandle, DWORD dwReason, LPVOID lpreserved)
{
	if (dwReason == DLL_PROCESS_ATTACH){
		DisableThreadLibraryCalls(hDllHandle);
		CreateThread(0, 0, (LPTHREAD_START_ROUTINE)init_consoleex, hDllHandle, 0, 0);
	}
	return TRUE;
}

The function hooked is the outgoing packet sender for the gameserver, this is before encryption so you can see the juicy details. It is an object method so ECX is the thisptr. In this object using what is provided, you can find a table holding handlers to each incoming packet handler. Along with encodings for parameters to expect in the packet. The encodings also exist for the ctos packets in a table above.

You can hook the stoc handlers using a simple vmt-like swap if desired.

GWCA::PatternScanner is a simple pattern scanner with wildcard support, nothing fancy, can use your own. (Make sure you start the scan from the module base, it changes)

GWCA::Hook is just a wrapper for a JMP hook w/ trampoline, so again, just use your own.

ConsoleEx can be found in the hacklib project [Only registered and activated users can see links. Click Here To Register...].

Have fun, would love to see some documentation on the ctos packets :D.

Also note, there are references to the thisptr in static mem, so you can retrieve it and emulate calls to the sender for hacks/bots if one were so inclined.
05/08/2016 14:44 LasseK#2
too high for me ^^
07/11/2016 22:57 int_#3
GW2 has such hard packets :(
if packet contains string then function ctos_detour receive in CtoGS::Packet* packet something like that <prefix><some unknown bytes><pointer to string in memory>
the same situation with CS skill packets(coordinates), SC NPC shop(prices), CS\SC chat(strings)
07/13/2016 14:20 Agedyn#4
Quote:
Originally Posted by int_ View Post
GW2 has such hard packets :(
if packet contains string then function ctos_detour receive in CtoGS::Packet* packet something like that <prefix><some unknown bytes><pointer to string in memory>
the same situation with CS skill packets(coordinates), SC NPC shop(prices), CS\SC chat(strings)
You are capturing unpacked version of the message before it enters the message queue. If you want to capture the packed version hook the sender function before call to RC4Crypt. Pointer to the packet should be in the EBX register.
07/16/2016 19:54 int_#5
Agedyn, thanks
RC4 is called for composite packets, which may contain several messages. To analyze these packages difficult, because you must correctly parse each component of its package.
I found a place where you can take outgoing packets prepared for sending ;)

Unfortunately, I do not know what to do with incoming packets, because they are compressed. You do not know what kind of compression algorithm used for incoming packets? I found the function of decompressing the data, but it is recursive, and dynamically allocates memory for uncompressed data, me too complicated to understand it :(
07/16/2016 20:38 Agedyn#6
Quote:
Originally Posted by int_ View Post
Agedyn, thanks
RC4 is called for composite packets, which may contain several messages. To analyze these packages difficult, because you must correctly parse each component of its package.
I found a place where you can take outgoing packets prepared for sending ;)

Unfortunately, I do not know what to do with incoming packets, because they are compressed. You do not know what kind of compression algorithm used for incoming packets? I found the function of decompressing the data, but it is recursive, and dynamically allocates memory for uncompressed data, me too complicated to understand it :(
Received messages are encrypted using RC4. They use the same key from sending and receiving.
LZ4 or LZMA is probably used for decompression (0xF5AF70).

Edit: LZ4 is the algorithm used for decompression. I found this by checking THIRDPARTYSOFTWAREREADME.txt for used compression algorithms (I found LZ4, LZMA and zlib) and then googling their working principle. The compression algorithm seemed similar to LZ4, so I tried to decompress packet using it and it worked just fine. LZ4 header starts at 4th byte.
Edit: First short is compressed size and second is uncompressed size.

Some messages contain compressed long fields. The size of these fields can be anywhere between 1 to 4 byte. uncompressLong is located at 0xDBA710. 0xDB9E50 is the compressLong function. .

As I'm working on a 'fake' client using NodeJS I have converted these algorithms to JavaScript.

Code:
    static compressLong(value, unk) {
        let compressed = 0;
        if (unk) {
            value &= -((value & 0x40000000) != 0);
        }
        let i = 0;
        do
        {
            let compressedByte = value & 0x7F;
            value >>= 7;
            if (value) {
                compressedByte |= 0x80;
            }
            compressed |= compressedByte << (i * 8);
            i++;
        } while(value);

        return compressed;
    }

    static uncompressLong(value) {
        let uncompressed = 0;
        let i = 0;
        let outSize = 0;
        for (; i < 4; ++i) {
            let byte = (value >> i * 8) & 0xFF;
            byte = ~(byte >>= 7);
            ++outSize;

            if ((byte & 1) != 0) {
                break;
            }
        }

        for (; i >= 0; --i) {
            let byte = (value >> i * 8) & 0xFF;
            uncompressed = uncompressed << 7 | byte & 0x7F;
        }
        return {value: uncompressed, size: outSize};
    }
0xDBA0C0 is used for unpacking message into a struct and then it's passed to dispatch function (0xDB87AD).

Here are ids for message fields:

Code:
# snippet from my python message dumper (may not be 100 % correct)
net_fields_send = {
    'MP_END': 0,
    'MP_MSGID': 1,
    'MP_BYTE': 2,
    'MP_SHORT': 3,
    'MP_COMPRESSED_LONG': 4,
    'MP_ALIGNED_LONGLONG': 5,
    'MP_FLOAT': 6,
    'MP_VEC2': 7,
    'MP_VEC3': 8,
    'MP_VEC4': 9,
    'MP_16BYTES_2': 10,
    'MP_16BYTES': 11,
    'MP_28BYTES': 12,
    'MP_STRING': 13,
    'MP_CSTRING': 14,
    'MP_OPTIONAL': 15,
    'MP_ARRAY_FIXED': 16, # message specific size
    'MP_ARRAY_SMALL': 17,
    'MP_ARRAY_LARGE': 18,
    'MP_BUFFER_FIXED': 19, # message specific size
    'MP_BUFFER_SMALL': 20,
    'MP_BUFFER_LARGE': 21,
    'MP_SRV_ALIGN': 22,
    'MP_SRV_END': 24
}

# gw2 unpack function decreases field id by one. Does not have MP_END field.
net_fields_recv = {
    'MP_MSGID': 1,
    'MP_BYTE': 2,
    'MP_SHORT': 3,
    'MP_COMPRESSED_LONG': 4,
    'MP_ALIGNED_LONGLONG': 5,
    'MP_FLOAT': 6,
    'MP_VEC2': 7,
    'MP_VEC3': 8,
    'MP_VEC4': 9,
    'MP_16BYTES_2': 10,
    'MP_16BYTES': 11,
    'MP_28BYTES': 12,
    'MP_STRING': 13,
    'MP_CSTRING': 14,
    'MP_OPTIONAL': 15,
    'MP_ARRAY_FIXED': 16, # message specific size
    'MP_ARRAY_SMALL': 17,
    'MP_ARRAY_LARGE': 18,
    'MP_BUFFER_FIXED': 19, # message specific size
    'MP_BUFFER_SMALL': 20,
    'MP_BUFFER_LARGE': 21,
    'MP_SRV_ALIGN': 22,
    'MP_SRV_END': 24
}
All addresses are for 63845 32-bit build

Edit: Renamed one net field.
07/28/2016 10:35 Agedyn#7
Updated compression to handle 4 byte values.

Code:
    static compressLong(value, unk) {
        let compressed = [];

        if (unk) {
            value &= -((value & 0x40000000) != 0);
        }
        let i = 0;
        do
        {
            let b = value & 0x7F; // get all bits except most significant
            value >>= 7;

            let bitsShifted = 7 * (i + 1);

            // check if maximum size exceeded
            if (bitsShifted > 35) {
                break;
            }

            if (bitsShifted == 35) {
                b &= 0x0F; // 4 most significant bits are overflow, so discard them
            } else if (value != 0) { // if not the last byte, set MSB to indicate continuation
                b |= 0x80;
            }
            compressed.push(b);
            i++;
        } while(value != 0);
        return compressed;
    }

    static uncompressLong(buffer) {
        let uncompressed = 0;
        let outSize = 0;

        for (let i = 0; i < buffer.length; ++i) {
            let b = buffer[i];
            uncompressed |= (b & 0x7F) << (i * 7);
            outSize++;

            // check if MSB indicates end
            if ((b & 0x80) == 0) {
                break;
            }
        }
        return {value: uncompressed >>> 0, size: outSize};
    }