├── dumpscan ├── kernel │ ├── __init__.py │ ├── plugins │ │ ├── __init__.py │ │ ├── symcrypt.py │ │ └── dumpcerts.py │ ├── filehandler.py │ ├── renderers.py │ └── vol.py ├── minidump │ ├── __init__.py │ ├── structs │ │ ├── common.py │ │ ├── MinidumpHeader.py │ │ ├── __init__.py │ │ ├── MinidumpMemoryList.py │ │ ├── MinidumpDirectory.py │ │ ├── MinidumpMemory64List.py │ │ └── MinidumpThreadList.py │ ├── constants.py │ └── minidumpfile.py ├── common │ ├── scanners │ │ ├── __init__.py │ │ ├── x509.py │ │ └── symcrypt.py │ ├── __init__.py │ ├── rules.py │ ├── output.py │ └── structs.py ├── __init__.py └── main.py ├── docs └── dumpscan.png ├── CHANGELOG.md ├── pyproject.toml ├── .gitignore ├── LICENSE ├── README.md └── poetry.lock /dumpscan/kernel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dumpscan/minidump/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dumpscan/common/scanners/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dumpscan/kernel/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/dumpscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daddycocoaman/dumpscan/HEAD/docs/dumpscan.png -------------------------------------------------------------------------------- /dumpscan/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .structs import * 2 | 3 | __all__ = [ 4 | "BCRYPT_RSAKEY", 5 | "SYMCRYPT_DIVISOR", 6 | "SYMCRYPT_INT", 7 | "SYMCRYPT_MODULUS", 8 | "SYMCRYPT_RSAKEY", 9 | ] 10 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/common.py: -------------------------------------------------------------------------------- 1 | from construct import Hex, Int16ul, Int32ul, Int64ul, Padding, Seek, Struct 2 | 3 | MINIDUMP_LOCATION_DESCRIPTOR = Struct("DataSize" / Int32ul, "Rva" / Hex(Int32ul)) 4 | MINIDUMP_LOCATION_DESCRIPTOR_64 = Struct("DataSize" / Int64ul, "Rva" / Hex(Int64ul)) 5 | 6 | UNICODE_STRING = Struct( 7 | "Length" / Int16ul, 8 | "MaximumLength" / Int16ul, 9 | Padding(4), 10 | "Buffer" / Hex(Int64ul), 11 | ) 12 | -------------------------------------------------------------------------------- /dumpscan/common/rules.py: -------------------------------------------------------------------------------- 1 | YARA_RULES = { 2 | # Locate x509 structures based on ASN.1 3 | "x509": { 4 | "x509": "rule x509 {strings: $a = {30 82 ?? ?? 30 82 ?? ??} condition: $a}", 5 | "pkcs": "rule pkcs {strings: $a = {30 82 ?? ?? 02 01 00} condition: $a}", 6 | }, 7 | # Locate RSA structs following "MSRK" magic header 8 | "symcrypt": { 9 | "rsa": "rule rsa {strings: $a = {28 00 00 00 4b 52 53 4d ?? ?? ?? ??} condition: $a}", 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/MinidumpHeader.py: -------------------------------------------------------------------------------- 1 | from construct import Const, FlagsEnum, Hex, Int16ul, Int32ul, Struct, Timestamp 2 | 3 | from ..constants import MINIDUMP_TYPE 4 | 5 | MINIDUMP_HEADER = Struct( 6 | "Signature" / Const(b"MDMP"), 7 | "Version" / Int16ul, 8 | "Implementation" / Int16ul, 9 | "NumberOfStreams" / Int32ul, 10 | "StreamDirectoryRva" / Int32ul, 11 | "Checksum" / Hex(Int32ul), 12 | "TimeDateStamp" / Timestamp(Int32ul, 1, 1970), 13 | "Flags" / FlagsEnum(Int32ul, MINIDUMP_TYPE), 14 | ) 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.1](https://github.com/daddycocoaman/dumpscan/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/daddycocoaman/dumpscan/compare/6bd8cdced2ec061ec436bac4a200526683fd0a1d...HEAD) 6 | 7 | **Merged pull requests:** 8 | 9 | - fix empty x509 list [\#2](https://github.com/daddycocoaman/dumpscan/pull/2) ([daddycocoaman](https://github.com/daddycocoaman)) 10 | 11 | 12 | 13 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 14 | -------------------------------------------------------------------------------- /dumpscan/kernel/filehandler.py: -------------------------------------------------------------------------------- 1 | from io import RawIOBase 2 | from pathlib import Path 3 | 4 | from volatility3.framework.interfaces.plugins import FileHandlerInterface 5 | 6 | 7 | class DumpscanFileHandler(FileHandlerInterface): 8 | """Handles writing files to disk""" 9 | 10 | output_dir: Path = Path(".") 11 | 12 | def __init__(self, filename: str) -> None: 13 | super().__init__(filename) 14 | if not self.output_dir.exists(): 15 | self.output_dir.mkdir(parents=True) 16 | 17 | def write(self, data: bytes) -> int | None: 18 | output_file: Path = self.output_dir / self._preferred_filename 19 | return output_file.write_bytes(data) 20 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import MINIDUMP_LOCATION_DESCRIPTOR, MINIDUMP_LOCATION_DESCRIPTOR_64 2 | from .MinidumpDirectory import MINIDUMP_DIRECTORY 3 | from .MinidumpHeader import MINIDUMP_HEADER 4 | from .MinidumpMemory64List import MINIDUMP_MEMORY64_LIST, MINIDUMP_MEMORY_DESCRIPTOR64 5 | from .MinidumpMemoryList import MINIDUMP_MEMORY_DESCRIPTOR, MINIDUMP_MEMORY_LIST 6 | 7 | __all__ = [ 8 | "MINIDUMP_DIRECTORY", 9 | "MINIDUMP_HEADER", 10 | "MINIDUMP_LOCATION_DESCRIPTOR", 11 | "MINIDUMP_LOCATION_DESCRIPTOR_64", 12 | "MINIDUMP_MEMORY64_LIST", 13 | "MINIDUMP_MEMORY_DESCRIPTOR", 14 | "MINIDUMP_MEMORY_DESCRIPTOR64", 15 | "MINIDUMP_MEMORY_LIST", 16 | ] 17 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/MinidumpMemoryList.py: -------------------------------------------------------------------------------- 1 | from construct import IfThenElse, Int32ul, Int64ul, Struct, this 2 | 3 | from .common import MINIDUMP_LOCATION_DESCRIPTOR, MINIDUMP_LOCATION_DESCRIPTOR_64 4 | 5 | MINIDUMP_MEMORY_DESCRIPTOR = Struct( 6 | "StartOfMemoryRange" / Int64ul, 7 | "MemoryLocation" 8 | / IfThenElse( 9 | this.StartOfMemoryRange < 0x100000000, 10 | MINIDUMP_LOCATION_DESCRIPTOR, 11 | MINIDUMP_LOCATION_DESCRIPTOR_64, 12 | ), 13 | "MemoryLocation" / MINIDUMP_LOCATION_DESCRIPTOR, 14 | ) 15 | 16 | MINIDUMP_MEMORY_LIST = Struct( 17 | "NumberOfMemoryRanges" / Int32ul, 18 | # "MemoryRanges" 19 | # / (MINIDUMP_MEMORY_DESCRIPTOR)[this.NumberOfMemoryRanges] 20 | # * _track_rva_addr, 21 | ) 22 | -------------------------------------------------------------------------------- /dumpscan/common/output.py: -------------------------------------------------------------------------------- 1 | from rich import box 2 | from rich.console import Console 3 | from rich.table import Table 4 | from rich.theme import Theme 5 | from rich.style import Style 6 | 7 | theme = Theme( 8 | { 9 | "repr.attrib_value": "#047bd6", 10 | "repr.call": Style.null(), 11 | "repr.ipv6 ": "#008df8", 12 | "repr.none": "italic #d24a00", 13 | "repr.number": "#008df8", 14 | "repr.str": "bright_blue", 15 | "table.border": "#0084a8", 16 | "table.cell": "#00a5fa", 17 | "table.header": "bold italic #d24a00", 18 | } 19 | ) 20 | 21 | 22 | def get_dumpscan_console(): 23 | return Console(theme=theme) 24 | 25 | 26 | def get_dumpscan_table(): 27 | return Table( 28 | expand=False, box=box.SIMPLE_HEAVY, highlight=True, border_style="#047bd6" 29 | ) 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "dumpscan" 3 | version = "0.1.1" 4 | description = "Scanning memory dumps for secrets using volatility and yara" 5 | authors = ["Leron Gray "] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | typer = "^0.4.1" 12 | rich = "^12.3.0" 13 | yara-python = ">=3.8.0" 14 | cryptography = "^37.0.2" 15 | cffi = "^1.15.0" 16 | construct = "^2.10.68" 17 | rich-click = {extras = ["typer"], version = "^1.4"} 18 | pycryptodome = "^3.14.1" 19 | jsonschema = "^4.5.1" 20 | capstone = "^5.0.0rc2" 21 | arrow = "^1.2.2" 22 | pefile = "^2022.5.30" 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | isort = "^5.10.1" 26 | black = "^22.3.0" 27 | volatility3 = {git = "https://github.com/volatilityfoundation/volatility3", rev = "develop"} 28 | 29 | [tool.poetry.scripts] 30 | dumpscan = "dumpscan.main:app" 31 | 32 | [tool.isort] 33 | profile = "black" 34 | 35 | [build-system] 36 | requires = ["poetry-core>=1.1.0a6"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/MinidumpDirectory.py: -------------------------------------------------------------------------------- 1 | import construct 2 | from construct import Int32ul, Pointer, Struct, Switch, this 3 | 4 | from ..constants import MINIDUMP_STREAM_TYPE 5 | from .common import MINIDUMP_LOCATION_DESCRIPTOR 6 | from .MinidumpMemory64List import MINIDUMP_MEMORY64_LIST 7 | from .MinidumpMemoryList import MINIDUMP_MEMORY_LIST 8 | from .MinidumpThreadList import MINIDUMP_THREAD_LIST 9 | 10 | MINIDUMP_DIRECTORY = Struct( 11 | "StreamType" / construct.Enum(Int32ul, MINIDUMP_STREAM_TYPE), 12 | "Location" / MINIDUMP_LOCATION_DESCRIPTOR, 13 | "Data" 14 | / Pointer( 15 | this.Location.Rva, 16 | Switch( 17 | lambda this: int(this.StreamType), 18 | { 19 | MINIDUMP_STREAM_TYPE.MemoryListStream.value: MINIDUMP_MEMORY_LIST, 20 | MINIDUMP_STREAM_TYPE.Memory64ListStream.value: MINIDUMP_MEMORY64_LIST, 21 | MINIDUMP_STREAM_TYPE.ThreadListStream.value: MINIDUMP_THREAD_LIST, 22 | }, 23 | default=None, 24 | ), 25 | ), 26 | ) 27 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/MinidumpMemory64List.py: -------------------------------------------------------------------------------- 1 | from itertools import accumulate 2 | 3 | import construct 4 | from construct import Hex, Int64ul, Pass, Struct, this 5 | 6 | 7 | def _track_rva_addr(memranges, ctx): 8 | """Adds the RVA to each range enumerated""" 9 | 10 | # Starting with the BaseRva, we need to keep track of each DataSize to know where the 11 | # StartOfMemoryRange maps to the RVA in the minidump 12 | rva_list = list( 13 | accumulate([memrange.DataSize for memrange in memranges], initial=ctx.BaseRva) 14 | ) 15 | for index, memrange in enumerate(memranges): 16 | memrange.DumpRva = rva_list[index] 17 | # print(memrange.Rva, memrange.DataSize) 18 | 19 | 20 | MINIDUMP_MEMORY_DESCRIPTOR64 = Struct( 21 | "StartOfMemoryRange" / Hex(Int64ul), 22 | "DataSize" / Hex(Int64ul), 23 | "DumpRva" / Pass, # We add Pass here as a placeholder for _track_rva_addr 24 | ) 25 | 26 | MINIDUMP_MEMORY64_LIST = Struct( 27 | "NumberOfMemoryRanges" / Int64ul, 28 | "BaseRva" / Hex(Int64ul), 29 | "MemoryRanges" 30 | / (MINIDUMP_MEMORY_DESCRIPTOR64)[this.NumberOfMemoryRanges] 31 | * _track_rva_addr, 32 | ) 33 | -------------------------------------------------------------------------------- /dumpscan/minidump/structs/MinidumpThreadList.py: -------------------------------------------------------------------------------- 1 | from construct import Hex, Int32ul, Int64ul, Padding, Pointer, Seek, Struct, this 2 | 3 | from .common import ( 4 | MINIDUMP_LOCATION_DESCRIPTOR, 5 | MINIDUMP_LOCATION_DESCRIPTOR_64, 6 | UNICODE_STRING, 7 | ) 8 | from .MinidumpMemoryList import MINIDUMP_MEMORY_DESCRIPTOR 9 | from .MinidumpMemory64List import MINIDUMP_MEMORY_DESCRIPTOR64 10 | 11 | MINIDUMP_PEB = Struct("Padding" / Padding(0x20), "ProcessParameters" / Hex(Int64ul)) 12 | MINIDUMP_TEB = Struct("Padding" / Padding(0x60), "PEB" / Hex(Int64ul)) 13 | 14 | MINIDUMP_PROCESS_PARAMETERS = Struct( 15 | "Padding" / Padding(0x50), 16 | "DllPath" / UNICODE_STRING, 17 | "ImagePathName" / UNICODE_STRING, 18 | "CommandLine" / UNICODE_STRING, 19 | "Environment" / Hex(Int64ul), 20 | ) 21 | 22 | MINIDUMP_THREAD = Struct( 23 | "ThreadId" / Hex(Int32ul), 24 | "SuspendCount" / Int32ul, 25 | "PriorityClass" / Int32ul, 26 | "Priority" / Int32ul, 27 | "Teb" / Hex(Int64ul), 28 | "Stack" / MINIDUMP_MEMORY_DESCRIPTOR64, 29 | "ThreadContext" / MINIDUMP_LOCATION_DESCRIPTOR, 30 | ) 31 | 32 | MINIDUMP_THREAD_LIST = Struct( 33 | "NumberOfThreads" / Int32ul, "Threads" / MINIDUMP_THREAD[this.NumberOfThreads] 34 | ) 35 | -------------------------------------------------------------------------------- /dumpscan/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | 4 | import rich_click as click 5 | from cryptography.utils import CryptographyDeprecationWarning 6 | from rich import traceback 7 | from rich.logging import RichHandler 8 | 9 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) 10 | traceback.install(show_locals=True) 11 | 12 | logging.basicConfig( 13 | level="INFO", 14 | format="%(message)s", 15 | datefmt="[%X]", 16 | handlers=[RichHandler(rich_tracebacks=True, show_path=False, show_time=False)], 17 | ) 18 | 19 | # ***** RICH CLICK STYLE ***** 20 | click.rich_click.MAX_WIDTH = 100 21 | # click.rich_click.USE_RICH_MARKUP = True 22 | click.rich_click.USE_MARKDOWN = True 23 | click.rich_click.SHOW_METAVARS_COLUMN = True 24 | click.rich_click.APPEND_METAVARS_HELP = False 25 | 26 | click.rich_click.STYLE_HELPTEXT_FIRST_LINE = "#d24a00" 27 | 28 | click.rich_click.STYLE_OPTION = "#f9c300" 29 | click.rich_click.STYLE_OPTIONS_TABLE_BOX = "SIMPLE" 30 | click.rich_click.STYLE_OPTIONS_PANEL_BORDER = "bold #0084a8" 31 | click.rich_click.STYLE_OPTIONS_TABLE_ROW_STYLES = ["#f9c300"] 32 | 33 | click.rich_click.STYLE_COMMANDS_TABLE_BOX = "SIMPLE" 34 | click.rich_click.STYLE_COMMANDS_PANEL_BORDER = "bold #0084a8" 35 | click.rich_click.STYLE_COMMANDS_TABLE_ROW_STYLES = ["#f9c300"] 36 | 37 | click.rich_click.STYLE_USAGE = "bold #0084a8" 38 | click.rich_click.STYLE_USAGE_COMMAND = "#d24a00 italic" 39 | -------------------------------------------------------------------------------- /dumpscan/common/structs.py: -------------------------------------------------------------------------------- 1 | from construct import Array, Const, Hex, Int32sl, Int32ul, Int64ul, Struct, Union, this 2 | 3 | BCRYPT_RSAKEY = Struct( 4 | "Length" / Int32ul, 5 | "Magic" / Const(b"KRSM"), 6 | "Algid" / Hex(Int32ul), 7 | "ModBitLen" / Int32ul, 8 | "Unknown1" / Int32sl, 9 | "Unknown2" / Int32sl, 10 | "pAlg" / Hex(Int64ul), 11 | "pKey" / Hex(Int64ul), 12 | ) 13 | 14 | SYMCRYPT_RSAKEY = Struct( 15 | "cbTotalSize" / Int32ul, 16 | "hasPrivateKey" / Int32ul, 17 | "nSetBitsOfModulus" / Int32ul, 18 | "nBitsOfModulus" / Int32ul, 19 | "nDigitsOfModulus" / Int32ul, 20 | "nPubExp" / Int32ul, 21 | "nPrimes" / Int32ul, 22 | "nBitsOfPrimes" / Array(2, Int32ul), 23 | "nDigitsOfPrimes" / Array(2, Int32ul), 24 | "nMaxDigitsOfPrimes" / Array(1, Int32ul), 25 | "au64PubExp" / Hex(Int64ul), 26 | "pbPrimes" / Array(2, Hex(Int64ul)), 27 | "pbCrtInverses" / Array(2, Hex(Int64ul)), 28 | "pbPrivExps" / Array(1, Hex(Int64ul)), 29 | "pbCrtPrivExps" / Array(2, Hex(Int64ul)), 30 | "pmModulus" / Hex(Int64ul), 31 | "pmPrimes" / Array(2, Hex(Int64ul)), 32 | "peCrtInverses" / Array(2, Hex(Int64ul)), 33 | "piPrivExps" / Array(1, Hex(Int64ul)), 34 | "piCrtPrivExps" / Array(2, Hex(Int64ul)), 35 | "magic" / Hex(Int64ul), 36 | ) 37 | 38 | SYMCRYPT_MODULUS_MONTGOMERY = Struct("inv64" / Hex(Int64ul), "rsqr" / Hex(Int32ul)) 39 | SYMCRYPT_MODULUS_PSUEDOMERSENNE = Struct("k" / Int32ul) 40 | 41 | SYMCRYPT_INT = Struct( 42 | "type" / Int32ul, 43 | "nDigits" / Int32ul, 44 | "cbSize" / Int32ul, 45 | "magic" / Int64ul, 46 | "unknown1" / Int64ul, 47 | "unknown2" / Int32ul, 48 | "fdef" / Array((this.cbSize - 0x20) // 4, Int32ul), 49 | ) 50 | 51 | SYMCRYPT_DIVISOR = Struct( 52 | "type" / Int32ul, 53 | "nDigits" / Int32ul, 54 | "cbSize" / Int32ul, 55 | "nBits" / Int32ul, 56 | "magic" / Int64ul, 57 | "td" / Int64ul, 58 | "int" / SYMCRYPT_INT, 59 | ) 60 | 61 | SYMCRYPT_MODULUS = Struct( 62 | "type" / Int32ul, 63 | "nDigits" / Int32ul, 64 | "cbSize" / Int32ul, 65 | "flags" / Int32ul, 66 | "cbModElement" / Int32ul, 67 | "magic" / Int64ul, 68 | "tm" 69 | / Union( 70 | 0, 71 | "montgomery" / SYMCRYPT_MODULUS_MONTGOMERY, 72 | "pseudoMersenne" / SYMCRYPT_MODULUS_PSUEDOMERSENNE, 73 | ), 74 | "pUnknown" / Hex(Int64ul), 75 | "pUnknown2" / Hex(Int64ul), 76 | "pUnknown3" / Hex(Int64ul), 77 | "divisor" / SYMCRYPT_DIVISOR, 78 | ) 79 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode/ 132 | 133 | samples/ 134 | certs/ -------------------------------------------------------------------------------- /dumpscan/minidump/constants.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | # Thanks to Skelsec - https://github.com/skelsec/minidump/blob/4945b1011ac202c58003b0198820bc8521eb5af5/minidump/constants.py 5 | class MINIDUMP_STREAM_TYPE(enum.IntEnum): 6 | """Enum for Minidump stream types""" 7 | 8 | UnusedStream = 0 9 | ReservedStream0 = 1 10 | ReservedStream1 = 2 11 | ThreadListStream = 3 12 | ModuleListStream = 4 13 | MemoryListStream = 5 14 | ExceptionStream = 6 15 | SystemInfoStream = 7 16 | ThreadExListStream = 8 17 | Memory64ListStream = 9 18 | CommentStreamA = 10 19 | CommentStreamW = 11 20 | HandleDataStream = 12 21 | FunctionTableStream = 13 22 | UnloadedModuleListStream = 14 23 | MiscInfoStream = 15 24 | MemoryInfoListStream = 16 25 | ThreadInfoListStream = 17 26 | HandleOperationListStream = 18 27 | TokenStream = 19 28 | JavaScriptDataStream = 20 29 | SystemMemoryInfoStream = 21 30 | ProcessVmCountersStream = 22 31 | ThreadNamesStream = 24 32 | ceStreamNull = 0x8000 33 | ceStreamSystemInfo = 0x8001 34 | ceStreamException = 0x8002 35 | ceStreamModuleList = 0x8003 36 | ceStreamProcessList = 0x8004 37 | ceStreamThreadList = 0x8005 38 | ceStreamThreadContextList = 0x8006 39 | ceStreamThreadCallStackList = 0x8007 40 | ceStreamMemoryVirtualList = 0x8008 41 | ceStreamMemoryPhysicalList = 0x8009 42 | ceStreamBucketParameters = 0x800A 43 | ceStreamProcessModuleMap = 0x800B 44 | ceStreamDiagnosisList = 0x800C 45 | LastReservedStream = 0xFFFF 46 | 47 | 48 | # Thanks to Skelsec - https://github.com/skelsec/minidump/blob/4945b1011ac202c58003b0198820bc8521eb5af5/minidump/constants.py 49 | class MINIDUMP_TYPE(enum.IntFlag): 50 | """Enum for Minidump types""" 51 | 52 | MiniDumpNormal = 0x00000000 53 | MiniDumpWithDataSegs = 0x00000001 54 | MiniDumpWithFullMemory = 0x00000002 55 | MiniDumpWithHandleData = 0x00000004 56 | MiniDumpFilterMemory = 0x00000008 57 | MiniDumpScanMemory = 0x00000010 58 | MiniDumpWithUnloadedModules = 0x00000020 59 | MiniDumpWithIndirectlyReferencedMemory = 0x00000040 60 | MiniDumpFilterModulePaths = 0x00000080 61 | MiniDumpWithProcessThreadData = 0x00000100 62 | MiniDumpWithPrivateReadWriteMemory = 0x00000200 63 | MiniDumpWithoutOptionalData = 0x00000400 64 | MiniDumpWithFullMemoryInfo = 0x00000800 65 | MiniDumpWithThreadInfo = 0x00001000 66 | MiniDumpWithCodeSegs = 0x00002000 67 | MiniDumpWithoutAuxiliaryState = 0x00004000 68 | MiniDumpWithFullAuxiliaryState = 0x00008000 69 | MiniDumpWithPrivateWriteCopyMemory = 0x00010000 70 | MiniDumpIgnoreInaccessibleMemory = 0x00020000 71 | MiniDumpWithTokenInformation = 0x00040000 72 | MiniDumpWithModuleHeaders = 0x00080000 73 | MiniDumpFilterTriage = 0x00100000 74 | MiniDumpValidTypeFlags = 0x001FFFFF 75 | -------------------------------------------------------------------------------- /dumpscan/kernel/renderers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | 4 | from rich import box 5 | from rich.console import Console 6 | from rich.table import Table 7 | from volatility3.cli.text_renderer import QuickTextRenderer 8 | from volatility3.framework import interfaces 9 | from volatility3.framework.renderers import format_hints 10 | 11 | from ..common.output import get_dumpscan_console, get_dumpscan_table 12 | 13 | log = logging.getLogger("rich") 14 | 15 | 16 | class RichTableRenderer(QuickTextRenderer): 17 | 18 | name = "richtable" 19 | 20 | def render(self, grid: interfaces.renderers.TreeGrid) -> Table: 21 | """Renders each column using Rich Table output. 22 | Args: 23 | grid: The TreeGrid object to render 24 | """ 25 | 26 | table = get_dumpscan_table() 27 | [table.add_column(c.name, overflow="fold") for c in grid.columns] 28 | 29 | # This function doesn't need to return anything at all and just updates existing Table object 30 | def visitor(node: interfaces.renderers.TreeNode, accumulator: None) -> None: 31 | row = [] 32 | for column_index in range(len(grid.columns)): 33 | column = grid.columns[column_index] 34 | renderer = self._type_renderers.get( 35 | column.type, self._type_renderers["default"] 36 | ) 37 | column_rich_text = renderer(node.values[column_index]) 38 | if column_index == 0: 39 | column_str = "|" if node.path_depth - 1 else "" 40 | row.append( 41 | column_str + "-" * (node.path_depth - 1) + str(column_rich_text) 42 | ) 43 | else: 44 | row.append(str(column_rich_text)) 45 | table.add_row(*row) 46 | 47 | if not grid.populated: 48 | grid.populate(visitor, None) 49 | else: 50 | grid.visit(node=None, function=visitor, initial_accumulator=None) 51 | 52 | return table 53 | 54 | 55 | class RichTextRenderer(QuickTextRenderer): 56 | 57 | name = "richtext" 58 | 59 | def render(self, grid: interfaces.renderers.TreeGrid) -> str: 60 | """Renders each column using Rich Table output. 61 | Args: 62 | grid: The TreeGrid object to render 63 | """ 64 | 65 | console = get_dumpscan_console() 66 | 67 | line = [] 68 | for column in grid.columns: 69 | # Ignore the type because namedtuples don't realize they have accessible attributes 70 | line.append(f"{column.name}") 71 | console.print("\n{}".format("\t".join(line))) 72 | 73 | def visitor(node: interfaces.renderers.TreeNode, accumulator: Console): 74 | # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case 75 | accumulator.print( 76 | "*" * max(0, node.path_depth - 1) 77 | + ("" if (node.path_depth <= 1) else " ") 78 | ) 79 | line = [] 80 | for column_index in range(len(grid.columns)): 81 | column = grid.columns[column_index] 82 | renderer = self._type_renderers.get( 83 | column.type, self._type_renderers["default"] 84 | ) 85 | line.append(renderer(node.values[column_index])) 86 | accumulator.print("{}".format("\t".join(line)), end="") 87 | accumulator.file.flush() 88 | return accumulator 89 | 90 | if not grid.populated: 91 | grid.populate(visitor, console) 92 | else: 93 | grid.visit(node=None, function=visitor, initial_accumulator=console) 94 | 95 | console.print() 96 | 97 | 98 | class RichRenderOption(str, Enum): 99 | TABLE = "table" 100 | TEXT = "text" 101 | 102 | def __init__(self, render: str) -> None: 103 | if render == "table": 104 | self.renderer = RichTableRenderer() 105 | else: 106 | self.renderer = RichTextRenderer() 107 | -------------------------------------------------------------------------------- /dumpscan/kernel/vol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | from pathlib import Path 4 | from typing import List 5 | 6 | from rich import inspect, print 7 | from volatility3 import framework, plugins 8 | from volatility3.framework import ( 9 | automagic, 10 | constants, 11 | contexts, 12 | interfaces, 13 | objects, 14 | renderers, 15 | ) 16 | from volatility3.plugins.linux.pslist import PsList as NixPsList 17 | from volatility3.plugins.mac.pslist import PsList as MacPsList 18 | from volatility3.plugins.windows.pslist import PsList as WinPsList 19 | 20 | from .filehandler import DumpscanFileHandler 21 | from .plugins.dumpcerts import Dumpcerts 22 | from .plugins.symcrypt import Symcrypt 23 | from .renderers import RichRenderOption 24 | 25 | from volatility3.plugins.windows.envars import Envars # isort: skip 26 | from volatility3.plugins.windows.cmdline import CmdLine 27 | 28 | log = logging.getLogger("rich") 29 | 30 | 31 | class OS(str, Enum): 32 | WINDOWS = "windows" 33 | LINUX = "linux" 34 | MAC = "mac" 35 | 36 | 37 | PSLIST_PLUGINS = {OS.WINDOWS: WinPsList, OS.LINUX: NixPsList, OS.MAC: MacPsList} 38 | 39 | 40 | class Volatility: 41 | def __init__( 42 | self, dumpfile: Path, renderer: RichRenderOption, output_dir: Path 43 | ) -> None: 44 | # Ensure minimum framework version 45 | framework.require_interface_version(2, 0, 0) 46 | 47 | self.dumpfile = dumpfile 48 | self.ctx = contexts.Context() 49 | self.ctx.config[ 50 | "automagic.LayerStacker.single_location" 51 | ] = self.dumpfile.as_uri() 52 | self.automagics = automagic.available(self.ctx) 53 | 54 | # Add the plugins path to framework plugins path 55 | plugins.__path__ = [ 56 | str(Path(__file__).parent / "plugins") 57 | ] + constants.PLUGINS_PATH 58 | framework.import_files(plugins, True) 59 | 60 | # Add a file handler 61 | self.file_handler = DumpscanFileHandler 62 | if output_dir: 63 | self.file_handler.output_dir = output_dir 64 | 65 | # Set the render mode 66 | self.render_mode = renderer 67 | 68 | def _run_plugin(self, plugin: interfaces.plugins.PluginInterface): 69 | """Runs a plugin""" 70 | automagics = automagic.choose_automagic(self.automagics, plugin) 71 | plugin = framework.plugins.construct_plugin( 72 | self.ctx, automagics, plugin, "plugins", None, self.file_handler 73 | ) 74 | log.debug("Context config:", self.ctx.config) 75 | 76 | results: renderers.TreeGrid = plugin.run() 77 | return self.render_mode.renderer.render(results) 78 | 79 | def run_x509(self, pids: List[int], procnames: List[str], dump: bool): 80 | """Runs dumpcerts plugin on a kernel dump 81 | 82 | Args: 83 | pids (List[int]): List of pids to filter on 84 | """ 85 | 86 | self.ctx.config["plugins.Dumpcerts.pid"] = pids 87 | self.ctx.config["plugins.Dumpcerts.name"] = procnames 88 | self.ctx.config["plugins.Dumpcerts.dump"] = dump 89 | 90 | return self._run_plugin(Dumpcerts) 91 | 92 | def run_symcrypt(self, pids: List[int], procnames: List[str], dump: bool): 93 | """Runs symcrypt plugin on a kernel dump 94 | 95 | Args: 96 | pids (List[int]): List of pids to filter on 97 | """ 98 | 99 | self.ctx.config["plugins.Symcrypt.pid"] = pids 100 | self.ctx.config["plugins.Symcrypt.name"] = procnames 101 | self.ctx.config["plugins.Symcrypt.dump"] = dump 102 | 103 | return self._run_plugin(Symcrypt) 104 | 105 | def run_pslist(self, os: OS): 106 | """Runs the pslist plugin for appropriate operating system 107 | 108 | Args: 109 | os (OS): Operating System 110 | """ 111 | pslist_plugin = PSLIST_PLUGINS.get(os) 112 | return self._run_plugin(pslist_plugin) 113 | 114 | def run_envar(self, pids: List[int]): 115 | """Runs envar plugin on a kernel dump 116 | 117 | Args: 118 | pids (List[int]): List of pids to filter on 119 | """ 120 | self.ctx.config["plugins.Envars.pid"] = pids 121 | 122 | return self._run_plugin(Envars) 123 | 124 | def run_cmdline(self, pids: List[int]): 125 | """Runs cmdline plugin on a kernel dump 126 | 127 | Args: 128 | pids (List[int]): List of pids to filter on 129 | """ 130 | self.ctx.config["plugins.CmdLine.pid"] = pids 131 | 132 | return self._run_plugin(CmdLine) 133 | -------------------------------------------------------------------------------- /dumpscan/minidump/minidumpfile.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from pathlib import Path 3 | from struct import unpack 4 | from typing import Dict, Generator, Tuple, TypeVar 5 | 6 | import construct 7 | import yara 8 | from construct import RepeatUntil, Seek, Struct, this, CString, Array 9 | 10 | from dumpscan.minidump.structs.MinidumpThreadList import ( 11 | MINIDUMP_PEB, 12 | MINIDUMP_PROCESS_PARAMETERS, 13 | MINIDUMP_TEB, 14 | ) 15 | 16 | from ..common.rules import YARA_RULES 17 | from .constants import MINIDUMP_STREAM_TYPE 18 | from .structs import * 19 | 20 | T = TypeVar("T") 21 | 22 | MINIDUMP_FILE = Struct( 23 | "Header" / MINIDUMP_HEADER, 24 | Seek(this.Header.StreamDirectoryRva), 25 | "Dirs" / construct.Array(this.Header.NumberOfStreams, MINIDUMP_DIRECTORY), 26 | ) 27 | 28 | 29 | class MinidumpFile: 30 | """Minidump class that parses Windows Minidump format""" 31 | 32 | def __init__(self, filepath: Path): 33 | self.file = filepath 34 | self.dump = MINIDUMP_FILE.parse_file(str(filepath.absolute())) 35 | self._memory_list = self._get_dir(MINIDUMP_STREAM_TYPE.MemoryListStream) 36 | self._memory64_list = self._get_dir(MINIDUMP_STREAM_TYPE.Memory64ListStream) 37 | self.thread_list = self._get_dir(MINIDUMP_STREAM_TYPE.ThreadListStream) 38 | self.memory = self._memory_list or self._memory64_list 39 | 40 | def _get_dir(self, dir_type: T) -> T | None: 41 | return next( 42 | (dir for dir in self.dump.Dirs if int(dir.StreamType) == dir_type), 43 | None, 44 | ) 45 | 46 | def find_section(self, address: int): 47 | """Finds the section of memory where the address lives""" 48 | 49 | for section in self.memory.Data.MemoryRanges: 50 | if ( 51 | int(section.StartOfMemoryRange) 52 | <= address 53 | <= int(section.StartOfMemoryRange) + int(section.DataSize) 54 | ): 55 | return section 56 | return None 57 | 58 | def read_all_memory64( 59 | self, 60 | ) -> Generator[Tuple[Struct, bytes], None, None]: 61 | """Generator to read all memory64 sections of minidump""" 62 | 63 | with self.file.open("rb") as reader: 64 | for section in self.memory.Data.MemoryRanges: 65 | reader.seek(section.DumpRva) 66 | yield section, reader.read(section.DataSize) 67 | 68 | def read_section(self, section, offset: int, size: int) -> bytes: 69 | """Reads bytes based on offset from memory section""" 70 | 71 | with self.file.open("rb") as reader: 72 | reader.seek(section.DumpRva + offset) 73 | return reader.read(size) 74 | 75 | def read_physical(self, address: int, size: int) -> bytes: 76 | """Reads bytes based on physical address in dump""" 77 | 78 | if section := self.find_section(address): 79 | offset = address - section.StartOfMemoryRange 80 | return self.read_section(section, offset, size) 81 | else: 82 | return None 83 | 84 | def get_peb(self): 85 | """Gets the Process Environment Block (PEB)""" 86 | 87 | thread = self.thread_list.Data.Threads[0] 88 | teb = MINIDUMP_TEB.parse(self.read_physical(thread.Teb, MINIDUMP_TEB.sizeof())) 89 | return MINIDUMP_PEB.parse(self.read_physical(teb.PEB, MINIDUMP_PEB.sizeof())) 90 | 91 | def get_process_parameters(self): 92 | """Gets the process parameter structure in the PEB""" 93 | 94 | peb = self.get_peb() 95 | return MINIDUMP_PROCESS_PARAMETERS.parse( 96 | self.read_physical( 97 | peb.ProcessParameters, MINIDUMP_PROCESS_PARAMETERS.sizeof() 98 | ) 99 | ) 100 | 101 | def get_commandline(self) -> str: 102 | """Gets the command line strings used to launch the process""" 103 | 104 | params = self.get_process_parameters() 105 | return self.read_physical( 106 | params.CommandLine.Buffer, params.CommandLine.Length 107 | ).decode() 108 | 109 | 110 | def get_envars(self) -> Dict[str, str]: 111 | """Gets the environment variables of the process""" 112 | params = self.get_process_parameters() 113 | 114 | # Assumption here that the all the envars are in the same section of memory 115 | section = self.find_section(params.Environment) 116 | remaining_length = ( 117 | int(section.StartOfMemoryRange) + int(section.DataSize) - params.Environment 118 | ) 119 | 120 | environ_str = self.read_physical(params.Environment, remaining_length) 121 | envars = RepeatUntil(lambda x, lst, ctx: len(x) == 0, CString("utf16")).parse(environ_str) #fmt: skip 122 | envar_dict = {} 123 | 124 | # The last value is returned in RepeatUntil and that should be blank so ignore 125 | for var in envars[:-1]: 126 | key, value = var.split("=", maxsplit=1) 127 | envar_dict[key] = value 128 | return envar_dict -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Leron Gray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | Volatility Software License 26 | Version 1.0 dated October 3, 2019. 27 | This license covers the Volatility software, Copyright 2019 Volatility Foundation. 28 | 29 | Software 30 | The software referred to in this license includes the software named above, and any data (such as operating system profiles or configuration information), and documentation provided with the software. 31 | 32 | Purpose 33 | This license gives you permission to use, share, and build with this software for free, but requires you to share source code for changes, additions, and software that you build with it. 34 | 35 | Acceptance 36 | In order to receive this license, you must agree to its rules. The rules of this license are both obligations under that agreement and conditions to your license. You must not do anything with this software that triggers a rule that you cannot or will not follow. 37 | 38 | Copyright 39 | Each contributor licenses you to do everything with this software that would otherwise infringe that contributor's copyright in it. 40 | 41 | Notices 42 | You must ensure that everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license or a link to https://www.volatilityfoundation.org/license/vsl-v1.0. You must not remove any copyright notice in the Software. 43 | 44 | Patent 45 | Each contributor licenses you to do everything with this software that would otherwise infringe any patent claims they can license or become able to license. 46 | 47 | Reliability 48 | No contributor can revoke this license. 49 | 50 | Copyleft 51 | If you make any Additions available to others, such as by providing copies of them or providing access to them over the Internet, you must make them publicly available, according to this paragraph. "Additions" includes changes or additions to the software, and any content or materials, including any training materials, you create that contain any portion of the software. "Additions" also includes any translations or ports of the software. "Additions" also includes any software designed to execute the software and parse its results, such as a wrapper written for the software, but does not include shell or execution menu software designed to execute software generally. When this license requires you to make Additions available: 52 | 53 | - You must publish all source code for software under this license, in the preferred form for making changes, through a freely accessible distribution system widely used for similar source code, so the developer and others can find and copy it. 54 | - You must publish all data or content under this license, in a format customarily used to make changes to it, through a freely accessible distribution system, so the developer and others can find and copy it. 55 | - You are responsible to ensure you have rights in Additions necessary to comply with this section. 56 | 57 | Contributing 58 | If you contribute (or offer to contribute) any materials to Volatility Foundation for the software, such as by submitting a pull request to the repository for the software or related content run by Volatility Foundation, you agree to contribute them under the under the BSD 2-Clause Plus Patent License (in the case of software) or the Creative Commons Zero Public Domain Dedication (in the case of content), unless you clearly mark them "Not a Contribution." 59 | 60 | Trademarks 61 | This license grants you no rights to any trademarks or service marks. 62 | 63 | Termination 64 | If you violate any term of this license, your license ends immediately. 65 | 66 | No Liability 67 | As far as the law allows, the software comes as is, without any warranty or condition, and no contributor will be liable to anyone for any damages related to this software or this license, under any kind of legal claim. 68 | 69 | Versions 70 | Volatility Foundation is the steward of this license and may publish new versions of this license with new version numbers. You may use the software under the version of this license under which you received the software, or, at your choice, any later version. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Public development on this tool has been paused but is still being worked on privately. Expect a massive update Q3/Q4 2024. 2 | 3 |

4 | 5 |

6 | 7 | **Dumpscan** is a command-line tool designed to extract and dump secrets from kernel and Windows Minidump formats. Kernel-dump parsing is provided by [volatility3](https://github.com/volatilityfoundation/volatility3). 8 | 9 | ## Features 10 | 11 | - x509 Public and Private key (PKCS #8/PKCS #1) parsing 12 | - [SymCrypt](https://github.com/microsoft/SymCrypt) parsing 13 | - Supported structures 14 | - **SYMCRYPT_RSAKEY** - Determines if the key structure also has a private key 15 | - Matching to public certificates found in the same process 16 | - More SymCrypt structures to come 17 | - Environment variables 18 | - Command line arguments 19 | 20 | **Note**: Testing has only been performed on Windows 10 and 11 64-bit hosts and processes. Feel free to file an issue for additional versions. Linux testing TBD. 21 | 22 | ## Installation 23 | 24 | As a command-line tool, installation is recommended using [pipx](https://github.com/pypa/pipx). This allows for easy updates and well and ensuring it is installed in its own virtual environment. 25 | 26 | ``` 27 | pipx install dumpscan 28 | pipx inject dumpscan git+https://github.com/volatilityfoundation/volatility3#39e812a 29 | ``` 30 | 31 | ## Usage 32 | 33 | ``` 34 | Usage: dumpscan [OPTIONS] COMMAND [ARGS]... 35 | 36 | Scan memory dumps for secrets and keys 37 | 38 | ╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ 39 | │ │ 40 | │ --help Show this message and exit. │ 41 | │ │ 42 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 43 | ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ 44 | │ │ 45 | │ kernel Scan kernel dump using volatility │ 46 | │ minidump Scan a user-mode minidump │ 47 | │ │ 48 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 49 | ``` 50 | 51 | In the case for subcommands that extract certificates, you can provide `--output/-o ` to output any discovered certificates to disk. 52 | 53 | ### Kernel Mode 54 | 55 | As mentioned, kernel analysis is performed by Volatility3. `cmdline`, `envar`, and `pslist` are direct calls to the Volatility3 plugins, while `symcrypt` and `x509` are custom plugins. 56 | 57 | ``` 58 | Usage: dumpscan kernel [OPTIONS] COMMAND [ARGS]... 59 | 60 | Scan kernel dump using volatility 61 | 62 | ╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ 63 | │ │ 64 | │ --help Show this message and exit. │ 65 | │ │ 66 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 67 | ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ 68 | │ │ 69 | │ cmdline List command line for processes (Only for Windows) │ 70 | │ envar List process environment variables (Only for Windows) │ 71 | │ pslist List all the processes and their command line arguments │ 72 | │ symcrypt Scan a kernel-mode dump for symcrypt objects │ 73 | │ x509 Scan a kernel-mode dump for x509 certificates │ 74 | │ │ 75 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 76 | ``` 77 | 78 | ### Minidump Mode 79 | 80 | Supports Windows Minidump format. 81 | 82 | **Note**: This has only been tested on 64-bit processes on Windows 10+. 32-bit processes requires additional work but isn't a priority. 83 | 84 | 85 | ``` 86 | Usage: dumpscan minidump [OPTIONS] COMMAND [ARGS]... 87 | 88 | Scan a user-mode minidump 89 | 90 | ╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ 91 | │ │ 92 | │ --help Show this message and exit. │ 93 | │ │ 94 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 95 | ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ 96 | │ │ 97 | │ cmdline Dump the command line string │ 98 | │ envar Dump the environment variables in a minidump │ 99 | │ symcrypt Scan a minidump for symcrypt objects │ 100 | │ x509 Scan a minidump for x509 objects │ 101 | │ │ 102 | ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ 103 | ``` 104 | 105 | ## Built With 106 | - [volatility3](https://github.com/volatilityfoundation/volatility3) 107 | - [construct](https://github.com/construct/construct) 108 | - [yara-python](https://github.com/VirusTotal/yara-python) 109 | - [typer](https://github.com/tiangolo/typer) 110 | - [rich](https://github.com/Textualize/rich) 111 | - [rich_click](https://github.com/ewels/rich-click) 112 | 113 | ## Acknowledgements 114 | - Thanks to [F-Secure](https://github.com/FSecureLABS) and the [physmem2profit](https://github.com/FSecureLABS/physmem2profit) project for providing the idea to use `construct` for parsing minidumps. 115 | - Thanks to [Skelsec](https://github.com/skelsec) and his [minidump](https://github.com/skelsec/minidump) project which helped me figure out to parse minidumps. 116 | 117 | 118 | ## To-Do 119 | 120 | - Verify use against 32-bit minidumps 121 | - Create a coredump parser for Linux process dumps 122 | - Verify volatility plugins work against Linux kernel dumps 123 | - Add an HTML report that shows all plugins 124 | - Code refactoring to make more extensible 125 | - MORE SECRETS 126 | -------------------------------------------------------------------------------- /dumpscan/common/scanners/x509.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from base64 import b64encode 3 | from pathlib import Path 4 | from struct import unpack 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.ec import ( 10 | EllipticCurvePrivateKey, 11 | EllipticCurvePublicKey, 12 | ) 13 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey 14 | from cryptography.hazmat.primitives.serialization import ( 15 | load_pem_private_key, 16 | Encoding, 17 | NoEncryption, 18 | PrivateFormat, 19 | ) 20 | from cryptography.x509 import Certificate, load_der_x509_certificate 21 | from rich import inspect 22 | from rich.console import Console, ConsoleOptions, RenderResult 23 | from rich.table import Table 24 | 25 | from ...minidump.minidumpfile import MinidumpFile 26 | from ...minidump.structs.MinidumpMemory64List import MINIDUMP_MEMORY_DESCRIPTOR64 27 | from ..output import get_dumpscan_table 28 | from ..rules import YARA_RULES 29 | 30 | 31 | class x509Scanner: 32 | def __init__(self, minidumpfile: MinidumpFile, output: Path) -> None: 33 | self.rules = yara.compile(sources=YARA_RULES["x509"]) 34 | self.dump = minidumpfile 35 | self.output = output 36 | self.matching_objects = {"x509": [], "pkcs": []} 37 | self.modulus_dict = {} 38 | self.public_private_matches = {} 39 | self.current_section: MINIDUMP_MEMORY_DESCRIPTOR64 = None 40 | 41 | if output and not output.exists(): 42 | output.mkdir(parents=True) 43 | 44 | def __rich_console__( 45 | self, console: Console, options: ConsoleOptions 46 | ) -> RenderResult: 47 | 48 | table = get_dumpscan_table() 49 | table.add_column("Rule", style="bold #f9c300") 50 | table.add_column("Result", style="#008df8") 51 | table.add_column("Thumbprint", style="#008df8") 52 | table.add_column("Public Integers (First 20 bytes)") 53 | 54 | for key, values in self.matching_objects.items(): 55 | found_certs = [] 56 | for value in values: 57 | if isinstance(value, Certificate): 58 | thumbprint = ( 59 | binascii.hexlify(value.fingerprint(hashes.SHA1())) 60 | .upper() 61 | .decode() 62 | ) 63 | 64 | # Clean up output by only printing unique certs 65 | if thumbprint in found_certs: 66 | continue 67 | found_certs.append(thumbprint) 68 | 69 | public_key = value.public_key() 70 | 71 | if isinstance(public_key, RSAPublicKey): 72 | pubints = format(public_key.public_numbers().n, "x")[ 73 | :40 74 | ].upper() 75 | 76 | elif isinstance(public_key, EllipticCurvePublicKey): 77 | pubints = f"X:{str(public_key.public_numbers().x)[:40]} | Y:{str(public_key.public_numbers().y)[:40]} " 78 | 79 | table.add_row( 80 | key, 81 | value.subject.rfc4514_string(), 82 | thumbprint, 83 | pubints, 84 | ) 85 | elif isinstance(value, RSAPrivateKey): 86 | result = str(value.key_size) 87 | if matching_cert := self.public_private_matches.get(value): 88 | result += f"-> {matching_cert.subject.rfc4514_string()}" 89 | table.add_row( 90 | key, 91 | result, 92 | None, 93 | format(value.private_numbers().public_numbers.n, "x")[ 94 | :40 95 | ].upper(), 96 | ) 97 | yield table 98 | 99 | def save_file(self, data: bytes, filename: str): 100 | if self.output: 101 | file = self.output / filename 102 | with file.open("wb") as f: 103 | f.write(data) 104 | 105 | @classmethod 106 | def minidump_scan(cls, minidumpfile: MinidumpFile, output: Path) -> "x509Scanner": 107 | scanner = cls(minidumpfile, output) 108 | 109 | for section, data in minidumpfile.read_all_memory64(): 110 | scanner.current_section = section 111 | scanner.rules.match( 112 | data=data, 113 | callback=scanner.parse_yara_match, 114 | which_callbacks=yara.CALLBACK_MATCHES, 115 | ) 116 | 117 | scanner.modulus_dict = {} 118 | for cert in scanner.matching_objects.get("x509", []): 119 | public_key = cert.public_key() 120 | 121 | if isinstance(public_key, RSAPublicKey): 122 | pub_modulus_str = format(public_key.public_numbers().n, "x").upper() 123 | scanner.modulus_dict[pub_modulus_str] = cert 124 | 125 | if private_keys := scanner.matching_objects.get("pkcs"): 126 | for private_key in private_keys: 127 | priv_modulus_str = format( 128 | private_key.private_numbers().public_numbers.n, "x" 129 | ) 130 | 131 | if match := scanner.modulus_dict.get(priv_modulus_str): 132 | scanner.public_private_matches[private_key, match] 133 | 134 | return scanner 135 | 136 | def parse_yara_match(self, data): 137 | rule = data["rule"] 138 | matching_objects = [] 139 | 140 | for match in data["strings"]: 141 | if obj := self.parse_results(match, rule): 142 | matching_objects.append(obj) 143 | 144 | if self.output: 145 | if rule == "pkcs": 146 | output_bytes = obj.private_bytes( 147 | Encoding.DER, 148 | PrivateFormat.PKCS8, 149 | NoEncryption(), 150 | ) 151 | filename = hex(match[0]) + "_" + str(obj.key_size) + ".key" 152 | 153 | elif rule == "x509": 154 | output_bytes = obj.public_bytes(Encoding.DER) 155 | thumbprint = ( 156 | binascii.hexlify(obj.fingerprint(hashes.SHA1())) 157 | .upper() 158 | .decode() 159 | ) 160 | 161 | filename = thumbprint + "_" 162 | filename += ( 163 | obj.subject._attributes[-1] 164 | ._attributes[-1] 165 | .value.strip('*"/[]:;|,') 166 | .split("/")[0] 167 | ) 168 | 169 | self.save_file(output_bytes, filename) 170 | 171 | self.matching_objects[rule].extend(matching_objects) 172 | return yara.CALLBACK_CONTINUE 173 | 174 | def parse_results(self, match: tuple, rule: str): 175 | 176 | # This is the offset from the bytes being scanned 177 | offset = match[0] 178 | 179 | # Only need first four bytes 180 | _, cert_size = unpack(">HH", match[2][:4]) 181 | cert_data = self.dump.read_section(self.current_section, offset, cert_size + 4) 182 | 183 | if rule == "x509": 184 | try: 185 | return load_der_x509_certificate(cert_data, backend=default_backend()) 186 | except: 187 | pass 188 | elif rule == "pkcs": 189 | pem = ( 190 | b"-----BEGIN RSA PRIVATE KEY-----\n" 191 | + b64encode(cert_data) 192 | + b"\n-----END RSA PRIVATE KEY-----" 193 | ) 194 | try: 195 | return load_pem_private_key(pem, None, default_backend()) 196 | except: 197 | pass 198 | -------------------------------------------------------------------------------- /dumpscan/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | 4 | import typer 5 | from rich import inspect 6 | from rich_click.typer import Typer 7 | 8 | from .common.output import get_dumpscan_console, get_dumpscan_table 9 | from .common.scanners.symcrypt import SymcryptScanner 10 | from .common.scanners.x509 import x509Scanner 11 | from .kernel.renderers import RichRenderOption 12 | from .kernel.vol import OS, Volatility 13 | from .minidump.minidumpfile import MinidumpFile 14 | 15 | app = Typer( 16 | name="dumpscan", help="Scan memory dumps for secrets and keys", add_completion=False 17 | ) 18 | kernel_app = Typer() 19 | minidump_app = Typer() 20 | app.add_typer(kernel_app, name="kernel", help="Scan kernel dump using volatility") 21 | app.add_typer(minidump_app, name="minidump", help="Scan a user-mode minidump") 22 | console = get_dumpscan_console() 23 | 24 | 25 | def lowercase_list(value: List[str]) -> List[str]: 26 | """Callback function to make all strings in list lowercase""" 27 | return list(map(str.lower, value)) 28 | 29 | 30 | @kernel_app.command( 31 | name="envar", help="List process environment variables (Only for Windows)" 32 | ) 33 | def kernel_envar( 34 | file: Path = typer.Option(..., "--file", "-f", help="Path to kernel dump"), 35 | pids: List[int] = typer.Option( 36 | [], "--pid", "-p", help="Pids to scan. Can be passed multiple times." 37 | ), 38 | render: RichRenderOption = typer.Option( 39 | RichRenderOption.TABLE, 40 | "--render", 41 | "-r", 42 | help="Render output (default: table)", 43 | case_sensitive=False, 44 | show_default=False, 45 | ), 46 | ): 47 | vol = Volatility(file, render, None) 48 | results = vol.run_envar(list(pids)) 49 | console.print(results) 50 | 51 | 52 | @kernel_app.command( 53 | name="cmdline", help="List command line for processes (Only for Windows)" 54 | ) 55 | def kernel_cmdline( 56 | file: Path = typer.Option(..., "--file", "-f", help="Path to kernel dump"), 57 | pids: List[int] = typer.Option( 58 | [], "--pid", "-p", help="Pids to scan. Can be passed multiple times." 59 | ), 60 | render: RichRenderOption = typer.Option( 61 | RichRenderOption.TABLE, 62 | "--render", 63 | "-r", 64 | help="Render output (default: table)", 65 | case_sensitive=False, 66 | show_default=False, 67 | ), 68 | ): 69 | vol = Volatility(file, render, None) 70 | results = vol.run_cmdline(list(pids)) 71 | console.print(results) 72 | 73 | 74 | @kernel_app.command( 75 | name="pslist", help="List all the processes and their command line arguments" 76 | ) 77 | def kernel_pslist( 78 | file: Path = typer.Option(..., "--file", "-f", help="Path to kernel dump"), 79 | os: OS = typer.Option( 80 | OS.WINDOWS, 81 | help="Operating system (default: windows)", 82 | case_sensitive=False, 83 | show_default=False, 84 | ), 85 | render: RichRenderOption = typer.Option( 86 | RichRenderOption.TABLE, 87 | "--render", 88 | "-r", 89 | help="Render output (default: table)", 90 | case_sensitive=False, 91 | show_default=False, 92 | ), 93 | ): 94 | vol = Volatility(file, render, None) 95 | results = vol.run_pslist(os) 96 | console.print(results) 97 | 98 | 99 | @kernel_app.command(name="x509", help="Scan a kernel-mode dump for x509 certificates") 100 | def kernel_x509( 101 | file: Path = typer.Option( 102 | ..., "--file", "-f", help="Path to kernel dump", dir_okay=False 103 | ), 104 | output: Path = typer.Option( 105 | None, "--output", "-o", help="Path to dump objects to disk", file_okay=False 106 | ), 107 | pids: List[int] = typer.Option( 108 | [], "--pid", "-p", help="Pids to scan. Can be passed multiple times." 109 | ), 110 | procnames: List[str] = typer.Option( 111 | [], 112 | "--procname", 113 | "-n", 114 | help="Process names to scan. Can be passed multiple times.", 115 | callback=lowercase_list, 116 | ), 117 | render: RichRenderOption = typer.Option( 118 | RichRenderOption.TABLE, 119 | "--render", 120 | "-r", 121 | help="Render output (default: table)", 122 | case_sensitive=False, 123 | show_default=False, 124 | ), 125 | ): 126 | vol = Volatility(file, render, output) 127 | results = vol.run_x509(list(pids), list(procnames), bool(output)) 128 | console.print(results) 129 | 130 | 131 | @kernel_app.command( 132 | name="symcrypt", help="Scan a kernel-mode dump for symcrypt objects" 133 | ) 134 | def kernel_symcrypt( 135 | file: Path = typer.Option( 136 | ..., "--file", "-f", help="Path to kernel dump", dir_okay=False 137 | ), 138 | output: Path = typer.Option( 139 | None, "--output", "-o", help="Path to dump objects to disk", file_okay=False 140 | ), 141 | pids: List[int] = typer.Option( 142 | [], "--pid", "-p", help="Pids to scan. Can be passed multiple times." 143 | ), 144 | procnames: List[str] = typer.Option( 145 | [], 146 | "--procname", 147 | "-n", 148 | help="Process names to scan. Can be passed multiple times.", 149 | callback=lowercase_list, 150 | ), 151 | render: RichRenderOption = typer.Option( 152 | RichRenderOption.TABLE, 153 | "--render", 154 | "-r", 155 | help="Render output (default: table)", 156 | case_sensitive=False, 157 | show_default=False, 158 | ), 159 | ): 160 | vol = Volatility(file, render, output) 161 | results = vol.run_symcrypt(list(pids), list(procnames), bool(output)) 162 | console.print(results) 163 | 164 | 165 | @minidump_app.command(name="x509", help="Scan a minidump for x509 objects") 166 | def minidump_x509( 167 | file: Path = typer.Option( 168 | ..., "--file", "-f", help="Path to minidump", dir_okay=False 169 | ), 170 | output: Path = typer.Option( 171 | None, "--output", "-o", help="Path to dump objects to disk", file_okay=False 172 | ), 173 | ): 174 | minidump_file = MinidumpFile(file.absolute()) 175 | scanner = x509Scanner.minidump_scan(minidump_file, output) 176 | console.print(scanner) 177 | 178 | 179 | @minidump_app.command(name="symcrypt", help="Scan a minidump for symcrypt objects") 180 | def minidump_symcrypt( 181 | file: Path = typer.Option( 182 | ..., "--file", "-f", help="Path to kernel dump", dir_okay=False 183 | ), 184 | output: Path = typer.Option( 185 | None, "--output", "-o", help="Path to dump objects to disk", file_okay=False 186 | ), 187 | ): 188 | 189 | # Get all the public certs first 190 | minidump_file = MinidumpFile(file.absolute()) 191 | x509_scanner = x509Scanner.minidump_scan(minidump_file, None) 192 | 193 | # Now look for symcrypt 194 | symcrypt_scanner = SymcryptScanner.minidump_scan( 195 | minidump_file, x509_scanner, output 196 | ) 197 | console.print(symcrypt_scanner) 198 | 199 | 200 | @minidump_app.command(name="envar", help="Dump the environment variables in a minidump") 201 | def minidump_envar( 202 | file: Path = typer.Option( 203 | ..., "--file", "-f", help="Path to kernel dump", dir_okay=False 204 | ), 205 | ): 206 | # Get all the public certs first 207 | minidump_file = MinidumpFile(file.absolute()) 208 | # print(minidump_file.dump) 209 | 210 | envars = minidump_file.get_envars() 211 | table = get_dumpscan_table() 212 | table.add_column("Variable", style="italic #f9c300") 213 | table.add_column("Value") 214 | [table.add_row(k, v) for k, v in envars.items()] 215 | console.print(table, highlight=False) 216 | 217 | 218 | @minidump_app.command(name="cmdline", help="Dump the command line string") 219 | def minidump_cmdline( 220 | file: Path = typer.Option( 221 | ..., "--file", "-f", help="Path to kernel dump", dir_okay=False 222 | ), 223 | ): 224 | # Get all the public certs first 225 | minidump_file = MinidumpFile(file.absolute()) 226 | console.print( 227 | minidump_file.get_commandline(), 228 | "\n", 229 | style="#f9c300", 230 | highlight=False, 231 | ) 232 | 233 | 234 | if __name__ == "__main__": 235 | app() 236 | -------------------------------------------------------------------------------- /dumpscan/common/scanners/symcrypt.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from collections import defaultdict 3 | from dataclasses import asdict, dataclass 4 | from pathlib import Path 5 | from struct import unpack 6 | from cryptography.hazmat.primitives import hashes 7 | from cryptography.hazmat.primitives.asymmetric.rsa import ( 8 | RSAPrivateNumbers, 9 | RSAPublicNumbers, 10 | rsa_crt_dmp1, 11 | rsa_crt_dmq1, 12 | rsa_crt_iqmp, 13 | rsa_recover_prime_factors, 14 | ) 15 | from cryptography.hazmat.primitives.serialization.pkcs12 import ( 16 | serialize_key_and_certificates, 17 | ) 18 | from cryptography.hazmat.primitives.serialization import ( 19 | Encoding, 20 | NoEncryption, 21 | PrivateFormat, 22 | ) 23 | 24 | import yara 25 | from rich import inspect 26 | from rich.console import Console, ConsoleOptions, RenderResult 27 | 28 | from ...common.structs import * 29 | from ...minidump.minidumpfile import MinidumpFile 30 | from ...minidump.structs.MinidumpMemory64List import MINIDUMP_MEMORY_DESCRIPTOR64 31 | from ..output import get_dumpscan_table 32 | from ..rules import YARA_RULES 33 | from .x509 import x509Scanner 34 | 35 | 36 | @dataclass 37 | class SymcryptRSAResult: 38 | address: str 39 | hasPrivateKey: int 40 | modulus: int 41 | match: str 42 | 43 | 44 | class SymcryptScanner: 45 | def __init__( 46 | self, minidumpfile: MinidumpFile, x509_scanner: x509Scanner, output: Path 47 | ) -> None: 48 | self.rules = yara.compile(sources=YARA_RULES["symcrypt"]) 49 | self.dump = minidumpfile 50 | self.output = output 51 | self.x509 = x509_scanner 52 | self.matching_objects = defaultdict(list) 53 | self.modulus_dict = {} 54 | self.public_private_matches = {} 55 | self.current_section: MINIDUMP_MEMORY_DESCRIPTOR64 = None 56 | 57 | if output and not output.exists(): 58 | output.mkdir(parents=True) 59 | 60 | def __rich_console__( 61 | self, console: Console, options: ConsoleOptions 62 | ) -> RenderResult: 63 | 64 | # RSA key results 65 | table = get_dumpscan_table() 66 | table.add_column("Rule", style="bold italic #f9c300") 67 | table.add_column("Address") 68 | table.add_column("HasPrivateKey") 69 | table.add_column("Modulus (First 20 bytes)", style="bold #f9c300") 70 | table.add_column("Matching Certificate") 71 | for result in self.matching_objects.get("rsa", []): 72 | table.add_row("rsa", *map(str, asdict(result).values())) 73 | yield table 74 | 75 | @classmethod 76 | def minidump_scan( 77 | cls, minidumpfile: MinidumpFile, x509_scanner: x509Scanner, output: Path 78 | ) -> "SymcryptScanner": 79 | scanner = cls(minidumpfile, x509_scanner, output) 80 | 81 | for section, data in minidumpfile.read_all_memory64(): 82 | scanner.current_section = section 83 | scanner.rules.match( 84 | data=data, 85 | callback=scanner.parse_yara_match, 86 | which_callbacks=yara.CALLBACK_MATCHES, 87 | ) 88 | return scanner 89 | 90 | def parse_yara_match(self, data): 91 | parsing_functions = {"rsa": self._parse_rsakey} 92 | 93 | rule = data["rule"] 94 | matching_objects = [] 95 | 96 | if parse_func := parsing_functions.get(rule): 97 | for match in data["strings"]: 98 | if obj := parse_func(match, rule): 99 | matching_objects.append(obj) 100 | 101 | self.matching_objects[rule].extend(matching_objects) 102 | return yara.CALLBACK_CONTINUE 103 | 104 | def _parse_rsakey(self, match: tuple, rule: str): 105 | 106 | # This is the offset from the bytes being scanned 107 | offset = match[0] 108 | physical_address = self.current_section.StartOfMemoryRange + offset 109 | 110 | # The expected size of the structure is 0x28 in length 111 | bcrypt_rsakey = BCRYPT_RSAKEY.parse( 112 | self.dump.read_section(self.current_section, offset, 0x28) 113 | ) 114 | key_size = unpack("I", self.dump.read_physical(bcrypt_rsakey.pKey, 4))[0] 115 | key = SYMCRYPT_RSAKEY.parse( 116 | self.dump.read_physical(bcrypt_rsakey.pKey, key_size) 117 | ) 118 | 119 | # Get the cbSize of modulus (pmModulus + 8) then parse into Modulus struct 120 | modulus_size = unpack("I", self.dump.read_physical(key.pmModulus + 8, 4))[0] 121 | modulus = SYMCRYPT_MODULUS.parse( 122 | self.dump.read_physical(key.pmModulus, modulus_size) 123 | ) 124 | # Zfill is important here for alignment 125 | # Additionally, we have to read the list of integers (def) backwards 126 | mod_str = "".join( 127 | [format(i, "x").zfill(8) for i in modulus.divisor.int.fdef[::-1]] 128 | ).upper() 129 | matching_cert = None 130 | matching_cert_value = "" 131 | 132 | if self.x509: 133 | if matching_cert := self.x509.modulus_dict.get(mod_str): 134 | thumbprint = ( 135 | binascii.hexlify(matching_cert.fingerprint(hashes.SHA1())) 136 | .upper() 137 | .decode() 138 | ) 139 | subject = matching_cert.subject.rfc4514_string() 140 | matching_cert_value = f"[green]{thumbprint}[/green] -> {subject}" 141 | 142 | if key.hasPrivateKey and self.output: 143 | # We could write extra code to parse each of the primes but we need to get the private exponent (d) anyway 144 | # So less work to just pull private exponent and derive the primes from n,e,d 145 | private_exp_size = unpack( 146 | "I", self.dump.read_physical(key.piPrivExps[0] + 8, 4) 147 | )[0] 148 | private_exp_modulus = SYMCRYPT_INT.parse( 149 | self.dump.read_physical(key.piPrivExps[0], private_exp_size) 150 | ) 151 | 152 | private_exp_hexstr = "".join( 153 | [ 154 | format(i, "x").zfill(8) 155 | for i in private_exp_modulus.fdef[::-1] 156 | ] 157 | ) 158 | 159 | # Get p and q from modulus, public exponent, and private exponent 160 | d = int(private_exp_hexstr, 16) # Private exponent 161 | n = int(mod_str, 16) # Modulus 162 | e = key.au64PubExp # Public exponent 163 | p, q = rsa_recover_prime_factors(n, e, d) 164 | 165 | # fmt: on 166 | 167 | # We need to create the public numbers to pass to the private numbers 168 | # All of these numbers exist in the parsed Structs, but easier to call helper functions 169 | public_numbers = RSAPublicNumbers(e, n) 170 | private_numbers = RSAPrivateNumbers( 171 | p=p, 172 | q=q, 173 | d=d, 174 | dmp1=rsa_crt_dmp1(d, p), 175 | dmq1=rsa_crt_dmq1(d, q), 176 | iqmp=rsa_crt_iqmp(p, q), 177 | public_numbers=public_numbers, 178 | ) 179 | private_key = private_numbers.private_key() 180 | if matching_cert: 181 | pfx = serialize_key_and_certificates( 182 | thumbprint.encode(), 183 | private_key, 184 | matching_cert, 185 | None, 186 | NoEncryption(), 187 | ) 188 | filename = thumbprint + "_" 189 | filename += ( 190 | matching_cert.subject._attributes[-1] 191 | ._attributes[-1] 192 | .value.strip('*."/[]:;|,') 193 | .split("/")[0] 194 | ) 195 | with open(f"{self.output / filename}.pfx", "wb") as f: 196 | f.write(pfx) 197 | 198 | return SymcryptRSAResult( 199 | hex(physical_address), key.hasPrivateKey, mod_str[:40], matching_cert_value 200 | ) 201 | -------------------------------------------------------------------------------- /dumpscan/kernel/plugins/symcrypt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | from struct import unpack 4 | from typing import Dict, List 5 | 6 | import yara 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 | NoEncryption, 20 | PrivateFormat, 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 | from rich import inspect 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 ...common.structs import * 36 | from .dumpcerts import Dumpcerts 37 | 38 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) 39 | 40 | log = logging.getLogger("rich") 41 | 42 | # fmt: off 43 | def get_yara_rules(): 44 | sources = {} 45 | 46 | # Follow the start of the BCRYPT_RSAKEY Struct 47 | 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}" 48 | 49 | return yara.compile(sources=sources) 50 | # fmt: on 51 | 52 | 53 | class Symcrypt(interfaces.plugins.PluginInterface): 54 | """Dump symcrypt keys""" 55 | 56 | _required_framework_version = (2, 0, 0) 57 | _version = (1, 0, 0) 58 | 59 | @classmethod 60 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 61 | return [ 62 | requirements.ModuleRequirement( 63 | name="kernel", 64 | description="Kernel Layer", 65 | architectures=["Intel32", "Intel64"], 66 | ), 67 | requirements.PluginRequirement( 68 | name="vadyarascanner", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) 69 | ), 70 | requirements.PluginRequirement( 71 | name="dumpcerts", plugin=Dumpcerts, version=(1, 0, 0) 72 | ), 73 | requirements.ListRequirement( 74 | name="pid", 75 | element_type=int, 76 | description="Process IDs to include (all other processes are excluded)", 77 | optional=True, 78 | ), 79 | requirements.ListRequirement( 80 | name="name", 81 | element_type=str, 82 | description="Process name to include (all other processes are excluded)", 83 | optional=True, 84 | ), 85 | requirements.ListRequirement( 86 | name="matches", 87 | description="List of modulus to compare to start of private key to match", 88 | default=[], 89 | optional=True, 90 | ), 91 | requirements.BooleanRequirement( 92 | name="dump", 93 | description="Dump PFX when a match is found", 94 | default=False, 95 | optional=True, 96 | ), 97 | ] 98 | 99 | def _generator(self): 100 | 101 | kernel = self.context.modules[self.config["kernel"]] 102 | pid_list = self.config.get("pid", []) 103 | name_list = self.config.get("name", []) 104 | 105 | def proc_name_and_pid_filter(proc: EPROCESS): 106 | if not pid_list and not name_list: 107 | return False 108 | 109 | if proc.is_valid() and proc.UniqueProcessId: 110 | try: 111 | proc_name = proc.ImageFileName.cast( 112 | "string", 113 | max_length=proc.ImageFileName.vol.count, 114 | errors="ignore", 115 | ).lower() 116 | return ( 117 | proc.UniqueProcessId not in pid_list 118 | and proc_name not in name_list 119 | ) 120 | # Specifically, if there is a smear, process info might not be valid 121 | # So ignore if the process name can't be cast to anything 122 | except: 123 | return True 124 | else: 125 | return True 126 | 127 | filter_func = proc_name_and_pid_filter 128 | output = self.config.get("dump") 129 | modulus_matches = self.config.get("matches") 130 | # Get all the processes 131 | for proc in pslist.PsList.list_processes( 132 | context=self.context, 133 | layer_name=kernel.layer_name, 134 | symbol_table=kernel.symbol_table_name, 135 | filter_func=filter_func, 136 | ): 137 | # If process can't be added, just ignore it 138 | try: 139 | layer_name = proc.add_process_layer() 140 | except: 141 | continue 142 | 143 | layer = self.context.layers[layer_name] 144 | proc_name = proc.ImageFileName.cast( 145 | "string", 146 | max_length=proc.ImageFileName.vol.count, 147 | errors="replace", 148 | ) 149 | 150 | # This will hold all of the public certificates to match against if a hit occurs 151 | public_certs: Dict[str, Certificate] = {} 152 | 153 | for offset, rule_name, _, _ in layer.scan( 154 | context=self.context, 155 | scanner=yarascan.YaraScanner(rules=get_yara_rules()), 156 | sections=vadyarascan.VadYaraScan.get_vad_maps(proc), 157 | ): 158 | # If there is a match for key, then... 159 | # Get all of the public certs in the process 160 | # Keep track of modulus and cert object 161 | if not public_certs: 162 | for _, _, _, cert in Dumpcerts.get_certs_by_process( 163 | context=self.context, 164 | proc=proc, 165 | key_types="public", 166 | ): 167 | try: 168 | # If an RSA key, grab the modulus and convert it to a string 169 | if isinstance(cert.public_key(), RSAPublicKey): 170 | public_certs[ 171 | format(cert.public_key().public_numbers().n, "x") 172 | ] = cert 173 | except: 174 | continue 175 | 176 | # Make the bcrypt_RSA_KEY structure to get the pointer to key 177 | bcrypt_rsakey = BCRYPT_RSAKEY.parse(layer.read(offset, 0x28)) 178 | 179 | # Parse the key into a SYMCRYPT_RSA_KEY struct 180 | # If we can't, just move on 181 | try: 182 | key_total_size = unpack("I", layer.read(bcrypt_rsakey.pKey, 4))[0] 183 | key = SYMCRYPT_RSAKEY.parse( 184 | layer.read(bcrypt_rsakey.pKey, key_total_size) 185 | ) 186 | except Exception as e: 187 | yield 0, ( 188 | format_hints.Hex(offset), 189 | proc.UniqueProcessId, 190 | proc_name, 191 | rule_name, 192 | -1, 193 | f"Corrupt - {e}", 194 | ) 195 | continue 196 | 197 | # Get the cbSize of modulus (pmModulus + 8) then parse into Modulus struct 198 | modulus_size = unpack("I", layer.read(key.pmModulus + 8, 4))[0] 199 | modulus = SYMCRYPT_MODULUS.parse( 200 | layer.read(key.pmModulus, modulus_size) 201 | ) 202 | 203 | # Zfill is important here for alignment 204 | # Additionally, we have to read the list of integers (def) backwards 205 | mod_str = "".join( 206 | [format(i, "x").zfill(8) for i in modulus.divisor.int.fdef[::-1]] 207 | ) 208 | 209 | # Look for a matching cert in the process. If one is found, print thumbprint and subject 210 | match_string = "" 211 | 212 | matching_cert = public_certs.get(mod_str, None) 213 | if matching_cert: 214 | thumbprint = "".join( 215 | "{:02X}".format(b) 216 | for b in matching_cert.fingerprint(hashes.SHA1()) 217 | ) 218 | 219 | subject = matching_cert.subject.rfc4514_string() 220 | match_string = f"[green]{thumbprint}[/green] -> {subject}" 221 | else: 222 | # If there's no matching cert, then print out first 20 bytes of modulus 223 | # Check to see if modulus matches what user requests 224 | if modulus_matches: 225 | for modulus in modulus_matches: 226 | if mod_str.upper().startswith(modulus.upper()): 227 | match_string = f"[green]{modulus}[/]" 228 | 229 | # If there is a matching cert and there is a private key, make PFX or print pem! 230 | if key.hasPrivateKey and output: 231 | # fmt: off 232 | 233 | # We could write extra code to parse each of the primes but we need to get the private exponent (d) anyway 234 | # So less work to just pull private exponent and derive the primes from n,e,d 235 | private_exp_size = unpack("I", layer.read(key.piPrivExps[0] + 8, 4))[0] 236 | private_exp_modulus = SYMCRYPT_INT.parse( 237 | layer.read(key.piPrivExps[0], private_exp_size) 238 | ) 239 | private_exp_hexstr = "".join( 240 | [format(i, "x").zfill(8) for i in private_exp_modulus.fdef[::-1]] 241 | ) 242 | 243 | # Get p and q from modulus, public exponent, and private exponent 244 | d = int(private_exp_hexstr, 16) # Private exponent 245 | n = int(mod_str, 16) # Modulus 246 | e = key.au64PubExp # Public exponent 247 | p, q = rsa_recover_prime_factors(n, e, d) 248 | 249 | # fmt: on 250 | 251 | # We need to create the public numbers to pass to the private numbers 252 | # All of these numbers exist in the parsed Structs, but easier to call helper functions 253 | public_numbers = RSAPublicNumbers(e, n) 254 | private_numbers = RSAPrivateNumbers( 255 | p=p, 256 | q=q, 257 | d=d, 258 | dmp1=rsa_crt_dmp1(d, p), 259 | dmq1=rsa_crt_dmq1(d, q), 260 | iqmp=rsa_crt_iqmp(p, q), 261 | public_numbers=public_numbers, 262 | ) 263 | private_key = private_numbers.private_key() 264 | if matching_cert: 265 | pfx = serialize_key_and_certificates( 266 | thumbprint.encode(), 267 | private_key, 268 | matching_cert, 269 | None, 270 | NoEncryption(), 271 | ) 272 | filename = thumbprint + "_" 273 | filename += ( 274 | matching_cert.subject._attributes[-1] 275 | ._attributes[-1] 276 | .value.strip('*."/[]:;|,') 277 | .split("/")[0] 278 | ) 279 | with self.open(f"{filename}.pfx") as f: 280 | f.write(pfx) 281 | else: 282 | with self.open(f"{mod_str[:40].upper()}_modulus.key") as f: 283 | f.write( 284 | private_key.private_bytes( 285 | encoding=Encoding.DER, 286 | format=PrivateFormat.PKCS8, 287 | encryption_algorithm=NoEncryption(), 288 | ) 289 | ) 290 | 291 | yield 0, ( 292 | format_hints.Hex(offset), 293 | proc.UniqueProcessId, 294 | f"[#f9c300]{proc_name}[/]", 295 | f"[#f9c300]{rule_name}[/]", 296 | key.hasPrivateKey, 297 | f"[#f9c300]{mod_str[:40].upper()}[/]", 298 | match_string, 299 | ) 300 | 301 | def run(self) -> renderers.TreeGrid: 302 | return renderers.TreeGrid( 303 | [ 304 | (f'{"Offset":<8}', format_hints.Hex), 305 | ("PID", int), 306 | (f'{"Process":<8}', str), 307 | ("Rule", str), 308 | ("HasPrivateKey", int), 309 | ("Modulus (First 20 Bytes)", str), 310 | ("Matching", str), 311 | ], 312 | self._generator(), 313 | ) 314 | -------------------------------------------------------------------------------- /dumpscan/kernel/plugins/dumpcerts.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import warnings 3 | from base64 import b64encode 4 | from struct import unpack 5 | from typing import Callable, Iterable, List, Tuple, Union 6 | 7 | import yara 8 | from cryptography.hazmat.backends import default_backend 9 | from cryptography.hazmat.primitives import hashes 10 | from cryptography.hazmat.primitives.asymmetric.types import PRIVATE_KEY_TYPES 11 | from cryptography.hazmat.primitives.serialization import ( 12 | Encoding, 13 | NoEncryption, 14 | PrivateFormat, 15 | load_pem_private_key, 16 | ) 17 | from cryptography.utils import CryptographyDeprecationWarning 18 | from cryptography.x509 import Certificate, load_der_x509_certificate 19 | from volatility3.framework import interfaces, objects, renderers 20 | from volatility3.framework.configuration import requirements 21 | from volatility3.framework.renderers import format_hints 22 | from volatility3.framework.symbols.windows.extensions import EPROCESS 23 | from volatility3.plugins import yarascan 24 | from volatility3.plugins.windows import pslist, vadyarascan 25 | 26 | warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) 27 | 28 | 29 | class Dumpcerts(interfaces.plugins.PluginInterface): 30 | """Dump public and private RSA keys based on ASN-1 structure""" 31 | 32 | _required_framework_version = (2, 0, 0) 33 | _version = (1, 0, 0) 34 | 35 | @classmethod 36 | def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: 37 | return [ 38 | requirements.ModuleRequirement( 39 | name="kernel", 40 | description="Windows kernel", 41 | architectures=["Intel32", "Intel64"], 42 | ), 43 | requirements.PluginRequirement( 44 | name="vadyarascanner", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) 45 | ), 46 | requirements.ListRequirement( 47 | name="pid", 48 | element_type=int, 49 | description="Process IDs to include (all other processes are excluded)", 50 | optional=True, 51 | ), 52 | requirements.ListRequirement( 53 | name="name", 54 | element_type=str, 55 | description="Process name to include (all other processes are excluded)", 56 | optional=True, 57 | ), 58 | requirements.ChoiceRequirement( 59 | ["all", "private", "public"], 60 | name="type", 61 | default="all", 62 | description="Types of keys to dump", 63 | optional=True, 64 | ), 65 | requirements.BooleanRequirement( 66 | name="dump", description="Dump keys", default=False, optional=True 67 | ), 68 | requirements.BooleanRequirement( 69 | name="physical", 70 | description="Scan physical memory instead of processes", 71 | default=False, 72 | optional=True, 73 | ), 74 | ] 75 | 76 | def _save_file(self, data: bytes, filename: str, rule: str): 77 | ext = ".key" if rule == "pkcs" else ".crt" 78 | with self.open(f"{filename}{ext}") as f: 79 | f.write(data) 80 | 81 | @classmethod 82 | def get_yara_rules(cls, key_type: str): 83 | sources = {} 84 | if key_type in ["all", "public"]: 85 | sources[ 86 | "x509" 87 | ] = "rule x509 {strings: $a = {30 82 ?? ?? 30 82 ?? ??} condition: $a}" 88 | 89 | if key_type in ["all", "private"]: 90 | sources[ 91 | "pkcs" 92 | ] = "rule pkcs {strings: $a = {30 82 ?? ?? 02 01 00} condition: $a}" 93 | 94 | return yara.compile(sources=sources) 95 | 96 | @classmethod 97 | def get_cert_or_pem( 98 | self, 99 | layer: interfaces.layers.DataLayerInterface, 100 | rule_name: str, 101 | offset: int, 102 | value: bytes, 103 | ) -> Union[Certificate, PRIVATE_KEY_TYPES]: 104 | """Parse the value from the layer and convert to an X509 cert or PEM. 105 | 106 | Args: 107 | layer (interfaces.layers.DataLayerInterface): Current layer 108 | rule_name (str): Rule that triggered the match 109 | offset (int): Offset address 110 | value (bytes): Bytes representing a certificate or pem 111 | 112 | Returns: 113 | Union[Certificate, PRIVATE_KEY_TYPES]: Either a Certificate or PEM type 114 | """ 115 | try: 116 | _, cert_size = unpack(">HH", value[0:4]) 117 | data = layer.read(offset, cert_size + 4) 118 | 119 | # If x509 triggered, try to create a DER x509 certificate to validate 120 | if rule_name == "x509": 121 | rsa_object = load_der_x509_certificate(data, default_backend()) 122 | 123 | # If pkcs triggered, try to create a PEM private key to validate 124 | elif rule_name == "pkcs": 125 | pem = ( 126 | b"-----BEGIN RSA PRIVATE KEY-----\n" 127 | + b64encode(data) 128 | + b"\n-----END RSA PRIVATE KEY-----" 129 | ) 130 | rsa_object = load_pem_private_key(pem, None, default_backend()) 131 | 132 | return rsa_object 133 | except: 134 | return None 135 | 136 | @classmethod 137 | def get_certs_by_process( 138 | cls, 139 | context: interfaces.context.ContextInterface, 140 | proc: interfaces.objects.ObjectInterface, 141 | key_types: str, 142 | ) -> Iterable[ 143 | Tuple[ 144 | int, 145 | interfaces.objects.ObjectInterface, 146 | str, 147 | Union[Certificate, PRIVATE_KEY_TYPES], 148 | ] 149 | ]: 150 | """Gets certificates or pem by process 151 | 152 | Args: 153 | context (interfaces.context.ContextInterface): Context 154 | proc (interfaces.objects.ObjectInterface): Process object to scan 155 | key_types (str): Type of key to scan for 156 | 157 | Yields: 158 | Iterable[ Tuple[ int, interfaces.objects.ObjectInterface, str, Union[Certificate, PRIVATE_KEY_TYPES], ] ]: Scan results 159 | """ 160 | layer_name = proc.add_process_layer() 161 | layer = context.layers[layer_name] 162 | 163 | for offset, rule_name, _, value in layer.scan( 164 | context=context, 165 | scanner=yarascan.YaraScanner(rules=cls.get_yara_rules(key_types)), 166 | sections=vadyarascan.VadYaraScan.get_vad_maps(proc), 167 | ): 168 | cert_or_pem = cls.get_cert_or_pem(layer, rule_name, offset, value) 169 | if cert_or_pem: 170 | yield (offset, proc, rule_name, cert_or_pem) 171 | 172 | @classmethod 173 | def get_process_certificates( 174 | cls, 175 | context: interfaces.context.ContextInterface, 176 | layer_name: str, 177 | symbol_table: str, 178 | filter_func: Callable[ 179 | [interfaces.objects.ObjectInterface], bool 180 | ] = lambda _: False, 181 | key_types: str = "all", 182 | ) -> Iterable[ 183 | Tuple[ 184 | int, 185 | interfaces.objects.ObjectInterface, 186 | str, 187 | Union[Certificate, PRIVATE_KEY_TYPES], 188 | ] 189 | ]: 190 | """Scans processes for RSA certificates 191 | 192 | Args: 193 | context: The context to retrieve required elements (layers, symbol tables) from 194 | layer_name: The name of the layer on which to operate 195 | symbol_table: The name of the table containing the kernel symbols 196 | filter_func: Filter function for listing processes 197 | key_types: Can be "all", "public", or "private" 198 | 199 | Yields: 200 | A tuple of offset, EPROCESS, rule name, and certificate or key found by scanning process layer 201 | """ 202 | for proc in pslist.PsList.list_processes( 203 | context=context, 204 | layer_name=layer_name, 205 | symbol_table=symbol_table, 206 | filter_func=filter_func, 207 | ): 208 | for offset, proc, rule_name, cert_or_pem in cls.get_certs_by_process( 209 | context, proc, key_types 210 | ): 211 | yield (offset, proc, rule_name, cert_or_pem) 212 | 213 | @classmethod 214 | def get_physical_certificates( 215 | cls, 216 | context: interfaces.context.ContextInterface, 217 | layer_name: str, 218 | key_type: str, 219 | ) -> Iterable[Tuple[int, str, Union[Certificate, PRIVATE_KEY_TYPES],]]: 220 | 221 | layer = context.layers[layer_name] 222 | for offset, rule_name, _, value in layer.scan( 223 | context=context, 224 | scanner=yarascan.YaraScanner(rules=cls.get_yara_rules(key_type)), 225 | ): 226 | cert_or_pem = cls.get_cert_or_pem(layer, rule_name, offset, value) 227 | if cert_or_pem: 228 | yield (offset, rule_name, cert_or_pem) 229 | 230 | def _generator(self, physical: bool): 231 | 232 | kernel = self.context.modules[self.config["kernel"]] 233 | pid_list = self.config.get("pid", []) 234 | name_list = self.config.get("name", []) 235 | 236 | def proc_name_and_pid_filter(proc: EPROCESS): 237 | try: 238 | proc_name = proc.ImageFileName.cast( 239 | "string", 240 | max_length=proc.ImageFileName.vol.count, 241 | errors="ignore", 242 | ).lower() 243 | return ( 244 | proc.UniqueProcessId not in pid_list and proc_name not in name_list 245 | ) 246 | # Specifically, if there is a smear, process info might not be valid 247 | # So ignore if the process name can't be cast to anything 248 | except: 249 | return True 250 | 251 | if pid_list or name_list: 252 | filter_func = proc_name_and_pid_filter 253 | else: 254 | filter_func = lambda x: False 255 | 256 | key_type = self.config.get("type") 257 | output = self.config.get("dump", False) 258 | 259 | if physical: 260 | 261 | for offset, rule_name, cert_or_pem in self.get_physical_certificates( 262 | context=self.context, layer_name=kernel.layer_name, key_type=key_type 263 | ): 264 | value, output_bytes, thumbprint = self._get_value_and_bytes( 265 | rule_name, cert_or_pem 266 | ) 267 | 268 | if output: 269 | if rule_name == "pkcs": 270 | self._save_file(output_bytes, value, rule_name) 271 | elif rule_name == "x509": 272 | filename = thumbprint + "_" 273 | filename += ( 274 | cert_or_pem.subject._attributes[-1] 275 | ._attributes[-1] 276 | .value.strip('*."/[]:;|,') 277 | .split("/")[0] 278 | ) 279 | self._save_file(output_bytes, filename, rule_name) 280 | 281 | yield 0, ( 282 | format_hints.Hex(offset), 283 | rule_name, 284 | value, 285 | ) 286 | else: 287 | for offset, proc, rule_name, rsa_object in self.get_process_certificates( 288 | context=self.context, 289 | layer_name=kernel.layer_name, 290 | symbol_table=kernel.symbol_table_name, 291 | filter_func=filter_func, 292 | key_types=key_type, 293 | ): 294 | 295 | proc_name = proc.ImageFileName.cast( 296 | "string", 297 | max_length=proc.ImageFileName.vol.count, 298 | errors="replace", 299 | ) 300 | 301 | value, output_bytes, thumbprint = self._get_value_and_bytes( 302 | rule_name, rsa_object 303 | ) 304 | 305 | if output: 306 | if rule_name == "pkcs": 307 | self._save_file( 308 | output_bytes, f"{hex(offset)}_{value}", rule_name 309 | ) 310 | elif rule_name == "x509": 311 | filename = thumbprint + "_" 312 | filename += ( 313 | rsa_object.subject._attributes[-1] 314 | ._attributes[-1] 315 | .value.strip('*."/\[]:;|,') 316 | ) 317 | 318 | self._save_file(output_bytes, filename, rule_name) 319 | 320 | yield 0, ( 321 | format_hints.Hex(offset), 322 | proc.UniqueProcessId, 323 | proc_name, 324 | rule_name, 325 | thumbprint, 326 | value, 327 | ) 328 | 329 | def _get_value_and_bytes( 330 | self, rule_name: str, rsa_object: Union[Certificate, PRIVATE_KEY_TYPES] 331 | ) -> Tuple[str, bytes, str]: 332 | """Helper method to get the value and bytes from a Certificate or PEM 333 | 334 | Args: 335 | rule_name (str): Rule that triggered the match 336 | rsa_object (Union[Certificate, PRIVATE_KEY_TYPES]): Certificate or PEM 337 | 338 | Returns: 339 | Tuple[str, bytes]: Value for output, output bytes for saving to file 340 | """ 341 | 342 | # If x509 triggered, value is equal to subject (or thumbprint if subject fails) 343 | if rule_name == "x509": 344 | 345 | try: 346 | value = str(rsa_object.subject.rfc4514_string()) 347 | thumbprint = ( 348 | binascii.hexlify(rsa_object.fingerprint(hashes.SHA1())) 349 | .upper() 350 | .decode() 351 | ) 352 | except: 353 | pass 354 | 355 | output_bytes = rsa_object.public_bytes(Encoding.DER) 356 | 357 | # If pkcs triggered, value is equal to the key size 358 | elif rule_name == "pkcs": 359 | value = str(rsa_object.key_size) 360 | thumbprint = "" 361 | output_bytes = rsa_object.private_bytes( 362 | Encoding.DER, 363 | PrivateFormat.PKCS8, 364 | NoEncryption(), 365 | ) 366 | 367 | return (value, output_bytes, thumbprint) 368 | 369 | def run(self) -> renderers.TreeGrid: 370 | physical = self.config.get("physical") 371 | if physical: 372 | return renderers.TreeGrid( 373 | [ 374 | (f'{"Offset":<8}', format_hints.Hex), 375 | ("Rule", str), 376 | ("Thumbprint", str), 377 | ("Value", str), 378 | ], 379 | self._generator(physical), 380 | ) 381 | else: 382 | return renderers.TreeGrid( 383 | [ 384 | (f'{"Offset":<8}', format_hints.Hex), 385 | ("PID", int), 386 | (f'{"Process":<8}', str), 387 | ("Rule", str), 388 | ("Thumbprint/Key Size", str), 389 | ("Value", str), 390 | ], 391 | self._generator(physical), 392 | ) 393 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "arrow" 3 | version = "1.2.2" 4 | description = "Better dates & times for Python" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | python-dateutil = ">=2.7.0" 11 | 12 | [[package]] 13 | name = "attrs" 14 | version = "21.4.0" 15 | description = "Classes Without Boilerplate" 16 | category = "main" 17 | optional = false 18 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 19 | 20 | [package.extras] 21 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 22 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 23 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 24 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 25 | 26 | [[package]] 27 | name = "black" 28 | version = "22.3.0" 29 | description = "The uncompromising code formatter." 30 | category = "dev" 31 | optional = false 32 | python-versions = ">=3.6.2" 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | pathspec = ">=0.9.0" 38 | platformdirs = ">=2" 39 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 40 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 41 | 42 | [package.extras] 43 | colorama = ["colorama (>=0.4.3)"] 44 | d = ["aiohttp (>=3.7.4)"] 45 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 46 | uvloop = ["uvloop (>=0.15.2)"] 47 | 48 | [[package]] 49 | name = "capstone" 50 | version = "5.0.0" 51 | description = "Capstone disassembly engine" 52 | category = "main" 53 | optional = false 54 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 55 | 56 | [[package]] 57 | name = "cffi" 58 | version = "1.15.0" 59 | description = "Foreign Function Interface for Python calling C code." 60 | category = "main" 61 | optional = false 62 | python-versions = "*" 63 | 64 | [package.dependencies] 65 | pycparser = "*" 66 | 67 | [[package]] 68 | name = "click" 69 | version = "8.1.3" 70 | description = "Composable command line interface toolkit" 71 | category = "main" 72 | optional = false 73 | python-versions = ">=3.7" 74 | 75 | [package.dependencies] 76 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 77 | 78 | [[package]] 79 | name = "colorama" 80 | version = "0.4.5" 81 | description = "Cross-platform colored terminal text." 82 | category = "main" 83 | optional = false 84 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 85 | 86 | [[package]] 87 | name = "commonmark" 88 | version = "0.9.1" 89 | description = "Python parser for the CommonMark Markdown spec" 90 | category = "main" 91 | optional = false 92 | python-versions = "*" 93 | 94 | [package.extras] 95 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 96 | 97 | [[package]] 98 | name = "construct" 99 | version = "2.10.68" 100 | description = "A powerful declarative symmetric parser/builder for binary data" 101 | category = "main" 102 | optional = false 103 | python-versions = ">=3.6" 104 | 105 | [package.extras] 106 | extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] 107 | 108 | [[package]] 109 | name = "cryptography" 110 | version = "37.0.3" 111 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 112 | category = "main" 113 | optional = false 114 | python-versions = ">=3.6" 115 | 116 | [package.dependencies] 117 | cffi = ">=1.12" 118 | 119 | [package.extras] 120 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 121 | docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 122 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 123 | sdist = ["setuptools_rust (>=0.11.4)"] 124 | ssh = ["bcrypt (>=3.1.5)"] 125 | test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 126 | 127 | [[package]] 128 | name = "future" 129 | version = "0.18.2" 130 | description = "Clean single-source support for Python 3 and 2" 131 | category = "main" 132 | optional = false 133 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 134 | 135 | [[package]] 136 | name = "isort" 137 | version = "5.10.1" 138 | description = "A Python utility / library to sort Python imports." 139 | category = "dev" 140 | optional = false 141 | python-versions = ">=3.6.1,<4.0" 142 | 143 | [package.extras] 144 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 145 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 146 | colors = ["colorama (>=0.4.3,<0.5.0)"] 147 | plugins = ["setuptools"] 148 | 149 | [[package]] 150 | name = "jsonschema" 151 | version = "4.6.0" 152 | description = "An implementation of JSON Schema validation for Python" 153 | category = "main" 154 | optional = false 155 | python-versions = ">=3.7" 156 | 157 | [package.dependencies] 158 | attrs = ">=17.4.0" 159 | pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" 160 | 161 | [package.extras] 162 | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] 163 | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] 164 | 165 | [[package]] 166 | name = "mypy-extensions" 167 | version = "0.4.3" 168 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 169 | category = "dev" 170 | optional = false 171 | python-versions = "*" 172 | 173 | [[package]] 174 | name = "pathspec" 175 | version = "0.9.0" 176 | description = "Utility library for gitignore style pattern matching of file paths." 177 | category = "dev" 178 | optional = false 179 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 180 | 181 | [[package]] 182 | name = "pefile" 183 | version = "2022.5.30" 184 | description = "Python PE parsing module" 185 | category = "main" 186 | optional = false 187 | python-versions = ">=3.6.0" 188 | 189 | [package.dependencies] 190 | future = "*" 191 | 192 | [[package]] 193 | name = "platformdirs" 194 | version = "2.5.2" 195 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 196 | category = "dev" 197 | optional = false 198 | python-versions = ">=3.7" 199 | 200 | [package.extras] 201 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 202 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 203 | 204 | [[package]] 205 | name = "pycparser" 206 | version = "2.21" 207 | description = "C parser in Python" 208 | category = "main" 209 | optional = false 210 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 211 | 212 | [[package]] 213 | name = "pycryptodome" 214 | version = "3.15.0" 215 | description = "Cryptographic library for Python" 216 | category = "main" 217 | optional = false 218 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 219 | 220 | [[package]] 221 | name = "pygments" 222 | version = "2.12.0" 223 | description = "Pygments is a syntax highlighting package written in Python." 224 | category = "main" 225 | optional = false 226 | python-versions = ">=3.6" 227 | 228 | [[package]] 229 | name = "pyrsistent" 230 | version = "0.18.1" 231 | description = "Persistent/Functional/Immutable data structures" 232 | category = "main" 233 | optional = false 234 | python-versions = ">=3.7" 235 | 236 | [[package]] 237 | name = "python-dateutil" 238 | version = "2.8.2" 239 | description = "Extensions to the standard Python datetime module" 240 | category = "main" 241 | optional = false 242 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 243 | 244 | [package.dependencies] 245 | six = ">=1.5" 246 | 247 | [[package]] 248 | name = "rich" 249 | version = "12.4.4" 250 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 251 | category = "main" 252 | optional = false 253 | python-versions = ">=3.6.3,<4.0.0" 254 | 255 | [package.dependencies] 256 | commonmark = ">=0.9.0,<0.10.0" 257 | pygments = ">=2.6.0,<3.0.0" 258 | 259 | [package.extras] 260 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 261 | 262 | [[package]] 263 | name = "rich-click" 264 | version = "1.5.1" 265 | description = "Format click help output nicely with rich" 266 | category = "main" 267 | optional = false 268 | python-versions = ">=3.7" 269 | 270 | [package.dependencies] 271 | click = ">=7" 272 | rich = ">=10.7.0" 273 | typer = {version = ">=0.4", optional = true, markers = "extra == \"typer\""} 274 | 275 | [package.extras] 276 | dev = ["pre-commit"] 277 | typer = ["typer (>=0.4)"] 278 | 279 | [[package]] 280 | name = "six" 281 | version = "1.16.0" 282 | description = "Python 2 and 3 compatibility utilities" 283 | category = "main" 284 | optional = false 285 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 286 | 287 | [[package]] 288 | name = "tomli" 289 | version = "2.0.1" 290 | description = "A lil' TOML parser" 291 | category = "dev" 292 | optional = false 293 | python-versions = ">=3.7" 294 | 295 | [[package]] 296 | name = "typer" 297 | version = "0.4.1" 298 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 299 | category = "main" 300 | optional = false 301 | python-versions = ">=3.6" 302 | 303 | [package.dependencies] 304 | click = ">=7.1.1,<9.0.0" 305 | 306 | [package.extras] 307 | all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] 308 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] 309 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] 310 | test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"] 311 | 312 | [[package]] 313 | name = "typing-extensions" 314 | version = "4.2.0" 315 | description = "Backported and Experimental Type Hints for Python 3.7+" 316 | category = "dev" 317 | optional = false 318 | python-versions = ">=3.7" 319 | 320 | [[package]] 321 | name = "volatility3" 322 | version = "2.2.0" 323 | description = "Memory forensics framework" 324 | category = "dev" 325 | optional = false 326 | python-versions = ">=3.6.0" 327 | develop = false 328 | 329 | [package.dependencies] 330 | pefile = ">=2017.8.1" 331 | 332 | [package.source] 333 | type = "git" 334 | url = "https://github.com/volatilityfoundation/volatility3" 335 | reference = "develop" 336 | resolved_reference = "39e812a207b7c2de61bf74549c2b60e534c86497" 337 | 338 | [[package]] 339 | name = "yara-python" 340 | version = "4.2.0" 341 | description = "Python interface for YARA" 342 | category = "main" 343 | optional = false 344 | python-versions = "*" 345 | 346 | [metadata] 347 | lock-version = "1.1" 348 | python-versions = "^3.9" 349 | content-hash = "79b947b53b42668f10169ade223946c87fa00ab4c0b783c560b865eafa9eb93d" 350 | 351 | [metadata.files] 352 | arrow = [ 353 | {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, 354 | {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, 355 | ] 356 | attrs = [ 357 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 358 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 359 | ] 360 | black = [ 361 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 362 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 363 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 364 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 365 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 366 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 367 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 368 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 369 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 370 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 371 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 372 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 373 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 374 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 375 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 376 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 377 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 378 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 379 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 380 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 381 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 382 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 383 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 384 | ] 385 | capstone = [ 386 | {file = "capstone-5.0.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:4255086c14a98037ae6c1d554f54691e2179b4bb792b23c9811ded2f35807904"}, 387 | {file = "capstone-5.0.0-py3-none-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:fabca634c49b48cb1aa3c9f8ceb9a0b7919582e34369d228e0d7b00980c7044e"}, 388 | {file = "capstone-5.0.0-py3-none-manylinux1_i686.whl", hash = "sha256:b63c2c0e4492f3a9ffdaaaf55ceff5fe73e5267244e5cda4095f9e1d9174fa4b"}, 389 | {file = "capstone-5.0.0-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f736353b1983045bd282a0627124600f52aa544c3c21819243522d0540e26a"}, 390 | {file = "capstone-5.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:9717b35221fcc9d8db9240ff6b7b007808805f5e0409daf81c0831c13ff37653"}, 391 | {file = "capstone-5.0.0-py3-none-win32.whl", hash = "sha256:9ac2ea94a798790c996d8ced727fe9afa92882f7dc04ce423342aeab98ff83d6"}, 392 | {file = "capstone-5.0.0-py3-none-win_amd64.whl", hash = "sha256:e0c59d401452237a838ad5067eccc4f72a26af4b2b865d166822721de306eba5"}, 393 | {file = "capstone-5.0.0.tar.gz", hash = "sha256:6e18ee140463881c627b7ff7fd655752ddf37d9036295d3dba7b130408fbabaf"}, 394 | ] 395 | cffi = [ 396 | {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, 397 | {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, 398 | {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, 399 | {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, 400 | {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, 401 | {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, 402 | {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, 403 | {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, 404 | {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, 405 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, 406 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, 407 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, 408 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, 409 | {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, 410 | {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, 411 | {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, 412 | {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, 413 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, 414 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, 415 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, 416 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, 417 | {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, 418 | {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, 419 | {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, 420 | {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, 421 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, 422 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, 423 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, 424 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, 425 | {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, 426 | {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, 427 | {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, 428 | {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, 429 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, 430 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, 431 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, 432 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, 433 | {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, 434 | {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, 435 | {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, 436 | {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, 437 | {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, 438 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, 439 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, 440 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, 441 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, 442 | {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, 443 | {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, 444 | {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, 445 | {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, 446 | ] 447 | click = [ 448 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 449 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 450 | ] 451 | colorama = [ 452 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 453 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 454 | ] 455 | commonmark = [ 456 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 457 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 458 | ] 459 | construct = [ 460 | {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, 461 | ] 462 | cryptography = [ 463 | {file = "cryptography-37.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:d10413d493e98075060d3e62e5826de372912ea653ccc948f3c41b21ddca087f"}, 464 | {file = "cryptography-37.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:cd64147ff16506632893ceb2569624b48c84daa3ba4d89695f7c7bc24188eee9"}, 465 | {file = "cryptography-37.0.3-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:17c74f7d9e9e9bb7e84521243695c1b4bdc3a0e44ca764e6bcf8f05f3de3d0df"}, 466 | {file = "cryptography-37.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0713bee6c8077786c56bdec9c5d3f099d40d2c862ff3200416f6862e9dd63156"}, 467 | {file = "cryptography-37.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9c2008417741cdfbe945ef2d16b7b7ba0790886a0b49e1de533acf93eb66ed6"}, 468 | {file = "cryptography-37.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646905ff7a712e415bf0d0f214e0eb669dd2257c4d7a27db1e8baec5d2a1d55f"}, 469 | {file = "cryptography-37.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:dcafadb5a06cb7a6bb49fb4c1de7414ee2f8c8e12b047606d97c3175d690f582"}, 470 | {file = "cryptography-37.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0b4bfc5ccfe4e5c7de535670680398fed4a0bbc5dfd52b3a295baad42230abdf"}, 471 | {file = "cryptography-37.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a03dbc0d8ce8c1146c177cd0e3a66ea106f36733fb1b997ea4d051f8a68539ff"}, 472 | {file = "cryptography-37.0.3-cp36-abi3-win32.whl", hash = "sha256:190a24c14e91c1fa3101069aac7e77d11c5a73911c3904128367f52946bbb6fd"}, 473 | {file = "cryptography-37.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:b05c5478524deb7a019e240f2a970040c4b0f01f58f0425e6262c96b126c6a3e"}, 474 | {file = "cryptography-37.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891ed8312840fd43e0696468a6520a582a033c0109f7b14b96067bfe1123226b"}, 475 | {file = "cryptography-37.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:30d6aabf623a01affc7c0824936c3dde6590076b61f5dd299df3cc2c75fc5915"}, 476 | {file = "cryptography-37.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:31a7c1f1c2551f013d4294d06e22848e2ccd77825f0987cba3239df6ebf7b020"}, 477 | {file = "cryptography-37.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a94fd1ff80001cb97add71d07f596d8b865b716f25ef501183e0e199390e50d3"}, 478 | {file = "cryptography-37.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8a85dbcc770256918b40c2f40bd3ffd3b2ae45b0cf19068b561db8f8d61bf492"}, 479 | {file = "cryptography-37.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:773d5b5f2e2bd2c7cbb1bd24902ad41283c88b9dd463a0f82adc9a2870d9d066"}, 480 | {file = "cryptography-37.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0f9193428a55a4347af2d4fd8141a2002dedbcc26487e67fd2ae19f977ee8afc"}, 481 | {file = "cryptography-37.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bf652c73e8f7c32a3f92f7184bf7f9106dacdf5ef59c3c3683d7dae2c4972fb"}, 482 | {file = "cryptography-37.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c3c8b1ad2c266fdf7adc041cc4156d6a3d14db93de2f81b26a5af97ef3f209e5"}, 483 | {file = "cryptography-37.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2383d6c3088e863304c37c65cd2ea404b7fbb4886823eab1d74137cc27f3d2ee"}, 484 | {file = "cryptography-37.0.3.tar.gz", hash = "sha256:ae430d51c67ac638dfbb42edf56c669ca9c74744f4d225ad11c6f3d355858187"}, 485 | ] 486 | future = [ 487 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 488 | ] 489 | isort = [ 490 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 491 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 492 | ] 493 | jsonschema = [ 494 | {file = "jsonschema-4.6.0-py3-none-any.whl", hash = "sha256:1c92d2db1900b668201f1797887d66453ab1fbfea51df8e4b46236689c427baf"}, 495 | {file = "jsonschema-4.6.0.tar.gz", hash = "sha256:9d6397ba4a6c0bf0300736057f649e3e12ecbc07d3e81a0dacb72de4e9801957"}, 496 | ] 497 | mypy-extensions = [ 498 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 499 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 500 | ] 501 | pathspec = [ 502 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 503 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 504 | ] 505 | pefile = [ 506 | {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, 507 | ] 508 | platformdirs = [ 509 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 510 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 511 | ] 512 | pycparser = [ 513 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 514 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 515 | ] 516 | pycryptodome = [ 517 | {file = "pycryptodome-3.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff7ae90e36c1715a54446e7872b76102baa5c63aa980917f4aa45e8c78d1a3ec"}, 518 | {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2ffd8b31561455453ca9f62cb4c24e6b8d119d6d531087af5f14b64bee2c23e6"}, 519 | {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2ea63d46157386c5053cfebcdd9bd8e0c8b7b0ac4a0507a027f5174929403884"}, 520 | {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c9ed8aa31c146bef65d89a1b655f5f4eab5e1120f55fc297713c89c9e56ff0b"}, 521 | {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:5099c9ca345b2f252f0c28e96904643153bae9258647585e5e6f649bb7a1844a"}, 522 | {file = "pycryptodome-3.15.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:2ec709b0a58b539a4f9d33fb8508264c3678d7edb33a68b8906ba914f71e8c13"}, 523 | {file = "pycryptodome-3.15.0-cp27-cp27m-win32.whl", hash = "sha256:fd2184aae6ee2a944aaa49113e6f5787cdc5e4db1eb8edb1aea914bd75f33a0c"}, 524 | {file = "pycryptodome-3.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:7e3a8f6ee405b3bd1c4da371b93c31f7027944b2bcce0697022801db93120d83"}, 525 | {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b9c5b1a1977491533dfd31e01550ee36ae0249d78aae7f632590db833a5012b8"}, 526 | {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0926f7cc3735033061ef3cf27ed16faad6544b14666410727b31fea85a5b16eb"}, 527 | {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2aa55aae81f935a08d5a3c2042eb81741a43e044bd8a81ea7239448ad751f763"}, 528 | {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c3640deff4197fa064295aaac10ab49a0d55ef3d6a54ae1499c40d646655c89f"}, 529 | {file = "pycryptodome-3.15.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:045d75527241d17e6ef13636d845a12e54660aa82e823b3b3341bcf5af03fa79"}, 530 | {file = "pycryptodome-3.15.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9ee40e2168f1348ae476676a2e938ca80a2f57b14a249d8fe0d3cdf803e5a676"}, 531 | {file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:4c3ccad74eeb7b001f3538643c4225eac398c77d617ebb3e57571a897943c667"}, 532 | {file = "pycryptodome-3.15.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:1b22bcd9ec55e9c74927f6b1f69843cb256fb5a465088ce62837f793d9ffea88"}, 533 | {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:57f565acd2f0cf6fb3e1ba553d0cb1f33405ec1f9c5ded9b9a0a5320f2c0bd3d"}, 534 | {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:4b52cb18b0ad46087caeb37a15e08040f3b4c2d444d58371b6f5d786d95534c2"}, 535 | {file = "pycryptodome-3.15.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:092a26e78b73f2530b8bd6b3898e7453ab2f36e42fd85097d705d6aba2ec3e5e"}, 536 | {file = "pycryptodome-3.15.0-cp35-abi3-win32.whl", hash = "sha256:e244ab85c422260de91cda6379e8e986405b4f13dc97d2876497178707f87fc1"}, 537 | {file = "pycryptodome-3.15.0-cp35-abi3-win_amd64.whl", hash = "sha256:c77126899c4b9c9827ddf50565e93955cb3996813c18900c16b2ea0474e130e9"}, 538 | {file = "pycryptodome-3.15.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:9eaadc058106344a566dc51d3d3a758ab07f8edde013712bc8d22032a86b264f"}, 539 | {file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ff287bcba9fbeb4f1cccc1f2e90a08d691480735a611ee83c80a7d74ad72b9d9"}, 540 | {file = "pycryptodome-3.15.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:60b4faae330c3624cc5a546ba9cfd7b8273995a15de94ee4538130d74953ec2e"}, 541 | {file = "pycryptodome-3.15.0-pp27-pypy_73-win32.whl", hash = "sha256:a8f06611e691c2ce45ca09bbf983e2ff2f8f4f87313609d80c125aff9fad6e7f"}, 542 | {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9cc96e274b253e47ad33ae1fccc36ea386f5251a823ccb50593a935db47fdd2"}, 543 | {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ecaaef2d21b365d9c5ca8427ffc10cebed9d9102749fd502218c23cb9a05feb5"}, 544 | {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:d2a39a66057ab191e5c27211a7daf8f0737f23acbf6b3562b25a62df65ffcb7b"}, 545 | {file = "pycryptodome-3.15.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:9c772c485b27967514d0df1458b56875f4b6d025566bf27399d0c239ff1b369f"}, 546 | {file = "pycryptodome-3.15.0.tar.gz", hash = "sha256:9135dddad504592bcc18b0d2d95ce86c3a5ea87ec6447ef25cfedea12d6018b8"}, 547 | ] 548 | pygments = [ 549 | {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, 550 | {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, 551 | ] 552 | pyrsistent = [ 553 | {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, 554 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, 555 | {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, 556 | {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, 557 | {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, 558 | {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, 559 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, 560 | {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, 561 | {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, 562 | {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, 563 | {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, 564 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, 565 | {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, 566 | {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, 567 | {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, 568 | {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, 569 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, 570 | {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, 571 | {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, 572 | {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, 573 | {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, 574 | ] 575 | python-dateutil = [ 576 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 577 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 578 | ] 579 | rich = [ 580 | {file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"}, 581 | {file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"}, 582 | ] 583 | rich-click = [ 584 | {file = "rich-click-1.5.1.tar.gz", hash = "sha256:2ca72637d618c8033891ecb73e6e0c01af15afdac80d134ca93a58f1cbcd5ded"}, 585 | {file = "rich_click-1.5.1-py3-none-any.whl", hash = "sha256:cf16bf9e390d5c9aa3b379c3884c52b47f54c8e6e2e7ddb7b2a002f4420c35c0"}, 586 | ] 587 | six = [ 588 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 589 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 590 | ] 591 | tomli = [ 592 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 593 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 594 | ] 595 | typer = [ 596 | {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, 597 | {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, 598 | ] 599 | typing-extensions = [ 600 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 601 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 602 | ] 603 | volatility3 = [] 604 | yara-python = [ 605 | {file = "yara-python-4.2.0.tar.gz", hash = "sha256:d02f239f429c6c94e60b500246d376595fbed8d9124209d332b6f8e7cfb5ec6e"}, 606 | {file = "yara_python-4.2.0-cp310-cp310-win32.whl", hash = "sha256:1faec6480be87964056ac19cddae052da4c88755426c81c83bce53ed7d57ebfe"}, 607 | {file = "yara_python-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:76f711603de6a66a42be4501219288353397b032d6239b01a34e2bbcce4a2e50"}, 608 | {file = "yara_python-4.2.0-cp35-cp35m-win32.whl", hash = "sha256:cabef9144d35357ba9815f6ecacfaeed05fa2cc2a2ae0a0c37766839500555d9"}, 609 | {file = "yara_python-4.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:300581837e17114432d52599075bb8348dc2ce3e9ec8d3c325f5a797424791a4"}, 610 | {file = "yara_python-4.2.0-cp36-cp36m-win32.whl", hash = "sha256:c4c147495267a26b2d5fc39a486e1561bc501b170f973ab1ab0f39f2c00791da"}, 611 | {file = "yara_python-4.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9d43bb5a5148f1c50e972e8fc4653620d63bb0d1cb02701d41fa580eb44a1bcf"}, 612 | {file = "yara_python-4.2.0-cp37-cp37m-win32.whl", hash = "sha256:0de9d9482a6def6ef719ac9024dceae89809fa05d02656518a146374df93eb94"}, 613 | {file = "yara_python-4.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:963e089f5d70d614408d08830a9244f3146a63c87a2daedc130af72f6569d2af"}, 614 | {file = "yara_python-4.2.0-cp38-cp38-win32.whl", hash = "sha256:5da322141985e95f5018856603761ad908c3eb6dd8add566066c082fca8b4541"}, 615 | {file = "yara_python-4.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:4853d0bf9a320fd98f5a0beb94eb651639073f8ccc2e2f40c132c66f3ed64d74"}, 616 | {file = "yara_python-4.2.0-cp39-cp39-win32.whl", hash = "sha256:e5adfdcf01e2cc6bc07603eaadb314cf3515cab577e529db86c4de7bf9b327dd"}, 617 | {file = "yara_python-4.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:a92dc977e5e6add276ebdf5c8e0ddae7a79a85993c407a9c1d198dab518116e6"}, 618 | ] 619 | --------------------------------------------------------------------------------