-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from carbonblack/redxor
RedXOR support
- Loading branch information
Showing
5 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |