Skip to content

Instantly share code, notes, and snippets.

@fox-srt
Created May 22, 2026 14:08
Show Gist options
  • Select an option

  • Save fox-srt/6f838d0b574b095d578b2beed7dc2a24 to your computer and use it in GitHub Desktop.

Select an option

Save fox-srt/6f838d0b574b095d578b2beed7dc2a24 to your computer and use it in GitHub Desktop.
Decrypt RemotePE C2 Command Responses
# /// script
# dependencies = [
# "dissect-cstruct",
# "pycryptodome",
# ]
# ///
import sys
import zlib
from base64 import b64decode
from Crypto.Cipher import AES
from dissect.cstruct import cstruct, dumpstruct, hexdump
remotepe_defs = """
struct C2Message {
uint64_t aes_seed; // SplitMix64 seed for AES key and nonce
unsigned char aes_tag[16]; // AES authentication tag
BYTE ciphertext[EOF]; // AES-GCM encrypted data
};
struct C2CommandResponse {
uint32_t response_size;
uint32_t error; // error code, if any
uint32_t request_id; // used to respond to a C2Command request
unsigned char payload[response_size]; // variable length, compressed, response_size bytes
};
struct C2CommandResponseBatch {
uint16_t command_count;
C2CommandResponse commands[EOF]; // variable length, command_count entries
};
struct CabinetStream {
int magic;
int unk1;
uint64_t original_size;
uint64_t original_size2;
uint compressed_size;
unsigned char compressed_buf[compressed_size];
};
"""
c_parser = cstruct(remotepe_defs)
def splitmix64(state: int) -> int:
"""SplitMix64 pseudo-random number generator algorithm.
Args:
state (int): the initial state value.
Returns:
int: output of SplitMix64 algorithm.
"""
U64_MASK = 0xFFFFFFFFFFFFFFFF
state = (state - 0x61C8864680B583EB) & U64_MASK
state = ((state ^ (state >> 30)) * 0xBF58476D1CE4E5B9) & U64_MASK
state = ((state ^ (state >> 27)) * 0x94D049BB133111EB) & U64_MASK
return state ^ (state >> 31)
def generate_aes_key_nonce(seed: int) -> tuple[bytes, bytes]:
"""Generates an AES key and nonce from a given seed. This is how RemotePE computes the AES key and nonce.
Args:
seed (int): the initial seed value.
Returns:
tuple[bytes, bytes]: A tuple containing the generated AES key and nonce.
"""
numbers = []
for i in range(4):
seed = splitmix64(seed)
numbers.append(seed)
aes_key = b"".join(x.to_bytes(8, "little") for x in numbers)
numbers = []
for i in range(2):
seed = splitmix64(seed)
numbers.append(seed)
aes_nonce = b"".join(x.to_bytes(8, "little") for x in numbers)
return aes_key, aes_nonce[:12]
def decrypt_c2_message(base64_blob: str) -> bytes:
"""Decrypts the C2 messages of RemotePELoader or RemotePE.
Args:
base64_blob (str): The base64-encoded C2 message.
Returns:
bytes: The decrypted C2 message.
"""
enc_c2_message_bytes = b64decode(base64_blob)
enc_c2_message = c_parser.C2Message(enc_c2_message_bytes)
key, nonce = generate_aes_key_nonce(enc_c2_message.aes_seed)
cipher = AES.new(key, AES.MODE_GCM, nonce)
return cipher.decrypt(bytes(enc_c2_message.ciphertext))
def decompress_mszip(data) -> bytes:
"""Decompresses MSZIP compressed data using zlib."""
decomp = zlib.decompressobj(-zlib.MAX_WBITS)
return decomp.decompress(data[2:]) # skip
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <base64_blob>")
sys.exit(1)
base64_blob = sys.argv[1]
decrypted_c2_message = decrypt_c2_message(base64_blob)
print("Decrypted C2 Message:")
hexdump(decrypted_c2_message)
c2_command_batch = c_parser.C2CommandResponseBatch(decrypted_c2_message)
for i in range(c2_command_batch.command_count):
print(f"\nCommand {i + 1} of {c2_command_batch.command_count}:")
c2_command_response = c2_command_batch.commands[i]
print("C2 Command Response:")
dumpstruct(c2_command_response)
if not c2_command_response.payload:
continue
cabstream = c_parser.CabinetStream(c2_command_response.payload)
print("Compressed Stream:")
dumpstruct(cabstream)
print("\nDecompressed command output:")
hexdump(decompress_mszip(cabstream.compressed_buf))
@fox-srt

fox-srt commented May 28, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment