"""
name: Donut extractor
category: unpackers
author: volexity + malcat
icon: wxART_FILE_COMPRESSED

Decrypt and decompress a dotnut-shellcode payload

Adapted from https://github.com/volexity/donut-decryptor
"""
import json
import struct
import base64
from collections import namedtuple
from typing import List, Union

import malcat
malcat.setup()  # Add Malcat's data/ and bindings/ directories to sys.path when called in headless mode
from transforms.block import ChaskeyLTSDecrypt
from transforms.compress import APLibDecompress, Lznt1Decompress, XpressLz77Decompress


################################ DEFINITIONS

DONUT_SIGNATURES = [
    (
    "1.0", # custom 1.0 ?
     64,
     "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 81 EC 00 05 00 00 33 FF 48 8B D9 39 B9 38 02 00 00 0F 84 D1 00 00 00 4C 8B 41 28",
     base64.b64encode(b"\x48\x89\x5c\x24\x08\x48\x89\x6c\x24\x10\x48\x89\x74\x24\x18\x57\x41\x56\x41\x57\x48\x81\xec\x00\x05\x00\x00\x33\xff\x48\x8b\xd9\x39\xb9\x38\x02\x00\x00\x0f\x84\xce\x00\x00\x00\x4c\x8b\x41\x28") ,
    ),

    (
     "1.0",
     64,
     "48 89 5c 24 08 48 89 6c 24 10 48 89 74 24 18 57 41 56 41 57 48 81 ec 00 05 00 00 33 ff 48 8b d9 39 b9 38 02 00 00 0f 84 ce 00 00 00 4c 8b 41 28",
     base64.b64encode(b"\x48\x89\x5c\x24\x08\x48\x89\x6c\x24\x10\x48\x89\x74\x24\x18\x57\x41\x56\x41\x57\x48\x81\xec\x00\x05\x00\x00\x33\xff\x48\x8b\xd9\x39\xb9\x38\x02\x00\x00\x0f\x84\xce\x00\x00\x00\x4c\x8b\x41\x28") ,
    ),

    (
     "1.0",
     32,
     "81 ec d4 02 00 00 53 55 56 8b b4 24 e4 02 00 00 33 db 57 8b fb 39 9e 38 02 00 00 0f 84 ea 00 00 00 ff 76 2c ff 76 28 ff b6 8c 00 00 00 ff b6 88",
     base64.b64encode(b"\x81\xec\xd4\x02\x00\x00\x53\x55\x56\x8b\xb4\x24\xe4\x02\x00\x00\x33\xdb\x57\x8b\xfb\x39\x9e\x38\x02\x00\x00\x0f\x84\xea\x00\x00\x00\xff\x76\x2c\xff\x76\x28\xff\xb6\x8c\x00\x00\x00\xff\xb6\x88") ,
    ),

    (
     "0.9.3_V1",
     64,
     "48 89 5c 24 08 48 89 6c 24 10 48 89 74 24 18 57 48 81 ec 00 05 00 00 33 ff 48 8b d9 48 39 b9 38 02 00 00 0f 84 c0 00 00 00 4c 8b 41 28 48 8b 91",
     base64.b64encode(b"\x48\x89\x5c\x24\x08\x48\x89\x6c\x24\x10\x48\x89\x74\x24\x18\x57\x48\x81\xec\x00\x05\x00\x00\x33\xff\x48\x8b\xd9\x48\x39\xb9\x38\x02\x00\x00\x0f\x84\xc0\x00\x00\x00\x4c\x8b\x41\x28\x48\x8b\x91") ,
    ),

    (
     "0.9.3_V2",
     64,
     "55 48 81 EC 30 05 00 00 48 8D AC 24 80 00 00 00 48 89 8D C0 04 00 00 48 C7 85 A8 04 00 00 00 00 00 00 48 8B 85 C0 04 00 00 48 8B 80 38 02 00 00",
     base64.b64encode(b"\x55\x48\x81\xEC\x30\x05\x00\x00\x48\x8D\xAC\x24\x80\x00\x00\x00\x48\x89\x8D\xC0\x04\x00\x00\x48\xC7\x85\xA8\x04\x00\x00\x00\x00\x00\x00\x48\x8B\x85\xC0\x04\x00\x00\x48\x8B\x80\x38\x02\x00\x00") ,
    ),

    (
     "0.9.3_V1",
     32,
     "81 ec cc 02 00 00 53 55 56 8b b4 24 dc 02 00 00 33 db 57 8b fb 8b 86 38 02 00 00 0b 86 3c 02 00 00 0f 84 d4 00 00 00 ff 76 2c ff 76 28 ff b6 8c",
     base64.b64encode(b"\x81\xec\xcc\x02\x00\x00\x53\x55\x56\x8b\xb4\x24\xdc\x02\x00\x00\x33\xdb\x57\x8b\xfb\x8b\x86\x38\x02\x00\x00\x0b\x86\x3c\x02\x00\x00\x0f\x84\xd4\x00\x00\x00\xff\x76\x2c\xff\x76\x28\xff\xb6\x8c") ,
    ),


    (
     "0.9.3_V2",
     32,
     "55 89 E5 56 53 81 EC 10 03 00 00 C7 45 F4 00 00 00 00 8B 4D 08 8B 99 3C 02 00 00 8B 89 38 02 00 00 89 CE 83 F6 00 89 F0 80 F7 00 89 DA 09 D0 85",
     base64.b64encode(b"\x55\x89\xE5\x56\x53\x81\xEC\x10\x03\x00\x00\xC7\x45\xF4\x00\x00\x00\x00\x8B\x4D\x08\x8B\x99\x3C\x02\x00\x00\x8B\x89\x38\x02\x00\x00\x89\xCE\x83\xF6\x00\x89\xF0\x80\xF7\x00\x89\xDA\x09\xD0\x85") ,
    ),

    (
     "0.9.3_V3",
     32,
     "55 89 E5 56 53 81 EC 10 03 00 00 C7 45 F4 00 00 00 00 8B 45 08 8B 90 3C 02 00 00 8B 80 38 02 00 00 89 C6 83 F6 00 89 F1 89 D0 80 F4 00 89 C3 89",
     base64.b64encode(b"\x55\x89\xE5\x56\x53\x81\xEC\x10\x03\x00\x00\xC7\x45\xF4\x00\x00\x00\x00\x8B\x45\x08\x8B\x90\x3C\x02\x00\x00\x8B\x80\x38\x02\x00\x00\x89\xC6\x83\xF6\x00\x89\xF1\x89\xD0\x80\xF4\x00\x89\xC3\x89") ,
    ),

    (
     "0.9.3_V4",
     32,
     "55 8B EC 81 EC 10 03 00 00 83 65 BC 00 6A 5C 68 0C B0 42 00 E8 67 C5 00 00 59 59 85 C0 74 14 6A 5C 68 1C B0 42 00 E8 55 C5 00 00 59 59 40 89 45",
     base64.b64encode(b"\x55\x8B\xEC\x81\xEC\x10\x03\x00\x00\x83\x65\xBC\x00\x6A\x5C\x68\x0C\xB0\x42\x00\xE8\x67\xC5\x00\x00\x59\x59\x85\xC0\x74\x14\x6A\x5C\x68\x1C\xB0\x42\x00\xE8\x55\xC5\x00\x00\x59\x59\x40\x89\x45") ,
    ),

    (
     "0.9.2",
     64,
     "55 48 89 e5 48 81 ec b0 00 00 00 48 89 4d 10 48 8b 45 10 48 89 45 e8 48 8b 45 e8 48 8b 40 48 48 89 45 e0 48 8b 45 e8 48 8b 48 28 48 8b 55 e0 48",
     base64.b64encode(b"\x55\x48\x89\xe5\x48\x81\xec\xb0\x00\x00\x00\x48\x89\x4d\x10\x48\x8b\x45\x10\x48\x89\x45\xe8\x48\x8b\x45\xe8\x48\x8b\x40\x48\x48\x89\x45\xe0\x48\x8b\x45\xe8\x48\x8b\x48\x28\x48\x8b\x55\xe0\x48") ,
    ),

    (
     "0.9.2",
     32,
     "83 ec 20 53 55 56 57 8b 7c 24 34 ff 77 2c ff 77 28 ff 77 4c ff 77 48 57 e8 d1 1a 00 00 ff 77 2c 8b f0 ff 77 28 ff 77 54 ff 77 50 57 e8 bd 1a 00",
     base64.b64encode(b"\x83\xec\x20\x53\x55\x56\x57\x8b\x7c\x24\x34\xff\x77\x2c\xff\x77\x28\xff\x77\x4c\xff\x77\x48\x57\xe8\xd1\x1a\x00\x00\xff\x77\x2c\x8b\xf0\xff\x77\x28\xff\x77\x54\xff\x77\x50\x57\xe8\xbd\x1a\x00") ,
    ),

    (
     "0.9.1",
     64,
     "48 89 5c 24 08 48 89 74 24 10 57 48 83 ec 60 33 d2 48 8b f9 48 8d 4c 24 20 44 8d 42 40 e8 a2 10 00 00 44 8b 0f 4c 8d 47 24 41 83 e9 24 48 8d 57",
     base64.b64encode(b"\x48\x89\x5c\x24\x08\x48\x89\x74\x24\x10\x57\x48\x83\xec\x60\x33\xd2\x48\x8b\xf9\x48\x8d\x4c\x24\x20\x44\x8d\x42\x40\xe8\xa2\x10\x00\x00\x44\x8b\x0f\x4c\x8d\x47\x24\x41\x83\xe9\x24\x48\x8d\x57") ,
    ),

    (
     "0.9.1",
     32,
     "83 ec 20 8d 04 24 53 55 56 57 6a 20 6a 00 50 e8 69 0e 00 00 8b 74 24 40 8b 06 83 e8 24 50 8d 46 24 50 8d 46 14 50 8d 46 04 50 e8 2a 0c 00 00 ff",
     base64.b64encode(b"\x83\xec\x20\x8d\x04\x24\x53\x55\x56\x57\x6a\x20\x6a\x00\x50\xe8\x69\x0e\x00\x00\x8b\x74\x24\x40\x8b\x06\x83\xe8\x24\x50\x8d\x46\x24\x50\x8d\x46\x14\x50\x8d\x46\x04\x50\xe8\x2a\x0c\x00\x00\xff") ,
    ),

    (
     "0.9",
     64,
     "55 48 89 e5 48 83 c4 80 48 89 4d 10 48 8b 45 10 48 89 45 f0 c7 45 ec 24 00 00 00 8b 55 ec 48 8b 45 f0 48 01 d0 48 89 45 e0 48 8b 45 f0 8b 00 2b",
     base64.b64encode(b"\x55\x48\x89\xe5\x48\x83\xc4\x80\x48\x89\x4d\x10\x48\x8b\x45\x10\x48\x89\x45\xf0\xc7\x45\xec\x24\x00\x00\x00\x8b\x55\xec\x48\x8b\x45\xf0\x48\x01\xd0\x48\x89\x45\xe0\x48\x8b\x45\xf0\x8b\x00\x2b") ,
    ),

    (
     "0.9",
     32,
     "55 89 e5 56 53 83 ec 60 8b 45 08 89 45 f0 c7 45 ec 24 00 00 00 8b 55 f0 8b 45 ec 01 d0 89 45 e8 8b 45 f0 8b 00 2b 45 ec 8b 55 f0 8d 4a 14 8b 55",
     base64.b64encode(b"\x55\x89\xe5\x56\x53\x83\xec\x60\x8b\x45\x08\x89\x45\xf0\xc7\x45\xec\x24\x00\x00\x00\x8b\x55\xf0\x8b\x45\xec\x01\xd0\x89\x45\xe8\x8b\x45\xf0\x8b\x00\x2b\x45\xec\x8b\x55\xf0\x8d\x4a\x14\x8b\x55") ,
    ),
]

ENTROPY_TYPES = (
    'DONUT_ENTROPY_NONE',    # No entropy
    'DONUT_ENTROPY_RANDOM',  # Random names
    'DONUT_ENTROPY_DEFAULT'  # Random names + Encryption
)

MOD_TYPES = (
    'DONUT_MODULE_INVALID',
    'DONUT_MODULE_NET_DLL',   # .NET DLL
    'DONUT_MODULE_NET_EXE',   # .NET EXE
    'DONUT_MODULE_DLL',       # Unmanaged DLL
    'DONUT_MODULE_EXE',       # Unmanaged EXE
    'DONUT_MODULE_VBS',       # VBScript
    'DONUT_MODULE_JS'         # JavaScript or JScript
)

COMP_TYPES = (
    'DONUT_COMPRESS_NONE',
    'DONUT_COMPRESS_APLIB',
    'DONUT_COMPRESS_LZNT1',
    'DONUT_COMPRESS_XPRESS'
)

INST_TYPES = (
    'DONUT_INSTANCE_EMBED',  # Module is embedded
    'DONUT_INSTANCE_HTTP',   # Module is downloaded from remote HTTP/HTTPS server
    'DONUT_INSTANCE_DNS'     # Module is downloaded from remote DNS server
)

offset = namedtuple('offset', ['pos', 'format'])
loader_mapping = namedtuple('loader_mapping', ['offsets', 'version'])
loader_offset = namedtuple('loader_offset', ['pos', 'value'])


instance_offset_map = {
    '0.9': {
        'size_instance': 0x588,
        'encryption_start': 0x24,
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x128, 'Q'),
        'instance_signature': offset(0x3AC, '64s'),
        'instance_mac': offset(0x3F0, 'Q'),
        'instance_type': offset(0x318, 'i'),
        'download_uri': offset(0x31C, '128s'),
        'module_type': offset(0x420, 'I'),
        'module_key': offset(0x3F8, '16s'),
        'module_nonce': offset(0x408, '16s'),
        'module_length': offset(0x418, 'Q')
    },
    '0.9.1': {
        'size_instance': 0xC88,
        'encryption_start': 0x24,
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x2A8, 'Q'),
        'instance_signature': offset(0x62C, '256s'),
        'instance_mac': offset(0x730, 'Q'),
        'instance_type': offset(0x518, 'i'),
        'download_uri': offset(0x51C, '256s'),
        'module_type': offset(0x760, 'I'),
        'module_key': offset(0x738, '16s'),
        'module_nonce': offset(0x748, '16s'),
        'module_length': offset(0x758, 'Q')
    },
    '0.9.2': {
        'size_instance': 0x2060,
        'encryption_start': 0x230,
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0x618, '256s'),
        'instance_mac': offset(0x718, 'Q'),
        'instance_type': offset(0x50C, 'i'),
        'download_uri': offset(0x510, '256s'),
        'module_type': offset(0x748, 'I'),
        'module_key': offset(0x720, '16s'),
        'module_nonce': offset(0x730, '16s'),
        'module_length': offset(0x2058, 'Q')
    },
    '0.9.3': {
        'size_instance': 0xE48,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0x7F0, '256s'),
        'instance_mac': offset(0x8F0, 'Q'),
        'instance_type': offset(0x6E4, 'i'),
        'download_uri': offset(0x6E8, '256s'),
        'module_key': offset(0x8F8, '256s'),
        'module_nonce': offset(0x908, '256s'),
        'module_length': offset(0x918, 'Q'),
        'module_type': offset(0x920, 'i'),
        'module_compression_type': offset(0x928, 'i'),
        'module_compressed_len': offset(0xE40, 'I')
    },
    '0.9.3_A': {
        'size_instance': 0xF48,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0x8F0, '256s'),
        'instance_mac': offset(0x9F0, 'Q'),
        'instance_type': offset(0x6E4, 'i'),
        'download_uri': offset(0x6E8, '256s'),
        'download_password': offset(0x7E8, '256s'),
        'module_key': offset(0x7F8, '16s'),
        'module_nonce': offset(0x808, '16s'),
        'module_length': offset(0xA18, 'Q'),
        'module_type': offset(0xA20, 'i'),
        'module_compression_type': offset(0xA28, 'i'),
        'module_compressed_len': offset(0xF40, 'I')
    },
    '0.9.3_B': {
        'size_instance': 0x1048,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0x9F0, '256s'),
        'instance_mac': offset(0xAF0, 'Q'),
        'instance_type': offset(0x6E4, 'i'),
        'download_uri': offset(0x6E8, '256s'),
        'download_username': offset(0x7E8, '256s'),
        'download_password': offset(0x8E8, '256s'),
        'module_key': offset(0xAF8, '16s'),
        'module_nonce': offset(0xB08, '16s'),
        'module_length': offset(0xB18, 'Q'),
        'module_type': offset(0xB20, 'i'),
        'module_compression_type': offset(0xB28, 'i'),
        'module_compressed_len': offset(0x1040, 'I')
    },
    '0.9.3_C': {
        'size_instance': 0x1060,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0xA08, '256s'),
        'instance_mac': offset(0xB08, 'Q'),
        'instance_type': offset(0x6FC, 'i'),
        'download_uri': offset(0x700, '256s'),
        'download_username': offset(0x800, '256s'),
        'download_password': offset(0x900, '256s'),
        'module_key': offset(0xB10, '16s'),
        'module_nonce': offset(0xB20, '16s'),
        'module_length': offset(0xB30, 'Q'),
        'module_type': offset(0xB38, 'i'),
        'module_compression_type': offset(0xB40, 'i'),
        'module_compressed_len': offset(0x1058, 'I')
    },
    '0.9.3_D': {
        'size_instance': 0x1078,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0xA1C, '256s'),
        'instance_mac': offset(0xB20, 'Q'),
        'instance_type': offset(0x710, 'i'),
        'download_uri': offset(0x714, '256s'),
        'download_username': offset(0x814, '256s'),
        'download_password': offset(0x914, '256s'),
        'module_key': offset(0xB28, '16s'),
        'module_nonce': offset(0xB38, '16s'),
        'module_length': offset(0xB48, 'Q'),
        'module_type': offset(0xB50, 'i'),
        'module_compression_type': offset(0xB58, 'i'),
        'module_compressed_len': offset(0x1070, 'I')
    },
    '0.9.3_E': {
        'size_instance': 0x1078,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0xA20, '256s'),
        'instance_mac': offset(0xB20, 'Q'),
        'instance_type': offset(0x714, 'i'),
        'download_uri': offset(0x718, '256s'),
        'download_username': offset(0x818, '256s'),
        'download_password': offset(0x918, '256s'),
        'module_key': offset(0xB28, '16s'),
        'module_nonce': offset(0xB38, '16s'),
        'module_length': offset(0xD48, 'Q'),
        'module_type': offset(0xB50, 'i'),
        'module_compression_type': offset(0xB58, 'i'),
        'module_compressed_len': offset(0x1070, 'I'),
    },
    '0.9.3_F': {
        'size_instance': 0x1288,
        'encryption_start': 0x240,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0xC30, '256s'),
        'instance_mac': offset(0xD30, 'Q'),
        'instance_type': offset(0x924, 'i'),
        'download_uri': offset(0x928, '256s'),
        'download_username': offset(0xA28, '256s'),
        'download_password': offset(0xB28, '256s'),
        'module_key': offset(0xD38, '16s'),
        'module_nonce': offset(0xD48, '16s'),
        'module_length': offset(0xD58, 'Q'),
        'module_type': offset(0xD60, 'i'),
        'module_compression_type': offset(0xD68, 'i'),
        'module_compressed_len': offset(0x1280, 'I'),
        'decoy_module': offset(0x629, '520s')
    },
    '1.0': {
        'size_instance': 0x1288,
        'encryption_start': 0x23C,
        'entropy': offset(0x234, 'i'),
        'instance_key': offset(4, '16s'),
        'instance_nonce': offset(0x14, '16s'),
        'hash_iv': offset(0x28, 'Q'),
        'instance_signature': offset(0xC2C, '256s'),
        'instance_mac': offset(0xD30, 'Q'),
        'instance_type': offset(0x920, 'i'),
        'download_uri': offset(0x924, '256s'),
        'download_username': offset(0xA24, '256s'),
        'download_password': offset(0xB24, '256s'),
        'module_key': offset(0xD38, '16s'),
        'module_nonce': offset(0xD48, '16s'),
        'module_length': offset(0xD58, 'Q'),
        'module_type': offset(0xD60, 'i'),
        'module_compression_type': offset(0xD68, 'i'),
        'module_compressed_len': offset(0x1280, 'I'),
        'decoy_module': offset(0x625, '520s')
    },
}

#   The below dictionary uses specific known offsets to disambiguate all known
#   variants of the donut loader from both tagged releases and development
#   builds. It can then be used to correlate the loader to a known instance
#   alignment using the dictionary `instance_offset_map`.

loader_version_map = {
    '0.9_64': [loader_mapping(None, '0.9')],
    '0.9_32': [loader_mapping(None, '0.9')],
    '0.9.1_64': [loader_mapping(None, '0.9.1')],
    '0.9.1_32': [loader_mapping(None, '0.9.1')],
    '0.9.2_64': [loader_mapping(None, '0.9.2')],
    '0.9.2_32': [loader_mapping(None, '0.9.2')],
    '0.9.3_V1_64': [
        loader_mapping(
            [   # 093_191221_loader_exe_x64
                loader_offset(0x35, 0xbb)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093__1__loader_exe_x64
                loader_offset(0x35, 0x6f)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093__2__loader_exe_x64
                loader_offset(0x35, 0x1f)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200107_loader_exe_x64
                loader_offset(0x35, 0x8f)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200317_loader_exe_x64
                loader_offset(0x35, 0x33)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200319_loader_exe_x64
                loader_offset(0x35, 0x43),
                loader_offset(0x64a, 0x07),
                loader_offset(0x70b, 0x09),
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200327_loader_exe_x64
                loader_offset(0x35, 0x43),
                loader_offset(0x64a, 0x08),
                loader_offset(0x70b, 0x0a),
            ],
            '0.9.3_A'
        ),
        loader_mapping(
            [   # 093_200403_loader_exe_x64
                loader_offset(0x35, 0xff),
                loader_offset(0x36, 0x24)
            ],
            '0.9.3_B'
        ),
        loader_mapping(
            [   # 093_200411_loader_exe_x64
                loader_offset(0x35, 0x9f),
                loader_offset(0x36, 0x25),
                loader_offset(0x12E, 0x0c)
            ],
            '0.9.3_C'
        ),
        loader_mapping(
            [   # 093_200614_loader_exe_x64
                loader_offset(0x35, 0x9f),
                loader_offset(0x36, 0x25),
                loader_offset(0x12E, 0x20)
            ],
            '0.9.3_D'
        ),
        loader_mapping(
            [   # 093_210415_loader_exe_x64
                loader_offset(0x35, 0x97),
                loader_offset(0x36, 0x27)
            ],
            '0.9.3_D'
        ),
        loader_mapping(
            [   # 093_210521_loader_exe_x64
                loader_offset(0x35, 0xa7)
            ],
            '0.9.3_E'
        ),
        loader_mapping(
            [   # 093_221207_loader_exe_x64
                loader_offset(0x35, 0xaf),
                loader_offset(0x36, 0x2c)
            ],
            '0.9.3_F'
        )
    ],
    '0.9.3_V2_64': [
        loader_mapping(
            [   # 093_200218_loader_exe_x64
                loader_offset(0x35, 0xc9),
                loader_offset(0x6e, 0x2a)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200330_loader_exe_x64
                loader_offset(0x35, 0xc9),
                loader_offset(0x6e, 0x02),
                loader_offset(0x6f, 0x0f)
            ],
            '0.9.3_A'
        ),
        loader_mapping(
            [   # 093_210129_loader_exe_x64
                loader_offset(0x35, 0xd4),
                loader_offset(0x6e, 0xf0),
                loader_offset(0x6f, 0x0e)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_211231_loader_exe_x64
                loader_offset(0x35, 0xd4),
                loader_offset(0x6e, 0x0b),
                loader_offset(0x6f, 0x0f)
            ],
            '0.9.3_F'
        ),
        loader_mapping(
            [   # 093_220104_loader_exe_x64
                loader_offset(0x35, 0xd4),
                loader_offset(0x6e, 0xc3),
                loader_offset(0x6f, 0x14)
            ],
            '0.9.3_F'
        )
    ],
    '0.9.3_V1_32': [
        loader_mapping(
            [   # 093_191221_loader_exe_x86
                loader_offset(0x3b, 0xb8),
                loader_offset(0x3c, 0x20)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093__1__loader_exe_x86
                loader_offset(0x3b, 0x70),
                loader_offset(0x3c, 0x20)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200317_loader_exe_x86
                loader_offset(0x3b, 0x54),
                loader_offset(0x3c, 0x20)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200319_loader_exe_x86
                loader_offset(0x3b, 0x72),
                loader_offset(0x3c, 0x20),
                loader_offset(0x6f6, 0x07)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200327_loader_exe_x86
                loader_offset(0x3b, 0x72),
                loader_offset(0x3c, 0x20),
                loader_offset(0x6f6, 0x08)
            ],
            '0.9.3_A'
        ),
        loader_mapping(
            [   # 093_200403_loader_exe_x86
                loader_offset(0x3b, 0xe3),
                loader_offset(0x3c, 0x21)
            ],
            '0.9.3_B'
        ),
        loader_mapping(
            [   # 093_200624_loader_exe_x86
                loader_offset(0x3b, 0x57),
                loader_offset(0x3c, 0x22)
            ],
            '0.9.3_D'
        ),
        loader_mapping(
            [   # 093_210415_loader_exe_x86
                loader_offset(0x3b, 0x0c),
                loader_offset(0x3c, 0x23)
            ],
            '0.9.3_D'
        ),
        loader_mapping(
            [   # 093_210521_loader_exe_x86
                loader_offset(0x3b, 0x1d),
                loader_offset(0x3c, 0x23)
            ],
            '0.9.3_E'
        ),
        loader_mapping(
            [   # 093_221207_loader_exe_x86
                loader_offset(0x3b, 0x22),
                loader_offset(0x3c, 0x27)
            ],
            '0.9.3_F'
        )
    ],
    '0.9.3_V2_32': [
        loader_mapping(
            [   # 093_200218_loader_exe_x86
                loader_offset(0x84, 0xd6)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_200330_loader_exe_x86
                loader_offset(0x84, 0xa6)
            ],
            '0.9.3_A'
        )
    ],
    '0.9.3_V3_32': [
        loader_mapping(
            [   # 093_210129_loader_exe_x86
                loader_offset(0x76, 0x82),
                loader_offset(0x77, 0x0c)
            ],
            '0.9.3'
        ),
        loader_mapping(
            [   # 093_211231_loader_exe_x86
                loader_offset(0x76, 0xbc),
                loader_offset(0x77, 0x0c)
            ],
            '0.9.3_F'
        ),
        loader_mapping(
            [   # 093_220104_loader_exe_x86
                loader_offset(0x76, 0x43),
                loader_offset(0x77, 0x11)
            ],
            '0.9.3_F'
        )
    ],
    '0.9.3_V4_32': [loader_mapping(None, '0.9.3_C')],
    '1.0_64': [loader_mapping(None, '1.0')],
    '1.0_32': [loader_mapping(None, '1.0')]
}

################################ Decryptor

class DonutDecryptorForMalcat():
    """Extractor/Decryptor for the donut binary obfuscator."""

    def _map_loader_to_instance(self, loader_mappings: List[loader_mapping]) -> Union[str, None]:
        if not loader_mappings:
            return None

        if len(loader_mappings) == 1:
            return loader_mappings[0].version

        # Have to read at least 0x70b (1803) bytes base on the largest
        # disambiguation offset listed in definitions.py
        loader_chunk = self.file.read(self.offset_loader_start, 1804)

        for mapping in loader_mappings:
            c = 0
            while c < len(mapping.offsets):
                if loader_chunk[mapping.offsets[c].pos] != mapping.offsets[c].value:
                    break
                c += 1
            if c == len(mapping.offsets):
                return mapping.version

    def __init__(self,
                 file: malcat.File,
                 loader_version: str,
                 bitness: int,
                 offset_loader: int) -> None:
        """Initialize a donut_decryptor.

        NOTE: donut_decryptor is not designed to be initialized directly. It's
        recommended to call donut_decryptor.find_donuts() instead.

        Args:
            self: Object instance
            file (malcat.File): File to parse
            version (str): A supported donut version string. Any of:
                        '1.0', '0.9.3', '0.9.2', '0.9.1', or '0.9'
            bitness (int): Indicates the bit width of the shellcode. Must be one of:
                        32 or 64
            offset_loader (int): Offset in file to start of the donut loader

        Returns:
            None
        """
        if bitness not in (32, 64):
            raise ValueError(f'Error: Unsupported bitness value provided: {bitness}')


        self.file = file
        self.offset_loader_start = offset_loader
        self.loader_version = loader_version + '_' + str(bitness)

        loader_mappings = loader_version_map.get(self.loader_version, None)

        if not loader_mappings:
            raise ValueError(f'Error: unsupported loader version: {self.loader_version}')

        self.instance_version = self._map_loader_to_instance(loader_mappings)

        print(f"Parsing donut with loader version: "
                    f"{self.loader_version}, and instance version: {self.instance_version}")

        if self.instance_version not in instance_offset_map:
            raise ValueError(f'Error: unsupported instance version: {self.instance_version}')

        self.offsets = instance_offset_map[self.instance_version]

    @classmethod
    def find_donuts(cls, file: malcat.File):
        """Find donuts in file.

        Class method to find donuts in `file` for donut shellcode and return a
        list of DonutDecryptor objects for each located Instance

        Args:
            cls : donut_decryptor class object
            file (malcat.File): the file to scan

        Returns:
            list[donut_dcryptor]: Contains one entry per unique instance
        """
        results = []
        found_x64_loader = False
        for loader_version, bitness, pattern_hex, pattern_b64 in DONUT_SIGNATURES:
            if bitness == 32 and found_x64_loader:
                continue
            off, sz = file.search_bytes(pattern_hex)
            if not sz:
                b64_off, b64_sz = file.search(pattern_b64)
                if b64_sz:
                    print("Warning: found unsupported instance format...skipping")
            if not sz:
                continue
            if bitness == 64:
                found_x64_loader = True
            results.append(DonutDecryptorForMalcat(file, loader_version, bitness, off))
        return results

    def _locate_instance(self) -> bytes:
        # Read file at least up to the instance offset
        b = self.file.read(0, self.offset_loader_start)

        print(f"Locating instance in file")
        # Search backwards for the 'pop rcx'
        instance_head = 0
        for x in range(len(b)-1, 0, -1):
            if b[x] == 0x59:
                instance_head = x
                print(f"Found instance preamble starting at: {hex(x)}")
                break
        # Search backwards for a 'call' instruction with offset to 'pop rcx'
        for x in range(instance_head, -1, -1):
            if b[x] == 0xe8:
                call_offset = struct.unpack_from('<I', b, x+1)[0]
                interval = instance_head - x
                if interval >= call_offset + 5 and interval - call_offset + 5 <= 16:
                    print(f"Found instance at: {hex(x+5)}")
                    return b[x+5:instance_head]

        raise ValueError(f"Failed to find instance in file")

    def _decrypt_instance(self) -> bool:
        raw_instance = self._locate_instance()

        self.entropy = None
        if 'entropy' in self.offsets:
            off = self.offsets['entropy']
            entropy = (
                struct.unpack_from(off.format, raw_instance, off.pos)[0]
            )
            if entropy and entropy <= len(ENTROPY_TYPES):
                self.entropy = ENTROPY_TYPES[entropy-1]
            elif entropy == 0:
                # Handling for anomalous value that can occur in 0.9.3_2 instances
                self.entropy = ENTROPY_TYPES[0]
            else:
                raise ValueError(f"Error: Invalid entropy type: {entropy}")
            print(f"Entropy type: {self.entropy}. Entropy enum: {entropy}")

        if self.entropy is None or self.entropy == 'DONUT_ENTROPY_DEFAULT':
            # Extract Key and Nonce from instance
            key_offset = self.offsets['instance_key']
            nonce_offset = self.offsets['instance_nonce']
            key = struct.unpack_from(key_offset.format,
                                     raw_instance,
                                     key_offset.pos)[0]
            nonce = struct.unpack_from(nonce_offset.format,
                                       raw_instance,
                                       nonce_offset.pos)[0]

            # Extract and decrypt cipher text from instance
            print(f"Decrypting instance of length: {len(raw_instance)}"
                         f"with {key} and nonce: {nonce}")

            dec = ChaskeyLTSDecrypt().run(raw_instance[self.offsets['encryption_start']:], key=key, counter=nonce)
            if not dec:
                return False

            self.instance = raw_instance[:self.offsets['encryption_start']] + dec
            return True
        else:
            print("No encryption, setting instance to raw instance")
            self.instance = raw_instance
            return True

    def _decompress_module(self) -> bytes:
        mod_data = self.instance[self.offsets['size_instance']:]
        if self.compression_type_name is not None:
            off = self.offsets['module_compressed_len']
            compressed_len = struct.unpack_from(off.format, self.instance, off.pos)[0]
            print(f"Decompressing compression_type: {self.compression_type_name}")
            if self.compression_type_name != "DONUT_COMPRESS_NONE":
                mod_data = mod_data[:compressed_len]
                if self.compression_type_name == 'DONUT_COMPRESS_APLIB':
                    mod_data = APLibDecompress().run(mod_data)
                elif self.compression_type_name == 'DONUT_COMPRESS_LZNT1':
                    mod_data = Lznt1Decompress().run(mod_data)
                elif self.compression_type_name == 'DONUT_COMPRESS_XPRESS':
                    mod_data = XpressLz77Decompress().run(mod_data)
                else:
                    raise ValueError(f"Unexpected compression_type encountered: {self.compression_type_name}")
        return mod_data

    def unpack(self) -> None:
        if not self._decrypt_instance():
            raise ValueError("Could not decrypt instance")

        inst_data = {}
        off = self.offsets['instance_type']
        instance_type = struct.unpack_from(off.format, self.instance, off.pos)[0]

        if instance_type <= len(INST_TYPES):
            instance_type_name = INST_TYPES[instance_type-1]
            inst_data['Instance Type'] = instance_type_name
        else:
            raise ValueError("Error: Instance type parsing failed")
        print(f"Got instance of type: {instance_type} , {instance_type_name}")

        if self.entropy:
            inst_data['Entropy Type'] = self.entropy

        if 'decoy_module' in self.offsets:
            off = self.offsets['decoy_module']
            decoy = struct.unpack_from(off.format, self.instance, off.pos)[0]
            inst_data['Decoy Module'] = decoy.decode().strip('\0')

        if instance_type_name == 'DONUT_INSTANCE_EMBED':
            # Get module information if type is DONUT_INSTANCE_EMBED
            off = self.offsets['module_type']
            module_type = (
                struct.unpack_from(off.format, self.instance, off.pos)[0]
            )
            if module_type <= len(MOD_TYPES):
                inst_data['Module Type'] = MOD_TYPES[module_type-1]
            else:
                raise ValueError("Error: module type parsing failed")
            # Compression added in 0.9.3, only output if offset is present
            if 'module_compression_type' not in self.offsets:
                self.compression_type_name = None
            else:
                off = self.offsets['module_compression_type']
                comp_type = (
                    struct.unpack_from(off.format, self.instance, off.pos)[0]
                )
                if comp_type <= len(COMP_TYPES):
                    self.compression_type_name = COMP_TYPES[comp_type-1]
                    print(f"Setting compression type to: {self.compression_type_name}")
                    inst_data['Compression Type'] = self.compression_type_name
                else:
                    raise ValueError("Error: module compression type parsing failed")
            return inst_data, self._decompress_module()

        elif instance_type_name in ['DONUT_INSTANCE_HTTP', 'DONUT_INSTANCE_DNS']:
            off = self.offsets['download_uri']
            uri = struct.unpack_from(off.format, self.instance, off.pos)[0]
            inst_data['Download URL'] = uri.decode().strip('\0')
            # Username and Password added in 1.0, only output if offset is
            # present
            if 'download_username' in self.offsets:
                off = self.offsets['download_username']
                username = struct.unpack_from(off.format,
                                              self.instance,
                                              off.pos)[0]
                inst_data['Download Username'] = username.decode().strip('\0')
            if 'download_password' in self.offsets:
                off = self.offsets['download_password']
                password = struct.unpack_from(off.format,
                                              self.instance,
                                              off.pos)[0]
                inst_data['Download Password'] = password.decode().strip('\0')

            off = self.offsets['module_key']
            mod_key = struct.unpack_from(off.format,
                                         self.instance,
                                         off.pos)[0]
            inst_data['Module Key'] = mod_key

            off = self.offsets['module_nonce']
            mod_nonce = (
                struct.unpack_from(off.format, self.instance, off.pos)[0]
            )
            inst_data['Module Nonce'] = mod_nonce
            return inst_data, None
        else:
            raise ValueError("Invalid instance type. Something went very wrong")
       


################################ MAIN
    
if __name__ == "__main__":
    if "analysis" in globals():
        gui_mode = True
    else:
        gui_mode = False
        # called in headless mode, we need to analyse a file first
        import optparse
        usage = "usage: %prog <file1>"
        parser = optparse.OptionParser(usage=usage, description="""Extract donut payload""")
        options, args = parser.parse_args()
        if len(args) != 1:
            parser.error("Please give path to a file")

        analysis = malcat.analyse(args[0])
    instances = DonutDecryptorForMalcat.find_donuts(analysis.file)
    if not instances:
        print("No known DonutLoader instance found")
    for instance in instances:
        try:
            instance_data, module = instance.unpack()
            print(f"INSTANCE_DATA = {json.dumps(instance_data, indent=4)}")
            if gui_mode and module is not None:
                gui.open_after(module, "donut_embedded")
            break
        except Exception as e:
            print(e)
