An agent package defines the cryptographic protocol, wire format, capability declarations, and build pipeline for a class of agents.
Components
#Each agent package provides four components:
- CryptoProvider — key exchange, session key derivation, encryption/decryption
- ProtocolCodec — encoding/decoding between internal format and wire format
- AgentPackage — ties them together with magic bytes, build templates, and capability declarations
- Capability declarations — module formats accepted, built-in commands, and agent capabilities
CryptoProvider
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| from tantoc2.server.crypto_provider import CryptoProviderBase, CryptoSession
class MyCryptoProvider(CryptoProviderBase):
@classmethod
def plugin_name(cls) -> str:
return "my_crypto"
def generate_keypair(self) -> tuple[bytes, bytes]:
"""Generate a server-side keypair. Returns (public_key, private_key)."""
...
def create_session(
self, registration_data: bytes, server_private_key: bytes
) -> tuple[CryptoSession, bytes]:
"""Process a registration request.
Called when magic bytes match and session_token is all-zeros.
Args:
registration_data: Raw registration payload from the agent.
server_private_key: The server's private key for this agent package.
Returns:
A tuple of (new CryptoSession, response bytes to send back).
"""
...
def decrypt(self, session: CryptoSession, data: bytes) -> bytes:
"""Decrypt inbound agent data using session keys."""
...
def encrypt(self, session: CryptoSession, data: bytes) -> bytes:
"""Encrypt outbound data for the agent."""
...
def complete_handshake(self, session: CryptoSession, data: bytes) -> CryptoSession:
"""Finalize a multi-step key exchange (e.g., ECDH).
Returns an updated session with state "established"."""
...
def rotate_session_key(self, session: CryptoSession) -> tuple[CryptoSession, bytes]:
"""Rotate the session key.
Returns (updated CryptoSession, rotation message bytes for the agent)."""
...
|
CryptoSession is a dataclass holding per-agent session state:
1
2
3
4
5
| @dataclass
class CryptoSession:
session_token: bytes # 16-byte token assigned at registration
state: str = "handshake" # "handshake" or "established"
session_data: dict[str, Any] = field(default_factory=dict) # provider-specific data
|
ProtocolCodec
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from tantoc2.server.protocol_codec import ProtocolCodecBase
from tantoc2.server.messages import InternalMessage
class MyProtocolCodec(ProtocolCodecBase):
@classmethod
def plugin_name(cls) -> str:
return "my_codec"
def decode(self, data: bytes) -> InternalMessage:
"""Decode decrypted bytes into an InternalMessage."""
...
def encode(self, message: InternalMessage) -> bytes:
"""Encode an InternalMessage into bytes for encryption."""
...
|
InternalMessage is the canonical internal message representation:
1
2
3
4
5
6
7
8
| @dataclass
class InternalMessage:
msg_type: MessageType
agent_id: str | None = None
engagement_id: str | None = None
payload: dict[str, Any] = field(default_factory=dict)
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
metadata: dict[str, Any] = field(default_factory=dict)
|
AgentPackage
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| from tantoc2.server.agent_package import AgentPackageBase, AgentCapabilities
class MyAgentPackage(AgentPackageBase):
@classmethod
def plugin_name(cls) -> str:
return "my_agent"
@classmethod
def magic_bytes(cls) -> bytes:
"""4-byte magic identifier. Must be unique across all packages."""
return b"\xca\xfe\xba\xbe"
@classmethod
def crypto_provider_name(cls) -> str:
"""Return the name of the CryptoProvider plugin this agent uses."""
return "my_crypto"
@classmethod
def protocol_codec_name(cls) -> str:
"""Return the name of the ProtocolCodec plugin this agent uses."""
return "my_codec"
@classmethod
def supported_module_formats(cls) -> list[str]:
"""Module formats this agent can load (e.g., ['bof', 'shellcode']).
Return empty list if agent does not support module loading."""
return ["bof", "shellcode"]
@classmethod
def built_in_commands(cls) -> list[str]:
"""Commands the agent natively supports."""
return ["survey", "upload", "download", "persist", "unpersist",
"beacon_config", "kill", "load_module", "unload_module"]
@classmethod
def supports_daemonize(cls) -> bool:
"""Whether this agent supports daemonized module loading."""
return True
@classmethod
def supports_relay(cls) -> bool:
"""Whether this agent supports acting as a P2P relay."""
return False
@classmethod
def capabilities(cls) -> AgentCapabilities:
"""Capability metadata for UI/CLI presentation and filtering.
Default implementation aggregates the other capability methods."""
return AgentCapabilities(
module_formats=cls.supported_module_formats(),
built_in_commands=cls.built_in_commands(),
supports_daemonize=cls.supports_daemonize(),
supports_relay=cls.supports_relay(),
)
|
Note that crypto_provider_name() and protocol_codec_name() are @classmethod methods returning the string name of the corresponding plugin, not instances. The teamserver looks up the actual CryptoProvider and ProtocolCodec from the plugin registry using these names.
Build Interface
#Buildable agent packages additionally override the build methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| from tantoc2.server.agent_package import (
AgentPackageBase,
AgentTemplate,
BuildConfig,
CryptoMaterial,
)
class MyBuildablePackage(AgentPackageBase):
# ... magic_bytes, plugin_name, crypto/codec names ...
@classmethod
def is_buildable(cls) -> bool:
return True
@classmethod
def get_templates(cls) -> list[AgentTemplate]:
return [
AgentTemplate(
name="my_beacon",
platform="linux",
arch="x86_64",
format="elf",
description="Linux ELF beacon agent",
),
]
@classmethod
def get_config_schema(cls) -> dict[str, OptionSchema]:
"""Return config schema for build options."""
return { ... }
@classmethod
def stamp(
cls,
template_name: str,
config: BuildConfig,
crypto_material: CryptoMaterial,
) -> bytes:
"""Stamp configuration and crypto material into an agent binary.
Returns the built agent binary as bytes."""
...
|
Supporting dataclasses:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| @dataclass
class AgentTemplate:
name: str # e.g. "dev_beacon"
platform: str # "linux", "windows", "python"
arch: str # "x86_64", "any"
format: str # "py", "exe", "dll", "elf", "shellcode"
description: str = ""
@dataclass
class BuildConfig:
callbacks: list[CallbackAddress]
kill_date: datetime
beacon_interval: int = 60
beacon_jitter: int = 10
extra: dict[str, Any] = field(default_factory=dict)
@dataclass
class CryptoMaterial:
public_key: bytes
private_key: bytes
server_public_key: bytes
@dataclass
class CallbackAddress:
host: str
port: int
protocol: str = "https"
|
Capability Declarations
#Agent packages declare their capabilities so the teamserver can:
- Filter agent modules — only offer modules whose format matches
supported_module_formats() - Present available commands — show operators which commands the agent supports
- Validate module loading — reject
load_module requests for incompatible formats - Gate daemonize requests — reject
daemonize=true if the agent doesn’t support it
Agents with no module loading capability return an empty list from supported_module_formats(). The teamserver will not offer any agent modules for those agents.
Wire Protocol
#All agent packages use this header structure:
1
2
3
4
5
| +--------+--------+------------------+
| Magic | Session| Payload |
| 4 bytes| Token | (variable) |
| | 16 bytes| |
+--------+--------+------------------+
|
- Magic bytes (4 bytes): Identifies the agent package. The pipeline routes to the correct CryptoProvider and ProtocolCodec.
- Session token (16 bytes): Assigned during registration. All zeros for registration messages.
- Payload: CryptoProvider-encrypted, ProtocolCodec-encoded data.
Registration Flow
#
sequenceDiagram
participant Agent
participant Server
Agent->>Server: magic + null_token + agent_public_key + codec(register_msg)
Server->>Server: Generate keypair, derive session key, assign token
Server->>Agent: magic + session_token + server_public_key + codec(register_response)
Note over Agent,Server: Both sides now have the session key
Agent->>Server: magic + session_token + encrypt(codec(checkin_msg))
Server->>Agent: magic + session_token + encrypt(codec(tasks))
Message Types
#| Type | Direction | Description |
|---|
register | Agent -> Server | Initial registration with metadata and capabilities |
checkin | Agent -> Server | Periodic check-in, receives queued tasks |
task_result | Agent -> Server | Completed task result |
task_assignment | Server -> Agent | Task assignments (including module payloads) |
beacon_config | Server -> Agent | Beacon configuration update |
kill | Server -> Agent | Kill the agent |
survey_result | Agent -> Server | System metadata survey data |
key_rotation_request | Server -> Agent | Request session key rotation |
key_rotation_response | Agent -> Server | Acknowledge key rotation |
relay_forward | Both | Relay traffic through a P2P agent |
plugin_message | Both | Plugin-defined message type |
Dev Agent Capabilities
#The built-in dev agent package declares:
| Capability | Value |
|---|
| Plugin name | dev_agent |
| Magic bytes | \xde\xad\xc2\x01 |
| CryptoProvider | dev_crypto (ECDH + AES-256-GCM) |
| ProtocolCodec | dev_codec (JSON + zlib) |
| Module formats | ["py"] |
| Built-in commands | ls, cat, pwd, cd, whoami, env, ps, netstat, upload, download, load_module, unload_module |
| Supports daemonize | Yes |
| Supports relay | Yes |
| Templates | dev_beacon (Python beacon for testing), dev_session (Python session mode for testing) |
Dev Agent Crypto
#The built-in dev agent package uses:
| Component | Implementation |
|---|
| Key exchange | ECDH with P-256 (secp256r1) |
| Key derivation | HKDF-SHA256 with info b"tantoc2-dev-agent-session" |
| Encryption | AES-256-GCM with 12-byte random nonces |
| Wire format | [4B counter][12B nonce][ciphertext + GCM tag] |
| Codec | JSON + zlib compression |
| Anti-replay | Monotonic counter in encrypted payload |
Deployment
#Place the package in plugins/agent_packages/my_agent.py. It is discovered on the next server start. Alternatively, install the package as a Python package with an entry point in the tantoc2.agent_packages group.