Skip to content

TLV protocol

The Type-Length-Value (TLV) protocol serves as the cornerstone of internal communication within the Tuoni framework. It is a versatile and structured format that facilitates the exchange of information between the agent and the Command and Control (C2) system, as well as among the internal components of the agent itself. The TLV protocol is specifically designed to support hierarchical data structures, making it well-suited for complex communication scenarios.

Communication between the agent and the C2 is encapsulated within an outer protocol, which is implemented by the listener plugin and its corresponding shellcode. This encapsulation ensures that the TLV protocol can operate seamlessly across different network environments and security contexts, by leveraging the outer protocol for transmission.

Furthermore, the TLV protocol is not limited to just the agent-C2 interaction. It is equally integral to the internal workings of the agent. Agent components communicate with each other and with plugin shellcodes using the TLV structure. This includes the transmission of configuration parameters and the relay of result data back to the agent core or other relevant parts of the system.

Plugins and their shellcodes, provided by ShellDot, also employ the TLV structure for sending configuration data and receiving results. This uniformity in communication protocol across the system simplifies the development and integration of new plugins and shellcodes, as they adhere to a common standard for data exchange.

Protocol general specifications

The TLV (Type-Length-Value) structure, a fundamental component of the communication protocol within the Tuoni framework, comprises three distinct parts, each serving a specific purpose in the encoding and interpretation of data:

  • Type: This segment is represented by a 1-byte value. Within this byte, the most significant bit is used to indicate the nature of the TLV structure. Specifically, if this bit is set to 0x80 (in hexadecimal) or 10000000 (in binary), the TLV is identified as a parent type. This designation means that the TLV can contain other TLV structures within it. The remaining 7 bits of this byte are allocated for the ID value, which is contextual and determined by the parser handling the TLV. For instance, an ID of 0x92 could signify different data or commands in varying contexts; however, if its most significant bit is set, it unequivocally identifies the structure as a parent type, capable of encapsulating other TLVs.
  • Length: Following the type portion is the length segment, which is a 4-byte integer formatted in little-endian order. This design choice allows for a substantial maximum size for the value part, up to 4GB. The length specifies the size of the subsequent value segment, enabling the parser to accurately extract and process the contained data.
  • Value: The final part of the TLV structure is the value itself, whose size is explicitly defined by the length part mentioned earlier. This segment contains the actual data or information that the TLV is meant to convey. The nature and format of this data can vary widely, depending on the type of TLV and the specific application or context in which it is used.

Tuoni Type name reference

  • EMPTY - empty value
  • BYTE - one byte value
  • INT16 - two byte integer
  • INT32 - four byte integer
  • GUID - 16 byte GUID/UUID
  • STR_UTF8 - UTF8 string (by default TLV should use UTF8 strings only, it's not enforced in any way, you can use any encoding if both sides agree, but it's nice to do it in a standardized way)
  • BLOB - binary blob
  • UNDEFINED - Not required to be some exact type so can change according to context

Arrays

In the TLV (Type-Length-Value) structure, a parent element can contain multiple child elements that share the same type value. When this occurs, these child elements are treated as an array within the context of the parent TLV. This interpretation aligns with the flexible and hierarchical nature of the TLV protocol, allowing for a rich representation of data structures.

This concept essentially means that each type value, even when appearing singularly, can be considered as an array with a single element. This approach simplifies the handling of TLV structures, as it standardizes the way data is parsed and accessed, regardless of the number of elements present. It ensures consistency in data structure interpretation, facilitating the development of parsing logic that can elegantly handle both singular and multiple occurrences of the same type value within a parent TLV.

Example

One parent (id 0x12) with two child (ids 0x1 and 0x2) values

  • TYPE = 0x12 PARENT VALUE = contains 2 other values, so it's a parent type
    • TYPE = 0x1 VALUE = ascii string "ABC"
    • TYPE = 0x2 VALUE = 4 byte integer 0x12345678 in little endian
TYPE + ID LENGTH (SubElements) TYPE - First Child LENGTH - First Child VALUE - First Child TYPE - Second Child Length - Second Child VALUE - Second Child
92 11 00 00 00 01 03 00 00 00 41 42 43 02 04 00 00 00 78 56 34 12

External references