Introduction

In the recent releases of Havoc Pro and Cobalt Strike, a few new features caught my attention and found them really cool. The first one was Havoc Pro’s Runtime Channel Switching. It’s built into the Kaine-kit extension and allows the operator to reconfigure implant listener configuration or switch between different listeners at runtime.

A feature has been introduced to kaine that allows operators to dynamically update a session’s listener configuration at runtime. Additionally, sessions can switch between entirely different listener protocols at runtime, such as transitioning from HTTP to SMB or vice versa, regardless of whether the listener is configured as P2P or Direct.Havoc Professional Release

The second one was Cobalt Strike’s UDC2 (User-Defined Command and Control). It allows the operator to develop custom C2 channels by implementing their communication logic directly into a BOF (Beacon Object File). This BOF is then patched into beacon payload.

… UDC2 addresses these issues by enabling Cobalt Strike users to develop custom C2 channels as BOFs. The UDC2 BOF is patched in on payload creation and is invoked by Beacon to proxy out all its traffic over the custom channel. This makes it possible to combine custom C2 channels with custom UDRLs/transformations.  — Cobalt Strike 4.12: Fix Up, Look Sharp!

Building on these foundations, my goal from building TinyC2 is to study and reimplement these features.

Architecture

  • Plugin Management: TinyC2 supports dynamically loading Go plugins at runtime to extend TinyC2 functionality. Currently, TinyC2 only support listener plugins.
  • Listener Management: This component handles the listeners, allowing the user to start or stop them.
  • Implant Management: This component responsible for managing implant sessions and allow the user to interact with each implant session with commands that are supported by the implant. also its responsible for generating implant payload (exe at this moment)

c2-architecture

Bring Your Own Listener

TinyC2 supports adding custom listener through a flexible plugin architecture.

To create a custom listener, your plugin must implement the following IPlugin interface:

type IPlugin interface {
	Initialize(engine sdk.IEngine)
	Meta() map[string]string
	NewAdapter() sdk.IAdapterListener
}
  • Initialize: Called automatically when the plugin is initialized.
  • Meta: Returns basic plugin metadata, such as the plugin’s name and type.
  • NewAdapter: Returns the listener adapter that the Listener Manager will use to manage the listener.

The listener adapter returned by NewAdapter must then implement the IAdapterListener interface:

type IAdapterListener interface {
	Start(name string, config string) error
	Stop() error
	Config() map[string]any
	Extension() ([]byte, error)
}
  • Start: Starts the listener using the desired configuration (passed as a YAML file).
  • Stop: Stop the listener.
  • Config: Returns the listener’s current configuration.
  • Extension: Returns an implant extension as Position Independent Code (PIC), which gets built using the Crystal Palace linker.

Implant Extension

The extension (PIC) returned by the Extension method should follow this C template. This template establishes the protocol initialization, data transmission, and cleanup on the implant side:

BOOL ProtocolSend(CHANNEL_CONTEXT* Context, CONST CHAR* Data, DWORD Size, BOOL Register) {
	return TRUE;
}

BOOL ProtocolReceive(CHANNEL_CONTEXT* Context, CHAR** Data, DWORD* Size) {
	return TRUE;
}

BOOL ProtocolInitialize(CHANNEL_CONTEXT* Context) {
	return TRUE;
}

BOOL ProtocolCleanup(CHANNEL_CONTEXT* Context) {
	return TRUE;
}

char __CONFIG__[0] __attribute__((section("config")));
char * findAppendedConfig() {
    return (char *)&__CONFIG__;
}

BOOL go(IImplant* Implant, IChannel* Channel) {
    datap            Parser;
    _RESOURCE*       Config  = (_RESOURCE *)findAppendedConfig();
    
	Channel->Context = malloc(sizeof(CHANNEL_CONTEXT));
	if (Channel->Context == NULL) {
		return FALSE;
	}

	Implant->BeaconDataParse( &Parser, Config->value, Config->length );

	Channel->ID 		= Implant->BeaconDataInt(&Parser);
	Channel->Initialize = ProtocolInitialize;
	Channel->Send 		= ProtocolSend;
	Channel->Receive 	= ProtocolReceive;
	Channel->Cleanup 	= ProtocolCleanup;
	
	return TRUE;
}

Credits