Created
May 22, 2026 14:08
-
-
Save fox-srt/6f838d0b574b095d578b2beed7dc2a24 to your computer and use it in GitHub Desktop.
Decrypt RemotePE C2 Command Responses
This file contains hidden or 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
| # /// 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)) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Blog post: https://blog.fox-it.com/2026/05/22/remotepe-the-lazarus-rat-that-lives-in-memory/