├── .gitignore ├── README.md ├── plugins ├── __init__.py ├── dumpcerts.py ├── pypykatz.py └── symcrypt.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | .vscode/ 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # volplugins 2 | Plugins for Volatility 3 written by Leron Gray (daddycocoaman) 3 | 4 | - **dumpcerts.Dumpcerts:** Extract RSA Certificates and PKCS #8 Keys by scanning ASN.1 structures 5 | - **pypykatz.Pypykatz:** Basically stolen and updated from https://github.com/skelsec/pypykatz 6 | - **symcrypt.Symcrypt:** Extract SymCrypt related structures -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daddycocoaman/volplugins/977bd3fea7f765b81122a3150cb32302a1a28080/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/dumpcerts.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from base64 import b64encode 3 | from struct import unpack 4 | from typing import Callable, Iterable, List, Tuple, Union 5 | 6 | import yara 7 | from cryptography.hazmat.backends import default_backend 8 | from cryptography.hazmat.primitives import hashes 9 | from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES 10 | from cryptography.hazmat.primitives.serialization import ( 11 | Encoding, 12 | NoEncryption, 13 | PrivateFormat, 14 | load_pem_private_key, 15 | ) 16 | from cryptography.utils import CryptographyDeprecationWarning 17 | from cryptography.x509 import Certificate, load_der_x509_certificate 18 | from volatility3.framework import interfaces, objects, renderers 19 | from volatility3.framework.configuration import requirements 20 | from volatility3.framework.renderers import format_hints 21 | from volatility3.plugins import yarascan 22 | from volatility3.plugins.windows import pslist, vadyarascan 23 | 24 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) 25 | 26 | 27 | class Dumpcerts(interfaces.plugins.PluginInterface): 28 | """Dump public and private RSA keys based on ASN-1 structure""" 29 | 30 | _required_framework_version = (2, 0, 0) 31 | _version = (1, 0, 0) 32 | 33 | @classmethod 34 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 35 | return [ 36 | requirements.ModuleRequirement( 37 | name="kernel", 38 | description="Windows kernel", 39 | architectures=["Intel32", "Intel64"], 40 | ), 41 | requirements.PluginRequirement( 42 | name="vadyarascanner", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) 43 | ), 44 | requirements.ListRequirement( 45 | name="pid", 46 | element_type=int, 47 | description="Process IDs to include (all other processes are excluded)", 48 | optional=True, 49 | ), 50 | requirements.ListRequirement( 51 | name="name", 52 | element_type=str, 53 | description="Process name to include (all other processes are excluded)", 54 | optional=True, 55 | ), 56 | requirements.ChoiceRequirement( 57 | ["all", "private", "public"], 58 | name="type", 59 | default="all", 60 | description="Types of keys to dump", 61 | optional=True, 62 | ), 63 | requirements.BooleanRequirement( 64 | name="dump", description="Dump keys", default=False, optional=True 65 | ), 66 | requirements.BooleanRequirement( 67 | name="physical", 68 | description="Scan physical memory instead of processes", 69 | default=False, 70 | optional=True, 71 | ), 72 | ] 73 | 74 | def _save_file(self, data: bytes, value: str, rule: str): 75 | ext = ".key" if rule == "pkcs" else ".crt" 76 | with self.open(f"{value}{ext}") as f: 77 | f.write(data) 78 | 79 | @classmethod 80 | def get_yara_rules(cls, key_type: str): 81 | sources = {} 82 | if key_type in ["all", "public"]: 83 | sources[ 84 | "x509" 85 | ] = "rule x509 {strings: $a = {30 82 ?? ?? 30 82 ?? ??} condition: $a}" 86 | 87 | if key_type in ["all", "private"]: 88 | sources[ 89 | "pkcs" 90 | ] = "rule pkcs {strings: $a = {30 82 ?? ?? 02 01 00} condition: $a}" 91 | 92 | return yara.compile(sources=sources) 93 | 94 | @classmethod 95 | def get_cert_or_pem( 96 | self, 97 | layer: interfaces.layers.DataLayerInterface, 98 | rule_name: str, 99 | offset: int, 100 | value: bytes, 101 | ) -> Union[Certificate, PRIVATE_KEY_TYPES]: 102 | """Parse the value from the layer and convert to an X509 cert or PEM. 103 | 104 | Args: 105 | layer (interfaces.layers.DataLayerInterface): Current layer 106 | rule_name (str): Rule that triggered the match 107 | offset (int): Offset address 108 | value (bytes): Bytes representing a certificate or pem 109 | 110 | Returns: 111 | Union[Certificate, PRIVATE_KEY_TYPES]: Either a Certificate or PEM type 112 | """ 113 | try: 114 | _, cert_size = unpack(">HH", value[0:4]) 115 | data = layer.read(offset, cert_size + 4) 116 | 117 | # If x509 triggered, try to create a DER x509 certificate to validate 118 | if rule_name == "x509": 119 | rsa_object = load_der_x509_certificate(data, default_backend()) 120 | 121 | # If pkcs triggered, try to create a PEM private key to validate 122 | elif rule_name == "pkcs": 123 | pem = ( 124 | b"-----BEGIN RSA PRIVATE KEY-----\n" 125 | + b64encode(data) 126 | + b"\n-----END RSA PRIVATE KEY-----" 127 | ) 128 | rsa_object = load_pem_private_key(pem, None, default_backend()) 129 | 130 | return rsa_object 131 | except: 132 | return None 133 | 134 | @classmethod 135 | def get_certs_by_process( 136 | cls, 137 | context: interfaces.context.ContextInterface, 138 | proc: interfaces.objects.ObjectInterface, 139 | key_types: str, 140 | ) -> Iterable[ 141 | Tuple[ 142 | int, 143 | interfaces.objects.ObjectInterface, 144 | str, 145 | Union[Certificate, PRIVATE_KEY_TYPES], 146 | ] 147 | ]: 148 | """Gets certificates or pem by process 149 | 150 | Args: 151 | context (interfaces.context.ContextInterface): Context 152 | proc (interfaces.objects.ObjectInterface): Process object to scan 153 | key_types (str): Type of key to scan for 154 | 155 | Yields: 156 | Iterable[ Tuple[ int, interfaces.objects.ObjectInterface, str, Union[Certificate, PRIVATE_KEY_TYPES], ] ]: Scan results 157 | """ 158 | layer_name = proc.add_process_layer() 159 | layer = context.layers[layer_name] 160 | 161 | for offset, rule_name, _, value in layer.scan( 162 | context=context, 163 | scanner=yarascan.YaraScanner(rules=cls.get_yara_rules(key_types)), 164 | sections=vadyarascan.VadYaraScan.get_vad_maps(proc), 165 | ): 166 | cert_or_pem = cls.get_cert_or_pem(layer, rule_name, offset, value) 167 | if cert_or_pem: 168 | yield (offset, proc, rule_name, cert_or_pem) 169 | 170 | @classmethod 171 | def get_process_certificates( 172 | cls, 173 | context: interfaces.context.ContextInterface, 174 | layer_name: str, 175 | symbol_table: str, 176 | filter_func: Callable[ 177 | [interfaces.objects.ObjectInterface], bool 178 | ] = lambda _: False, 179 | key_types: str = "all", 180 | ) -> Iterable[ 181 | Tuple[ 182 | int, 183 | interfaces.objects.ObjectInterface, 184 | str, 185 | Union[Certificate, PRIVATE_KEY_TYPES], 186 | ] 187 | ]: 188 | """Scans processes for RSA certificates 189 | 190 | Args: 191 | context: The context to retrieve required elements (layers, symbol tables) from 192 | layer_name: The name of the layer on which to operate 193 | symbol_table: The name of the table containing the kernel symbols 194 | filter_func: Filter function for listing processes 195 | key_types: Can be "all", "public", or "private" 196 | 197 | Yields: 198 | A tuple of offset, EPROCESS, rule name, and certificate or key found by scanning process layer 199 | """ 200 | for proc in pslist.PsList.list_processes( 201 | context=context, 202 | layer_name=layer_name, 203 | symbol_table=symbol_table, 204 | filter_func=filter_func, 205 | ): 206 | for offset, proc, rule_name, cert_or_pem in cls.get_certs_by_process( 207 | context, proc, key_types 208 | ): 209 | yield (offset, proc, rule_name, cert_or_pem) 210 | 211 | @classmethod 212 | def get_physical_certificates( 213 | cls, 214 | context: interfaces.context.ContextInterface, 215 | layer_name: str, 216 | key_type: str, 217 | ) -> Iterable[Tuple[int, str, Union[Certificate, PRIVATE_KEY_TYPES],]]: 218 | 219 | layer = context.layers[layer_name] 220 | for offset, rule_name, _, value in layer.scan( 221 | context=context, 222 | scanner=yarascan.YaraScanner(rules=cls.get_yara_rules(key_type)), 223 | ): 224 | cert_or_pem = cls.get_cert_or_pem(layer, rule_name, offset, value) 225 | if cert_or_pem: 226 | yield (offset, rule_name, cert_or_pem) 227 | 228 | def _generator(self, physical: bool): 229 | 230 | kernel = self.context.modules[self.config["kernel"]] 231 | pid_list = self.config.get("pid", []) 232 | name_list = self.config.get("name", []) 233 | 234 | if pid_list or name_list: 235 | filter_func = ( 236 | lambda proc: proc.UniqueProcessId not in pid_list 237 | and objects.utility.array_to_string(proc.ImageFileName).lower() 238 | not in name_list 239 | ) 240 | else: 241 | filter_func = lambda x: False 242 | 243 | key_type = self.config.get("type") 244 | output = self.config.get("dump", False) 245 | 246 | if physical: 247 | 248 | for offset, rule_name, cert_or_pem in self.get_physical_certificates( 249 | context=self.context, layer_name=kernel.layer_name, key_type=key_type 250 | ): 251 | value, output_bytes = self._get_value_and_bytes(rule_name, cert_or_pem) 252 | 253 | if output: 254 | self._save_file(output_bytes, proc_name, hex(offset), rule_name) 255 | 256 | yield 0, ( 257 | format_hints.Hex(offset), 258 | rule_name, 259 | value, 260 | ) 261 | else: 262 | for offset, proc, rule_name, rsa_object in self.get_process_certificates( 263 | context=self.context, 264 | layer_name=kernel.layer_name, 265 | symbol_table=kernel.symbol_table_name, 266 | filter_func=filter_func, 267 | key_types=key_type, 268 | ): 269 | 270 | proc_name = proc.ImageFileName.cast( 271 | "string", 272 | max_length=proc.ImageFileName.vol.count, 273 | errors="replace", 274 | ) 275 | 276 | value, output_bytes = self._get_value_and_bytes(rule_name, rsa_object) 277 | 278 | if output: 279 | self._save_file(output_bytes, value.split("_")[0], rule_name) 280 | 281 | yield 0, ( 282 | format_hints.Hex(offset), 283 | proc.UniqueProcessId, 284 | proc_name, 285 | rule_name, 286 | value, 287 | ) 288 | 289 | def _get_value_and_bytes( 290 | self, rule_name: str, rsa_object: Union[Certificate, PRIVATE_KEY_TYPES] 291 | ) -> Tuple[str, bytes]: 292 | """Helper method to get the value and bytes from a Certificate or PEM 293 | 294 | Args: 295 | rule_name (str): Rule that triggered the match 296 | rsa_object (Union[Certificate, PRIVATE_KEY_TYPES]): Certificate or PEM 297 | 298 | Returns: 299 | Tuple[str, bytes]: Value for output, output bytes for saving to file 300 | """ 301 | 302 | # If x509 triggered, value is equal to subject (or thumbprint if subject fails) 303 | if rule_name == "x509": 304 | 305 | value = "".join( 306 | "{:02X}".format(b) for b in rsa_object.fingerprint(hashes.SHA1()) 307 | ) 308 | try: 309 | value += "_" + str(rsa_object.subject.rfc4514_string()) 310 | except: 311 | pass 312 | 313 | output_bytes = rsa_object.public_bytes(Encoding.DER) 314 | 315 | # If pkcs triggered, value is equal to the key size 316 | elif rule_name == "pkcs": 317 | value = str(rsa_object.key_size) 318 | output_bytes = rsa_object.private_bytes( 319 | Encoding.DER, 320 | PrivateFormat.PKCS8, 321 | NoEncryption(), 322 | ) 323 | 324 | return (value, output_bytes) 325 | 326 | def run(self) -> renderers.TreeGrid: 327 | physical = self.config.get("physical") 328 | if physical: 329 | return renderers.TreeGrid( 330 | [ 331 | (f'{"Offset":<8}', format_hints.Hex), 332 | ("Rule", str), 333 | ("Value", str), 334 | ], 335 | self._generator(physical), 336 | ) 337 | else: 338 | return renderers.TreeGrid( 339 | [ 340 | (f'{"Offset":<8}', format_hints.Hex), 341 | ("PID", int), 342 | (f'{"Process":<8}', str), 343 | ("Rule", str), 344 | ("Value", str), 345 | ], 346 | self._generator(physical), 347 | ) 348 | -------------------------------------------------------------------------------- /plugins/pypykatz.py: -------------------------------------------------------------------------------- 1 | # 2 | # Author: 3 | # Tamas Jos (@skelsec) 4 | # Leron Gray (@daddycocoaman) 5 | # 6 | # Updated version of the pypykatz plugin for Volatility 3 7 | 8 | 9 | import logging 10 | from typing import List 11 | 12 | from volatility3.framework import interfaces, renderers 13 | from volatility3.framework.configuration import requirements 14 | from volatility3.plugins.windows import pslist 15 | 16 | from pypykatz.pypykatz import pypykatz as pparser 17 | 18 | vollog = logging.getLogger(__name__) 19 | 20 | 21 | class pypykatz(interfaces.plugins.PluginInterface): 22 | 23 | _required_framework_version = (2, 0, 0) 24 | 25 | @classmethod 26 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 27 | return [ 28 | requirements.TranslationLayerRequirement( 29 | name="primary", 30 | description="Memory layer for the kernel", 31 | architectures=["Intel32", "Intel64"], 32 | ), 33 | requirements.SymbolTableRequirement( 34 | name="nt_symbols", description="Windows kernel symbols" 35 | ), 36 | requirements.PluginRequirement( 37 | name="pslist", plugin=pslist.PsList, version=(2, 0, 0) 38 | ), 39 | ] 40 | 41 | def run(self): 42 | return pparser.go_volatility3(self) -------------------------------------------------------------------------------- /plugins/symcrypt.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from struct import unpack 3 | from typing import Dict, List 4 | 5 | import yara 6 | from construct import Array, Const, Hex, Int32ul, Int32sl, Int64ul, Struct, Union, this 7 | from cryptography.hazmat.primitives import hashes 8 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 9 | RSAPrivateNumbers, 10 | RSAPublicKey, 11 | RSAPublicNumbers, 12 | rsa_crt_dmp1, 13 | rsa_crt_dmq1, 14 | rsa_crt_iqmp, 15 | rsa_recover_prime_factors, 16 | ) 17 | from cryptography.hazmat.primitives.serialization import ( 18 | Encoding, 19 | PrivateFormat, 20 | NoEncryption, 21 | ) 22 | from cryptography.hazmat.primitives.serialization.pkcs12 import ( 23 | serialize_key_and_certificates, 24 | ) 25 | from cryptography.utils import CryptographyDeprecationWarning 26 | from cryptography.x509 import Certificate 27 | 28 | from volatility3.framework import interfaces, renderers 29 | from volatility3.framework.configuration import requirements 30 | from volatility3.framework.renderers import format_hints 31 | from volatility3.framework.symbols.windows.extensions import EPROCESS 32 | from volatility3.plugins import yarascan 33 | from volatility3.plugins.windows import pslist, vadyarascan 34 | 35 | from .dumpcerts import Dumpcerts 36 | 37 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) 38 | 39 | BCRYPT_RSAKEY = Struct( 40 | "Length" / Int32ul, 41 | "Magic" / Const(b"KRSM"), 42 | "Algid" / Int32ul, 43 | "ModBitLen" / Int32ul, 44 | "Unknown1" / Int32sl, 45 | "Unknown2" / Int32sl, 46 | "pAlg" / Hex(Int64ul), 47 | "pKey" / Hex(Int64ul), 48 | ) 49 | 50 | SYMCRYPT_RSAKEY = Struct( 51 | "cbTotalSize" / Int32ul, 52 | "hasPrivateKey" / Int32ul, 53 | "nSetBitsOfModulus" / Int32ul, 54 | "nBitsOfModulus" / Int32ul, 55 | "nDigitsOfModulus" / Int32ul, 56 | "nPubExp" / Int32ul, 57 | "nPrimes" / Int32ul, 58 | "nBitsOfPrimes" / Array(2, Int32ul), 59 | "nDigitsOfPrimes" / Array(2, Int32ul), 60 | "nMaxDigitsOfPrimes" / Array(1, Int32ul), 61 | "au64PubExp" / Hex(Int64ul), 62 | "pbPrimes" / Array(2, Hex(Int64ul)), 63 | "pbCrtInverses" / Array(2, Hex(Int64ul)), 64 | "pbPrivExps" / Array(1, Hex(Int64ul)), 65 | "pbCrtPrivExps" / Array(2, Hex(Int64ul)), 66 | "pmModulus" / Hex(Int64ul), 67 | "pmPrimes" / Array(2, Hex(Int64ul)), 68 | "peCrtInverses" / Array(2, Hex(Int64ul)), 69 | "piPrivExps" / Array(1, Hex(Int64ul)), 70 | "piCrtPrivExps" / Array(2, Hex(Int64ul)), 71 | "magic" / Hex(Int64ul), 72 | ) 73 | 74 | SYMCRYPT_MODULUS_MONTGOMERY = Struct("inv64" / Hex(Int64ul), "rsqr" / Hex(Int32ul)) 75 | SYMCRYPT_MODULUS_PSUEDOMERSENNE = Struct("k" / Int32ul) 76 | 77 | SYMCRYPT_INT = Struct( 78 | "type" / Int32ul, 79 | "nDigits" / Int32ul, 80 | "cbSize" / Int32ul, 81 | "magic" / Int64ul, 82 | "unknown1" / Int64ul, 83 | "unknown2" / Int32ul, 84 | "fdef" / Array((this.cbSize - 0x20) // 4, Int32ul), 85 | ) 86 | 87 | SYMCRYPT_DIVISOR = Struct( 88 | "type" / Int32ul, 89 | "nDigits" / Int32ul, 90 | "cbSize" / Int32ul, 91 | "nBits" / Int32ul, 92 | "magic" / Int64ul, 93 | "td" / Int64ul, 94 | "int" / SYMCRYPT_INT, 95 | ) 96 | 97 | SYMCRYPT_MODULUS = Struct( 98 | "type" / Int32ul, 99 | "nDigits" / Int32ul, 100 | "cbSize" / Int32ul, 101 | "flags" / Int32ul, 102 | "cbModElement" / Int32ul, 103 | "magic" / Int64ul, 104 | "tm" 105 | / Union( 106 | 0, 107 | "montgomery" / SYMCRYPT_MODULUS_MONTGOMERY, 108 | "pseudoMersenne" / SYMCRYPT_MODULUS_PSUEDOMERSENNE, 109 | ), 110 | "pUnknown" / Hex(Int64ul), 111 | "pUnknown2" / Hex(Int64ul), 112 | "pUnknown3" / Hex(Int64ul), 113 | "divisor" / SYMCRYPT_DIVISOR, 114 | ) 115 | 116 | 117 | # fmt: off 118 | def get_yara_rules(): 119 | sources = {} 120 | 121 | # Follow the start of the BCRYPT_RSAKEY Struct 122 | sources["symcrypt_rsa_key"] = "rule symcrypt_rsa_key {strings: $a = {28 00 00 00 4b 52 53 4d ?? ?? ?? ?? 00 (08 | 0c | 10) 00 00 01} condition: $a}" 123 | 124 | return yara.compile(sources=sources) 125 | # fmt: on 126 | 127 | 128 | class Symcrypt(interfaces.plugins.PluginInterface): 129 | """Dump symcrypt keys""" 130 | 131 | _required_framework_version = (2, 0, 0) 132 | _version = (1, 0, 0) 133 | 134 | @classmethod 135 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 136 | return [ 137 | requirements.ModuleRequirement( 138 | name="kernel", 139 | description="Kernel Layer", 140 | architectures=["Intel32", "Intel64"], 141 | ), 142 | requirements.PluginRequirement( 143 | name="vadyarascanner", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) 144 | ), 145 | requirements.PluginRequirement( 146 | name="dumpcerts", plugin=Dumpcerts, version=(1, 0, 0) 147 | ), 148 | requirements.ListRequirement( 149 | name="pid", 150 | element_type=int, 151 | description="Process IDs to include (all other processes are excluded)", 152 | optional=True, 153 | ), 154 | requirements.ListRequirement( 155 | name="name", 156 | element_type=str, 157 | description="Process name to include (all other processes are excluded)", 158 | optional=True, 159 | ), 160 | requirements.ListRequirement( 161 | name="matches", 162 | description="List of modulus to compare to start of private key to match", 163 | default=[], 164 | optional=True, 165 | ), 166 | requirements.BooleanRequirement( 167 | name="dump", 168 | description="Dump PFX when a match is found", 169 | default=False, 170 | optional=True, 171 | ), 172 | ] 173 | 174 | def _generator(self): 175 | 176 | kernel = self.context.modules[self.config["kernel"]] 177 | pid_list = self.config.get("pid", []) 178 | name_list = self.config.get("name", []) 179 | 180 | def proc_name_and_pid_filter(proc: EPROCESS): 181 | if not pid_list and not name_list: 182 | return False 183 | 184 | if proc.is_valid() and proc.UniqueProcessId: 185 | try: 186 | proc_name = proc.ImageFileName.cast( 187 | "string", 188 | max_length=proc.ImageFileName.vol.count, 189 | errors="ignore", 190 | ).lower() 191 | return ( 192 | proc.UniqueProcessId not in pid_list 193 | and proc_name not in name_list 194 | ) 195 | # Specifically, if there is a smear, process info might not be valid 196 | # So ignore if the process name can't be cast to anything 197 | except: 198 | return True 199 | else: 200 | return True 201 | 202 | filter_func = proc_name_and_pid_filter 203 | output = self.config.get("dump") 204 | modulus_matches = self.config.get("matches") 205 | 206 | # Get all the processes 207 | for proc in pslist.PsList.list_processes( 208 | context=self.context, 209 | layer_name=kernel.layer_name, 210 | symbol_table=kernel.symbol_table_name, 211 | filter_func=filter_func, 212 | ): 213 | # If process can't be added, just ignore it 214 | try: 215 | layer_name = proc.add_process_layer() 216 | except: 217 | continue 218 | 219 | layer = self.context.layers[layer_name] 220 | proc_name = proc.ImageFileName.cast( 221 | "string", 222 | max_length=proc.ImageFileName.vol.count, 223 | errors="replace", 224 | ) 225 | 226 | # This will hold all of the public certificates to match against if a hit occurs 227 | public_certs: Dict[str, Certificate] = {} 228 | 229 | for offset, rule_name, _, _ in layer.scan( 230 | context=self.context, 231 | scanner=yarascan.YaraScanner(rules=get_yara_rules()), 232 | sections=vadyarascan.VadYaraScan.get_vad_maps(proc), 233 | ): 234 | # If there is a match for key, then... 235 | # Get all of the public certs in the process 236 | # Keep track of modulus and cert object 237 | if not public_certs: 238 | for _, _, _, cert in Dumpcerts.get_certs_by_process( 239 | context=self.context, 240 | proc=proc, 241 | key_types="public", 242 | ): 243 | try: 244 | # If an RSA key, grab the modulus and convert it to a string 245 | if isinstance(cert.public_key(), RSAPublicKey): 246 | public_certs[ 247 | format(cert.public_key().public_numbers().n, "x") 248 | ] = cert 249 | except: 250 | continue 251 | 252 | # Make the bcrypt_RSA_KEY structure to get the pointer to key 253 | bcrypt_rsakey = BCRYPT_RSAKEY.parse(layer.read(offset, 0x28)) 254 | 255 | # Parse the key into a SYMCRYPT_RSA_KEY struct 256 | # If we can't, just move on 257 | try: 258 | key_total_size = unpack("I", layer.read(bcrypt_rsakey.pKey, 4))[0] 259 | key = SYMCRYPT_RSAKEY.parse( 260 | layer.read(bcrypt_rsakey.pKey, key_total_size) 261 | ) 262 | except Exception as e: 263 | yield 0, ( 264 | format_hints.Hex(offset), 265 | proc.UniqueProcessId, 266 | proc_name, 267 | rule_name, 268 | -1, 269 | f"Corrupt - {e}", 270 | ) 271 | continue 272 | 273 | # Get the cbSize of modulus (pmModulus + 8) then parse into Modulus struct 274 | modulus_size = unpack("I", layer.read(key.pmModulus + 8, 4))[0] 275 | modulus = SYMCRYPT_MODULUS.parse( 276 | layer.read(key.pmModulus, modulus_size) 277 | ) 278 | 279 | # Zfill is important here for alignment 280 | # Additionally, we have to read the list of integers (def) backwards 281 | mod_str = "".join( 282 | [format(i, "x").zfill(8) for i in modulus.divisor.int.fdef[::-1]] 283 | ) 284 | 285 | # Look for a matching cert in the process. If one is found, print thumbprint and subject 286 | match_string = "" 287 | 288 | matching_cert = public_certs.get(mod_str, None) 289 | if matching_cert: 290 | thumbprint = "".join( 291 | "{:02X}".format(b) 292 | for b in matching_cert.fingerprint(hashes.SHA1()) 293 | ) 294 | 295 | subject = matching_cert.subject.rfc4514_string() 296 | match_string = f"{thumbprint} -> {subject}" 297 | else: 298 | # If there's no matching cert, then print out first 20 bytes of modulus 299 | # Check to see if modulus matches what user requests 300 | if modulus_matches: 301 | for modulus in modulus_matches: 302 | if mod_str.upper().startswith(modulus.upper()): 303 | match_string = modulus 304 | 305 | # If there is a matching cert and there is a private key, make PFX or print pem! 306 | if key.hasPrivateKey and output: 307 | # fmt: off 308 | 309 | # We could write extra code to parse each of the primes but we need to get the private exponent (d) anyway 310 | # So less work to just pull private exponent and derive the primes from n,e,d 311 | private_exp_size = unpack("I", layer.read(key.piPrivExps[0] + 8, 4))[0] 312 | private_exp_modulus = SYMCRYPT_INT.parse( 313 | layer.read(key.piPrivExps[0], private_exp_size) 314 | ) 315 | private_exp_hexstr = "".join( 316 | [format(i, "x").zfill(8) for i in private_exp_modulus.fdef[::-1]] 317 | ) 318 | 319 | # Get p and q from modulus, public exponent, and private exponent 320 | d = int(private_exp_hexstr, 16) # Private exponent 321 | n = int(mod_str, 16) # Modulus 322 | e = key.au64PubExp # Public exponent 323 | p, q = rsa_recover_prime_factors(n, e, d) 324 | 325 | # fmt: on 326 | 327 | # We need to create the public numbers to pass to the private numbers 328 | # All of these numbers exist in the parsed Structs, but easier to call helper functions 329 | public_numbers = RSAPublicNumbers(e, n) 330 | private_numbers = RSAPrivateNumbers( 331 | p=p, 332 | q=q, 333 | d=d, 334 | dmp1=rsa_crt_dmp1(d, p), 335 | dmq1=rsa_crt_dmq1(d, q), 336 | iqmp=rsa_crt_iqmp(p, q), 337 | public_numbers=public_numbers, 338 | ) 339 | private_key = private_numbers.private_key() 340 | if matching_cert: 341 | pfx = serialize_key_and_certificates( 342 | thumbprint.encode(), 343 | private_key, 344 | matching_cert, 345 | None, 346 | NoEncryption(), 347 | ) 348 | filename = thumbprint + "_" 349 | filename += ( 350 | matching_cert.subject._attributes[-1] 351 | ._attributes[-1] 352 | .value.strip('*."/[]:;|,') 353 | .split("/")[0] 354 | ) 355 | with self.open(f"{filename}.pfx") as f: 356 | f.write(pfx) 357 | else: 358 | with self.open(f"{mod_str[:40].upper()}_modulus.key") as f: 359 | f.write( 360 | private_key.private_bytes( 361 | encoding=Encoding.DER, 362 | format=PrivateFormat.PKCS8, 363 | encryption_algorithm=NoEncryption(), 364 | ) 365 | ) 366 | 367 | yield 0, ( 368 | format_hints.Hex(offset), 369 | proc.UniqueProcessId, 370 | proc_name, 371 | rule_name, 372 | key.hasPrivateKey, 373 | mod_str[:40].upper(), 374 | match_string, 375 | ) 376 | 377 | def run(self) -> renderers.TreeGrid: 378 | return renderers.TreeGrid( 379 | [ 380 | (f'{"Offset":<8}', format_hints.Hex), 381 | ("PID", int), 382 | (f'{"Process":<8}', str), 383 | ("Rule", str), 384 | ("HasPrivateKey", int), 385 | ("Modulus (First 20 Bytes)", str), 386 | ("Matching", str), 387 | ], 388 | self._generator(), 389 | ) 390 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Required for plugins handling certificate objects 2 | cryptography>=37.0.1 3 | construct>=2.10.68 4 | pypykatz>=0.6.6 --------------------------------------------------------------------------------