diff --git a/README.md b/README.md index 95393be..7e80a6b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ rather than trying to route out over the internet. * [HOTCROISSANT](docs/hotcroissant.md) * [MATA](docs/mata.md) * [ObliqueRAT](docs/obliquerat.md) +* [RedXOR](docs/redxor.md) * [Rifdoor](docs/rifdoor.md) * [SLICKSHOES](docs/slickshoes.md) * [Yort](docs/yort.md) diff --git a/docs/redxor.md b/docs/redxor.md new file mode 100644 index 0000000..08515d5 --- /dev/null +++ b/docs/redxor.md @@ -0,0 +1,22 @@ +# RedXOR + +RedXOR is a backdoor targetting Linux systems. It masquerades as the [polkitd](https://linux.die.net/man/8/polkitd) daemon. Based on victims as well as Tactics, Techniques, and Procedures (TTPs), it is believed to be attributed to a high profile Chinese threat actor. It makes use of a network communication protocol that looks like normal HTTP traffic but has XOR encoded payloads in the content body. + +## Network Setup + +The `0423258b94e8a9af58ad63ea493818618de2d8c60cf75ec7980edcaa34dcc919` sample makes use of a DNS name of `update.cloudjscdn.com` for it's C2 communication. Simply modify the `hosts` file on the machine where the malware is running and point it to the IP address of your MockC2 server. + +## Links + +* [https://www.intezer.com/blog/malware-analysis/new-linux-backdoor-redxor-likely-operated-by-chinese-nation-state-actor/](https://www.intezer.com/blog/malware-analysis/new-linux-backdoor-redxor-likely-operated-by-chinese-nation-state-actor/) + +## IOCs + +| Indicator | Type | Context | +|------------------------------------------------------------------|----------|-------------------| +| 0423258b94e8a9af58ad63ea493818618de2d8c60cf75ec7980edcaa34dcc919 | SHA256 | RedXOR 64-bit ELF | +| 0a76c55fa88d4c134012a5136c09fb938b4be88a382f88bf2804043253b0559f | SHA256 | RedXOR 64-bit ELF | +| 0423258b94e8a9af58ad63ea493818618de2d8c60cf75ec7980edcaa34dcc919 | SHA256 | RedXOR 64-bit ELF | +| update.cloudjscdn.com | TCP/8080 | RedXOR C2 | +| 158.247.208.230 | TCP/8080 | RedXOR C2 | +| www.centosupdateonline.com | TCP/8080 | RedXOR C2 | diff --git a/internal/cli/cli.go b/internal/cli/cli.go index f77decc..d523ff5 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -47,6 +47,7 @@ func (s *Shell) initCompleters() { readline.PcItem("hotcroissant"), readline.PcItem("mata"), readline.PcItem("obliquerat"), + readline.PcItem("redxor"), readline.PcItem("rifdoor"), readline.PcItem("slickshoes"), readline.PcItem("yort"), diff --git a/pkg/c2/server.go b/pkg/c2/server.go index 8c619b0..50df771 100644 --- a/pkg/c2/server.go +++ b/pkg/c2/server.go @@ -18,6 +18,7 @@ import ( "github.com/carbonblack/mockc2/pkg/protocol/hotcroissant" "github.com/carbonblack/mockc2/pkg/protocol/mata" "github.com/carbonblack/mockc2/pkg/protocol/obliquerat" + "github.com/carbonblack/mockc2/pkg/protocol/redxor" "github.com/carbonblack/mockc2/pkg/protocol/rifdoor" "github.com/carbonblack/mockc2/pkg/protocol/slickshoes" "github.com/carbonblack/mockc2/pkg/protocol/yort" @@ -51,6 +52,8 @@ func handlerFromString(protocol string) (protocol.Handler, error) { return &mata.Handler{}, nil case "obliquerat": return &obliquerat.Handler{}, nil + case "redxor": + return &redxor.Handler{}, nil case "rifdoor": return &rifdoor.Handler{}, nil case "slickshoes": diff --git a/pkg/protocol/redxor/redxor.go b/pkg/protocol/redxor/redxor.go new file mode 100644 index 0000000..26c6425 --- /dev/null +++ b/pkg/protocol/redxor/redxor.go @@ -0,0 +1,252 @@ +package redxor + +import ( + "bufio" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "strconv" + + "github.com/carbonblack/mockc2/internal/log" + "github.com/carbonblack/mockc2/pkg/protocol" +) + +const ( + opHostInfo = "0000" + opHostInfoResp = "0001" + opDownload = "2054" + opUploadStart = "2055" + opUploadData = "2066" + opDownloadDone = "2088" + opShellStart = "3000" + opShellExec = "3058" + opShellStop = "3999" +) + +// Handler is a RedXOR protocol handler. +type Handler struct { + delegate protocol.Delegate + pr *io.PipeReader + pw *io.PipeWriter + b *bufio.Reader + shellStarted bool + source string + destination string + file *os.File + fileSize int64 +} + +// SetDelegate saves the delegate for later use. +func (h *Handler) SetDelegate(delegate protocol.Delegate) { + h.delegate = delegate +} + +// Accept gives the Handler a chance to do something as soon as an agent +// connects. +func (h *Handler) Accept() { +} + +// ReceiveData just logs information about data received. +func (h *Handler) ReceiveData(data []byte) { + log.Debug("received\n" + hex.Dump(data)) + + if h.b == nil { + h.pr, h.pw = io.Pipe() + h.b = bufio.NewReader(h.pr) + go h.processData() + } + + h.pw.Write(data) +} + +func (h *Handler) processData() { + for { + req, err := http.ReadRequest(h.b) + if err != nil && err != io.EOF { + log.Warn("redxor error reading request: %v", err) + } + + if err == io.EOF { + break + } + + if req != nil { + contentLength := req.Header.Get("Content-Length") + totalLength := req.Header.Get("Total-Length") + + cookie, err := req.Cookie("JSESSIONID") + if err != nil { + h.delegate.CloseConnection() + return + } + + log.Debug("JSESSIONID: %s\nContent-Length: %s\nTotal-Length: %s", cookie.Value, contentLength, totalLength) + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + h.delegate.CloseConnection() + return + } + + key, err := strconv.Atoi(contentLength) + if err != nil { + h.delegate.CloseConnection() + return + } + + adder, err := strconv.Atoi(totalLength) + if err != nil { + h.delegate.CloseConnection() + return + } + + body = cipher(body, uint8(key), uint8(adder)) + + log.Debug("body\n" + hex.Dump(body)) + + switch cookie.Value { + case opHostInfo: + h.sendCommand(opHostInfo, 9, 9, []byte("all right")) + case opHostInfoResp: + hash := sha256.Sum256(body) + id := hex.EncodeToString(hash[:]) + h.delegate.AgentConnected(id) + case opShellExec: + log.Info(string(body)) + case opDownload: + h.file.Write(body) + case opDownloadDone: + if h.file != nil { + h.file.Close() + } + + h.file = nil + h.fileSize = 0 + h.source = "" + h.destination = "" + + log.Success("Download complete") + case opUploadStart: + buf := make([]byte, 0x1000) + for { + bytesRead, err := h.file.Read(buf) + + if err != nil { + if err != io.EOF { + log.Warn("Error reading source file; %v", err) + } + break + } + + h.sendCommand(opUploadData, bytesRead, int(h.fileSize), buf[:bytesRead]) + } + + if h.file != nil { + h.file.Close() + } + + h.file = nil + h.fileSize = 0 + h.source = "" + h.destination = "" + + log.Success("Upload complete") + } + } + } +} + +// Execute runs a command on the connected agent. +func (h *Handler) Execute(name string, args []string) { + if !h.shellStarted { + h.sendCommand(opShellStart, 0, 0, []byte{}) + h.shellStarted = true + } + + commandLine := strings.TrimSpace(name + " " + strings.Join(args, " ")) + h.sendCommand(opShellExec, len(commandLine), len(commandLine), []byte(commandLine)) +} + +// Upload sends a file to the connected agent. +func (h *Handler) Upload(source string, destination string) { + file, err := os.Open(source) + if err != nil { + log.Warn("Error opening source file: %v", err) + return + } + + fi, err := file.Stat() + if err != nil { + log.Warn("Error opening source file: %v", err) + return + } + + h.file = file + h.fileSize = fi.Size() + h.source = source + h.destination = destination + + data := []byte(destination+"#0") + h.sendCommand(opUploadStart, len(data), len(data), data) +} + +// Download retrieves a file from the connected agent. +func (h *Handler) Download(source string, destination string) { + file, err := os.Create(destination) + if err != nil { + log.Warn("Error opening destination file: %v", err) + return + } + + h.file = file + h.source = source + h.destination = destination + + data := []byte(source+"#0") + h.sendCommand(opDownload, len(data), len(data), data) +} + +// Close cleans up any uzed resources +func (h *Handler) Close() { + h.sendCommand(opShellStop, 0, 0, []byte{}) + h.shellStarted = false + h.pw.Close() +} + +// NeedsTLS returns whether the protocol runs over TLS or not. +func (h *Handler) NeedsTLS() bool { + return false +} + +func (h *Handler) sendCommand(opcode string, contentLength int, totalLength int, data []byte) { + encrypted := cipher(data, uint8(contentLength), uint8(totalLength)) + + headerFormat := + "HTTP/1.1 200 OK\r\n" + + "Set-Cookie: JSESSIONID=%s\r\n" + + "Content-Type: text/html\r\n" + + "Content-Length: %010d\r\n" + + "Total-Length: %010d\r\n" + + "\r\n" + + header := fmt.Sprintf(headerFormat, opcode, contentLength, totalLength) + + h.delegate.SendData([]byte(header)) + h.delegate.SendData(encrypted) +} + +func cipher(input []byte, key uint8, adder uint8) []byte { + output := make([]byte, len(input)) + + for i := 0; i < len(input); i++ { + output[i] = input[i] ^ key + key += adder + } + + return output +}