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