Tasks

Each instruction sent over from the server is referred to as a 'task'. Implants receive tasks, interpret and execute them, and return the response.

During development, each task must be assigned a numeric opcode, to discourage the sending of text-based task names over the connection. As these opcodes are specified by the developer, an implant should have a built-in mechanism to efficiently route tasks to their controller. This will be referred to as 'task management'

Task manager

Here is a simple, single-threaded implementation of a task manager from Empress.

package c2

import (
	"github.com/pygrum/Empress/config"
	"github.com/pygrum/Empress/transport"
	"strconv"
)

type Handler func(*transport.Request, *transport.Response)

type Router struct {
	handlers map[int32]Handler
}

func NewRouter() *Router {
	return &Router{
		handlers: make(map[int32]Handler),
	}
}

func (r *Router) HandleFunc(opcode int32, handler Handler) {
	r.handlers[opcode] = handler
}

func (r *Router) handle(req *transport.Request) *transport.Response {
	resp := &transport.Response{
		AgentID:   config.C.AgentID,
		RequestID: req.RequestID,
	}
	handleFunc, ok := r.handlers[req.Opcode]
	if !ok {
		resp.Responses = append(resp.Responses, transport.ResponseDetail{
			Status: transport.StatusErrorWithMessage,
			Dest:   transport.DestStdout,
			Data:   []byte("unknown opcode " + strconv.Itoa(int(req.Opcode))),
		})
		return resp
	}
	handleFunc(req, resp)
	return resp
}

The router has various callback functions registered for each opcode received from the C2. This means that once a client receives a server request (transport.Request), it can simply call the handle function on the router, which will send the request to the appropriate handler and return the response.

// c is our HTTP(S) client in this case
taskResp := c.router.handle(taskReq)
c.SetResponse(taskResp)
// will loop and send response on the next connection (long polling)
return nil, nil

Now, whenever a new function is created, the developer simply needs to register the function with the client's internal router. Here is an example of integrating the function ls .

func CmdLS(req *transport.Request, response *transport.Response) {
	if len(req.Args) == 0 {
		req.Args = append(req.Args, []byte("."))
	}
	for _, path := range req.Args {
		entries, err := os.ReadDir(string(path))
		if err != nil {
			transport.ResponseWithError(response, err)
			return
		}
		var b bytes.Buffer
		w := tabwriter.NewWriter(&b, 1, 1, 2, ' ', 0)
		rowFmt := "%v\t%s\t%d\t%s\t%s\t\n"
		for _, d := range entries {
			var t = "file"
			if d.IsDir() {
				t = "dir"
			}
			info, err := d.Info()
			if err != nil {
				transport.ResponseWithError(response, err)
				_, _ = fmt.Fprintf(w, rowFmt,
					"--", t, 0, "--", d.Name())
				continue
			}
			// permissions, type (dir/file), size, last modified, name
			_, _ = fmt.Fprintf(w, rowFmt,
				info.Mode(), t, info.Size(), info.ModTime().Format(time.DateTime+" MST"), d.Name())
		}
		_ = w.Flush()
		transport.AddResponse(response, transport.ResponseDetail{
			Status: transport.StatusSuccess,
			Dest:   transport.DestStdout,
			Data:   b.Bytes(),
		})
	}
	return
}

You may notice that CmdLS has the function signature of a router callback. So we can simply go ahead and register it as one of the HandleFunc:

r.HandleFunc(opLs, fs.CmdLS)

The server integration page goes into more detail about how registration and polling objects are structured.

Last updated