Skip to content

Reverse engineering Packet Format(s)

Andrew Gutekanst edited this page Jan 16, 2022 · 8 revisions

This is a reference/guide that covers reverse engineering a DDON packet format (via transferring symbols from an old debug version of the game).

Prerequisites

  1. IDA Pro 7.5 (+HexRays decompiler for x86/x64)
  2. PS4 module loader IDA plugin
  3. Dumped/unpacked version of the PC client (v03.04.007)
  4. Debug PS4 client (v02020005) [2016-12-21]

In addition, this document expects existing knowledge/experience with reverse engineering and IDA.

Setup

IDA analysis -- PS4 client

Special steps are needed to load the DWARF debug symbols for the PS4 version:

  1. Ensure the IDA PS4 module loader plugin is installed.
  2. Load the DDBORBIS.elf file and allow it to fully load and finish auto-analysis.
  3. Go to Edit->Plugins->Load DWARF file and select the DDORBIS.elf file.
    • This will take a long time, even on high-end computers. Be patient.

IDA analysis -- PC client

  1. Open the unpacked PC client with IDA and let the auto-analysis completely finish.
  2. Go to View->Local Types, right click on entry area and select Insert, then paste the following definition:
struct CPacket {
	void *vft;
	unsigned __int8 *data_start;
	unsigned __int8 *data_end;
	unsigned __int8 *read_offset;
	unsigned __int8 *write_offset;
};
  1. Go to the following addresses and rename the functions:
CPacket::ReadInt64 => 0x008BC660
CPacket::Read8byte => 0x008BC6A0
CPacket::Read4byte_ => 0x008BC480
CPacket::Read4byte => 0x008BC4D0
CPacket::ReadUint16 => 0x008BC440
CPacket::ReadByte => 0x008BC410
CPacket::ReadBool => 0x008BC6E0
CPacket::ReadLengthPrefixedMtString => 0x008BC5B0
CPacket::ReadBuffer => 0x008BC750

NOTE: These were what I named the functions in my IDB before the debug symbols were found.

Spend a moment to understand and take note of how these functions work, as you might need to RE many other type-specific reader functions. Using CPacket::Read4byte as an example:

image

The first argument is the class instance this pointer, and should be typed to CPacket*. The second argument is the output pointer:

image

The function gets the pointer to the current data index in the packet reader, swaps the endianness and stores it to the output points, then increments the reader and returns the number of bytes read.

Reverse engineering a format

Finding the packet parser on the PC client

First, take the packet name or ID and find it's handler or writer address on the following document:

https://github.com/Andoryuuta/DDON_RE/blob/master/packet_docs/GamePackets.md

In this example, we will use be reverse engineering S2C_STAGE_GET_STAGE_LIST_RES. The handler address is at 0x7fd9e0 in the PC client.

image

As this is a game server packet (S2C not L2C), this handler is a member method of the cSeedGameSvConnection, so I've called it cSeedGameSvConnection::OnPacket_S2C_STAGE_GET_STAGE_LIST_RES.

The first function argument is the this pointer, second argument is the CPacket* packetReader. Like such:

image

Find the first function call that uses the packetReader within the function. I've renamed it to S2C_STAGE_GET_STAGE_LIST_RES::Read:

image

After renaming and retyping the second argument to CPacket* packetReader, we are finally at the packet parser function.

image

Finding the packet parser on the PS4 client

The PS4 debug client is a much older version of the game, so the packet names have changed a bit. Search for different substrings of the PC packet name in the IDA functions window until you find the packet.

For example, the PC packet name S2C_STAGE_GET_STAGE_LIST_RES does not directly appear in the PS4 binary. However, searching for GET_STAGE_LIST_RES does yield results:

image

Unlike the PC handler, the PS4 handler directly parses the packet (without a sub parser function):

image

NOTE: The PS4's read functions (e.g. nPacket::Read_0, nPacket::Read_1, nPacket::Read_...) do not include the type or byte size in the name. These are auto-generated names from somewhere during the build process for this generic/templated C++ read function. I recommend renaming these functions to their correct types as determined by their second argument type.

For example, nPacket::Read_0 has an output pointer type of u16, so I will be renaming this to nPacket::Read_u16.

image

Comparing the packet parsers and transferring debug symbols

Now that we have the packet parser code located in both the PC and PS4 client, we can begin to compare the two and transfer names.

Right away, we can see that that there are three function calls that take our packet reader:

image

Based on the number of function calls that use the packet reader (and order), we can make an educated guess and copy the name of the nPacket::Read_CDataStageInfo_ function over to our PC client.

However, when we compare the first two fields there is a difference in size:

  • PC:
    • 4 byte
    • 4 byte
  • PS4:
    • 2 byte (uint16)
    • 1 byte (uint8)

This discrepancy is due to change in the network protocol over the years. These two fields are parsed at the start of every packet from server -> client, and represent error and result (some other functions have these properly named on the PS4 client). At some point in the past few years, these fields were updated to both being uint32 (as we see on the PC client).

Now that we have the first two fields reconciled, we can look deeper into the nPacket::Read_CDataStageInfo_ function. This function takes in a MtTypedArray, which means it's a list of serialized objects.

image

It is standard for these MtTypedArray serializers to start with a uint32 field containing the count of objects in the list/array:

image

Comparing further into the function, we find that the PC version only has 1 function call using the packetReader, while the PS4 version has 6 calls:

image

This is due to function inlining. The single function call on the PC client has been directly expanded into the function on the PS4 version. If we go into the call on the PC client and compare again, we see code that is much more similar in nature:

image

We now know that the PC function (sub_77A130) is used to read a single CDataStageInfo entity, and name it as such (CPacket::Read_CDataStageInfo) .

On the PS4 side, we now want to get symbols to determine what these fields actually represent. To do so, we simply need to change the type of the allocated CDataStageInfo memory. Click on the output variable of the operator_new call, press the y hotkey and enter the type CDataStageInfo*:

image

After the appropriate type is set, we can now see each field name:

image

Then we can copy over the field & function names as appropriate (again -- educated guesses based on the datatype sizes and order. If a field was added, removed, or had it's data type changed, these would not align perfectly like this).

image

Now that we have the fields for the CDataStageInfo figured out, we repeat the same process for the sub-entity CDataStageAttribute: image Oh no! We've run into an issue, the PC version has 11 bools, while the PS4 version only had 9. How do we know which field is which? We cant. This is one of the many scenarios in which something has changed in the netcode, but we cannot easily transfer symbols between the two. For now, we will just note that the PC version has 11 bools, and document the possible names from the PS4 client. Later on, we can manually try each bool and figure out why functionality it causes the client to take, and work from there.

Bringing it all together.

After transferring the symbols (where possible) from the PS4 client, we can now note down the exact format (field sizes and order) for the PC client:

typedef struct {
	uint32 Error;
	uint32 Result;
	List<CDataStageInfo>  StageInfoList;
} S2C_STAGE_GET_STAGE_LIST_RES;

typedef struct {
	uint32 ID;
	uint32 StageNo;
	uint32 RandomStageGroupID;
	uint32 Type;
	CDataStageAttribute StageAttribute;
	bool IsAutoSetBloodEnemy;
} CDataStageInfo;

typedef struct {
	/*
	Changed since PS4, unable to know where bools were added in the struct.
	Possible names:
		IsSolo
		IsEnablePartyFunc 
		IsAdventureCountKeep 
		IsEnableCraft 
		IsEnableStorage 
		IsEnableStorageInCharge 
		IsNotSessionReturn 
		IsEnableBaggage 
		IsClanBase
	*/
	bool unk0;
	bool unk1;
	bool unk2;
	bool unk3;
	bool unk4;
	bool unk5;
	bool unk6;
	bool unk7;
	bool unk8;
	bool unk9;
	bool unk10;
} CDataStageAttribute;

Each of these structs should then be converted into a Entity serializer struct/class at Arrowgene.Ddon.Shared/Entity/Structure and then registered within Arrowgene.Ddon.Shared/Entity/EntitySerializer.cs.