Contents

Remcos RAT 3.4.0 protocol

Contents

/posts/malware-remcos-rat-3.4.0-protocol/remcos-rat.png

Remcos RAT is known for being very feature rich, with a lite version to test. They even provide an option to disable the TLS, making it very easy to reverse engineer the protocol. I’m not aware if the paid version has a different protocol.

The binary protocol is very simple:

  • packets start with $\x04\xff\x00
  • then is the packet type
  • then 7 usually miscellaneous bytes
  • then often string arguments separated by |\x1e\x1e\x1f|

I’ve mapped out a significant amount of the protocol available in the lite version. Protocols are generally a pain to document, so I’ve included my packet parsing code.

def parse_packet(packets, packet, payload, server):
    if server:
        if payload[:5] == b'$\x04\xff\x00\x0c':
            return {
                "type": "ping",
                "settings": payload[12:].split(b"|\x1e\x1e\x1f|")
            }
        elif payload[:5] == b'$\x04\xff\x00\x0b':
            return {
                "type": "ping-latency",
                "settings": payload[12:].split(b"|\x1e\x1e\x1f|")
            }
        elif payload[:5] == b'$\x04\xff\x00\x0e':
            return {
                "type": "execute-cmd-1",
                "cmd": [str(a, "utf16") for a in payload[12:].split(b"|\x1e\x1e\x1f|")]
            }
        elif payload[:5] == b'$\x04\xff\x00(':
            return {
                "type": "execute-cmd-2",
                "cmd": [str(a, "utf16") for a in payload[12:].split(b"|\x1e\x1e\x1f|")]
            }
        elif payload[:8] == b'$\x04\xff\x00\x04\x00\x00\x00' and chr(payload[8]) in ["(", "*"]:
            return {
                "type": "get-clipboard"
            }
        elif payload[:4] == b'$\x04\xff\x00' and chr(payload[4]) in ["\x18", "\xf2"]:
            return {
                "type": "set-clipboard",
                "contents": str(payload[12:], "utf16")
            }
        elif payload[:9] == b'$\x04\xff\x00\x04\x00\x00\x00\x03':
            return {
                "type": "get-installed-programs"
            }
        elif payload[:9] == b'$\x04\xff\x00\x04\x00\x00\x00\x08':
            return {
                "type": "get-windows"
            }
        elif payload[:9] == b'$\x04\xff\x00\x04\x00\x00\x00\x06':
            return {
                "type": "get-processes"
            }
        elif payload[:5] == b'$\x04\xff\x00\n':
            if payload[8] == 0xa:
                return {
                    "type": "maximize-window",
                    "window": str(payload[12:])
                }
            elif payload[8] == 0x9:
                return {
                    "type": "minimize-window",
                    "window": str(payload[12:])
                }
            elif payload[8] == 0x0b:
                return {
                    "type": "show-window",
                    "window": str(payload[12:])
                }
            elif payload[8] == 0xae:
                return {
                    "type": "hide-window",
                    "window": str(payload[12:])
                }
            elif payload[8] == 0xad:
                return {
                    "type": "visible-window",
                    "window": str(payload[12:])
                }
            elif payload[8] == 0x0c:
                return {
                    "type": "kill-window",
                    "window": str(payload[12:])
                }
        elif payload[:5] == b'$\x04\xff\x00h':
            info = payload[12:].split(b"|\x1e\x1e\x1f|")
            return {
                "type": "set-window-title",
                "window": info[0],
                "title": str(info[1], "utf16")
            }
        elif payload[:4] == b'$\x04\xff\x00' and chr(payload[4]) in ["k", "]", "w", "M"]:
            info = payload[12:].split(b"|\x1e\x1e\x1f|")

            if payload[8] == 0x05:
                location = "local"
                contents = info[4]
            elif payload[8] == 0x04:
                location = "url"
                contents = str(info[4], "utf16")
            else:
                location = payload[8]
                contents = info[4]

            if info[2] == b"1":
                path = "application-path"
            elif info[2] == b"7":
                path = "user-profile"
            elif info[2] == b"6":
                path = "appdata"
            elif info[2] == b"0":
                path = "temp"
            elif info[2] == b"2":
                path = "root"
            elif info[2] == b"3":
                path = "windows"
            elif info[2] == b"4":
                path = "system32"
            elif info[2] == b"5":
                path = "program-files"
            else:
                path = info[2]

            if info[1] == b'\x00':
                is_exec = False
            elif info[1] == b'\x01':
                is_exec = True
            else:
                is_exec = info[1]

            return {
                "type": "download",
                "location": location,
                "unidentified-1": info[0],
                "exec": is_exec,
                "path": path,
                "name": str(info[3], "utf16"),
                "contents": contents
            }
    else:
        if payload[:4] == b'$\x04\xff\x00' and chr(payload[4]) in ["\xaa", "\xae", "\xa9"]:
            info = payload[12:].split(b"|\x1e\x1e\x1f|")
            return {
                "type": "init",
                "assigned-name": info[0],
                "computer-user-name": str(info[1], "utf16"),
                "location": info[2],
                "os": info[3],
                "ram": info[5],  # bytes
                "version": info[6],
                "path": str(info[8], "utf16"),
                "path-2": str(info[10], "utf16"),
                "unknown-2": info[11],
                "idle-time": info[12],  # milliseconds
                "system-uptime": info[13],  # milliseconds
                "unknown-5": info[14],
                "ip": info[15],
                "mutex": info[16],
                "unknown-7": info[17],
                "cpu": info[19],
                "agent-type": info[20]
            }
        elif payload[:4] == b'$\x04\xff\x00' and chr(payload[4]) in ["c", "d", "e", "f", "g", "i", "0", "3", "="] and payload[5:9] == b'\x00\x00\x00L':
            info = payload[12:].split(b"|\x1e\x1e\x1f|")
            return {
                "type": "pong",
                "unknown-1": info[0],
                "active-window": str(info[1], "utf16"),
                "idle-time": info[2],
                "system-uptime": info[3]
            }
        elif payload[:4] == b'$\x04\xff\x00' and chr(payload[4]) in ["\xf0", "\x04", "\x16", "h"]:
            return {
                "type": "clipboard-data",
                "contents": str(payload[12:], "utf16")
            }
        elif payload[:5] == b'$\x04\xff\x00\x80':
            packet_i = packets.index(packet)
            while payload[len(payload) - 4:len(payload)] != b'\x09\x00\x0a\x00':
                packet_i += 1
                payload += bytes(packets[packet_i]['TCP'].payload)

            programs = []

            for raw_program in str(payload[12:], "utf16").split("\t\n"):
                if raw_program == "":
                    break
                program = raw_program.split("\t")
                programs.append({
                    "name": program[0],
                    "version": program[1],
                    "date-installed": program[2],
                    "publisher": program[3],
                    "location": program[4],
                    "uninstall-string": program[5]
                })

            return {
                "type": "installed-programs",
                "programs": programs,
                "_skip": packet_i - packets.index(packet)
            }
        elif payload[:5] == b'$\x04\xff\x00\xc8':
            packet_i = packets.index(packet)
            while payload[len(payload) - 2:len(payload)] != b'\x00\x1e':
                packet_i += 1
                payload += bytes(packets[packet_i]['TCP'].payload)[12:]

            windows = []
            parts = payload[12:].split(b"\x1f")
            for i in range(1, len(parts), 2):
                if len(parts[i + 1]) == 2:
                    title = ""
                else:
                    title = str(parts[i + 1][:-2], "utf16", "ignore")
                windows.append({
                    "handle": parts[i],
                    "title": title,
                    "visible": parts[i + 1][-1]
                })

            return {
                "type": "windows-list",
                "windows": windows,
                "_skip": packet_i - packets.index(packet)
            }
        elif payload[:9] == b'$\x04\xff\x00\x05\x00\x00\x00\xb3':
            if chr(payload[12]) == "0":
                status = "download-successful"
            elif chr(payload[12]) == "1":
                status = "download-execute-successful"
            elif chr(payload[12]) == "2":
                status = "download-error"
            elif chr(payload[12]) == "3":
                status = "execute-error"
            else:
                status = payload[12]

            return {
                "type": "download-file-status",
                "status": status
            }

The full python script is available here.

$ python3 parse.py 
usage: parse.py [-h] --pcap <pcap file name> --server <server ip address> --client <client ip address>