├── malchive ├── __init__.py ├── utilities │ ├── data │ │ └── apihashes.db │ ├── reverse_bytes.py │ ├── byteflip.py │ ├── negate.py │ ├── hashes.py │ ├── rotate.py │ ├── killaslr.py │ ├── pecarver.py │ ├── xor_pairwise.py │ ├── guid_recovery.py │ ├── entropycalc.py │ ├── b64dump.py │ ├── findapihash.py │ ├── genrsa.py │ ├── hiddencab.py │ ├── xor.py │ ├── sub.py │ ├── add.py │ ├── ssl_cert.py │ ├── peresources.py │ ├── brute_xor.py │ ├── comguidtoyara.py │ ├── dotnetdumper.py │ ├── apihash.py │ └── pepdb.py ├── helpers │ ├── crypt_plaintexts.py │ ├── winfunc.py │ ├── myRC4.py │ ├── BinDataHelper.py │ └── apLib.py ├── extras │ ├── spivy_test_response.py │ ├── active_discovery_template.py │ └── decoder_template.py ├── decoders │ ├── rzstreet_dumper.py │ ├── apollo.py │ ├── sunburst.py │ ├── cobaltstrike_payload.py │ └── sunburst_dga.py └── active_discovery │ ├── shadowpad.py │ ├── spivy.py │ └── cobaltstrike_beacon.py ├── MANIFEST.in ├── LICENSE ├── .pre-commit-config.yaml ├── .gitignore ├── setup.py └── README.md /malchive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include malchive/utilities/data/* 2 | -------------------------------------------------------------------------------- /malchive/utilities/data/apihashes.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MITRECND/malchive/HEAD/malchive/utilities/data/apihashes.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright(c) 2021 The MITRE Corporation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | 6 | You may obtain a copy of the License at: 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /malchive/helpers/crypt_plaintexts.py: -------------------------------------------------------------------------------- 1 | plaintexts = { 2 | 'DosModeHeader': [ 3 | # Beginning DOS header 4 | bytearray(b'\x4d\x5a\x90\x00\x03\x00\x00\x00\x04\x00' 5 | b'\x00\x00\xff\xff\x00\x00\xb8\x00\x00\x00' 6 | b'\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00' 7 | b'\x00\x00'), 8 | # DOSMODE text 9 | bytearray(b'\x54\x68\x69\x73\x20\x70\x72\x6f\x67\x72' 10 | b'\x61\x6d\x20\x63\x61\x6e\x6e\x6f\x74\x20' 11 | b'\x62\x65\x20\x72\x75\x6e\x20\x69\x6e\x20' 12 | b'\x44\x4f\x53\x20\x6d\x6f'), 13 | # WIN32 text 14 | bytearray(b'\x54\x68\x69\x73\x20\x70\x72\x6f\x67\x72' 15 | b'\x61\x6d\x20\x6d\x75\x73\x74\x20\x62\x65' 16 | b'\x20\x72\x75\x6e\x20\x75\x6e\x64\x65\x72' 17 | b'\x20\x57\x69\x6e\x33\x32') 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks.git 3 | rev: v2.4.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-executables-have-shebangs 7 | - id: check-docstring-first 8 | - id: check-case-conflict 9 | - id: check-merge-conflict 10 | - id: debug-statements 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | - repo: https://gitlab.com/pycqa/flake8.git 14 | rev: "3.7.9" 15 | hooks: 16 | - id: flake8 17 | args: 18 | - "--max-line-length=100" 19 | - repo: https://github.com/pre-commit/mirrors-mypy.git 20 | rev: v0.761 21 | hooks: 22 | - id: mypy 23 | name: malchive utilities 24 | files: ^malchive/utilities 25 | - id: mypy 26 | name: malchive decoders 27 | files: ^malchive/decoders/ 28 | - id: mypy 29 | name: malchive active discovery 30 | files: ^malchive/active_discovery/ 31 | - id: mypy 32 | name: malchive extras 33 | files: ^malchive/extras/ 34 | - id: mypy 35 | name: malchive helpers 36 | files: ^malchive/helpers/ 37 | -------------------------------------------------------------------------------- /malchive/utilities/reverse_bytes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import logging 6 | import argparse 7 | 8 | __version__ = "1.0.0" 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def initialize_parser(): 14 | parser = argparse.ArgumentParser( 15 | description='Reverse data stream and write to STDOUT. That\'s it.') 16 | parser.add_argument('infile', type=argparse.FileType('r'), default='-', 17 | help='Data stream to process ' 18 | '(stdin, denoted by a \'-\').') 19 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 20 | help='Output additional information when processing ' 21 | '(mostly for debugging purposes).') 22 | return parser 23 | 24 | 25 | def main(): 26 | 27 | p = initialize_parser() 28 | args = p.parse_args() 29 | 30 | root = logging.getLogger() 31 | logging.basicConfig() 32 | if args.verbose: 33 | root.setLevel(logging.DEBUG) 34 | else: 35 | root.setLevel(logging.WARNING) 36 | 37 | stream = bytearray(args.infile.buffer.read()) 38 | stream.reverse() 39 | sys.stdout.buffer.write(stream) 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /malchive/extras/spivy_test_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import camellia 18 | import socket 19 | import struct 20 | from random import choice, randint 21 | 22 | __version__ = "1.0.0" 23 | __author__ = "Jason Batchelor" 24 | 25 | 26 | # Simple test illustrating who one might emulate the initial 27 | # negotiation for SPIVY. 28 | 29 | 30 | HOST = '10.10.10.1' 31 | PORT = 8080 32 | PASSWORD = 'admin' 33 | 34 | key_size = 32 35 | 36 | 37 | def fake_challenge_response(request, key=PASSWORD): 38 | 39 | if len(key) < key_size: 40 | key += '\x00' * (key_size - len(key)) 41 | 42 | junk_size = randint(1, 16) 43 | 44 | junk_data = bytearray( 45 | [ 46 | choice([i for i in range(0, 256)]) 47 | for i in range(0, junk_size) 48 | ]) 49 | 50 | challenge_request = request[-0x100:] 51 | 52 | c = camellia.CamelliaCipher(bytes(key.encode('utf-8')), 53 | mode=camellia.MODE_ECB) 54 | 55 | challenge_response = c.encrypt(challenge_request) 56 | 57 | payload = \ 58 | struct.pack('B', junk_size) + \ 59 | junk_data + \ 60 | struct.pack('B', (junk_size*2 & 0xff)) + \ 61 | challenge_response 62 | 63 | return payload 64 | 65 | 66 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 67 | s.bind((HOST, PORT)) 68 | s.listen() 69 | conn, addr = s.accept() 70 | with conn: 71 | print('Connected by', addr) 72 | while True: 73 | data = conn.recv(1024) 74 | resp = fake_challenge_response(data) 75 | if not data: 76 | break 77 | conn.sendall(resp) 78 | -------------------------------------------------------------------------------- /malchive/helpers/winfunc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Desc: 18 | # ----- 19 | # Python implementations designed to emulate certain Windows functions. 20 | # 21 | # Usage: 22 | # ------ 23 | # >>> from malchive.helpers import winfunc 24 | # >>> winfunc.CryptDeriveKey(b'test') 25 | # 26 | # 27 | 28 | import logging 29 | import hashlib 30 | 31 | __version__ = "1.0.0" 32 | __author__ = "Jason Batchelor" 33 | 34 | log = logging.getLogger(__name__) 35 | 36 | 37 | def CryptDeriveKey(password, key_len=0x20): 38 | """ 39 | Emulate MS CryptDeriveKey 40 | https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptderivekey 41 | 42 | :param bytes password: Password to base the key off of. 43 | :param int key_len: Length of key to be used. 44 | 45 | :return: Sequence of bytes representing the key to use. 46 | :rtype: bytes 47 | """ 48 | 49 | base_data = hashlib.sha1(password).digest() 50 | b1, b2 = bytearray(b'\x00' * 0x40), bytearray(b'\x00' * 0x40) 51 | for i in range(0, 0x40): 52 | b1[i] = 0x36 53 | b2[i] = 0x5c 54 | if i < len(base_data): 55 | b1[i] ^= base_data[i] 56 | b2[i] ^= base_data[i] 57 | key_hash = hashlib.sha1(b1).digest() + hashlib.sha1(b2).digest() 58 | return key_hash[:key_len] 59 | 60 | 61 | def msvcrt_rand(s=0, size=1): 62 | """ 63 | Emulate interplay of srand() and rand() 64 | 65 | :param int s: The seed. 66 | :param int size: Desired length of returned data. 67 | 68 | :return: A sequence of bytes computed using the supplied arguments. 69 | :rtype: bytearray 70 | """ 71 | 72 | result = bytearray() 73 | for i in range(0, size): 74 | s = (214013*s + 2531011) & 0x7fffffff 75 | result.append(s >> 16 & 0xff) 76 | return result 77 | -------------------------------------------------------------------------------- /.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 | # PyCharm 132 | .idea/ 133 | -------------------------------------------------------------------------------- /malchive/utilities/byteflip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from malchive.helpers import BinDataHelper 20 | 21 | __version__ = "1.0.0" 22 | __author__ = "Jason Batchelor" 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class GenericByteflip(BinDataHelper.LiteCrypt): 28 | 29 | def run_crypt(self): 30 | """ 31 | Swap bytes of supplied data. 32 | 33 | :return: processed data 34 | :rtype: bytearray 35 | """ 36 | 37 | if self.total_size % 2 != 0: 38 | raise ValueError('Total size of processed bytes must be even' 39 | 'length for byteflip.') 40 | 41 | flipped = bytearray() 42 | 43 | for i in range(self.offset, self.total_size, 2): 44 | flipped.append(self.buff[i + 1]) 45 | flipped.append(self.buff[i]) 46 | 47 | return flipped 48 | 49 | 50 | def initialize_parser(): 51 | 52 | description = 'Process data stream and swap each byte. ' \ 53 | 'Numeric values may be provided ' \ 54 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 55 | 56 | parser = BinDataHelper.generic_args(description) 57 | 58 | return parser 59 | 60 | 61 | def main(): 62 | p = initialize_parser() 63 | args = p.parse_args() 64 | 65 | root = logging.getLogger() 66 | logging.basicConfig() 67 | if args.verbose: 68 | root.setLevel(logging.DEBUG) 69 | else: 70 | root.setLevel(logging.WARNING) 71 | 72 | buff = args.infile.buffer.read() 73 | 74 | s = GenericByteflip( 75 | buff=buff, 76 | offset=args.offset, 77 | size=args.size, 78 | ) 79 | 80 | return_data = s.run_crypt() 81 | 82 | sys.stdout.buffer.write(return_data) 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /malchive/utilities/negate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from malchive.helpers import BinDataHelper 20 | 21 | __version__ = "1.0.0" 22 | __author__ = "Jason Batchelor" 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class GenericNegate(BinDataHelper.LiteCrypt): 28 | 29 | def __init__(self, 30 | skip_nulls: bool = False, 31 | *args, 32 | **kwargs): 33 | 34 | self.skip_nulls: bool = False 35 | self.skip_nulls = skip_nulls 36 | super().__init__(*args, **kwargs) 37 | 38 | def run_crypt(self): 39 | """ 40 | Run a generic negation on supplied data. 41 | 42 | :return: processed data 43 | :rtype: bytearray 44 | """ 45 | 46 | negated = bytearray() 47 | for i in range(self.offset, self.total_size): 48 | byte = self.buff[i] 49 | if self.skip_nulls and byte == 0x00: 50 | negated.append(byte) 51 | else: 52 | byte = ~byte & 0xff 53 | negated.append(byte) 54 | 55 | return negated 56 | 57 | 58 | def initialize_parser(): 59 | 60 | description = 'Process data stream and negate each byte. ' \ 61 | 'Numeric values may be provided ' \ 62 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 63 | 64 | parser = BinDataHelper.generic_args(description) 65 | 66 | parser.add_argument('-sn', '--skip-nulls', 67 | action='store_true', 68 | default=False, 69 | help='When processing the buffer, skip all null ' 70 | '(\'0x00\') bytes.') 71 | 72 | return parser 73 | 74 | 75 | def main(): 76 | p = initialize_parser() 77 | args = p.parse_args() 78 | 79 | root = logging.getLogger() 80 | logging.basicConfig() 81 | if args.verbose: 82 | root.setLevel(logging.DEBUG) 83 | else: 84 | root.setLevel(logging.WARNING) 85 | 86 | buff = args.infile.buffer.read() 87 | 88 | s = GenericNegate( 89 | skip_nulls=args.skip_nulls, 90 | buff=buff, 91 | offset=args.offset, 92 | size=args.size, 93 | ) 94 | 95 | return_data = s.run_crypt() 96 | 97 | sys.stdout.buffer.write(return_data) 98 | 99 | 100 | if __name__ == '__main__': 101 | main() 102 | -------------------------------------------------------------------------------- /malchive/utilities/hashes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | import logging 6 | import argparse 7 | import pefile 8 | import hashlib 9 | import tlsh 10 | 11 | __version__ = "1.0.0" 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class HashInfo: 17 | """ 18 | Return hashes based on supplied buffer such as; 19 | sha, md5, imphash, etc.. 20 | """ 21 | 22 | def __init__(self, buff): 23 | """ 24 | Initialize HashInfo. 25 | 26 | :param bytes buff: The stream of bytes to be processed. 27 | """ 28 | 29 | self.buff = buff 30 | self.imphash = self.get_imphash() 31 | self.sha256 = hashlib.sha256(self.buff).hexdigest() 32 | self.sha1 = hashlib.sha1(self.buff).hexdigest() 33 | self.md5 = hashlib.md5(self.buff).hexdigest() 34 | self.tlsh = tlsh.hash(self.buff).lower() 35 | 36 | def get_imphash(self): 37 | 38 | imphash = "" 39 | try: 40 | pe = pefile.PE(data=self.buff) 41 | imphash = pe.get_imphash() 42 | except pefile.PEFormatError: 43 | log.info('Data is not a pe...') 44 | return imphash 45 | 46 | 47 | def initialize_parser(): 48 | parser = argparse.ArgumentParser( 49 | description='Get various hashes on supplied filename.') 50 | parser.add_argument('infile', metavar='FILE', nargs='*', 51 | help='Full path to the file(s) to be processed.') 52 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 53 | help='Output additional information when processing ' 54 | '(mostly for debugging purposes).') 55 | 56 | return parser 57 | 58 | 59 | def main(): 60 | import json 61 | 62 | p = initialize_parser() 63 | args = p.parse_args() 64 | 65 | root = logging.getLogger() 66 | logging.basicConfig() 67 | if args.verbose: 68 | root.setLevel(logging.DEBUG) 69 | else: 70 | root.setLevel(logging.WARNING) 71 | 72 | if len(args.infile) == 0: 73 | p.print_help() 74 | sys.exit(2) 75 | 76 | for fname in args.infile: 77 | 78 | basename = os.path.basename(fname) 79 | 80 | if not os.path.isfile(fname): 81 | log.warning('Failed to find file %s. Skipping...' % fname) 82 | continue 83 | 84 | with open(fname, 'rb') as f: 85 | stream = f.read() 86 | 87 | hi = HashInfo(stream) 88 | 89 | hashDict = { 90 | 'Name': basename, 91 | 'ImpHash': "N/A" if len(hi.imphash) == 0 else hi.imphash, 92 | 'SHA256': hi.sha256, 93 | 'SHA1': hi.sha1, 94 | 'MD5': hi.md5, 95 | 'TLSH': hi.tlsh 96 | } 97 | 98 | try: 99 | print(json.dumps(hashDict, indent=4, sort_keys=False)) 100 | except UnicodeDecodeError: 101 | log.warning('There was a Unicode decoding error when processing %s' 102 | % fname) 103 | continue 104 | 105 | 106 | if __name__ == '__main__': 107 | main() 108 | -------------------------------------------------------------------------------- /malchive/utilities/rotate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from malchive.helpers import BinDataHelper 20 | 21 | __version__ = "1.0.0" 22 | __author__ = "Jason Batchelor" 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class GenericRotate(BinDataHelper.LiteCrypt): 28 | 29 | def __init__(self, 30 | count: int = 0, 31 | right: bool = False, 32 | *args, 33 | **kwargs): 34 | 35 | self.count: int = 0 36 | self.right: bool = False 37 | 38 | # Only rotating single bytes 39 | count = count % 8 40 | if right: 41 | log.debug('Modifying count to align with right rotation...') 42 | count = 8 - count 43 | 44 | self.count = count 45 | self.right = right 46 | 47 | super().__init__(*args, **kwargs) 48 | 49 | def run_crypt(self): 50 | """ 51 | Perform bitwise rotation of supplied BYTE N times. 52 | 53 | :return: processed data 54 | :rtype: bytearray 55 | """ 56 | 57 | rotated = bytearray() 58 | 59 | for i in range(self.offset, self.total_size): 60 | byte = self.buff[i] 61 | rotated.append( 62 | (byte << self.count | byte >> (8 - self.count)) 63 | & 0xff) 64 | 65 | return rotated 66 | 67 | 68 | def initialize_parser(): 69 | 70 | description = 'Process data stream and rotate each byte. ' \ 71 | 'Numeric values may be provided ' \ 72 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 73 | 74 | parser = BinDataHelper.generic_args(description) 75 | 76 | parser.add_argument('count', type=BinDataHelper.autoint, 77 | help='Number of times to perform rotation. ' 78 | 'Defaults to the left.') 79 | parser.add_argument('-r', '--right', action='store_true', 80 | default=False, 81 | help='Override default rotation direction, and ' 82 | 'instead rotate bits to the right.') 83 | 84 | return parser 85 | 86 | 87 | def main(): 88 | p = initialize_parser() 89 | args = p.parse_args() 90 | 91 | root = logging.getLogger() 92 | logging.basicConfig() 93 | if args.verbose: 94 | root.setLevel(logging.DEBUG) 95 | else: 96 | root.setLevel(logging.WARNING) 97 | 98 | buff = args.infile.buffer.read() 99 | 100 | s = GenericRotate( 101 | count=args.count, 102 | right=args.right, 103 | buff=buff, 104 | offset=args.offset, 105 | size=args.size, 106 | ) 107 | 108 | return_data = s.run_crypt() 109 | 110 | sys.stdout.buffer.write(return_data) 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /malchive/utilities/killaslr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import sys 19 | import logging 20 | import argparse 21 | import pefile 22 | 23 | __version__ = "1.0.0" 24 | __author__ = "Jason Batchelor" 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | # Reference: 29 | # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=vs.85).aspx 30 | dynamicbase_flag = 0x0040 31 | 32 | 33 | def patch_aslr(pe): 34 | """ 35 | Disable ASLR protection from a binary. 36 | 37 | :param pefile.PE pe: A pe object passed from the pefile project. 38 | 39 | :return: List of stream containing the data. 40 | :rtype: pefile.PE 41 | """ 42 | pe.OPTIONAL_HEADER.DllCharacteristics ^= dynamicbase_flag 43 | return pe 44 | 45 | 46 | def initialize_parser(): 47 | parser = argparse.ArgumentParser( 48 | description='Patch provided PE to disable ASLR. ' 49 | 'Write new PE with \'noaslr\' prefix.') 50 | parser.add_argument('infile', metavar='FILE', nargs='*', 51 | help='Full path to the file(s) to be processed.') 52 | parser.add_argument('-o', '--overwrite', action='store_true', 53 | default=False, 54 | help='Patch existing file instead of creating a ' 55 | 'new one.') 56 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 57 | help='Output additional information when processing ' 58 | '(mostly for debugging purposes).') 59 | 60 | return parser 61 | 62 | 63 | def main(): 64 | p = initialize_parser() 65 | args = p.parse_args() 66 | 67 | root = logging.getLogger() 68 | logging.basicConfig() 69 | if args.verbose: 70 | root.setLevel(logging.DEBUG) 71 | else: 72 | root.setLevel(logging.WARNING) 73 | 74 | if len(args.infile) == 0: 75 | p.print_help() 76 | sys.exit(2) 77 | 78 | for fname in args.infile: 79 | 80 | basename = os.path.basename(fname) 81 | log.info('Patching %s...' % basename) 82 | 83 | if not os.path.isfile(fname): 84 | log.warning('Failed to find file %s. Skipping...' % fname) 85 | continue 86 | 87 | with open(fname, 'rb') as f: 88 | stream = f.read() 89 | 90 | try: 91 | pe = pefile.PE(data=stream) 92 | except pefile.PEFormatError: 93 | log.warning('%s not a pe, skipping...' % basename) 94 | continue 95 | 96 | if pe.OPTIONAL_HEADER.DllCharacteristics & dynamicbase_flag: 97 | pe = patch_aslr(pe) 98 | 99 | if args.overwrite: 100 | outname = basename 101 | else: 102 | outname = basename + '.noaslr' 103 | pe.write(outname) 104 | print('Patched file written as %s...' % outname) 105 | 106 | else: 107 | print('%s was not found to have ASLR enabled...' % basename) 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /malchive/utilities/pecarver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import argparse 4 | import logging 5 | import pefile 6 | from struct import unpack_from 7 | 8 | __version__ = "1.0.0" 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | def carve_pe_files(data, include_start=False): 14 | 15 | results = [] 16 | 17 | matches = re.finditer(b'MZ', data, flags=0) 18 | pe_size = 0 19 | for m in matches: 20 | if m.start() == 0 and not include_start: 21 | continue 22 | 23 | if m.start() + 0x40 > len(data): 24 | continue 25 | 26 | e_lfanew = unpack_from('I', data[m.start() + 0x3c:])[0] 27 | if m.start() + e_lfanew + 0x2 > len(data): 28 | continue 29 | 30 | if not data[m.start() + e_lfanew:].startswith(b'PE'): 31 | continue 32 | 33 | log.info('Embedded PE content detected at 0x%x!' % m.start()) 34 | try: 35 | pe = pefile.PE(data=data[m.start():]) 36 | pe_size = pe.sections[-1].PointerToRawData + pe.sections[-1].SizeOfRawData 37 | except Exception as e: 38 | log.error('PEFile could not load detected PE. Error: %s' % e) 39 | continue 40 | 41 | if m.start() + pe_size > len(data): 42 | log.warning('Expressed PE size at 0x%x larger than file. ' 43 | 'Incomplete result.' % m.start()) 44 | 45 | results.append((m.start(), data[m.start():m.start() + pe_size])) 46 | 47 | return results 48 | 49 | 50 | def initialize_parser(): 51 | parser = argparse.ArgumentParser( 52 | description='Checks for embedded PE files within candidate. Writes result(s) to disk.') 53 | parser.add_argument('candidates', metavar='FILE', nargs='*', 54 | help='candidate file(s).') 55 | parser.add_argument('--include-start', action='store_true', default=False, 56 | help='For PE files that themselves have embedded PEs, this option will ' 57 | 'include the beginnnig PE file among the carved payloads.') 58 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 59 | help='Output additional information when processing ' 60 | '(mostly for debugging purposes).') 61 | return parser 62 | 63 | 64 | def main(): 65 | import os 66 | import hashlib 67 | 68 | p = initialize_parser() 69 | args = p.parse_args() 70 | 71 | root = logging.getLogger() 72 | logging.basicConfig() 73 | if args.verbose: 74 | root.setLevel(logging.DEBUG) 75 | else: 76 | root.setLevel(logging.WARNING) 77 | 78 | # Iterate through list of files in bulk. 79 | for candidate in args.candidates: 80 | 81 | filename = os.path.basename(candidate) 82 | log.info('Processing file %s...' % filename) 83 | 84 | if not os.path.isfile(candidate): 85 | log.warning('Failed to find file %s' % candidate) 86 | continue 87 | 88 | results = [] 89 | with open(candidate, 'rb') as f: 90 | results = carve_pe_files(f.read(), args.include_start) 91 | 92 | if len(results) == 0: 93 | print('No embedded PE files found within %s' % filename) 94 | continue 95 | 96 | print('Found %s embedded PE file(s) within %s' % (len(results), filename)) 97 | for o, r in results: 98 | sha256 = hashlib.sha256(r).hexdigest() 99 | out_name = 'pe_0x%x_%s' % (o, sha256) 100 | with open(out_name, 'wb') as f: 101 | f.write(r) 102 | print('Processed %s and written as %s' % (filename, out_name)) 103 | 104 | 105 | if __name__ == '__main__': 106 | main() 107 | -------------------------------------------------------------------------------- /malchive/helpers/myRC4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Desc: 18 | # ----- 19 | # Python 2/3 compatible version of RC4 cipher. Easy enough to tinker with 20 | # for custom implementations or just experimenting. 21 | # 22 | # Usage: 23 | # ------ 24 | # import myRC4 25 | # d = myRC4.Crypter(key, buff) 26 | # sys.stdout.buffer.write(d.crypted) 27 | # 28 | 29 | from __future__ import print_function 30 | 31 | __version__ = "1.0.0" 32 | __author__ = "Jason Batchelor" 33 | 34 | 35 | class Crypter: 36 | """ 37 | En/Decrypts malware payload. 38 | 39 | :ivar bytearray crypted: Crypted contents of supplied data. 40 | """ 41 | 42 | def __init__(self, key, buff, sbox_size=256): 43 | self.buff = bytearray(buff) 44 | self.key = bytearray(key) 45 | self.sbox_size = sbox_size 46 | self.crypted = self._crypt() 47 | 48 | def _generate_key_ring(self): 49 | """ 50 | Generate SBOX using supplied key. 51 | 52 | :return: Populated list of SBOX elements for the algorithm. 53 | :rtype: list 54 | """ 55 | 56 | keyring = bytearray(b'\x00' * self.sbox_size) 57 | 58 | for i in range(0, len(keyring)): 59 | keyring[i] = i % self.sbox_size 60 | 61 | byte_idx = 0 62 | for i in range(0, len(keyring)): 63 | byte_idx = (self.key[i % len(self.key)] + byte_idx + keyring[i]) \ 64 | % self.sbox_size 65 | t = keyring[i] 66 | keyring[i] = keyring[byte_idx] 67 | keyring[byte_idx] = t 68 | 69 | return keyring 70 | 71 | def _crypt(self): 72 | """ 73 | Process and crypt data. 74 | 75 | :return: Crypted string. 76 | :rtype: bytearray 77 | """ 78 | 79 | keyring = self._generate_key_ring() 80 | crypted = bytearray() 81 | sch_idx_1, sch_idx_2 = 0, 0 82 | 83 | for i in range(0, len(self.buff)): 84 | sch_idx_1 = (i + 1) % self.sbox_size 85 | sch_idx_2 = (sch_idx_2 + keyring[sch_idx_1]) % self.sbox_size 86 | 87 | t = keyring[sch_idx_1] 88 | keyring[sch_idx_1] = keyring[sch_idx_2] 89 | keyring[sch_idx_2] = t 90 | 91 | key_idx = (keyring[sch_idx_2] + 92 | keyring[sch_idx_1]) % self.sbox_size 93 | crypted.append(self.buff[i] ^ keyring[key_idx]) 94 | 95 | return crypted 96 | 97 | 98 | if __name__ == '__main__': 99 | 100 | import sys 101 | import os 102 | 103 | if not os.path.isfile('enc.bin'): 104 | print('[+] enc.bin file not found!') 105 | sys.exit(2) 106 | 107 | if not os.path.isfile('key.bin'): 108 | print('[+] key.bin file not found!') 109 | sys.exit(2) 110 | 111 | with open('enc.bin', 'rb') as f: 112 | buff = f.read() 113 | 114 | with open('key.bin', 'rb') as f: 115 | key = f.read() 116 | 117 | d = Crypter(key, buff) 118 | if hasattr(sys.stdout, 'buffer'): 119 | sys.stdout.buffer.write(d.crypted) 120 | else: 121 | sys.stdout.write(d.crypted) 122 | -------------------------------------------------------------------------------- /malchive/utilities/xor_pairwise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from malchive.helpers import BinDataHelper 20 | 21 | __version__ = "1.0.0" 22 | __author__ = "Jason Batchelor" 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class PairwiseXor(BinDataHelper.LiteCrypt): 28 | 29 | def __init__(self, 30 | key: int = 0, 31 | incrementing: bool = False, 32 | *args, 33 | **kwargs): 34 | 35 | self.key: int = 0 36 | self.incrementing: bool = False 37 | 38 | if 0x00 <= key <= 0xff: 39 | self.key = key 40 | else: 41 | raise ValueError('Key must be between 0x00 and 0xff.') 42 | 43 | self.incrementing = incrementing 44 | 45 | super().__init__(*args, **kwargs) 46 | 47 | def run_crypt(self): 48 | """ 49 | Run pairwise xor on supplied data. 50 | 51 | :return: processed data 52 | :rtype: bytearray 53 | """ 54 | 55 | data = bytearray(self.buff[self.offset:self.total_size]) 56 | 57 | if self.total_size % 2 != 0: 58 | raise ValueError('Total size of processed bytes must ' 59 | 'be even length for pairwise decode.') 60 | 61 | if self.incrementing: 62 | data[0] ^= self.key 63 | for i in range(0, len(data) - 1, 1): 64 | data[i + 1] ^= data[i] 65 | else: 66 | for i in range(len(data) - 1, 0, -1): 67 | data[i] ^= data[i - 1] 68 | data[0] ^= self.key 69 | return data 70 | 71 | 72 | def initialize_parser(): 73 | 74 | description = 'Instead of a standard single byte xor operation, ' \ 75 | 'xor end byte with previous byte and continue in a ' \ 76 | 'decrementing fashion until the final byte is ' \ 77 | 'reached at the beginning.' 78 | 79 | parser = BinDataHelper.generic_args(description) 80 | 81 | parser.add_argument('-r', '--reverse', 82 | action='store_true', 83 | default=False, 84 | help='Reverse the process, applying pairwise ' 85 | 'at the beginning rather than the end.') 86 | parser.add_argument('-k', '--pw-xor-key', 87 | type=BinDataHelper.autoint, 88 | default=0, 89 | help='Key to use to start or end the XOR ' 90 | '(depending on if \'r\' is used). ' 91 | 'Must be 0x00-0xff. Defaults to 0x00.') 92 | 93 | return parser 94 | 95 | 96 | def main(): 97 | p = initialize_parser() 98 | args = p.parse_args() 99 | 100 | root = logging.getLogger() 101 | logging.basicConfig() 102 | if args.verbose: 103 | root.setLevel(logging.DEBUG) 104 | else: 105 | root.setLevel(logging.WARNING) 106 | 107 | buff = args.infile.buffer.read() 108 | 109 | s = PairwiseXor( 110 | key=args.pw_xor_key, 111 | incrementing=args.reverse, 112 | buff=buff, 113 | offset=args.offset, 114 | size=args.size, 115 | ) 116 | 117 | return_data = s.run_crypt() 118 | 119 | sys.stdout.buffer.write(return_data) 120 | 121 | 122 | if __name__ == '__main__': 123 | main() 124 | -------------------------------------------------------------------------------- /malchive/decoders/rzstreet_dumper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import logging 5 | 6 | __version__ = "1.0.0" 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | # Reference: 11 | # https://blog.xorhex.com/blog/mustangpandaplugx-1/ 12 | # Reference sample: 13 | # https://www.virustotal.com/gui/file/589e87d4ac0a2c350e98642ac53f4940fcfec38226c16509da21bb551a8f8a36 14 | 15 | 16 | class ExtractBinFile: 17 | """ 18 | Carve and extract RZStreet encrypted PE file. 19 | 20 | :ivar bytearray decrypted: Decrypted data stream. 21 | :ivar list key: Multibyte key value used to decrypt. 22 | """ 23 | 24 | def __init__(self, data): 25 | self.buff = data 26 | self.key = [] 27 | self.decrypted = bytearray() 28 | 29 | if self.get_rz_payload(): 30 | log.debug('Payload extraction success!') 31 | 32 | def get_rz_payload(self): 33 | 34 | dec = bytearray() 35 | offset = self.buff.find(b'\x00') 36 | if offset > 0x100: 37 | log.debug('Null offset to large to be RZStreet.') 38 | return False 39 | 40 | crypted = self.buff[offset + 1:] 41 | self.key = bytearray(self.buff[:offset]) 42 | 43 | for i in range(0, len(crypted)): 44 | dec.append(crypted[i] ^ self.key[i % len(self.key)]) 45 | 46 | if dec.startswith(b'\x4d\x5a\xe8\x00\x00\x00\x00\x5b'): 47 | self.decrypted = dec 48 | return True 49 | else: 50 | log.debug('Decrypted content did not match RZStreet PIC') 51 | return False 52 | 53 | 54 | def initialize_parser(): 55 | parser = argparse.ArgumentParser( 56 | description='Process candidate. Prints results in table format.') 57 | parser.add_argument('candidates', metavar='FILE', nargs='*', 58 | help='candidate file(s).') 59 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 60 | help='Output additional information when processing ' 61 | '(mostly for debugging purposes).') 62 | parser.add_argument('-w', '--write', action='store_true', 63 | help='Write the file(s) to disk with MD5 hash ' 64 | 'and \'_rz\' prefix of the provided sample.') 65 | return parser 66 | 67 | 68 | def main(): 69 | import os 70 | import hashlib 71 | import binascii 72 | import json 73 | 74 | p = initialize_parser() 75 | args = p.parse_args() 76 | 77 | root = logging.getLogger() 78 | logging.basicConfig() 79 | if args.verbose: 80 | root.setLevel(logging.DEBUG) 81 | else: 82 | root.setLevel(logging.WARNING) 83 | 84 | # Iterate through list of files in bulk. 85 | for filename in args.candidates: 86 | 87 | log.info('Processing file %s...' % filename) 88 | 89 | if not os.path.isfile(filename): 90 | log.warning('Failed to find file %s' % filename) 91 | continue 92 | 93 | with open(filename, 'rb') as f: 94 | rz = ExtractBinFile(f.read()) 95 | 96 | if len(rz.decrypted) == 0: 97 | log.info('No decrypted content found in candidate.') 98 | continue 99 | 100 | sha256 = hashlib.sha256(rz.buff).hexdigest() 101 | results = { 102 | 'Sha256': sha256, 103 | 'XOR Key': '0x%s' % binascii.hexlify(rz.key).decode('ascii'), 104 | 'Decrypted Sha256': hashlib.sha256(rz.decrypted).hexdigest(), 105 | 'Decrypted Size': len(rz.decrypted), 106 | } 107 | 108 | if args.write: 109 | with open('%s_rz' % sha256, 'wb') as f: 110 | f.write(rz.decrypted) 111 | 112 | try: 113 | print(json.dumps(results, indent=4, sort_keys=False)) 114 | except UnicodeDecodeError: 115 | log.warning('There was a Unicode decoding error when ' 116 | 'processing %s' % os.path.basename(filename)) 117 | 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /malchive/utilities/guid_recovery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import struct 5 | import re 6 | import binascii 7 | import logging 8 | import argparse 9 | 10 | __version__ = "1.0.0" 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def get_guids(buff): 16 | 17 | results = [] 18 | storage_signature = b'\x42\x53\x4A\x42' # 'BSBJ' 19 | guid_signature = b'\x23\x47\x55\x49\x44\x00\x00\x00' # '#GUID\x00\x00' 20 | 21 | storage_offsets = re.finditer(storage_signature, buff) 22 | 23 | for match in storage_offsets: 24 | 25 | guid_offset = buff[match.start():].find(guid_signature) - 0x8 26 | 27 | if guid_offset == -1: 28 | log.warning('GUID offset mismatch at 0x%x.' % guid_offset) 29 | continue 30 | 31 | iOffset, iSize = struct.unpack_from('II', buff[match.start() + 32 | guid_offset:]) 33 | gLocation = iOffset + match.start() 34 | raw_guid = bytearray(buff[gLocation: gLocation + iSize]) 35 | 36 | guid = \ 37 | '{' + \ 38 | binascii.hexlify( 39 | raw_guid[0x0:0x4][::-1] 40 | ).decode('ascii') + \ 41 | '-' + \ 42 | binascii.hexlify( 43 | raw_guid[0x4:0x6][::-1] 44 | ).decode('ascii') + \ 45 | '-' + \ 46 | binascii.hexlify( 47 | raw_guid[0x6:0x8][::-1] 48 | ).decode('ascii') + \ 49 | '-' + \ 50 | binascii.hexlify( 51 | raw_guid[0x8:0xa] 52 | ).decode('ascii') + \ 53 | '-' + \ 54 | binascii.hexlify( 55 | raw_guid[0xa:] 56 | ).decode('ascii') + \ 57 | '}' 58 | 59 | raw_ascii = binascii.hexlify(raw_guid).decode('ascii') 60 | results.append((gLocation, raw_ascii, guid)) 61 | return results 62 | 63 | 64 | def initialize_parser(): 65 | parser = argparse.ArgumentParser( 66 | description='Fetch embedded .NET GUIDs within a binary stream.' 67 | ' Like a process dump for example.') 68 | parser.add_argument('infile', metavar='FILE', nargs='*', 69 | help='Full path to the file(s) to be processed.') 70 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 71 | help='Output additional information when processing ' 72 | '(mostly for debugging purposes).') 73 | 74 | return parser 75 | 76 | 77 | def main(): 78 | 79 | import os 80 | import json 81 | import hashlib 82 | 83 | p = initialize_parser() 84 | args = p.parse_args() 85 | 86 | root = logging.getLogger() 87 | logging.basicConfig() 88 | if args.verbose: 89 | root.setLevel(logging.DEBUG) 90 | else: 91 | root.setLevel(logging.WARNING) 92 | 93 | if len(args.infile) == 0: 94 | p.print_help() 95 | sys.exit(2) 96 | 97 | for fname in args.infile: 98 | 99 | basename = os.path.basename(fname) 100 | log.info('Processing %s...' % basename) 101 | 102 | if not os.path.isfile(fname): 103 | log.warning('Failed to find file %s. Skipping...' % fname) 104 | continue 105 | 106 | with open(fname, 'rb') as f: 107 | stream = f.read() 108 | 109 | results = get_guids(stream) 110 | 111 | if len(results) == 0: 112 | log.info('No results for %s' % basename) 113 | continue 114 | 115 | i = 1 116 | parent = { 117 | 'Sha256': hashlib.sha256(stream).hexdigest() 118 | } 119 | for res in results: 120 | child = { 121 | 'offset': hex(res[0]), 122 | 'raw_data': res[1], 123 | 'guid': res[2] 124 | } 125 | parent['result_%s' % i] = child 126 | i += 1 127 | 128 | try: 129 | print(json.dumps(parent, indent=4, sort_keys=False)) 130 | except UnicodeDecodeError: 131 | log.warning('There was a Unicode decoding error when ' 132 | 'processing %s' % os.path.basename(basename)) 133 | continue 134 | 135 | 136 | if __name__ == '__main__': 137 | main() 138 | -------------------------------------------------------------------------------- /malchive/helpers/BinDataHelper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # 7 | # You may obtain a copy of the License at: 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | import argparse 18 | import binascii 19 | 20 | __version__ = "1.1.0" 21 | __author__ = "Jason Batchelor" 22 | 23 | log = logging.getLogger(__name__) 24 | root = logging.getLogger() 25 | logging.basicConfig() 26 | 27 | 28 | class LiteCrypt(): 29 | """ 30 | LiteCrypt object. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | buff: bytearray = bytearray(), 36 | offset: int = 0, 37 | size: int = -1, 38 | **kwargs 39 | ): 40 | 41 | self.buff: bytearray = bytearray() 42 | self.offset: int = 0 43 | self.total_size: int = 0 44 | self.buff = buff 45 | 46 | if offset > len(buff) or offset < 0: 47 | raise IndexError('Invalid start position supplied. Exceeds ' 48 | 'range of supplied buffer.') 49 | self.offset = offset 50 | 51 | if size == -1: 52 | total_size = len(buff) 53 | else: 54 | total_size = size + offset 55 | if total_size > len(buff) or total_size < offset: 56 | raise IndexError('Invalid size position supplied. ' 57 | 'Exceeds range of supplied parameters.') 58 | 59 | self.total_size = total_size 60 | 61 | def run_crypt(self): 62 | """ 63 | Should be overridden. 64 | Apply the example crypto method. 65 | 66 | :return: processed data 67 | :rtype: bytearray 68 | """ 69 | 70 | log.warning('Method should be overridden!') 71 | 72 | return bytearray() 73 | 74 | 75 | def autokey(x): 76 | key = '%x' % autoint(x) 77 | 78 | # hex data must be supplied in pairs 79 | if x.startswith('0x') and len(x) % 2 != 0: 80 | log.error('Hex data must be provided in even pairs (ex: 0x01 as opposed to 0x1).') 81 | raise ValueError 82 | 83 | # if provided hex bytes, honor the leading nulls if present 84 | if x.startswith('0x00') and len(x) > 4: 85 | i = 2 86 | while x[i:i+2] == '00': 87 | key = '00' + key 88 | i += 2 89 | 90 | # ensures single digits can be unhexlified 91 | if len(key) % 2 != 0: 92 | key = '0' + key 93 | 94 | key = [x for x in binascii.unhexlify(key)] 95 | return key 96 | 97 | 98 | def autoint(x): 99 | 100 | if x.startswith('0x'): 101 | x = int(x, 16) 102 | else: 103 | x = int(x) 104 | return x 105 | 106 | 107 | def generic_args(info='BinDataHelper Default Parser Description'): 108 | 109 | parser = argparse.ArgumentParser( 110 | description=info) 111 | parser.add_argument('-v', '--verbose', 112 | action='store_true', 113 | default=False, 114 | help='Output additional information when processing ' 115 | '(mostly for debugging purposes).') 116 | parser.add_argument('infile', 117 | type=argparse.FileType('r'), 118 | default='-', 119 | help='Data stream to process. ' 120 | '(stdin, denoted by a \'-\').') 121 | parser.add_argument('-o', '--offset', 122 | type=autoint, 123 | default=0, 124 | help='Starting point within the supplied buffer to ' 125 | 'begin processing.') 126 | parser.add_argument('-s', '--size', 127 | type=autoint, 128 | default=-1, 129 | help='The total number of bytes to process. Defaults ' 130 | 'to size of supplied data.') 131 | 132 | return parser 133 | -------------------------------------------------------------------------------- /malchive/active_discovery/shadowpad.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import aiohttp 4 | import asyncio 5 | import logging 6 | from malchive.helpers import discovery 7 | from async_timeout import timeout 8 | 9 | __version__ = "1.0.0" 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class ShadowPadController(discovery.Discover): 15 | 16 | # Reference: SASCon - https://youtu.be/YCwyc6SctYs?t=1360 17 | fake_not_found = b'Page not found\x0d\x0a' 18 | 19 | async def tickle_http(self, co): 20 | 21 | verdict = False 22 | url = f"{co.protocol}://{co.ip}:{co.port}/" 23 | 24 | headers = { 25 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 26 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 27 | 'Chrome/79.0.3945.117 Safari/537.36' 28 | } 29 | 30 | log.info('Making attempt using: %s' % url) 31 | 32 | conn = aiohttp.TCPConnector() 33 | async with aiohttp.ClientSession(connector=conn) as session: 34 | try: 35 | async with timeout(self.timeout): 36 | verdict = await self.get_resp_verdict( 37 | url, 38 | headers, 39 | session 40 | ) 41 | 42 | except aiohttp.ClientConnectionError as e: 43 | log.debug('Failure: ClientConnectionError %s: %s:%s:%s' % 44 | (str(e), co.protocol, co.ip, co.port)) 45 | 46 | except asyncio.TimeoutError: 47 | log.debug('Failure: TimeoutError: %s:%s:%s' % 48 | (co.protocol, co.ip, co.port)) 49 | 50 | except aiohttp.TooManyRedirects: 51 | log.debug('Failure: TooManyRedirects: %s:%s:%s' % 52 | (co.protocol, co.ip, co.port)) 53 | 54 | except Exception as e: 55 | log.debug('General failure: %s: %s:%s:%s' % 56 | (str(e), co.protocol, co.ip, co.port)) 57 | 58 | if verdict: 59 | co.success = True 60 | 61 | return co 62 | 63 | async def get_resp_verdict(self, url, headers, session): 64 | 65 | async with session.get(url, 66 | headers=headers, 67 | read_bufsize=len(self.fake_not_found), 68 | verify_ssl=False 69 | ) as resp: 70 | 71 | # Ensure the headers we care about exist 72 | if 'content-length' not in resp.headers or \ 73 | 'Server' not in resp.headers: 74 | log.info('Response headers did not have content-length.') 75 | return False 76 | 77 | # Check if the length of our content 78 | if int(resp.headers['content-length']) != len(self.fake_not_found): 79 | log.info('Response content size mismatch. %s bytes' 80 | % resp.headers['content-length']) 81 | return False 82 | 83 | # Check content, status code, and server name 84 | if resp.status == 404 and \ 85 | resp.headers['server'] == 'nginx': 86 | payload = await resp.content.read() 87 | if payload == self.fake_not_found: 88 | return True 89 | return False 90 | 91 | 92 | def initialize_parser(): 93 | parser = discovery.generic_args() 94 | return parser 95 | 96 | 97 | def main(): 98 | 99 | p = initialize_parser() 100 | args = p.parse_args() 101 | 102 | root = logging.getLogger() 103 | logging.basicConfig() 104 | if args.verbose: 105 | root.setLevel(logging.DEBUG) 106 | else: 107 | root.setLevel(logging.WARNING) 108 | 109 | d = ShadowPadController( 110 | ips=args.ipaddress, 111 | ports=args.port, 112 | domains=args.domain, 113 | timeout=args.timeout, 114 | protocols=args.protocol, 115 | ) 116 | 117 | comms = discovery.create_comms( 118 | d.protocols, 119 | d.ips, 120 | d.ports 121 | ) 122 | 123 | results = [] 124 | asyncio.run(d.run(comms, results)) 125 | 126 | for co in results: 127 | if co.success: 128 | print('Successfully discovered candidate! [%s] %s:%s' % 129 | (co.protocol, co.ip, co.port)) 130 | 131 | 132 | if __name__ == '__main__': 133 | main() 134 | -------------------------------------------------------------------------------- /malchive/decoders/apollo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import logging 5 | import struct 6 | import re 7 | 8 | __version__ = "1.0.0" 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | # Reference: 13 | # https://github.com/MythicAgents/Apollo 14 | # Reference sample: 15 | # https://www.virustotal.com/gui/file/f78628c0ecca360a4c9d33f67bf394955c8b89929e9ca611e2f2dbe6e170ece5 16 | 17 | 18 | class GetConfig: 19 | 20 | def __init__(self, buff): 21 | """ 22 | Initialize decoder instance. 23 | 24 | :param bytes buff: The stream of bytes to be processed. 25 | """ 26 | 27 | self.buff = buff 28 | self.elements = [] 29 | self.parse_config() 30 | 31 | def parse_config(self): 32 | 33 | parameters = { 34 | 'callback_interval': "", 35 | 'callback_jitter': "", 36 | 'callback_port': "", 37 | 'callback_host': "", 38 | 'post_uri': "", 39 | 'encrypted_exchange_check': "", 40 | 'proxy_host': "", 41 | 'proxy_port': "", 42 | 'proxy_user': "", 43 | 'proxy_pass': "", 44 | 'killdate': "", 45 | 'User-Agent': "", 46 | } 47 | 48 | pRex = [] 49 | for k, v in parameters.items(): 50 | pRex.append(k.encode('utf-16').strip(b'\xFF\xFE')) 51 | 52 | r = re.compile( 53 | pRex[0] + b"(.*?)" + 54 | pRex[1] + b"(.*?)" + 55 | pRex[2] + b"(.*?)" + 56 | pRex[3] + b"(.*?)" + 57 | pRex[4] + b"(.*?)" + 58 | pRex[5] + b"(.*?)" + 59 | pRex[6] + b"(.*?)" + 60 | pRex[7] + b"(.*?)" + 61 | pRex[8] + b"(.*?)" + 62 | pRex[9] + b"(.*?)" + 63 | pRex[10] + b"(.*?)" + 64 | pRex[11] + b"(.*?)" + 65 | b'\x00\x00' 66 | ) 67 | 68 | m = re.search(r, self.buff) 69 | if not m: 70 | log.debug('No match on config regex...') 71 | return 72 | 73 | for index in range(1, m.lastindex + 1): 74 | 75 | data = m.group(index) 76 | size = struct.unpack_from('>H', data)[0] 77 | 78 | value = data[0x2:0x2 + size].decode('utf-8').replace('\x00', '') 79 | key = pRex[index - 1].decode('utf-8').replace('\x00', '') 80 | parameters[key] = value 81 | 82 | self.elements = parameters 83 | 84 | 85 | def initialize_parser(): 86 | parser = argparse.ArgumentParser( 87 | description='Process candidate. Prints results in JSON format.') 88 | parser.add_argument('candidates', metavar='FILE', 89 | nargs='*', help='candidate file(s).') 90 | parser.add_argument('-v', '--verbose', action='store_true', 91 | default=False, 92 | help='Output additional information when processing ' 93 | '(mostly for debugging purposes).') 94 | return parser 95 | 96 | 97 | def main(): 98 | import os 99 | import json 100 | import hashlib 101 | import pefile 102 | 103 | p = initialize_parser() 104 | args = p.parse_args() 105 | 106 | root = logging.getLogger() 107 | logging.basicConfig() 108 | if args.verbose: 109 | root.setLevel(logging.DEBUG) 110 | else: 111 | root.setLevel(logging.WARNING) 112 | 113 | # Iterate through list of files in bulk. 114 | for filename in args.candidates: 115 | 116 | log.info('Processing file %s...' % filename) 117 | 118 | if not os.path.isfile(filename): 119 | log.warning('Failed to find file %s' % filename) 120 | continue 121 | 122 | f = open(filename, 'rb') 123 | stream = f.read() 124 | 125 | try: 126 | pefile.PE(data=stream) 127 | except pefile.PEFormatError: 128 | log.warning('%s not a pe, skipping...' % f.name) 129 | continue 130 | 131 | d = GetConfig(stream) 132 | 133 | config_dict = { 134 | 'Sha256': hashlib.sha256(stream).hexdigest(), 135 | 'Config': d.elements 136 | } 137 | 138 | try: 139 | print(json.dumps(config_dict, indent=4, sort_keys=False)) 140 | except UnicodeDecodeError: 141 | log.warning('There was a Unicode decoding error when ' 142 | 'processing %s' % os.path.basename(filename)) 143 | continue 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /malchive/utilities/entropycalc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import sys 19 | import logging 20 | import argparse 21 | import hashlib 22 | import operator 23 | import math 24 | from collections import Counter 25 | from tabulate import tabulate 26 | 27 | __version__ = "1.0.0" 28 | __author__ = "Jason Batchelor" 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | 33 | # Reference: 34 | # https://stackoverflow.com/questions/15450192/fastest-way-to-compute-entropy-in-python 35 | def entropy(data, unit='natural'): 36 | """ 37 | Compute and return the entropy of a stream of data. 38 | 39 | :param bytearray data: Stream to compute entropy on. 40 | :param str,optional unit: Passed to math.log calculation. 41 | Default is natural. 42 | 43 | :return: Entropy calculation 44 | :rtype: float 45 | """ 46 | base = { 47 | 'shannon': 2., 48 | 'natural': math.exp(1), 49 | 'hartley': 10. 50 | } 51 | 52 | if len(data) <= 1: 53 | return 0 54 | 55 | counts = Counter() 56 | 57 | for d in data: 58 | counts[d] += 1 59 | 60 | probs = [float(c) / len(data) for c in list(counts.values())] 61 | probs = [p for p in probs if p > 0.] 62 | 63 | ent = 0 64 | 65 | for p in probs: 66 | if p > 0.: 67 | ent -= p * math.log(p, base[unit]) 68 | 69 | return ent 70 | 71 | 72 | def autoint(x): 73 | if x.startswith('0x'): 74 | x = int(x, 16) 75 | else: 76 | x = int(x) 77 | return x 78 | 79 | 80 | def initialize_parser(): 81 | parser = argparse.ArgumentParser( 82 | description='Take a series of provided files and print the entropy, ' 83 | 'alongside other information like the filename and SHA256.') 84 | parser.add_argument('infile', metavar='FILE', nargs='*', 85 | help='Full path to the file to be processed.') 86 | parser.add_argument('-o', '--offset', type=autoint, default=0, 87 | help='Starting point within the supplied buffer to ' 88 | 'begin processing.') 89 | parser.add_argument('-s', '--size', type=autoint, default=-1, 90 | help='The total number of bytes to process. Defaults ' 91 | 'to size of supplied data.') 92 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 93 | help='Output additional information when processing ' 94 | '(mostly for debugging purposes).') 95 | 96 | return parser 97 | 98 | 99 | def main(): 100 | p = initialize_parser() 101 | args = p.parse_args() 102 | 103 | root = logging.getLogger() 104 | logging.basicConfig() 105 | if args.verbose: 106 | root.setLevel(logging.DEBUG) 107 | else: 108 | root.setLevel(logging.WARNING) 109 | 110 | if len(args.infile) == 0: 111 | p.print_help() 112 | sys.exit(2) 113 | 114 | results = [] 115 | for fname in args.infile: 116 | 117 | basename = os.path.basename(fname) 118 | size = args.size 119 | offset = args.offset 120 | 121 | if not os.path.isfile(fname): 122 | log.warning('Failed to find file %s. Skipping...' % fname) 123 | continue 124 | 125 | with open(fname, 'rb') as f: 126 | stream = f.read() 127 | 128 | if offset > len(stream) or offset < 0: 129 | log.error('Invalid start position supplied. Exceeds range of ' 130 | 'supplied buffer. Skipping...') 131 | continue 132 | 133 | if size == -1: 134 | total_size = len(stream) 135 | else: 136 | total_size = size + offset 137 | if total_size > len(stream) or total_size < offset: 138 | log.error('Invalid size position supplied. Exceeds range of ' 139 | 'supplied buffer. Skipping...') 140 | continue 141 | 142 | stream = stream[offset:total_size] 143 | 144 | sha256 = hashlib.sha256(stream).hexdigest() 145 | ent = entropy(stream, 'shannon') 146 | 147 | results.append((sha256, basename, ent)) 148 | 149 | results = sorted(results, key=operator.itemgetter(2, 1, 0)) 150 | if len(results) > 0: 151 | print(tabulate(results, headers=["SHA256", "Filename", "Entropy"], 152 | tablefmt="grid")) 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /malchive/decoders/sunburst.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import argparse 18 | import pefile 19 | import logging 20 | import re 21 | import base64 22 | import zlib 23 | 24 | __version__ = "1.0.0" 25 | __author__ = "Jason Batchelor" 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | class GetConfig: 31 | 32 | def __init__(self, buff): 33 | """ 34 | Initialize decoder instance. 35 | 36 | :param bytes buff: The stream of bytes to be processed. 37 | """ 38 | 39 | self.buff = buff 40 | self.elements = [] 41 | 42 | try: 43 | pe = pefile.PE(data=self.buff) 44 | self.decode_config(pe) 45 | except pefile.PEFormatError: 46 | log.debug('Supplied file must be a valid PE!') 47 | 48 | def decode_config(self, pe): 49 | 50 | unicode_candidates = self.find_unicode() 51 | elements = self.find_decode_suspect_data(unicode_candidates) 52 | self.elements = elements 53 | 54 | def find_unicode(self, modifier=4): 55 | """ 56 | Find unicode characters within supplied data. 57 | """ 58 | wide = [] 59 | matches = re.finditer(b'([\x20-\x7e]\x00){' + 60 | str(modifier).encode('ascii') + b',}', self.buff) 61 | 62 | if matches: 63 | for m in matches: 64 | wide.append(m.group(0).decode('utf-16')) 65 | return wide 66 | 67 | def find_decode_suspect_data(self, candidates): 68 | """ 69 | From a list of candidate strings, try to decode using b64 + DEFLATE. 70 | """ 71 | elements = [] 72 | # Reference: Seems to reliably detect with minimal FPs 73 | # https://github.com/ctxis/CAPE/blob/master/lib/cuckoo/common/office/olevba.py#L444 74 | b64_rex = re.compile( 75 | r'(?:[A-Za-z0-9+/]{4}){1,}(?:[A-Za-z0-9+/]{2}' 76 | r'[AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?' 77 | ) 78 | 79 | for c in candidates: 80 | if b64_rex.match(c): 81 | try: 82 | b64 = base64.b64decode(c, validate=True) 83 | deflate = zlib.decompress(b64, -15) 84 | e = deflate.decode('ascii') 85 | log.info('Candidate found! %s' % c) 86 | elements.append(e) 87 | except Exception: 88 | pass 89 | 90 | return sorted(set(elements)) 91 | 92 | 93 | def initialize_parser(): 94 | parser = argparse.ArgumentParser( 95 | description='Process candidate. Prints results in JSON format.') 96 | parser.add_argument('candidates', metavar='FILE', 97 | nargs='*', help='candidate file(s).') 98 | parser.add_argument('-v', '--verbose', action='store_true', 99 | default=False, 100 | help='Output additional information when processing ' 101 | '(mostly for debugging purposes).') 102 | return parser 103 | 104 | 105 | def main(): 106 | import os 107 | import json 108 | import datetime 109 | import hashlib 110 | 111 | p = initialize_parser() 112 | args = p.parse_args() 113 | 114 | root = logging.getLogger() 115 | logging.basicConfig() 116 | if args.verbose: 117 | root.setLevel(logging.DEBUG) 118 | else: 119 | root.setLevel(logging.WARNING) 120 | 121 | # Iterate through list of files in bulk. 122 | for filename in args.candidates: 123 | 124 | log.info('Processing file %s...' % filename) 125 | 126 | if not os.path.isfile(filename): 127 | log.warning('Failed to find file %s' % filename) 128 | continue 129 | 130 | f = open(filename, 'rb') 131 | stream = f.read() 132 | 133 | try: 134 | pe = pefile.PE(data=stream) 135 | timestamp = datetime.datetime.utcfromtimestamp( 136 | pe.FILE_HEADER.TimeDateStamp) 137 | except pefile.PEFormatError: 138 | log.warning('%s not a pe, skipping...' % f.name) 139 | continue 140 | 141 | d = GetConfig(stream) 142 | 143 | config_dict = { 144 | 'Compile Time': '%s UTC' % timestamp, 145 | 'MD5': hashlib.md5(stream).hexdigest(), 146 | 'Decoded Elements': d.elements, 147 | } 148 | 149 | try: 150 | print(json.dumps(config_dict, indent=4, sort_keys=False)) 151 | except UnicodeDecodeError: 152 | log.warning('There was a Unicode decoding error when ' 153 | 'processing %s' % os.path.basename(filename)) 154 | continue 155 | 156 | 157 | if __name__ == '__main__': 158 | main() 159 | -------------------------------------------------------------------------------- /malchive/utilities/b64dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import re 19 | import sys 20 | import base64 21 | import logging 22 | import argparse 23 | import hashlib 24 | from tabulate import tabulate 25 | 26 | __version__ = "1.0.0" 27 | __author__ = "Jason Batchelor" 28 | 29 | log = logging.getLogger(__name__) 30 | 31 | 32 | def get_b64_encoded_streams(buff, modifier=0): 33 | """ 34 | Decode embedded Base64 blobs. 35 | 36 | :param bytes buff: Encoded candidate stream to search. 37 | :return: Base64 decoded results. 38 | :rtype: list 39 | """ 40 | 41 | b64_blobs = [] 42 | # Reference: Seems to reliably detect with minimal FPs 43 | # https://github.com/ctxis/CAPE/blob/master/lib/cuckoo/common/office/olevba.py#L444 44 | b64_rex = re.compile( 45 | rb'(?:[A-Za-z0-9+/]{4}){1,}(?:[A-Za-z0-9+/]{2}' 46 | rb'[AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==)?' 47 | ) 48 | 49 | for m in b64_rex.findall(buff): 50 | if len(m) < modifier: 51 | continue 52 | log.debug('Possible Base64 match... %s bytes' % len(m)) 53 | try: 54 | d = base64.b64decode(m, validate=True) 55 | b64_blobs.append(d) 56 | except Exception: 57 | log.debug('Error when trying to decode Base64 content.') 58 | pass 59 | 60 | return b64_blobs 61 | 62 | 63 | def initialize_parser(): 64 | parser = argparse.ArgumentParser( 65 | description='Parse provided files and extract and decode candidate ' 66 | 'Base64 data streams.') 67 | parser.add_argument('infile', metavar='FILE', nargs='*', 68 | help='Full path to the file(s) to be processed.') 69 | parser.add_argument('-v', '--verbose', action='store_true', 70 | default=False, 71 | help='Output additional information when processing ' 72 | '(mostly for debugging purposes).') 73 | parser.add_argument('-s', '--suppress-write', action='store_true', 74 | help='Prevent results from being written to disk. ' 75 | 'Opting instead for just a table view of the ' 76 | 'results.') 77 | parser.add_argument('-t', '--target-directory', type=str, default='.', 78 | help='Target directory to write files. Defaults to ' 79 | 'executing directory.') 80 | parser.add_argument('-m', '--modifier', type=int, default=500, 81 | help='Amount of Base64 characters encountered before ' 82 | 'a string is recognized (default is 500).') 83 | 84 | return parser 85 | 86 | 87 | def main(): 88 | p = initialize_parser() 89 | args = p.parse_args() 90 | 91 | root = logging.getLogger() 92 | logging.basicConfig() 93 | if args.verbose: 94 | root.setLevel(logging.DEBUG) 95 | else: 96 | root.setLevel(logging.WARNING) 97 | 98 | if len(args.infile) == 0: 99 | p.print_help() 100 | sys.exit(2) 101 | 102 | b64_content = [] 103 | target_dir = args.target_directory 104 | for fname in args.infile: 105 | 106 | basename = os.path.basename(fname) 107 | 108 | if not os.path.isfile(fname): 109 | log.warning('Failed to find file %s. Skipping...' % fname) 110 | continue 111 | 112 | with open(fname, 'rb') as f: 113 | stream = f.read() 114 | 115 | decodes = get_b64_encoded_streams(stream, args.modifier) 116 | 117 | log.info('Found %s candiates in %s...' % (len(decodes), basename)) 118 | 119 | if not args.suppress_write and target_dir != '.': 120 | log.info('Writing to directory: %s' % target_dir) 121 | 122 | for d in decodes: 123 | _sha256 = hashlib.sha256(d).hexdigest() 124 | b64_content.append((basename, len(d), _sha256)) 125 | 126 | if not args.suppress_write: 127 | if target_dir != '.': 128 | try: 129 | os.makedirs(target_dir) 130 | except OSError: 131 | if not os.path.isdir(target_dir): 132 | log.error('Could not create %s. Exiting...' 133 | % target_dir) 134 | sys.exit(2) 135 | with open('%s/%s' % (target_dir, _sha256), 'wb') as f: 136 | f.write(d) 137 | 138 | if len(b64_content) > 0: 139 | print(tabulate(b64_content, 140 | headers=["Filename", "Size", "SHA256"], 141 | tablefmt="grid")) 142 | 143 | 144 | if __name__ == '__main__': 145 | main() 146 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # flake8: noqa 18 | 19 | from setuptools import setup 20 | 21 | # --- SCRIPTS ---------------------------------------------------------------- 22 | 23 | # Entry points to create convenient scripts automatically 24 | 25 | entry_points = { 26 | 'console_scripts': [ 27 | # decoders 28 | 'maldec-pivy=malchive.decoders.pivy:main', 29 | 'maldec-cobaltstrike_payload=malchive.decoders.cobaltstrike_payload:main', 30 | 'maldec-sunburst=malchive.decoders.sunburst:main', 31 | 'maldec-sunburst_dga=malchive.decoders.sunburst_dga:main', 32 | 'maldec-apollo=malchive.decoders.apollo:main', 33 | 'maldec-rzstreet_dumper=malchive.decoders.rzstreet_dumper:main', 34 | 35 | # utilities 36 | 'malutil-add=malchive.utilities.add:main', 37 | 'malutil-apihash=malchive.utilities.apihash:main', 38 | 'malutil-byteflip=malchive.utilities.byteflip:main', 39 | 'malutil-comguidtoyara=malchive.utilities.comguidtoyara:main', 40 | 'malutil-petimestamp=malchive.utilities.petimestamp:main', 41 | 'malutil-entropycalc=malchive.utilities.entropycalc:main', 42 | 'malutil-findapihash=malchive.utilities.findapihash:main', 43 | 'malutil-genrsa=malchive.utilities.genrsa:main', 44 | 'malutil-gensig=malchive.utilities.gensig:main', 45 | 'malutil-hiddencab=malchive.utilities.hiddencab:main', 46 | 'malutil-killaslr=malchive.utilities.killaslr:main', 47 | 'malutil-negate=malchive.utilities.negate:main', 48 | 'malutil-rotate=malchive.utilities.rotate:main', 49 | 'malutil-sub=malchive.utilities.sub:main', 50 | 'malutil-superstrings=malchive.utilities.superstrings:main', 51 | 'malutil-vtinspect=malchive.utilities.vtinspect:main', 52 | 'malutil-xor=malchive.utilities.xor:main', 53 | 'malutil-xor-pairwise=malchive.utilities.xor_pairwise:main', 54 | 'malutil-dotnetdumper=malchive.utilities.dotnetdumper:main', 55 | 'malutil-aplibdumper=malchive.utilities.aplibdumper:main', 56 | 'malutil-peresources=malchive.utilities.peresources:main', 57 | 'malutil-pepdb=malchive.utilities.pepdb:main', 58 | 'malutil-b64dump=malchive.utilities.b64dump:main', 59 | 'malutil-cobaltstrike_malleable_restore=malchive.utilities.cobaltstrike_malleable_restore:main', 60 | 'malutil-ssl_cert=malchive.utilities.ssl_cert:main', 61 | 'malutil-brute_xor=malchive.utilities.brute_xor:main', 62 | 'malutil-hashes=malchive.utilities.hashes:main', 63 | 'malutil-pecarver=malchive.utilities.pecarver:main', 64 | 'malutil-reverse_bytes=malchive.utilities.reverse_bytes:main', 65 | 'malutil-guid_recovery=malchive.utilities.guid_recovery:main', 66 | 67 | # active discovery 68 | 'maldisc-meterpreter_reverse_shell=malchive.active_discovery.meterpreter_reverse_shell:main', 69 | 'maldisc-spivy=malchive.active_discovery.spivy:main', 70 | 'maldisc-cobaltstrike_beacon=malchive.active_discovery.cobaltstrike_beacon:main', 71 | 'maldisc-shadowpad=malchive.active_discovery.shadowpad:main', 72 | ], 73 | } 74 | 75 | # --- PACKAGES --------------------------------------------------------------- 76 | 77 | packages = [ 78 | 'malchive', 79 | 'malchive.utilities', 80 | 'malchive.helpers', 81 | 'malchive.decoders', 82 | 'malchive.active_discovery', 83 | ] 84 | 85 | install_requires = [ 86 | 'asyncio', 87 | 'async_timeout', 88 | 'aiohttp', 89 | 'asyncio_dgram', 90 | 'cffi', 91 | 'timeout', 92 | 'pefile>=2023.2.7', 93 | 'capstone', 94 | 'tabulate', 95 | 'progressbar2', 96 | 'pycryptodome', 97 | 'python-registry', 98 | 'requests', 99 | 'netstruct', 100 | 'pysqlite3', 101 | 'ipaddress', 102 | 'netaddr', 103 | 'validators', 104 | 'py-tlsh', 105 | 'tld', 106 | ] 107 | 108 | 109 | def main(): 110 | 111 | setup( 112 | name='malchive', 113 | version='2.1', 114 | packages=packages, 115 | include_package_data=True, 116 | python_requires='>=3.11', 117 | extras_require={ 118 | 'extras': ['python-camellia'], 119 | }, 120 | install_requires=install_requires, 121 | url='https://github.com/MITRECND/malchive', 122 | license='Copyright 2021 The MITRE Corporation. All rights reserved.', 123 | author='MITRE CTAC', 124 | description='Malware analysis tools, libraries, and decoders.', 125 | entry_points=entry_points, 126 | download_url='https://github.com/MITRECND/malchive', 127 | ) 128 | 129 | 130 | if __name__ == "__main__": 131 | main() 132 | -------------------------------------------------------------------------------- /malchive/utilities/findapihash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import re 19 | import sys 20 | import struct 21 | import sqlite3 22 | import logging 23 | import argparse 24 | import operator 25 | import datetime 26 | from tabulate import tabulate 27 | 28 | __version__ = "1.0.0" 29 | __author__ = "Jason Batchelor" 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | SQLITE_MAX_VARIABLE_NUMBER = 999 34 | 35 | 36 | def chunks(l, n): 37 | """Yield successive n-sized chunks from l.""" 38 | for x in range(0, len(l), n): 39 | yield l[x:x + n] 40 | 41 | 42 | def initialize_parser(): 43 | parser = argparse.ArgumentParser( 44 | description='Searches supplied shellcode binary for any API hash ' 45 | 'matches. Results are displayed as a table denoting ' 46 | 'function and hash matches.') 47 | parser.add_argument('file', metavar='FILE', 48 | help='Binary file containing shellcode.') 49 | parser.add_argument('-db', '--database-name', type=str, default=None, 50 | help='Name of the database to reference for hashes. ' 51 | 'Will search the data directory located ' 52 | 'in the same path as the executing script unless ' 53 | 'an alternate name is provided ' 54 | '(default: data/apihashes.db).') 55 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 56 | help='Output additional information when processing ' 57 | '(mostly for debugging purposes).') 58 | 59 | return parser 60 | 61 | 62 | def main(): 63 | 64 | p = initialize_parser() 65 | args = p.parse_args() 66 | logging.basicConfig() 67 | 68 | root = logging.getLogger() 69 | logging.basicConfig() 70 | if args.verbose: 71 | root.setLevel(logging.DEBUG) 72 | else: 73 | root.setLevel(logging.WARNING) 74 | 75 | database = args.database_name 76 | if database is None: 77 | database = os.path.dirname(os.path.abspath(__file__)) + \ 78 | '/data/apihashes.db' 79 | else: 80 | database = args.database_name 81 | 82 | sc = args.file 83 | if not os.path.isfile(database): 84 | log.error('Failed to find hash database %s, exiting...' % database) 85 | sys.exit(2) 86 | 87 | if not os.path.isfile(sc): 88 | log.error('Failed to find provided shellcode file %s, exiting...' % sc) 89 | sys.exit(2) 90 | 91 | buff = '' 92 | with open(sc, 'rb') as f: 93 | buff = f.read() 94 | 95 | if len(buff) < 4: 96 | log.error('Supplied file must be at least four bytes. Exiting...') 97 | sys.exit(2) 98 | 99 | f.close() 100 | 101 | log.info('Processing %s...' % os.path.basename(sc)) 102 | 103 | i = 0 104 | candidates = [] 105 | start = datetime.datetime.now() 106 | while i < len(buff) - 3: 107 | candidates.append(struct.unpack(' 0: 143 | print(tabulate(results, 144 | headers=["Algorithm", "Library", "Function", 145 | "Hash", "Offset(s)"], 146 | tablefmt="grid")) 147 | else: 148 | print('No results were found in %s.' % sc) 149 | end = datetime.datetime.now() 150 | log.info('Processed %s candidate(s) in %s...' % 151 | (num_candidates, end - start)) 152 | 153 | 154 | if __name__ == '__main__': 155 | main() 156 | -------------------------------------------------------------------------------- /malchive/extras/active_discovery_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import logging 18 | import aiohttp 19 | import asyncio 20 | from async_timeout import timeout 21 | from malchive.helpers import discovery 22 | 23 | __version__ = "2.0.0" 24 | __author__ = "First Last" 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | class ExampleTemplate(discovery.Discover): 30 | 31 | def __init__(self, 32 | *args, 33 | **kwargs): 34 | # any overrides, or busy work that needs to be done 35 | # pre-scan can be done here... 36 | self.magic_request = b'GET / HTTP/1.1\r\n\r\n' 37 | self.magic_response = b'HTTP/1.1 200 OK' 38 | 39 | super().__init__(*args, **kwargs) 40 | 41 | async def tickle_http(self, co): 42 | """ 43 | Probe the server for data and make a determination based on 44 | request/response. 45 | """ 46 | 47 | headers = { 48 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) ' 49 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 50 | 'Chrome/34.0.1847.116 Safari/537.36' 51 | } 52 | 53 | url = f"{co.protocol}://{co.ip}:{co.port}" 54 | 55 | log.info('Making attempt using: %s' % url) 56 | 57 | conn = aiohttp.TCPConnector() 58 | async with aiohttp.ClientSession(connector=conn) as session: 59 | try: 60 | async with timeout(self.timeout): 61 | async with session.get(url, 62 | headers=headers, 63 | read_bufsize=10000, 64 | verify_ssl=False 65 | ) as resp: 66 | if resp.status == 200: 67 | co.success = True 68 | 69 | except aiohttp.ClientConnectionError as e: 70 | log.debug('Failure: ClientConnectionError %s: %s:%s:%s' % 71 | (str(e), co.protocol, co.ip, co.port)) 72 | 73 | except asyncio.TimeoutError: 74 | log.debug('Failure: TimeoutError: %s:%s:%s' % 75 | (co.protocol, co.ip, co.port)) 76 | 77 | except aiohttp.TooManyRedirects: 78 | log.debug('Failure: TooManyRedirects: %s:%s:%s' % 79 | (co.protocol, co.ip, co.port)) 80 | 81 | except Exception as e: 82 | log.debug('General failure: %s: %s:%s:%s' % 83 | (str(e), co.protocol, co.ip, co.port)) 84 | 85 | return co 86 | 87 | async def tickle_tcp(self, co): 88 | """ 89 | Probe the server for data and make a determination based on 90 | send/recv traffic. 91 | """ 92 | 93 | try: 94 | async with timeout(self.timeout): 95 | reader, writer = await asyncio.open_connection(co.ip, co.port) 96 | 97 | writer.write(self.magic_request) 98 | data = await reader.read(len(self.magic_response)) 99 | 100 | if data == self.magic_response: 101 | co.success = True 102 | 103 | except asyncio.TimeoutError: 104 | log.debug('Failure: TimeoutError: %s:%s:%s' % 105 | (co.protocol, co.ip, co.port)) 106 | 107 | except ConnectionRefusedError: 108 | log.debug('Failure: ConnectionRefusedError: %s:%s:%s' % 109 | (co.protocol, co.ip, co.port)) 110 | 111 | except Exception as e: 112 | log.debug('General failure: %s: %s:%s:%s' % 113 | (str(e), co.protocol, co.ip, co.port)) 114 | 115 | return co 116 | 117 | 118 | def initialize_parser(): 119 | parser = discovery.generic_args() 120 | return parser 121 | 122 | 123 | def main(): 124 | 125 | p = initialize_parser() 126 | args = p.parse_args() 127 | 128 | root = logging.getLogger() 129 | logging.basicConfig() 130 | if args.verbose: 131 | root.setLevel(logging.DEBUG) 132 | else: 133 | root.setLevel(logging.WARNING) 134 | 135 | d = ExampleTemplate( 136 | ips=args.ipaddress, 137 | ports=args.port, 138 | domains=args.domain, 139 | timeout=args.timeout, 140 | protocols=args.protocol, 141 | ) 142 | 143 | comms = discovery.create_comms( 144 | d.protocols, 145 | d.ips, 146 | d.ports 147 | ) 148 | 149 | results = [] 150 | asyncio.run(d.run(comms, results)) 151 | 152 | for co in results: 153 | if co.success: 154 | print('Successfully discovered candidate! [%s] %s:%s' % 155 | (co.protocol, co.ip, co.port)) 156 | 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /malchive/utilities/genrsa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | import argparse 20 | import hashlib 21 | from Crypto.PublicKey import RSA 22 | sys.setrecursionlimit(1000000) 23 | 24 | __version__ = "1.0.0" 25 | __author__ = "Jason Batchelor" 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | # Extended Euclidean Recursive algorithm 31 | # Reference: 32 | # https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm#Python 33 | def egcd(a, b): 34 | if a == 0: 35 | return (b, 0, 1) 36 | else: 37 | g, x, y = egcd(b % a, a) 38 | return (g, y - (b // a) * x, x) 39 | 40 | 41 | def gen_private_key(p, q, e): 42 | """ 43 | Use supplied values to generate an RSA private key. 44 | 45 | :param int p: First prime. 46 | :param int q: Second prime. 47 | :param int e: Public exponent. 48 | 49 | :return: Private key 50 | :rtype: str 51 | """ 52 | 53 | # Calculate 'n', n = p x q 54 | n = p * q 55 | # Calculate 'd', d = e^(-1) mod [(p-1)x(q-1)] 56 | phi = (p - 1) * (q - 1) 57 | # Need to use extended euclidean algorithm for 'd' 58 | gcd, d, b = egcd(e, phi) 59 | 60 | # Assign key parameters 61 | key_params = (n, e, d, p, q) 62 | # Construct private key 63 | key = RSA.construct(key_params) 64 | 65 | return key.exportKey() 66 | 67 | 68 | def gen_public_key(n, e): 69 | """ 70 | Use supplied values to generate an RSA public key. 71 | 72 | :param int n: Public modulus . 73 | :param int e: Public exponent. 74 | 75 | :return: Public key 76 | :rtype: str 77 | """ 78 | 79 | # Assign key parameters 80 | key_params = (n, e) 81 | # Construct private key 82 | key = RSA.construct(key_params) 83 | 84 | return key.exportKey() 85 | 86 | 87 | def autoint(x): 88 | if x.startswith('0x'): 89 | x = int(x, 16) 90 | else: 91 | x = int(x) 92 | return x 93 | 94 | 95 | def initialize_parser(): 96 | parser = argparse.ArgumentParser( 97 | description='Generate RSA keys given corresponding values. Numeric ' 98 | 'values may be provided as regular integers or ' 99 | 'hexadecimal with the \'0x\' prefix.') 100 | parser.add_argument('-p', '--first-prime', type=autoint, default=0, 101 | help='First prime value. Used along with \'Q\' ' 102 | 'for private key generation.') 103 | parser.add_argument('-q', '--second-prime', type=autoint, default=0, 104 | help='Second prime value. Used along with \'P\' ' 105 | 'for private key generation.') 106 | parser.add_argument('-e', '--public-exponent', type=autoint, 107 | default=0x010001, 108 | help='Public exponent used for private and public key ' 109 | 'generation. (Default: 0x010001)') 110 | parser.add_argument('-n', '--public-modulus', type=autoint, default=0, 111 | help='Public modulus used when generating ' 112 | 'public keys.') 113 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 114 | help='Output additional information when processing ' 115 | '(mostly for debugging purposes).') 116 | return parser 117 | 118 | 119 | def main(): 120 | p = initialize_parser() 121 | args = p.parse_args() 122 | 123 | root = logging.getLogger() 124 | logging.basicConfig() 125 | if args.verbose: 126 | root.setLevel(logging.DEBUG) 127 | else: 128 | root.setLevel(logging.WARNING) 129 | 130 | e = args.public_exponent 131 | pub_key = b'' 132 | priv_key = b'' 133 | if args.first_prime > 0 and args.second_prime > 0: 134 | p = args.first_prime 135 | q = args.second_prime 136 | priv_key = gen_private_key(p, q, e) 137 | file_md5 = hashlib.md5(priv_key).hexdigest() 138 | name = '%s_priv.key' % file_md5 139 | if len(priv_key) > 0: 140 | with open(name, 'wb+') as f: 141 | f.write(priv_key) 142 | print('Private key generated. Written to %s' % name) 143 | 144 | if args.public_modulus > 0: 145 | n = args.public_modulus 146 | 147 | try: 148 | pub_key = gen_public_key(n, e) 149 | except ValueError: 150 | log.error('Invalid RSA public value(s).') 151 | 152 | file_md5 = hashlib.md5(pub_key).hexdigest() 153 | name = '%s_pub.key' % file_md5 154 | if len(pub_key) > 0: 155 | with open(name, 'wb+') as f: 156 | f.write(pub_key) 157 | print('Public key generated. Written to %s' % name) 158 | 159 | 160 | if __name__ == '__main__': 161 | main() 162 | -------------------------------------------------------------------------------- /malchive/utilities/hiddencab.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import re 19 | import sys 20 | import struct 21 | import logging 22 | import argparse 23 | import hashlib 24 | from tabulate import tabulate 25 | 26 | __version__ = "1.0.0" 27 | __author__ = "Jason Batchelor" 28 | 29 | log = logging.getLogger(__name__) 30 | 31 | 32 | def getcab(buff): 33 | """ 34 | Return a dictionary of information about cabs such as; size, offset, SHA256, 35 | and the cab buffer. 36 | 37 | :param bytes buff: File to look for cabs. 38 | 39 | :return: Dictionary containing cab information. 40 | :rtype: dict 41 | """ 42 | 43 | hidden_cab_regex = b'(.{4})\x00\x00\x00\x00(.{4})\x00\x00\x00\x00' \ 44 | b'(.{4})\x00\x00\x00\x00\x03\x01' \ 45 | b'(.{2})(.{2}).\x00.{2}\x00\x00' 46 | matches = re.finditer(hidden_cab_regex, buff, re.DOTALL) 47 | cabs = {} 48 | counter = 0 49 | 50 | for m in matches: 51 | cabsize = struct.unpack(' 0 and \ 55 | struct.unpack(' 0 and \ 56 | struct.unpack(' 0: 57 | start = m.start(0) + 4 58 | cab = b'\x4d\x53\x43\x46' + buff[start:start + (cabsize - 4)] 59 | cabs['Object_%s' % counter] = {'buff': cab, 60 | 'size': cabsize, 61 | 'offset': m.start(0) + 4, 62 | 'sha256': hashlib.sha256(cab).hexdigest() 63 | } 64 | counter += 1 65 | 66 | return cabs 67 | 68 | 69 | def initialize_parser(): 70 | parser = argparse.ArgumentParser( 71 | description='Search for hidden CAB files inside binary.') 72 | parser.add_argument('infile', metavar='FILE', nargs='*', 73 | help='Full path to the file to be processed.') 74 | parser.add_argument('-t', '--target-directory', type=str, default='.', 75 | help='Target directory to write files. \ 76 | Defaults to executing directory.') 77 | parser.add_argument('-s', '--suppress-write', action='store_true', 78 | help='Prevent results from being written to ' 79 | 'disk. Opting instead for just a table view ' 80 | 'of the results.') 81 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 82 | help='Output additional information when processing ' 83 | '(mostly for debugging purposes).') 84 | return parser 85 | 86 | 87 | def main(): 88 | p = initialize_parser() 89 | args = p.parse_args() 90 | 91 | root = logging.getLogger() 92 | logging.basicConfig() 93 | if args.verbose: 94 | root.setLevel(logging.DEBUG) 95 | else: 96 | root.setLevel(logging.WARNING) 97 | 98 | if len(args.infile) == 0: 99 | p.print_help() 100 | sys.exit(2) 101 | 102 | target_dir = args.target_directory 103 | for fname in args.infile: 104 | results = [] 105 | cabinfo = {} 106 | 107 | basename = os.path.basename(fname) 108 | 109 | if not os.path.isfile(fname): 110 | log.warning('Failed to find file %s. Skipping...' % fname) 111 | continue 112 | 113 | with open(fname, 'rb') as f: 114 | data = f.read() 115 | 116 | cabinfo = getcab(data) 117 | 118 | print('Found %s hidden cab(s) in %s...' % (len(cabinfo), basename)) 119 | if not args.suppress_write and target_dir != '.': 120 | log.info('Writing to directory: %s' % target_dir) 121 | 122 | for k, v in list(cabinfo.items()): 123 | results.append((v['sha256'], v['size'], v['offset'])) 124 | 125 | if not args.suppress_write: 126 | if target_dir != '.': 127 | try: 128 | os.makedirs(target_dir) 129 | except OSError: 130 | if not os.path.isdir(target_dir): 131 | log.error('Could not create %s. Exiting...' 132 | % target_dir) 133 | sys.exit(2) 134 | with open('%s/%s' % (target_dir, v['sha256']), 'wb') as f: 135 | f.write(v['buff']) 136 | 137 | if len(results) > 0: 138 | print(tabulate(results, 139 | headers=["SHA256", "Size", "Offset"], 140 | tablefmt="grid")) 141 | 142 | 143 | if __name__ == '__main__': 144 | main() 145 | -------------------------------------------------------------------------------- /malchive/utilities/xor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from typing import List 20 | from malchive.helpers import BinDataHelper 21 | 22 | __version__ = "1.1.0" 23 | __author__ = "Jason Batchelor" 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | class GenericXor(BinDataHelper.LiteCrypt): 29 | 30 | def __init__(self, 31 | key: list = None, 32 | count: int = 0, 33 | skip_nulls: bool = False, 34 | skip_key: bool = False, 35 | skip_strict: bool = False, 36 | *args, 37 | **kwargs): 38 | 39 | self.key: List[int] = [] 40 | self.key_count: int = 0 41 | self.skip_nulls: bool = False 42 | self.skip_key: bool = False 43 | self.skip_strict: bool = False 44 | 45 | if not isinstance(key, list): 46 | raise TypeError('Key sequence must be passed as a list.') 47 | if not all(k < 256 for k in key): 48 | raise ValueError('Key sequence must contain values within ' 49 | 'byte range (0x00 - 0xff).') 50 | if count > 256: 51 | raise ValueError('Count must be within byte range (0x00 - 0xff).') 52 | 53 | self.key = key 54 | self.key_count = count 55 | self.skip_nulls = skip_nulls 56 | self.skip_key = skip_key 57 | self.skip_strict = skip_strict 58 | 59 | log.info('Proceeding using key: [%s]' % 60 | ', '.join(hex(x) for x in self.key)) 61 | 62 | super().__init__(*args, **kwargs) 63 | 64 | def run_crypt(self): 65 | """ 66 | Run a generic xor on supplied data. 67 | 68 | :return: processed data 69 | :rtype: bytearray 70 | """ 71 | 72 | counter = 0 73 | decoded = bytearray() 74 | 75 | for i in range(self.offset, self.total_size): 76 | 77 | k = self.key[counter % len(self.key)] 78 | byte = self.buff[i] 79 | 80 | if (self.skip_nulls and byte == 0x00) or \ 81 | (self.skip_key and byte == k): 82 | decoded.append(byte) 83 | if self.skip_strict: 84 | continue 85 | else: 86 | decoded.append(byte ^ k & 0xff) 87 | 88 | k = k + self.key_count & 0xff 89 | self.key[counter % len(self.key)] = k 90 | counter += 1 91 | 92 | return decoded 93 | 94 | 95 | def initialize_parser(): 96 | 97 | description = 'Process data stream and xor each byte by ' \ 98 | 'the supplied key. Numeric values may be provided ' \ 99 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 100 | 101 | parser = BinDataHelper.generic_args(description) 102 | 103 | parser.add_argument('key', type=BinDataHelper.autokey, 104 | help='Single or multibyte key value. ' 105 | 'May be supplied as an integer or ' 106 | 'as a hex value with the \'0x\' prefix.') 107 | parser.add_argument('-c', '--count', 108 | type=BinDataHelper.autoint, 109 | default=0, 110 | help='Interval to increment key value on each byte ' 111 | 'iteration. Range of 0x00 - 0xff.') 112 | parser.add_argument('-sn', '--skip-nulls', 113 | action='store_true', 114 | default=False, 115 | help='When processing the buffer, skip all null ' 116 | '(\'0x00\') bytes.') 117 | parser.add_argument('-sk', '--skip-key', 118 | action='store_true', 119 | default=False, 120 | help='Skip bytes that match the supplied key value.') 121 | parser.add_argument('--skip-strict', 122 | action='store_true', 123 | default=False, 124 | help='Do not increment or modify key in any way if a ' 125 | 'skip condition is satisfied.') 126 | 127 | return parser 128 | 129 | 130 | def main(): 131 | p = initialize_parser() 132 | args = p.parse_args() 133 | 134 | root = logging.getLogger() 135 | logging.basicConfig() 136 | if args.verbose: 137 | root.setLevel(logging.DEBUG) 138 | else: 139 | root.setLevel(logging.WARNING) 140 | 141 | buff = args.infile.buffer.read() 142 | 143 | s = GenericXor( 144 | key=args.key, 145 | count=args.count, 146 | skip_nulls=args.skip_nulls, 147 | skip_key=args.skip_key, 148 | skip_strict=args.skip_strict, 149 | buff=buff, 150 | offset=args.offset, 151 | size=args.size, 152 | ) 153 | 154 | return_data = s.run_crypt() 155 | 156 | sys.stdout.buffer.write(return_data) 157 | 158 | 159 | if __name__ == '__main__': 160 | main() 161 | -------------------------------------------------------------------------------- /malchive/utilities/sub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from typing import List 20 | from malchive.helpers import BinDataHelper 21 | 22 | __version__ = "1.1.0" 23 | __author__ = "Jason Batchelor" 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | class GenericSubtract(BinDataHelper.LiteCrypt): 29 | 30 | def __init__(self, 31 | key: list = None, 32 | count: int = 0, 33 | skip_nulls: bool = False, 34 | skip_key: bool = False, 35 | skip_strict: bool = False, 36 | *args, 37 | **kwargs): 38 | 39 | self.key: List[int] = [] 40 | self.key_count: int = 0 41 | self.skip_nulls: bool = False 42 | self.skip_key: bool = False 43 | self.skip_strict: bool = False 44 | 45 | if not isinstance(key, list): 46 | raise TypeError('Key sequence must be passed as a list.') 47 | if not all(k < 256 for k in key): 48 | raise ValueError('Key sequence must contain values within ' 49 | 'byte range (0x00 - 0xff).') 50 | if count > 256: 51 | raise ValueError('Count must be within byte range (0x00 - 0xff).') 52 | 53 | self.key = key 54 | self.key_count = count 55 | self.skip_nulls = skip_nulls 56 | self.skip_key = skip_key 57 | self.skip_strict = skip_strict 58 | 59 | log.info('Proceeding using key: [%s]' % 60 | ', '.join(hex(x) for x in self.key)) 61 | 62 | super().__init__(*args, **kwargs) 63 | 64 | def run_crypt(self): 65 | """ 66 | Run a generic subtraction on supplied data. 67 | 68 | :return: processed data 69 | :rtype: bytearray 70 | """ 71 | 72 | counter = 0 73 | decoded = bytearray() 74 | 75 | for i in range(self.offset, self.total_size): 76 | 77 | k = self.key[counter % len(self.key)] 78 | byte = self.buff[i] 79 | 80 | if (self.skip_nulls and byte == 0x00) or \ 81 | (self.skip_key and byte == k): 82 | decoded.append(byte) 83 | if self.skip_strict: 84 | continue 85 | else: 86 | decoded.append(byte - k & 0xff) 87 | 88 | k = k + self.key_count & 0xff 89 | self.key[counter % len(self.key)] = k 90 | counter += 1 91 | 92 | return decoded 93 | 94 | 95 | def initialize_parser(): 96 | 97 | description = 'Process data stream and subtract each byte by ' \ 98 | 'the supplied key. Numeric values may be provided ' \ 99 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 100 | 101 | parser = BinDataHelper.generic_args(description) 102 | 103 | parser.add_argument('key', type=BinDataHelper.autokey, 104 | help='Single or multibyte key value. ' 105 | 'May be supplied as an integer or ' 106 | 'as a hex value with the \'0x\' prefix.') 107 | parser.add_argument('-c', '--count', 108 | type=BinDataHelper.autoint, 109 | default=0, 110 | help='Interval to increment key value on each byte ' 111 | 'iteration. Range of 0x00 - 0xff.') 112 | parser.add_argument('-sn', '--skip-nulls', 113 | action='store_true', 114 | default=False, 115 | help='When processing the buffer, skip all null ' 116 | '(\'0x00\') bytes.') 117 | parser.add_argument('-sk', '--skip-key', 118 | action='store_true', 119 | default=False, 120 | help='Skip bytes that match the supplied key value.') 121 | parser.add_argument('--skip-strict', 122 | action='store_true', 123 | default=False, 124 | help='Do not increment or modify key in any way if a ' 125 | 'skip condition is satisfied.') 126 | 127 | return parser 128 | 129 | 130 | def main(): 131 | p = initialize_parser() 132 | args = p.parse_args() 133 | 134 | root = logging.getLogger() 135 | logging.basicConfig() 136 | if args.verbose: 137 | root.setLevel(logging.DEBUG) 138 | else: 139 | root.setLevel(logging.WARNING) 140 | 141 | buff = args.infile.buffer.read() 142 | 143 | s = GenericSubtract( 144 | key=args.key, 145 | count=args.count, 146 | skip_nulls=args.skip_nulls, 147 | skip_key=args.skip_key, 148 | skip_strict=args.skip_strict, 149 | buff=buff, 150 | offset=args.offset, 151 | size=args.size, 152 | ) 153 | 154 | return_data = s.run_crypt() 155 | 156 | sys.stdout.buffer.write(return_data) 157 | 158 | 159 | if __name__ == '__main__': 160 | main() 161 | -------------------------------------------------------------------------------- /malchive/utilities/add.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys 18 | import logging 19 | from typing import List 20 | from malchive.helpers import BinDataHelper 21 | 22 | __version__ = "1.1.0" 23 | __author__ = "Jason Batchelor" 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | class GenericAdd(BinDataHelper.LiteCrypt): 29 | 30 | def __init__(self, 31 | key: list = None, 32 | count: int = 0, 33 | skip_nulls: bool = False, 34 | skip_key: bool = False, 35 | skip_strict: bool = False, 36 | *args, 37 | **kwargs): 38 | 39 | self.key: List[int] = [] 40 | self.key_count: int = 0 41 | self.skip_nulls: bool = False 42 | self.skip_key: bool = False 43 | self.skip_strict: bool = False 44 | 45 | if not isinstance(key, list): 46 | raise TypeError('Key sequence must be passed as a list.') 47 | if not all(k < 256 for k in key): 48 | raise ValueError('Key sequence must contain values within ' 49 | 'byte range (0x00 - 0xff).') 50 | if count > 256: 51 | raise ValueError('Count must be within byte range (0x00 - 0xff).') 52 | 53 | self.key = key 54 | self.key_count = count 55 | self.skip_nulls = skip_nulls 56 | self.skip_key = skip_key 57 | self.skip_strict = skip_strict 58 | 59 | log.info('Proceeding using key: [%s]' % 60 | ', '.join(hex(x) for x in self.key)) 61 | 62 | super().__init__(*args, **kwargs) 63 | 64 | def run_crypt(self): 65 | """ 66 | Run a generic addition on supplied data. 67 | 68 | :return: processed data 69 | :rtype: bytearray 70 | """ 71 | 72 | counter = 0 73 | decoded = bytearray() 74 | 75 | for i in range(self.offset, self.total_size): 76 | 77 | k = self.key[counter % len(self.key)] 78 | byte = self.buff[i] 79 | 80 | if (self.skip_nulls and byte == 0x00) or \ 81 | (self.skip_key and byte == k): 82 | decoded.append(byte) 83 | if self.skip_strict: 84 | continue 85 | else: 86 | decoded.append(byte + k & 0xff) 87 | 88 | k = k + self.key_count & 0xff 89 | self.key[counter % len(self.key)] = k 90 | counter += 1 91 | 92 | return decoded 93 | 94 | 95 | def initialize_parser(): 96 | 97 | description = 'Process data stream and add each byte by ' \ 98 | 'the supplied key. Numeric values may be provided ' \ 99 | 'as regular integers or hexadecimal with the \'0x\' prefix.' 100 | 101 | parser = BinDataHelper.generic_args(description) 102 | 103 | parser.add_argument('key', 104 | type=BinDataHelper.autokey, 105 | help='Single or multibyte key value. ' 106 | 'May be supplied as an integer or ' 107 | 'as a hex value with the \'0x\' prefix.') 108 | parser.add_argument('-c', '--count', 109 | type=BinDataHelper.autoint, 110 | default=0, 111 | help='Interval to increment key value on each byte ' 112 | 'iteration. Range of 0x00 - 0xff.') 113 | parser.add_argument('-sn', '--skip-nulls', 114 | action='store_true', 115 | default=False, 116 | help='When processing the buffer, skip all null ' 117 | '(\'0x00\') bytes.') 118 | parser.add_argument('-sk', '--skip-key', 119 | action='store_true', 120 | default=False, 121 | help='Skip bytes that match the supplied key value.') 122 | parser.add_argument('--skip-strict', 123 | action='store_true', 124 | default=False, 125 | help='Do not increment or modify key in any way if a ' 126 | 'skip condition is satisfied.') 127 | 128 | return parser 129 | 130 | 131 | def main(): 132 | p = initialize_parser() 133 | args = p.parse_args() 134 | 135 | root = logging.getLogger() 136 | logging.basicConfig() 137 | if args.verbose: 138 | root.setLevel(logging.DEBUG) 139 | else: 140 | root.setLevel(logging.WARNING) 141 | 142 | buff = args.infile.buffer.read() 143 | 144 | s = GenericAdd( 145 | key=args.key, 146 | count=args.count, 147 | skip_nulls=args.skip_nulls, 148 | skip_key=args.skip_key, 149 | skip_strict=args.skip_strict, 150 | buff=buff, 151 | offset=args.offset, 152 | size=args.size, 153 | ) 154 | 155 | return_data = s.run_crypt() 156 | 157 | sys.stdout.buffer.write(return_data) 158 | 159 | 160 | if __name__ == '__main__': 161 | main() 162 | -------------------------------------------------------------------------------- /malchive/decoders/cobaltstrike_payload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import argparse 19 | import logging 20 | import struct 21 | 22 | __version__ = "2.0.0" 23 | __author__ = "Marcus Eddy & Jason Batchelor" 24 | 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | class ExtractIt: 30 | 31 | def __init__(self, buff): 32 | """ 33 | Initialize decoder instance. 34 | 35 | :param bytes buff: The stream of bytes to be processed. 36 | """ 37 | 38 | self.key = 0 39 | self.buff = bytearray(buff) 40 | self.decoded_payload = bytearray() 41 | self.payload = self.decode_payload(self.buff) 42 | 43 | def decode_payload(self, buff): 44 | """ 45 | Find Xor Key and Decode Payload 46 | """ 47 | 48 | beacon_magic = [ 49 | b'\x90\x90', 50 | b'\x0f\x1f', 51 | b'\x66\x90', 52 | b'\x4d\x5a', 53 | ] 54 | 55 | ff_start = buff[:100].find(b'\xff\xff\xff') 56 | if ff_start > 1: 57 | log.info('Encrypted payload marker found!') 58 | 59 | buff = buff[ff_start+3:] 60 | 61 | _t, size = struct.unpack('II', buff[0x0:0x8]) 62 | size ^= _t 63 | 64 | dec = buff[0x8:] 65 | key = buff[:0x4] 66 | 67 | if size > len(dec): 68 | log.info('Bad size of %s bytes returned for payload %s bytes' % 69 | (size, len(dec))) 70 | return 71 | 72 | self.key = struct.unpack('>I', key)[0] 73 | 74 | decoded = self.rolling_xor_long(dec, key, size) 75 | if decoded[:2] in beacon_magic: 76 | self.decoded_payload = decoded 77 | else: 78 | log.info('Beacon PE data was not found in decrypted data.') 79 | 80 | @staticmethod 81 | def rolling_xor_long(dec, key, size): 82 | for i in range(0, size): 83 | dec[i] ^= key[i % len(key)] 84 | key[i % len(key)] ^= dec[i] 85 | 86 | return dec 87 | 88 | 89 | def initialize_parser(): 90 | parser = argparse.ArgumentParser( 91 | description='Process candidate. Prints results in JSON format.') 92 | parser.add_argument('candidates', metavar='FILE', 93 | nargs='*', help='candidate file(s).') 94 | parser.add_argument('-v', '--verbose', action='store_true', 95 | default=False, 96 | help='Output additional information when processing ' 97 | '(mostly for debugging purposes).') 98 | parser.add_argument('-w', '--write', action='store_true', 99 | default=False, 100 | help='Write retrieved payloads to disk ' 101 | 'using [MD5.beacon] as the filename.') 102 | parser.add_argument('-t', '--target-directory', type=str, default='.', 103 | help='Target directory to write files. Defaults to ' 104 | 'executing directory.') 105 | return parser 106 | 107 | 108 | def main(): 109 | import sys 110 | import os 111 | import json 112 | import hashlib 113 | 114 | p = initialize_parser() 115 | args = p.parse_args() 116 | 117 | root = logging.getLogger() 118 | logging.basicConfig() 119 | if args.verbose: 120 | root.setLevel(logging.DEBUG) 121 | else: 122 | root.setLevel(logging.WARNING) 123 | 124 | directory = args.target_directory 125 | if directory != '.': 126 | if not os.path.isdir(directory): 127 | log.error('Could not create %s. Exiting...' 128 | % directory) 129 | sys.exit(2) 130 | 131 | for filename in args.candidates: 132 | 133 | log.info('Processing file %s...' % filename) 134 | 135 | if not os.path.isfile(filename): 136 | log.warning('Failed to find file %s' % filename) 137 | continue 138 | 139 | f = open(filename, 'rb') 140 | stream = f.read() 141 | 142 | d = ExtractIt(stream) 143 | 144 | if len(d.decoded_payload) == 0: 145 | log.info('Payload not a Cobalt Beacon.') 146 | continue 147 | 148 | config_dict = { 149 | 'Input Sha256': hashlib.sha256(stream).hexdigest(), 150 | 'Input': filename, 151 | 'Output Sha256': hashlib.sha256(d.decoded_payload).hexdigest(), 152 | 'XOR Key': hex(d.key) 153 | } 154 | 155 | if args.write and len(d.decoded_payload): 156 | md5 = hashlib.md5(d.decoded_payload).hexdigest() 157 | fname = '%s/%s.beacon' % (directory, md5) 158 | with open(fname, 'wb') as f: 159 | f.write(d.decoded_payload) 160 | log.info('%s written to disk!' % fname) 161 | 162 | try: 163 | print((json.dumps(config_dict, indent=4, sort_keys=False))) 164 | except UnicodeDecodeError: 165 | log.warning('There was a Unicode decoding error when ' 166 | 'processing %s' % os.path.basename(filename)) 167 | continue 168 | 169 | 170 | if __name__ == '__main__': 171 | main() 172 | -------------------------------------------------------------------------------- /malchive/active_discovery/spivy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | import asyncio 18 | import logging 19 | import struct 20 | from malchive.helpers import discovery 21 | from random import choice, randint 22 | from string import digits 23 | from async_timeout import timeout 24 | 25 | __version__ = "2.0.0" 26 | __author__ = "Jason Batchelor" 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | class SecurePoisonIvy(discovery.Discover): 32 | 33 | def __init__(self, 34 | key: str = None, 35 | *args, 36 | **kwargs): 37 | 38 | self.payload = self.craft_payload() 39 | # informed by observations of traffic, we fuzz here 40 | self.cookie_id = ''.join( 41 | [ 42 | choice(digits) 43 | for i in range(0, 17) 44 | ]) 45 | 46 | # informed by observations of traffic, we fuzz here again 47 | self.uri_string = ''.join( 48 | [ 49 | choice(digits) 50 | for i in range(0, 16) 51 | ]) 52 | 53 | super().__init__(*args, **kwargs) 54 | 55 | def craft_payload(self): 56 | """ 57 | Craft an SPIVY packet mimicking the beginning of the challenge 58 | request/response chain. Save the expected response. 59 | """ 60 | 61 | junk_size = randint(1, 16) 62 | 63 | junk_data = bytearray( 64 | [ 65 | choice([i for i in range(0, 256)]) 66 | for i in range(0, junk_size) 67 | ]) 68 | 69 | challenge_request = bytes(b'\x00' * 0x100) 70 | 71 | payload = \ 72 | struct.pack('B', junk_size) + \ 73 | junk_data + \ 74 | struct.pack('B', (junk_size*2 & 0xff)) + \ 75 | challenge_request 76 | 77 | return payload 78 | 79 | async def tickle_tcp(self, co): 80 | """ 81 | Probe the server for data and make a determination based on 82 | request/response. 83 | """ 84 | 85 | # SPIVY 'technically' just uses HTTP over a TCP socket and 86 | # does not have an 'Accept-Encoding' header. 87 | url = f"{co.protocol}://{co.ip}:{co.port}/{self.uri_string}" 88 | http_data = f"POST {url} HTTP/1.1\r\n" \ 89 | f"Cookie: id={self.cookie_id}\r\n" \ 90 | f"Content-Length: {str(len(self.payload))}\r\n" \ 91 | f"\r\n" 92 | 93 | http_request = http_data.encode('utf-8') + self.payload 94 | 95 | try: 96 | async with timeout(self.timeout): 97 | reader, writer = await asyncio.open_connection(co.ip, co.port) 98 | writer.write(http_request) 99 | await writer.drain() 100 | 101 | response = await reader.read(0x200) 102 | writer.close() 103 | await writer.wait_closed() 104 | 105 | if await self.eval_response(response): 106 | co.success = True 107 | 108 | except asyncio.TimeoutError: 109 | log.debug('Failure: TimeoutError: %s:%s:%s' % 110 | (co.protocol, co.ip, co.port)) 111 | 112 | except ConnectionRefusedError: 113 | log.debug('Failure: ConnectionRefusedError: %s:%s:%s' % 114 | (co.protocol, co.ip, co.port)) 115 | 116 | except Exception as e: 117 | log.debug('General failure: %s: %s:%s:%s' % 118 | (str(e), co.protocol, co.ip, co.port)) 119 | 120 | return co 121 | 122 | async def eval_response(self, response): 123 | 124 | buff = bytearray(response) 125 | 126 | if len(buff) > 0x100: 127 | if self.validate_response(response): 128 | log.info('Positive match for SPIVY controller!') 129 | return True 130 | else: 131 | log.info('Retrieved data not an SPIVY challenge response.') 132 | else: 133 | log.info('Retrieved data too short for SPIVY response.') 134 | 135 | return False 136 | 137 | def validate_response(self, response): 138 | """ 139 | We can just take the last 0x100 bytes from the challenge 140 | response and compare rather than parsing the packet. 141 | """ 142 | crypted = response[-0x100:] 143 | # check that not all values are the same 144 | if all(v == crypted[0] for v in crypted): 145 | return False 146 | # return if chunks of 0x10 repeat 147 | return (len([True for i in range(0x10, len(crypted), 0x10) 148 | if crypted[:0x10] == crypted[i:i+0x10]])) == 0xf 149 | 150 | 151 | def initialize_parser(): 152 | parser = discovery.generic_args() 153 | return parser 154 | 155 | 156 | def main(): 157 | 158 | p = initialize_parser() 159 | args = p.parse_args() 160 | 161 | root = logging.getLogger() 162 | logging.basicConfig() 163 | if args.verbose: 164 | root.setLevel(logging.DEBUG) 165 | else: 166 | root.setLevel(logging.WARNING) 167 | 168 | d = SecurePoisonIvy( 169 | ips=args.ipaddress, 170 | ports=args.port, 171 | domains=args.domain, 172 | timeout=args.timeout, 173 | protocols=args.protocol, 174 | ) 175 | 176 | comms = discovery.create_comms( 177 | d.protocols, 178 | d.ips, 179 | d.ports 180 | ) 181 | 182 | results = [] 183 | asyncio.run(d.run(comms, results)) 184 | 185 | for co in results: 186 | if co.success: 187 | print('Successfully discovered candidate! [%s] %s:%s' % 188 | (co.protocol, co.ip, co.port)) 189 | 190 | 191 | if __name__ == '__main__': 192 | main() 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Malchive # 2 | 3 | The malchive serves as a compendium for a variety of capabilities mainly pertaining to malware analysis, such as scripts supporting day to day binary analysis and decoder modules for various components of malicious code. 4 | 5 | The goals behind the 'malchive' are to: 6 | * Allow teams to centralize efforts made in this realm and enforce communication and continuity 7 | * Have a shared corpus of tools for people to build on 8 | * Enforce clean coding practices 9 | * Allow others to interface with project members to develop their own capabilities 10 | * Promote a positive feedback loop between Threat Intel and Reverse Engineering staff 11 | * Make static file analysis more accessible 12 | * Serve as a vehicle to communicate the unique opportunity space identified via deep dive analysis 13 | 14 | ## Documentation ## 15 | 16 | At its core, malchive is a bunch of standalone scripts organized in a manner that the authors hope promotes the project's goals. 17 | 18 | To view the documentation associated with this project, **checkout the wiki page**! 19 | 20 | Scripts within the malchive are split up into the following core categories: 21 | 22 | * **Utilities** - These scripts may be run standalone to assist with static binary analysis or as modules supporting a broader program. Utilities always have a standalone component. 23 | * **Helpers** - These modules primarily serve to assist components in one or more of the other categories. They generally do not have a stand-alone component and instead serve the intents of those that do. 24 | * **Binary Decoders** - The purpose of scripts in this category is to retrieve, decrypt, and return embedded data (typically inside malware). 25 | * **Active Discovery** - Standalone scripts designed to emulate a small portion of a malware family's protocol for the purposes of discovering active controllers. 26 | 27 | ## Installation ## 28 | 29 | The malchive is a packaged distribution that is easily installed and will automatically create console stand-alone scripts. 30 | 31 | ### Steps ### 32 | 33 | * Python 3.11+ is required. You may need to download or add a new repository to install this. For example, using Ubuntu you will need to execute the following: 34 | ``` 35 | sudo add-apt-repository ppa:deadsnakes/ppa 36 | sudo apt install python3.11 python3.11-distutils python3.11-dev 37 | curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 38 | ``` 39 | You will need to install some dependencies for some of the required Python modules to function correctly. 40 | * First do a source install of [YARA](https://github.com/VirusTotal/yara/releases) and make sure you compile using `--dotnet` 41 | * Next source install the [YARA Python](https://github.com/VirusTotal/yara-python/releases/) package. 42 | * Ensure you have sqlite3-dev installed 43 | - Debian: libsqlite3-dev 44 | - Red Hat: sqlite-devel / `pip install pysqlite3` 45 | 46 | You can then clone the malchive repo and install... 47 | * `pip install .` when in the parent directory. 48 | * To remove, just `pip uninstall malchive` 49 | 50 | ### Scripts ### 51 | 52 | Console scripts stemming from `utilities` are appended with the prefix `malutil`, `decoders` are appended with `maldec`, and `active discovery` scripts are appended with `maldisc`. This allows for easily identifiable malchive scripts via tab autocompletion. 53 | 54 | ```buildoutcfg 55 | ; running superstrings from cmd line 56 | malutil-superstrings 1.exe -ss 57 | 0x9535 (stack) lstrlenA 58 | 0x9592 (stack) GetFileSize 59 | 0x95dd (stack) WriteFile 60 | 0x963e (stack) CreateFileA 61 | 0x96b0 (stack) SetFilePointer 62 | 0x9707 (stack) GetSystemDirectoryA 63 | 64 | ; running a decoder from cmd line 65 | maldec-pivy test.exe_ 66 | { 67 | "MD5": "2973ee05b13a575c06d23891ab83e067", 68 | "Config": { 69 | "PersistActiveSetupName": "StubPath", 70 | "DefaultBrowserKey": "SOFTWARE\\Classes\\http\\shell\\open\\command", 71 | "PersistActiveSetupKeyPart": "Software\\Microsoft\\Active Setup\\Installed Components\\", 72 | "ServerId": "TEST - WIN_XP", 73 | "Callbacks": [ 74 | { 75 | "callback": "192.168.1.104", 76 | "protocol": "Direct", 77 | "port": 3333 78 | }, 79 | { 80 | "callback": "192.168.1.111", 81 | "protocol": "Direct", 82 | "port": 4444 83 | } 84 | ], 85 | "ProxyCfgPresent": false, 86 | "Password": "test$321$", 87 | "Mutex": ")#V0qA.I4", 88 | "CopyAsADS": true, 89 | "Melt": true, 90 | "InjectPersist": true, 91 | "Inject": true 92 | } 93 | } 94 | 95 | ; cmd line use with other common utilities 96 | echo -ne 'eJw9kLFuwzAMRIEC7ZylrVGgRSFZiUbBZmwqsMUP0VfcnuQn+rMde7KLTBIPj0ce34tHyMUJjrnw 97 | p3apz1kicjoJrDRlQihwOXmpL4RmSR5qhEU9MqvgWo8XqGMLJd+sKNQPK0dIGjK+e5WANIT6NeOs 98 | k2mI5NmYAmcrkbn4oLPK5gZX+hVlRoKloMV20uQknv2EPunHKQtcig1cpHY4Jodie5pRViV+rp1t 99 | 629J6Dyu4hwLR97LINqY5rYILm1hhlvinoyJZavOKTrwBHTwpZ9yPSzidUiPt8PUTkZ0FBfayWLp 100 | a71e8U8YDrbtu0aWDj+/eBOu+jRkYabX+3hPu9LZ5fb41T+7fmRf' | base64 -d | zlib-flate -uncompress | malutil-xor - [KEY] 101 | ``` 102 | 103 | ### Interfacing ### 104 | 105 | Utilities, decoders, and discovery scripts in this collection are designed to support single ad-hoc analysis as well as inclusion into other frameworks. After installation, the malchive should be part of your Python path. At this point accessing any of the scripts is straight forward. 106 | 107 | Here are a few examples: 108 | 109 | ```buildoutcfg 110 | ; accessing decoder modules 111 | import sys 112 | from malchive.decoders import testdecoder 113 | 114 | p = testdecoder.GetConfig(open(sys.argv[1], 'rb').read()) 115 | print('password', p.rc4_key) 116 | for c in p.callbacks: 117 | print('c2 address', c) 118 | 119 | ; accessing utilities 120 | from malchive.utilities import xor 121 | ret = xor.GenericXor(buff=b'testing', key=[0x51], count=0xff) 122 | print(ret.run_crypt()) 123 | 124 | ; accessing helpers 125 | from malchive.helpers import winfunc 126 | key = winfunc.CryptDeriveKey(b'testdatatestdata') 127 | ``` 128 | 129 | To understand more about a given module, see the associated wiki entry. 130 | 131 | ## Contributing ## 132 | 133 | Contributing to the malchive is easy, just ensure the following requirements are met: 134 | * When writing utilities, decoders, or discovery scripts, consider using the [available templates](https://github.com/mitrecnd/malchive/blob/main/malchive/extras/) or review existing code if you're not sure how to get started. 135 | * Make sure modification or contributions pass pre-commit tests. 136 | * Ensure the contribution is placed in one of the component folders. 137 | * Updated the setup file if needed with an entry. 138 | * Python3 is a must. 139 | 140 | ## Legal ## 141 | 142 | ©2021 The MITRE Corporation. ALL RIGHTS RESERVED. 143 | 144 | Approved for Public Release; Distribution Unlimited. Public Release Case Number 21-0153 145 | -------------------------------------------------------------------------------- /malchive/extras/decoder_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import argparse 18 | import pefile 19 | import logging 20 | import re 21 | import struct 22 | 23 | __version__ = "1.0.0" 24 | __author__ = "First Last" 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | # Function or class that does the heavy lifting. It is important to 29 | # consider how you expect other people to interface with the program 30 | # while you are developing it. They will likely be importing it directly 31 | # into other frameworks. 32 | # 33 | # In some cases, it might make more sense to develop as a class instead 34 | # of a single method. For example, if you have a config with a lot of 35 | # data variables you are parsing out, it might make more sense to 36 | # 'group' those. Either way, documentation is key. 37 | 38 | 39 | class GetConfig: 40 | """ 41 | Gets specific configuration IoCs from the malware. 42 | 43 | :ivar list callbacks: Example callback domains or IP address. 44 | :ivar int key: Example key used to encode callback data. 45 | :ivar int port: Example port used for callback communication. 46 | """ 47 | 48 | def __init__(self, buff): 49 | """ 50 | Initialize decoder instance. 51 | 52 | :param bytes buff: The stream of bytes to be processed. 53 | """ 54 | 55 | self.callbacks = [] 56 | self.port = 0 57 | self.key = 0 58 | self.buff = buff 59 | self.config = bytearray() 60 | 61 | try: 62 | pe = pefile.PE(data=self.buff) 63 | self.decode_config(pe) 64 | except pefile.PEFormatError: 65 | log.debug('Supplied file must be a valid PE!') 66 | 67 | if len(self.config): 68 | self.parse_config() 69 | else: 70 | log.debug('Could not find config!') 71 | 72 | def decode_config(self, pe): 73 | """ 74 | Description of what the method does. 75 | 76 | :param bytes buff: Something describing your parameters. 77 | 78 | :return: Something describing what you're returning. 79 | :rtype: dict 80 | """ 81 | 82 | config_regex = b'MALWARE CONFIG' 83 | m = re.search(config_regex, self.buff, re.DOTALL) 84 | if m: 85 | # Feel free to use debug statements liberally to help troubleshoot, 86 | # they will only show if the logger is setup to show them 87 | # (-v in main) 88 | log.debug('Getting something now...') 89 | offset = m.end() 90 | # example config is always 0x800 bytes large 91 | self.config = bytearray(self.buff[offset:offset+0x800]) 92 | 93 | if len(self.config): 94 | # As with debug, these can be used to notify/troubleshoot issues. 95 | # Only shown with -v when invoked from main. 96 | log.info('We got something!') 97 | self.parse_config() 98 | else: 99 | # Something irrecoverable happened and we cannot proceed. 100 | # Shown by default. 101 | log.error('Something bad happened!') 102 | 103 | def parse_config(self): 104 | 105 | self.port = struct.unpack('I', self.config[0x0:0x4])[0] 106 | callbacks = self.config[0x4:].rstrip(b'\x00').split(b'|') 107 | 108 | for callback in callbacks: 109 | try: 110 | self.callbacks.append(callback.decode('ascii')) 111 | except UnicodeDecodeError: 112 | # Warnings that something Isn't right the user should know. 113 | # These will show by default. 114 | log.warning('Got something I didn\'t expect') 115 | 116 | return 117 | 118 | 119 | def initialize_parser(): 120 | parser = argparse.ArgumentParser( 121 | description='Process candidate. Prints results in JSON format.') 122 | parser.add_argument('candidates', metavar='FILE', 123 | nargs='*', help='candidate file(s).') 124 | parser.add_argument('-v', '--verbose', action='store_true', 125 | default=False, 126 | help='Output additional information when processing ' 127 | '(mostly for debugging purposes).') 128 | return parser 129 | 130 | 131 | # Code supporting standalone operations. This sequence will 132 | # iterate through a list of supplied files and generate IoCs for output 133 | # in a JSONified format. Files found not to meet criteria for processing 134 | # should not impede runtime. Errors should be output to console using the 135 | # logger module. 136 | def main(): 137 | # local imports 138 | import os 139 | import json 140 | import datetime 141 | import hashlib 142 | 143 | p = initialize_parser() 144 | args = p.parse_args() 145 | 146 | root = logging.getLogger() 147 | logging.basicConfig() 148 | if args.verbose: 149 | root.setLevel(logging.DEBUG) 150 | else: 151 | root.setLevel(logging.WARNING) 152 | 153 | # Iterate through list of files in bulk. 154 | for filename in args.candidates: 155 | 156 | log.info('Processing file %s...' % filename) 157 | 158 | if not os.path.isfile(filename): 159 | log.warning('Failed to find file %s' % filename) 160 | continue 161 | 162 | f = open(filename, 'rb') 163 | stream = f.read() 164 | 165 | # In some cases it helps to check if the file is a PE upfront. 166 | try: 167 | pe = pefile.PE(data=stream) 168 | timestamp = datetime.datetime.utcfromtimestamp( 169 | pe.FILE_HEADER.TimeDateStamp) 170 | except pefile.PEFormatError: 171 | log.warning('%s not a pe, skipping...' % f.name) 172 | continue 173 | 174 | d = GetConfig(stream) 175 | 176 | config_dict = { 177 | 'Compile Time': '%s UTC' % timestamp, 178 | 'MD5': hashlib.md5(stream).hexdigest(), 179 | 'Callbacks': d.callbacks, 180 | } 181 | 182 | try: 183 | print(json.dumps(config_dict, indent=4, sort_keys=False)) 184 | except UnicodeDecodeError: 185 | log.warning('There was a Unicode decoding error when ' 186 | 'processing %s' % os.path.basename(filename)) 187 | continue 188 | 189 | 190 | if __name__ == '__main__': 191 | main() 192 | -------------------------------------------------------------------------------- /malchive/utilities/ssl_cert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # ----------------- 18 | # 19 | import asyncio 20 | import logging 21 | import ssl 22 | import json 23 | import hashlib 24 | from malchive.helpers import discovery 25 | from async_timeout import timeout 26 | 27 | __version__ = "2.1.0" 28 | __author__ = "Marcus Eddy" 29 | __contributors__ = "Jason Batchelor" 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | class SSLInfo(discovery.Discover): 35 | 36 | def __init__(self, 37 | sni_host: str = "", 38 | *args, 39 | **kwargs): 40 | 41 | self.sni_host: str = sni_host 42 | if len(self.sni_host): 43 | log.debug('Using SNI hostname: \'%s\'' % self.sni_host) 44 | super().__init__(*args, **kwargs) 45 | 46 | async def tickle_tcp(self, co): 47 | """ 48 | Probe the server for data and make a determination based on 49 | send/recv traffic. 50 | """ 51 | 52 | log.info('Checking: %s:%s' % (co.ip, co.port)) 53 | 54 | try: 55 | async with timeout(self.timeout): 56 | reader, writer = await asyncio.open_connection(co.ip, co.port) 57 | context = ssl.create_default_context() 58 | context.check_hostname = False 59 | context.verify_mode = ssl.CERT_NONE 60 | context.set_ciphers('DEFAULT:@SECLEVEL=1') 61 | await writer.start_tls( 62 | context, 63 | server_hostname=self.sni_host 64 | ) 65 | 66 | obj = writer.get_extra_info('ssl_object') 67 | der_cert_bin = obj.getpeercert(True) 68 | 69 | meta = { 70 | 'Query': '%s:%s' % (co.ip, co.port), 71 | 'SNI': '%s' % self.sni_host, 72 | } 73 | 74 | co.details = meta 75 | co.payload = der_cert_bin 76 | co.success = True 77 | 78 | except asyncio.TimeoutError: 79 | log.debug('Failure: TimeoutError: %s:%s:%s' % 80 | (co.protocol, co.ip, co.port)) 81 | except ConnectionResetError: 82 | log.debug('Failure: ConnectionResetError: %s:%s:%s' % 83 | (co.protocol, co.ip, co.port)) 84 | except Exception as e: 85 | log.debug('General failure: %s: %s:%s:%s' % 86 | (str(e), co.protocol, co.ip, co.port)) 87 | 88 | return co 89 | 90 | 91 | def initialize_parser(): 92 | import argparse 93 | 94 | parser = argparse.ArgumentParser( 95 | description='Retrieve hashes of SSL certificates.') 96 | 97 | parser.add_argument('-i', '--ipaddress', nargs='*', 98 | help='One or more IP addresses to scan. To provide a ' 99 | 'range, use CIDR notation.') 100 | parser.add_argument('-d', '--domain', nargs='*', 101 | help='One or more domains to scan.') 102 | parser.add_argument('-p', '--port', nargs='*', 103 | help='Range of ports to test per host. May be ' 104 | 'specified as a series of integers (80 443 8080)' 105 | ', a range (80-9000), or both.') 106 | parser.add_argument('--timeout', nargs='?', default=5, type=int, 107 | help='How long (in seconds) to wait before timeout ' 108 | 'for each connection attempt. Defaults to five ' 109 | 'seconds.') 110 | parser.add_argument('--sni-host', nargs='?', type=str, 111 | default="", 112 | help='Apply the given domain as an SNI parameter ' 113 | 'for all domain related requests (when given). ' 114 | 'The provided domain is used when initiating ' 115 | 'a TLS/SSL handshake. This is required for some ' 116 | 'providers hosting multiple TLS enabled IPs off a ' 117 | 'single domain.') 118 | parser.add_argument('-w', '--write', action='store_true', 119 | default=False, 120 | help='Write retrieved DER to disk ' 121 | 'using [MD5.der] as the filename.') 122 | parser.add_argument('-t', '--target-directory', type=str, default='.', 123 | help='Target directory to write files. Defaults to ' 124 | 'executing directory.') 125 | parser.add_argument('-v', '--verbose', action='store_true', 126 | default=False, 127 | help='Output additional information when processing ' 128 | '(mostly for debugging purposes).') 129 | 130 | return parser 131 | 132 | 133 | def main(): 134 | import sys 135 | import os 136 | 137 | p = initialize_parser() 138 | args = p.parse_args() 139 | 140 | root = logging.getLogger() 141 | logging.basicConfig() 142 | if args.verbose: 143 | root.setLevel(logging.DEBUG) 144 | else: 145 | root.setLevel(logging.WARNING) 146 | 147 | directory = args.target_directory 148 | if directory != '.': 149 | if not os.path.isdir(directory): 150 | log.error('Could not create %s. Exiting...' 151 | % directory) 152 | sys.exit(2) 153 | 154 | d = SSLInfo( 155 | sni_host=args.sni_host, 156 | ips=args.ipaddress, 157 | ports=args.port, 158 | domains=args.domain, 159 | timeout=args.timeout, 160 | protocols=['tcp'], 161 | ) 162 | 163 | comms = discovery.create_comms( 164 | d.protocols, 165 | d.ips, 166 | d.ports 167 | ) 168 | 169 | results = [] 170 | asyncio.run(d.run(comms, results)) 171 | 172 | for co in results: 173 | if len(co.payload) > 0: 174 | thumb_sha256 = hashlib.sha256(co.payload).hexdigest() 175 | if co.success: 176 | success = { 177 | 'Cert Sha256': thumb_sha256, 178 | 'Details': co.details 179 | } 180 | 181 | jo = json.dumps(success, indent=4) 182 | print(jo) 183 | if args.write: 184 | fname = '%s/%s.der' % (directory, thumb_sha256) 185 | with open(fname, 'wb') as f: 186 | f.write(co.payload) 187 | log.info('%s written to disk!' % fname) 188 | 189 | 190 | if __name__ == '__main__': 191 | main() 192 | -------------------------------------------------------------------------------- /malchive/active_discovery/cobaltstrike_beacon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import aiohttp 4 | import asyncio 5 | import logging 6 | import hashlib 7 | from malchive.helpers import discovery 8 | from malchive.decoders import cobaltstrike_payload 9 | from string import ascii_letters, digits 10 | from random import sample, randint 11 | from async_timeout import timeout 12 | 13 | __version__ = "1.0.0" 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | # very similar to Meterpreter script, just validating CS beacon payloads... 19 | class CobaltStrikeBeacon(discovery.Discover): 20 | 21 | max_payload_size = 300000 22 | 23 | def __init__(self, 24 | *args, 25 | **kwargs): 26 | self.location = self.gen_msf_uri() 27 | super().__init__(*args, **kwargs) 28 | 29 | async def tickle_http(self, co): 30 | 31 | beacon = bytearray() 32 | url = f"{co.protocol}://{co.ip}:{co.port}/{self.location}" 33 | 34 | headers = { 35 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 36 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 37 | 'Chrome/79.0.3945.117 Safari/537.36' 38 | } 39 | 40 | log.info('Making attempt using: %s' % url) 41 | 42 | conn = aiohttp.TCPConnector() 43 | async with aiohttp.ClientSession(connector=conn) as session: 44 | try: 45 | async with timeout(self.timeout): 46 | beacon = await self.get_beacon( 47 | url, 48 | headers, 49 | session 50 | ) 51 | except aiohttp.ClientConnectionError as e: 52 | log.debug('Failure: ClientConnectionError %s: %s:%s:%s' % 53 | (str(e), co.protocol, co.ip, co.port)) 54 | 55 | except asyncio.TimeoutError: 56 | log.debug('Failure: TimeoutError: %s:%s:%s' % 57 | (co.protocol, co.ip, co.port)) 58 | 59 | except aiohttp.TooManyRedirects: 60 | log.debug('Failure: TooManyRedirects: %s:%s:%s' % 61 | (co.protocol, co.ip, co.port)) 62 | 63 | except Exception as e: 64 | log.debug('General failure: %s: %s:%s:%s' % 65 | (str(e), co.protocol, co.ip, co.port)) 66 | 67 | if len(beacon): 68 | co.payload = beacon 69 | co.success = True 70 | 71 | return co 72 | 73 | async def get_beacon(self, url, headers, session): 74 | 75 | decoded = bytearray() 76 | async with session.get(url, 77 | headers=headers, 78 | read_bufsize=self.max_payload_size, 79 | verify_ssl=False 80 | ) as resp: 81 | 82 | if 'content-length' not in resp.headers: 83 | return decoded 84 | 85 | if 'content-type' not in resp.headers: 86 | return decoded 87 | 88 | if int(resp.headers['content-length']) > self.max_payload_size: 89 | return decoded 90 | 91 | if resp.status == 200: 92 | payload = await resp.content.read(0x10) 93 | 94 | if not payload.isascii() and len(payload) != 0: 95 | payload += await resp.content.read() 96 | else: 97 | return decoded 98 | 99 | try: 100 | decoded = self.check_payload(payload) 101 | except Exception as e: 102 | log.warning('Failure encountered when checking payload ' 103 | 'with %s. Error: %s' % (url, str(e))) 104 | return decoded 105 | 106 | def checksum8(self, string: str) -> int: 107 | """ 108 | Calculate the 8-bit checksum for the given string. Taken from: 109 | https://www.veil-framework.com/veil-framework-2-4-0-reverse-http/ 110 | """ 111 | return sum([ord(char) for char in string]) % 0x100 112 | 113 | def gen_msf_uri(self, MIN_URI_LENGTH=26) -> str: 114 | """ 115 | Generate a MSF compatible URI. Taken from: 116 | https://www.veil-framework.com/veil-framework-2-4-0-reverse-http/ 117 | """ 118 | charset = ascii_letters + digits 119 | msf_uri = '' 120 | 121 | while True: 122 | uri = ''.join(sample(charset, MIN_URI_LENGTH)) 123 | r = uri[randint(0, len(uri)-1)] 124 | 125 | # URI_CHECKSUM_INITW (Windows) 126 | if self.checksum8(uri + r) == 92: 127 | msf_uri = uri + r 128 | break 129 | 130 | return msf_uri 131 | 132 | def check_payload(self, payload): 133 | 134 | d = cobaltstrike_payload.ExtractIt(payload) 135 | return d.decoded_payload 136 | 137 | def write_payload(self, payload, directory='.'): 138 | """ 139 | Write the retrieved payload to disk. 140 | """ 141 | 142 | md5 = hashlib.md5(payload).hexdigest() 143 | fname = '%s/%s.beacon' % (directory, md5) 144 | with open(fname, 'wb') as f: 145 | f.write(payload) 146 | log.info('%s written to disk!' % fname) 147 | 148 | 149 | def initialize_parser(): 150 | parser = discovery.generic_args() 151 | parser.add_argument('-w', '--write', action='store_true', 152 | default=False, 153 | help='Write retrieved meterpreter payloads to disk ' 154 | 'using [MD5.beacon] as the filename.') 155 | parser.add_argument('-t', '--target-directory', type=str, default='.', 156 | help='Target directory to write files. Defaults to ' 157 | 'executing directory.') 158 | return parser 159 | 160 | 161 | def main(): 162 | 163 | import os 164 | import sys 165 | 166 | p = initialize_parser() 167 | args = p.parse_args() 168 | 169 | root = logging.getLogger() 170 | logging.basicConfig() 171 | if args.verbose: 172 | root.setLevel(logging.DEBUG) 173 | else: 174 | root.setLevel(logging.WARNING) 175 | 176 | directory = args.target_directory 177 | if directory != '.': 178 | if not os.path.isdir(directory): 179 | log.error('Could not create %s. Exiting...' 180 | % directory) 181 | sys.exit(2) 182 | 183 | d = CobaltStrikeBeacon( 184 | ips=args.ipaddress, 185 | ports=args.port, 186 | domains=args.domain, 187 | timeout=args.timeout, 188 | protocols=args.protocol, 189 | ) 190 | 191 | comms = discovery.create_comms( 192 | d.protocols, 193 | d.ips, 194 | d.ports 195 | ) 196 | 197 | results = [] 198 | asyncio.run(d.run(comms, results)) 199 | 200 | for co in results: 201 | if co.success: 202 | print('Successfully discovered candidate! [%s] %s:%s' % 203 | (co.protocol, co.ip, co.port)) 204 | if args.write and len(co.payload) != 0: 205 | d.write_payload(co.payload, directory) 206 | 207 | 208 | if __name__ == '__main__': 209 | main() 210 | -------------------------------------------------------------------------------- /malchive/utilities/peresources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import sys 19 | import logging 20 | import argparse 21 | import pefile 22 | import hashlib 23 | from tabulate import tabulate 24 | 25 | __version__ = "1.1.0" 26 | __author__ = "Jason Batchelor" 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | resource_types = { 31 | 1: 'RT_CURSOR', 32 | 2: 'RT_BITMAP', 33 | 3: 'RT_ICON', 34 | 4: 'RT_MENU', 35 | 5: 'RT_DIALOG', 36 | 6: 'RT_STRING', 37 | 7: 'RT_FONTDIR', 38 | 8: 'RT_FONT', 39 | 9: 'RT_ACCELERATOR', 40 | 10: 'RT_RCDATA', 41 | 11: 'RT_MESSAGETABLE', 42 | 12: 'RT_GROUP_CURSOR', 43 | 14: 'RT_GROUP_ICON', 44 | 16: 'RT_VERSION', 45 | 17: 'RT_DLGINCLUDE', 46 | 19: 'RT_PLUGPLAY', 47 | 20: 'RT_VXD', 48 | 21: 'RT_ANICURSOR', 49 | 22: 'RT_ANIICON', 50 | 23: 'RT_HTML', 51 | 24: 'RT_MANIFEST', 52 | } 53 | 54 | 55 | def get_pe_resources(pe): 56 | """ 57 | Get resources from a Windows PE file. 58 | 59 | :param pefile.PE pe: A pe object passed from the pefile project. 60 | 61 | :return: List of tuples containing resource directory name, id, 62 | entry name, entry id, size, and binary data. 63 | :rtype: list 64 | """ 65 | 66 | rsrcs = [] 67 | 68 | for rsrc in pe.DIRECTORY_ENTRY_RESOURCE.entries: 69 | res_dir_name = '%s' % rsrc.name if rsrc.name is not None else '-' 70 | res_id = resource_types.get(rsrc.id, '-') 71 | 72 | entry_id, entry_name = ['-'] * 2 73 | entry_data = bytearray() 74 | entry_size = 0 75 | 76 | if len(rsrc.directory.entries) == 0: 77 | rsrcs.append((res_dir_name, res_id, entry_name, 78 | entry_id, entry_size, entry_data)) 79 | 80 | for entry in rsrc.directory.entries: 81 | offset = entry.directory.entries[0].data.struct.OffsetToData 82 | entry_size = entry.directory.entries[0].data.struct.Size 83 | entry_data = bytearray(pe.get_memory_mapped_image() 84 | [offset:offset + entry_size]) 85 | entry_id = '%s' % entry.id if entry.id is not None else '-' 86 | entry_name = '%s' % entry.name if entry.name is not None else '-' 87 | rsrcs.append((res_dir_name, res_id, entry_name, 88 | entry_id, entry_size, entry_data)) 89 | 90 | return rsrcs 91 | 92 | 93 | def initialize_parser(): 94 | parser = argparse.ArgumentParser( 95 | description='Dump embedded resources to disk. ' 96 | 'Entries with \'-\' are unlabeled.') 97 | parser.add_argument('infile', metavar='FILE', nargs='*', 98 | help='Full path to the file(s) to be processed.') 99 | parser.add_argument('-t', '--table', action='store_true', 100 | help='Show results in table format instead of JSON.') 101 | parser.add_argument('-w', '--write', action='store_true', 102 | help='Write the file(s) to disk. Creates a directory ' 103 | 'with SHA256 hash and \'_rsrc\' prefix of the ' 104 | 'provided sample and extracts payloads there.') 105 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 106 | help='Output additional information when processing ' 107 | '(mostly for debugging purposes).') 108 | 109 | return parser 110 | 111 | 112 | def main(): 113 | 114 | import json 115 | 116 | p = initialize_parser() 117 | args = p.parse_args() 118 | 119 | root = logging.getLogger() 120 | logging.basicConfig() 121 | if args.verbose: 122 | root.setLevel(logging.DEBUG) 123 | else: 124 | root.setLevel(logging.WARNING) 125 | 126 | if len(args.infile) == 0: 127 | p.print_help() 128 | sys.exit(2) 129 | 130 | for fname in args.infile: 131 | 132 | basename = os.path.basename(fname) 133 | 134 | if not os.path.isfile(fname): 135 | log.warning('Failed to find file %s. Skipping...' % fname) 136 | continue 137 | 138 | with open(fname, 'rb') as f: 139 | stream = f.read() 140 | 141 | try: 142 | pe = pefile.PE(data=stream) 143 | except pefile.PEFormatError: 144 | log.warning('%s not a pe, skipping...' % basename) 145 | continue 146 | 147 | try: 148 | resources = get_pe_resources(pe) 149 | except (AttributeError, ValueError) as e: 150 | log.error('%s' % e) 151 | continue 152 | 153 | if len(resources) == 0: 154 | print('No resources found in %s...' % basename) 155 | continue 156 | 157 | results = [] 158 | target_dir = '%s_rsrc' % hashlib.sha256(stream).hexdigest() 159 | if args.write: 160 | try: 161 | os.makedirs(target_dir, exist_ok=True) 162 | except OSError: 163 | if not os.path.isdir(target_dir): 164 | log.error('Could not create %s' % target_dir) 165 | 166 | for res_dir, res_type, e_name, e_id, e_size, e_data in resources: 167 | sha256 = hashlib.sha256(e_data).hexdigest() 168 | 169 | if args.table: 170 | results.append((sha256, res_dir, res_type, e_name, e_id, e_size)) 171 | else: 172 | results.append({ 173 | 'SHA256': sha256, 174 | 'Directory': res_dir, 175 | 'Type': res_type, 176 | 'Name': e_name, 177 | 'ID': e_id, 178 | 'Size': e_size, 179 | }) 180 | 181 | name = '%s/%s.bin' % (target_dir, sha256) 182 | if args.write: 183 | with open(name, 'wb+') as f: 184 | f.write(e_data) 185 | log.info('%s written to disk.' % name) 186 | 187 | if args.table and len(results) > 0: 188 | print('Found %s resources in %s...' % (len(resources), basename)) 189 | print(tabulate(results, 190 | headers=["SHA256", "Directory", "Type", 191 | "Name", "ID", "Size"], 192 | tablefmt="grid")) 193 | elif len(results) > 0: 194 | file_results = { 195 | 'file': basename, 196 | 'count': len(resources), 197 | 'results': results 198 | } 199 | 200 | try: 201 | print(json.dumps(file_results, indent=4, sort_keys=False)) 202 | except UnicodeDecodeError: 203 | log.warning('There was a Unicode decoding error when processing %s' 204 | % basename) 205 | continue 206 | 207 | 208 | if __name__ == '__main__': 209 | main() 210 | -------------------------------------------------------------------------------- /malchive/decoders/sunburst_dga.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import argparse 18 | import logging 19 | import binascii 20 | import base64 21 | import zlib 22 | 23 | __version__ = "1.0.0" 24 | __author__ = "Jason Batchelor" 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | GUID_SIZE = 16 29 | 30 | 31 | def sunburst_unzip(s): 32 | b64 = base64.b64decode(s) 33 | return zlib.decompress(b64, -15) 34 | 35 | 36 | # rq3gsalt6u1iyfzop572d49bnx8cvmkewhj 37 | sub_table = \ 38 | sunburst_unzip(b'Kyo0Ti9OzCkxKzXMrEyryi8wNTdKMbFMyquwSC7LzU4tz8gCAA==') 39 | 40 | # 0_-. 41 | sub_table2 = \ 42 | sunburst_unzip(b'M4jX1QMA') 43 | 44 | # ph2eifo3n5utg1j8d94qrvbmk0sal76c 45 | sub_table3 = \ 46 | sunburst_unzip(b'K8gwSs1MyzfOMy0tSTfMskixNCksKkvKzTYoTswxN0sGAA==') 47 | 48 | 49 | # implementation guidance 50 | # https://blog.prevasio.com/2020/12/sunburst-backdoor-part-ii-dga-list-of.html 51 | def sunburst_cipher_decode(data): 52 | encoded = bytearray(data.encode('ascii')) 53 | decoded = bytearray() 54 | 55 | i = 0 56 | while i < len(encoded): 57 | n = sub_table2.find(encoded[i]) 58 | if n < 0: 59 | decoded.append(sub_table[(sub_table.find(encoded[i]) - 4) 60 | % len(sub_table)]) 61 | elif i + 1 < len(encoded): 62 | n = sub_table.find(encoded[i + 1]) 63 | decoded.append(sub_table2[n % 4]) 64 | i += 1 65 | i += 1 66 | return decoded 67 | 68 | 69 | def sunburst_cipher_decode_base32(data): 70 | 71 | encoded = bytearray(data.encode('ascii')) 72 | decoded = bytearray() 73 | decoded_len = len(encoded) * 5 // 8 74 | 75 | bit_buff = (sub_table3.find(encoded[0]) | sub_table3.find(encoded[1]) << 5) 76 | if len(encoded) < 3: 77 | decoded.append(bit_buff & 0xff) 78 | return decoded 79 | 80 | bits_in_buff = 10 81 | curr_idx = 2 82 | for i in range(0, decoded_len): 83 | decoded.append(bit_buff & 0xff) 84 | bit_buff >>= 8 85 | bits_in_buff -= 8 86 | while bits_in_buff < 8 and curr_idx < len(encoded): 87 | bit_buff |= (sub_table3.find(encoded[curr_idx]) << bits_in_buff) 88 | curr_idx += 1 89 | bits_in_buff += 5 90 | return decoded 91 | 92 | 93 | def get_decoded_guid(data): 94 | 95 | decoded = sunburst_cipher_decode_base32(data) 96 | 97 | key = decoded[0] 98 | for i in range(1, len(decoded)): 99 | decoded[i] ^= key 100 | 101 | guid = binascii.hexlify(decoded[1:(GUID_SIZE//2) + 1]) 102 | return guid 103 | 104 | 105 | def process_dga_file(data, fname): 106 | 107 | results = [] 108 | for line in data: 109 | data = line.rstrip().split(".")[0] 110 | if len(data) > GUID_SIZE: 111 | guid = get_decoded_guid(data[:16]) 112 | candidate = data[16:] 113 | 114 | if candidate.startswith('00'): 115 | decoded = sunburst_cipher_decode_base32(candidate[2:]) 116 | else: 117 | decoded = sunburst_cipher_decode(candidate) 118 | results.append(( 119 | fname, 120 | line.rstrip(), 121 | guid.decode('ascii').upper(), 122 | decoded.decode('ascii'))) 123 | return results 124 | 125 | 126 | def initialize_parser(): 127 | parser = argparse.ArgumentParser( 128 | description='Process list of SUNBURST DGA domains. The list must be' 129 | ' full domain queries separated by newlines. Default ' 130 | 'output in csv.') 131 | parser.add_argument('candidates', 132 | metavar='FILE', 133 | nargs='*', 134 | help='candidate file(s).') 135 | parser.add_argument('-v', '--verbose', 136 | action='store_true', 137 | default=False, 138 | help='Output additional information when processing ' 139 | '(mostly for debugging purposes).') 140 | parser.add_argument('-t', '--table', 141 | action='store_true', 142 | default=False, 143 | help='Show results in a table.') 144 | parser.add_argument('-u', '--unique', 145 | action='store_true', 146 | default=False, 147 | help='Show only results unique to decoded GUID and' 148 | ' try to assemble original name.') 149 | 150 | return parser 151 | 152 | 153 | def main(): 154 | 155 | import os 156 | from tabulate import tabulate 157 | import operator 158 | 159 | p = initialize_parser() 160 | args = p.parse_args() 161 | 162 | root = logging.getLogger() 163 | logging.basicConfig() 164 | if args.verbose: 165 | root.setLevel(logging.DEBUG) 166 | else: 167 | root.setLevel(logging.WARNING) 168 | 169 | dga_decode = [] 170 | collapse = {} 171 | for filename in args.candidates: 172 | 173 | log.info('Processing file %s...' % filename) 174 | basename = os.path.basename(filename) 175 | 176 | if not os.path.isfile(filename): 177 | log.warning('Failed to find file %s' % filename) 178 | continue 179 | 180 | f = open(filename, 'r') 181 | stream = f.readlines() 182 | 183 | dga_decode.extend(process_dga_file(stream, basename)) 184 | 185 | if len(dga_decode) == 0: 186 | print('No decodes found!') 187 | 188 | # remove duplicates 189 | dga_decode = [t for t in (set(tuple(i) for i in dga_decode))] 190 | # sort by guid 191 | dga_decode = sorted(dga_decode, key=operator.itemgetter(2)) 192 | 193 | if args.unique: 194 | unique = [] 195 | for filename, encoded, guid, decoded in dga_decode: 196 | unique.append((guid, decoded)) 197 | unique = [t for t in (set(tuple(i) for i in unique))] 198 | 199 | for guid, decoded in unique: 200 | if guid not in collapse.keys(): 201 | collapse[guid] = decoded 202 | else: 203 | # try to make a good guess on where the decoded data 204 | # should be placed based on the size 205 | if len(decoded) > len(collapse[guid]): 206 | collapse[guid] = decoded + collapse[guid] 207 | else: 208 | collapse[guid] += decoded 209 | 210 | dga_decode = [(k, v) for k, v in collapse.items()] 211 | 212 | if args.table: 213 | if args.unique: 214 | print(tabulate(dga_decode, 215 | headers=["Guid", "Decoded"], 216 | tablefmt="grid")) 217 | else: 218 | print(tabulate(dga_decode, 219 | headers=["Filename", "Encoded", "Guid", "Decoded"], 220 | tablefmt="grid")) 221 | else: 222 | if args.unique: 223 | for guid, decoded in dga_decode: 224 | print('%s,%s' % (guid, decoded)) 225 | else: 226 | for filename, encoded, guid, decoded in dga_decode: 227 | print('%s,%s,%s,%s' % (filename, encoded, guid, decoded)) 228 | 229 | 230 | if __name__ == '__main__': 231 | main() 232 | -------------------------------------------------------------------------------- /malchive/helpers/apLib.py: -------------------------------------------------------------------------------- 1 | # Kabopan - Readable Algorithms. Public Domain, 2007-2009 2 | """ 3 | aPLib, LZSS based lossless compression algorithm 4 | Jorgen Ibsen U{http://www.ibsensoftware.com} 5 | Original: http://code.google.com/p/kabopan/source/browse/trunk/kbp/comp/aplib.py 6 | """ 7 | # flake8: noqa 8 | import sys 9 | import io 10 | 11 | 12 | class BitsDecompress: 13 | """bit machine for variable-sized auto-reloading tag decompression""" 14 | 15 | def __init__(self, data, tag_size, verbose=True): 16 | self.__current_bit = 0 # The count of bits available to use in the tag 17 | self.__tag = None # The tag is a bitstream dispersed through the file and read in chunks. 18 | # This is the current chunk, shifted so the MSB is the next bit. 19 | self.__tag_size = tag_size # Number of bytes per bitstream chunk, 1 by default 20 | self.__in = data # The stream 21 | self.out = bytearray() 22 | self.max_offset = 0 23 | self.max_match_length = 0 24 | self.bits_count = 0 25 | self.bytes_count = 0 26 | self.verbose = verbose 27 | 28 | def read_bit(self): 29 | """read next bit from the stream, reloads the tag if necessary""" 30 | 31 | if self.__current_bit != 0: 32 | # Move to the next bit 33 | self.__current_bit -= 1 34 | else: 35 | # Select the MSB 36 | self.__current_bit = (self.__tag_size * 8) - 1 37 | # Read new data 38 | self.__tag = self.read_byte() 39 | self.bytes_count -= 1 40 | for i in range(self.__tag_size - 1): 41 | self.__tag += self.read_byte() << (8 * (i + 1)) 42 | 43 | # Then extract the bit in question 44 | bit = (self.__tag >> ((self.__tag_size * 8) - 1)) & 0x01 45 | # And shift it out of the tag 46 | self.__tag <<= 1 47 | self.bits_count += 1 48 | return bit 49 | 50 | def read_byte(self): 51 | """read next byte from the stream""" 52 | result = self.__in.read(1)[0] 53 | self.bytes_count += 1 54 | return result 55 | 56 | def read_fixed_number(self, num_bits, init=0): 57 | """reads a fixed bit-length number""" 58 | result = init 59 | for i in range(num_bits): 60 | result = (result << 1) + self.read_bit() 61 | return result 62 | 63 | def read_variable_number(self): 64 | """return a variable bit-length number x, x >= 2 65 | reads a bit until the next bit in the pair is not set""" 66 | result = 1 67 | result = (result << 1) + self.read_bit() 68 | while self.read_bit(): 69 | result = (result << 1) + self.read_bit() 70 | return result 71 | 72 | def read_set_bits(self, max_bits, set_value=1): 73 | """read bits as long as their set or a maximum is reached""" 74 | # Reads consecutive set bits from the bitstream, up to max_bits or until a zero is encountered. 75 | # Returns the number of set bits read. 76 | result = 0 77 | while result < max_bits and self.read_bit() == set_value: 78 | result += 1 79 | return result 80 | 81 | def back_copy(self, offset, length=1): 82 | s = "offset %d, length %d:" % (offset, length) 83 | for i in range(length): 84 | b = self.out[-offset] 85 | s += " %02x" % b 86 | self.out.append(b) 87 | self.print(s) 88 | self.max_offset = max(self.max_offset, offset) 89 | self.max_match_length = max(self.max_match_length, length) 90 | return 91 | 92 | def read_literal(self, value=None): 93 | if value is None: 94 | b = self.read_byte() 95 | self.print("%02x" % b) 96 | self.out.append(b) 97 | else: 98 | self.print("%02x" % value) 99 | self.out.append(value) 100 | return False 101 | 102 | def print(self, *args, **kwargs): 103 | if self.verbose: 104 | print(*args, **kwargs) 105 | 106 | 107 | class Decompress(BitsDecompress): 108 | def __init__(self, data, verbose=True): 109 | BitsDecompress.__init__(self, data, tag_size=1, verbose=verbose) 110 | self.__pair = True # paired sequence 111 | self.__last_offset = 0 112 | self.__functions = [ 113 | self.__literal, # 0 = literal 114 | self.__block, # 1 = block 115 | self.__short_block, # 2 = short block 116 | self.__single_byte] # 3 = single byte 117 | return 118 | 119 | def __literal(self): 120 | self.print("Literal: ", end="") 121 | self.read_literal() 122 | self.__pair = True 123 | return False 124 | 125 | def __block(self): 126 | b = self.read_variable_number() - 2 127 | if b == 0 and self.__pair: # reuse the same offset 128 | offset = self.__last_offset 129 | length = self.read_variable_number() # 2- 130 | self.print("Block with reused ", end="") 131 | else: 132 | if self.__pair: 133 | b -= 1 134 | offset = b * 256 + self.read_byte() 135 | length = self.read_variable_number() # 2- 136 | length += self.__length_delta(offset) 137 | self.print("Block with encoded ", end="") 138 | self.__last_offset = offset 139 | self.back_copy(offset, length) 140 | self.__pair = False 141 | return False 142 | 143 | @staticmethod 144 | def __length_delta(offset): 145 | if offset < 0x80 or 0x7D00 <= offset: 146 | return 2 147 | elif 0x500 <= offset: 148 | return 1 149 | return 0 150 | 151 | def __short_block(self): 152 | b = self.read_byte() 153 | if b <= 1: # likely 0 154 | self.print("Short block offset %d: EOF" % b) 155 | return True 156 | length = 2 + (b & 0x01) # 2-3 157 | offset = b >> 1 # 1-127 158 | self.print("Short block ", end="") 159 | self.back_copy(offset, length) 160 | self.__last_offset = offset 161 | self.__pair = False 162 | return False 163 | 164 | def __single_byte(self): 165 | offset = self.read_fixed_number(4) # 0-15 166 | if offset: 167 | self.print("Single byte ", end="") 168 | self.back_copy(offset) 169 | else: 170 | self.print("Single byte zero: ", end="") 171 | self.read_literal(0) 172 | self.__pair = True 173 | return False 174 | 175 | def do(self): 176 | """returns decompressed buffer and consumed bytes counter""" 177 | # First byte is a literal 178 | self.print("Initial literal: ", end="") 179 | self.read_literal() 180 | while True: 181 | # Read the gamma-coded (?) bitstream and then execute the relevant decoder based on what's found 182 | if self.__functions[self.read_set_bits(3)](): 183 | break 184 | return self.out 185 | 186 | 187 | if __name__ == "__main__": 188 | input_file = open(sys.argv[1], "rb") 189 | decompressor = Decompress(input_file) 190 | decompressed = decompressor.do() 191 | output_file = open(sys.argv[1] + ".out", "wb") 192 | output_file.write(decompressed) 193 | output_file.close() 194 | input_file.close() 195 | print("Max backref distance %d, max backref length %d" % ( 196 | decompressor.max_offset, decompressor.max_match_length)) 197 | print("%d bits (= %d bytes) + %d bytes data" % ( 198 | decompressor.bits_count, 199 | decompressor.bits_count / 8, 200 | decompressor.bytes_count)) 201 | compressed_size = decompressor.bits_count / 8 + decompressor.bytes_count 202 | decompressed_size = len(decompressed) 203 | ratio = (decompressed_size - compressed_size) / decompressed_size 204 | print("Total: %d bytes decompressed to %d bytes (%.2f%% compression)" % ( 205 | compressed_size, decompressed_size, ratio * 100)) 206 | -------------------------------------------------------------------------------- /malchive/utilities/brute_xor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import logging 4 | import binascii 5 | import argparse 6 | from operator import itemgetter 7 | from tabulate import tabulate 8 | from malchive.helpers.crypt_plaintexts import plaintexts 9 | 10 | __version__ = "1.0.0" 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def hamming_distance(x, y): 16 | 17 | xor_bytes = bytearray() 18 | for i in range(0, len(x)): 19 | xor_bytes.append(x[i] ^ y[i]) 20 | bin_str = ''.join(format(byte, '08b') for byte in xor_bytes) 21 | return bin_str.count('1') 22 | 23 | 24 | def get_key_sizes(buff, max_key_size=25): 25 | 26 | results = [] 27 | top_n_avg_hd = [] 28 | for key_size in range(2, max_key_size): 29 | t, avg_hd, n_avg_hd = 0, 0, 0 30 | c = 1 31 | for i in range(0, len(buff), key_size): 32 | if i + (key_size * 2) > len(buff): 33 | break 34 | s1 = buff[i:i + key_size] 35 | s2 = buff[i + key_size:i + (key_size * 2)] 36 | c += 1 37 | t += hamming_distance(s1, s2) 38 | avg_hd = t / c 39 | n_avg_hd = avg_hd / key_size 40 | results.append((key_size, avg_hd, n_avg_hd)) 41 | 42 | log.info('Top Normalized Average Hamming Distances...') 43 | for k, hd, nhd in sorted(results, key=itemgetter(2))[:5]: 44 | log.info("Key Size: {:3d}\tAvg HD: {:5.2f}" 45 | "\tNAvg HD: {:f}".format(k, hd, nhd)) 46 | top_n_avg_hd.append(k) 47 | return top_n_avg_hd 48 | 49 | 50 | def check_repeat(x, k): 51 | 52 | if k*4 < len(x): 53 | # for small key sizes, do some 54 | # extra checks 55 | return x[:k] == x[k:k*2] and \ 56 | x[k:k*2] == x[k*2:k*3] and \ 57 | x[k*2:k*3] == x[k*3:k*4] 58 | elif k*2 < len(x): 59 | return x[:k] == x[k:k*2] 60 | elif k < len(x): 61 | # if buffer is larger than key*2, 62 | # still try to compare the difference 63 | c = k*2 - len(x) 64 | return x[:k-c] == x[k:k*2-c] 65 | 66 | # candidate key needs to be 67 | # bigger than the plaintext 68 | return False 69 | 70 | 71 | def derive_key(crypt_offset, key_size, key_data): 72 | 73 | key = bytearray() 74 | pos = key_size - (crypt_offset % key_size) 75 | key = key_data[pos:key_size] + key_data[:pos] 76 | if len(key) != key_size: 77 | log.info('Failed to derive all key bytes') 78 | 79 | # check if key is repeated 80 | # happens for key sizes that are multiples 81 | # of the true size 82 | temp = (key + key).find(key, 1, -1) 83 | if temp != -1: 84 | key = key[:temp] 85 | return key 86 | 87 | 88 | def brute_key(buff, pt, key_sizes): 89 | 90 | key = bytearray() 91 | pl = len(pt) 92 | 93 | # crypt derivation 94 | for d in range(0, len(pt)): 95 | chunks = [buff[i+d:i+d+pl] 96 | for i in range(0, len(buff), pl) 97 | if len(buff[i+d:i+d+pl]) == pl] 98 | 99 | c = 0 100 | for chunk in chunks: 101 | offset = c * pl + d 102 | x = bytearray() 103 | for b in range(0, len(chunk)): 104 | x.append(chunk[b] ^ pt[b % len(pt)]) 105 | for k in key_sizes: 106 | if check_repeat(x, k): 107 | key = derive_key(offset, k, x) 108 | return key 109 | c += 1 110 | 111 | return key 112 | 113 | 114 | def hunt_xor_key(buff, 115 | offset=0, 116 | sample_size=1024, 117 | myplaintexts=plaintexts): 118 | 119 | results = [] 120 | 121 | if offset > len(buff) or offset < 0: 122 | raise IndexError('Invalid start position supplied. Exceeds ' 123 | 'range of supplied buffer.') 124 | 125 | total_size = offset + sample_size 126 | if total_size > len(buff) or total_size < offset: 127 | raise IndexError('Invalid size position supplied. ' 128 | 'Exceeds range of supplied parameters.') 129 | 130 | sample_space = buff[offset:offset + sample_size] 131 | key_sizes = get_key_sizes(sample_space) 132 | for name, pts in myplaintexts.items(): 133 | for pt in pts: 134 | key = brute_key(buff[offset:], pt, key_sizes) 135 | if len(key) and key.count(b'\x00') != len(key): 136 | results.append((name, key)) 137 | break 138 | return results 139 | 140 | 141 | def autoint(x): 142 | 143 | if x.startswith('0x'): 144 | x = int(x, 16) 145 | else: 146 | x = int(x) 147 | return x 148 | 149 | 150 | def initialize_parser(): 151 | parser = argparse.ArgumentParser( 152 | description='Process candidate and attempt to brute force an' 153 | ' XOR key using hamming distance, coincidence ' 154 | ' index, and known plaintext. Prints results' 155 | ' in table format. Key is computed based on' 156 | ' the start of the offset specified.') 157 | parser.add_argument('candidates', metavar='FILE', nargs='*', 158 | help='candidate file(s).') 159 | parser.add_argument('-o', '--offset', 160 | type=autoint, 161 | default=0, 162 | help='Starting point within the supplied buffer to ' 163 | 'begin sampling the data. Should represent ' 164 | 'a rough idea where the XOR\'d data starts. ' 165 | 'Defaults to start of file.') 166 | parser.add_argument('-s', '--sample-size', 167 | type=autoint, 168 | default=1024, 169 | help='The total number of bytes to sample for ' 170 | 'candidate key size. This should comprise ' 171 | 'an area you know to be encrypted with ' 172 | 'XOR. Defaults to 1024.') 173 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 174 | help='Output additional information when processing ' 175 | '(mostly for debugging purposes).') 176 | return parser 177 | 178 | 179 | def main(): 180 | import os 181 | 182 | p = initialize_parser() 183 | args = p.parse_args() 184 | 185 | root = logging.getLogger() 186 | logging.basicConfig() 187 | if args.verbose: 188 | root.setLevel(logging.DEBUG) 189 | else: 190 | root.setLevel(logging.WARNING) 191 | 192 | results = [] 193 | # Iterate through list of files in bulk. 194 | for filename in args.candidates: 195 | 196 | log.info('Processing file %s...' % filename) 197 | entries = [] 198 | key_ascii = "-" 199 | key_hex = "-" 200 | 201 | if not os.path.isfile(filename): 202 | log.warning('Failed to find file %s' % filename) 203 | continue 204 | 205 | with open(filename, 'rb') as f: 206 | entries.extend(hunt_xor_key(f.read(), 207 | args.offset, 208 | args.sample_size)) 209 | 210 | if len(entries) == 0: 211 | log.info('No matches found for %s' % filename) 212 | continue 213 | 214 | for name, key in entries: 215 | if key.isascii(): 216 | key_ascii = key.decode('ascii') 217 | key_hex = "0x%s" % binascii.hexlify(key).decode('ascii') 218 | results.append((os.path.basename(filename), name, 219 | key_ascii, key_hex, len(key))) 220 | 221 | if len(results) > 0: 222 | print(tabulate(results, 223 | headers=["File", "Pattern", "Ascii XOR Key", 224 | "Hex XOR Key", "Size"], 225 | tablefmt="grid")) 226 | 227 | 228 | if __name__ == '__main__': 229 | main() 230 | -------------------------------------------------------------------------------- /malchive/utilities/comguidtoyara.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import re 18 | import os 19 | import sys 20 | import struct 21 | import binascii 22 | import logging 23 | import argparse 24 | import progressbar 25 | from datetime import datetime 26 | from Registry import Registry 27 | 28 | __version__ = "1.0.0" 29 | __author__ = "Jason Batchelor" 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | def iid_text_to_bin(iid): 35 | """ 36 | Process an IID and convert to a YARA compliant search string. 37 | 38 | Below describes the GUID structure used to describe an identifier 39 | for a MAPI interface: 40 | 41 | https://msdn.microsoft.com/en-us/library/office/cc815892.aspx 42 | 43 | :param str iid: Name of the IID to convert 44 | 45 | :return: bin_yara 46 | :rtype: str 47 | """ 48 | 49 | # remove begin and end brackets 50 | guid = re.sub('[{}-]', '', iid) 51 | # convert to binary representation 52 | bin_struc = struct.unpack("IHH8B", binascii.a2b_hex(guid)) 53 | bin_str = '%.8X%.4X%.4X%s' % \ 54 | (bin_struc[0], bin_struc[1], bin_struc[2], 55 | (''.join('{:02X}'.format(x) for x in bin_struc[3:]))) 56 | # create YARA compliant search string 57 | bin_yara = '{ ' + ' '.join(a + b for a, b in 58 | zip(bin_str[::2], bin_str[1::2])) + ' }' 59 | 60 | return bin_yara 61 | 62 | 63 | def enumerate_com_interfaces(reg_keys, show_bar=False): 64 | """ 65 | Iterate through registry keys and retrieve unique interface identifiers 66 | and their name. 67 | 68 | :param list reg_keys: List of registry key objects from python-registry 69 | module. 70 | :param bool show_bar: Show progressbar as subfiles are identified. 71 | :param bytes buff: File to look for subfiles. 72 | 73 | :return: com 74 | :rtype: dict 75 | """ 76 | 77 | total_iters = 0 78 | counter = 0 79 | com = {} 80 | 81 | for key in reg_keys: 82 | total_iters += len(key.subkeys()) 83 | 84 | if show_bar: 85 | print('Processing %s results...' % total_iters) 86 | bar = progressbar.ProgressBar(redirect_stdout=True, 87 | max_value=total_iters) 88 | 89 | for key in reg_keys: 90 | for subkey in key.subkeys(): 91 | for v in list(subkey.values()): 92 | 93 | # Per MS documentation, Interface names must start with the 94 | # 'I' prefix, so we limit our values here as well. 95 | # Not doing so can lead to some crazy names and conflicting 96 | # results! 97 | # https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 98 | if v.value_type() == Registry.RegSZ \ 99 | and v.name() == '(default)' \ 100 | and v.value().startswith('I'): 101 | 102 | bin_guid = iid_text_to_bin(subkey.name()) 103 | 104 | # Names with special characters/spaces are truncated 105 | stop_chars = ['_', '<', '[', ' '] 106 | index = min(v.value().find(i) 107 | if i in v.value() 108 | else 109 | len(v.value()) 110 | for i in stop_chars) 111 | value = v.value()[:index] 112 | 113 | if value not in com: 114 | com[value] = [bin_guid] 115 | 116 | elif bin_guid not in com[value]: 117 | com[value].append(bin_guid) 118 | 119 | if show_bar: 120 | bar.update(counter) 121 | counter += 1 122 | 123 | if show_bar: 124 | bar.finish() 125 | 126 | return com 127 | 128 | 129 | def initialize_parser(): 130 | parser = argparse.ArgumentParser( 131 | description="Crawls windows registry to hunt for and convert IIDs for " 132 | "COM interfaces to binary YARA signatures. The submitted " 133 | "hives must be from HKLM\\SOFTWARE. Make copies of " 134 | "these files off an active Windows OS using the command " 135 | "'reg save HKLM\\SOFTWARE hklm_sft.hiv' when running as " 136 | "administrator.") 137 | parser.add_argument('hive', metavar='FILE', nargs='*', 138 | help='Full path to the registry hive to be processed.') 139 | parser.add_argument('-o', '--output-filename', type=str, 140 | default='com_interface_ids.yara', 141 | help='Filename to write YARA signatures ' 142 | 'to (default: com_interface_ids.yara)') 143 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 144 | help='Output additional information when processing ' 145 | '(mostly for debugging purposes).') 146 | 147 | return parser 148 | 149 | 150 | def main(): 151 | p = initialize_parser() 152 | args = p.parse_args() 153 | 154 | root = logging.getLogger() 155 | logging.basicConfig() 156 | if args.verbose: 157 | root.setLevel(logging.DEBUG) 158 | else: 159 | root.setLevel(logging.WARNING) 160 | 161 | if len(args.hive) == 0: 162 | p.print_help() 163 | sys.exit(2) 164 | 165 | keys = [] 166 | for hive in args.hive: 167 | 168 | print('Collecting IIDs from %s...' % hive) 169 | 170 | if not os.path.isfile(hive): 171 | log.warning('Failed to find file %s. Skipping...' % hive) 172 | continue 173 | 174 | try: 175 | reg = Registry.Registry(hive) 176 | except Registry.RegistryParse.ParseException: 177 | log.warning('Error parsing %s. Skipping...' % hive) 178 | continue 179 | 180 | try: 181 | keys.append(reg.open("Classes\\Interface")) 182 | except Registry.RegistryKeyNotFoundException: 183 | log.warning("Couldn't find 'Classes\\Interface' key in %s." % hive) 184 | 185 | try: 186 | keys.append(reg.open("Classes\\Wow6432Node\\Interface")) 187 | except Registry.RegistryKeyNotFoundException: 188 | log.warning("Couldn't find 'Classes\\Wow6432Node\\Interface\\ " 189 | "key in %s." % hive) 190 | 191 | com_signatures = enumerate_com_interfaces(keys, True) 192 | 193 | counter = 0 194 | total_rules = len(com_signatures) 195 | print('Generating %s YARA signatures...' % total_rules) 196 | 197 | bar = progressbar.ProgressBar(redirect_stdout=True, max_value=total_rules) 198 | yara_rule = '// %s\n// COM IID YARA sig collection.\n// ' \ 199 | 'Autogenerated on %s\n\n' % (__author__, datetime.now()) 200 | for name, rules in com_signatures.items(): 201 | yara_rule += 'rule %s\n{\n\t' \ 202 | 'strings:' % name 203 | if len(rules) > 1: 204 | for i in range(0, len(rules)): 205 | yara_rule += '\n\t\t$%s_%s = %s' % (name, i, rules[i]) 206 | else: 207 | yara_rule += '\n\t\t$%s = %s' % (name, rules[0]) 208 | 209 | yara_rule += '\n\tcondition:\n\t\tany of them\n}\n' 210 | 211 | bar.update(counter) 212 | counter += 1 213 | bar.finish() 214 | 215 | print('Writing YARA rules to %s' % args.output_filename) 216 | with open(args.output_filename, 'w') as f: 217 | f.write(yara_rule) 218 | f.close() 219 | 220 | 221 | if __name__ == '__main__': 222 | main() 223 | -------------------------------------------------------------------------------- /malchive/utilities/dotnetdumper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2019 Wesley Shields. All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 | # SUCH DAMAGE. 25 | 26 | import argparse 27 | import logging 28 | import os 29 | import struct 30 | import sys 31 | 32 | import yara 33 | 34 | 35 | __version__ = "1.0.0" 36 | __author__ = "Wesley Shields" 37 | __contributor__ = "Mike Goffin" 38 | 39 | log = logging.getLogger(__name__) 40 | 41 | # It is really important you read this! 42 | # Documentation on the manifest resource file format: 43 | # http://www.ntcore.com/files/manifestres.htm 44 | 45 | 46 | class YaraScanner: 47 | """ 48 | Uses YARA to identify potential embedded .NET content 49 | 50 | :ivar list results: List of tuples for each entry, indicating the given 51 | filename by the class and data. 52 | """ 53 | 54 | RSRC_MAGIC = 0xBEEFCACE 55 | 56 | def __init__(self, filename, data): 57 | self.filename = filename 58 | self.results = [] 59 | self.metadata = {} 60 | self.data = data 61 | 62 | try: 63 | rule = 'import "dotnet" rule a { condition: false }' 64 | self.rules = yara.compile(source=rule) 65 | except yara.SyntaxError as e: 66 | log.error(f"Exception while compiling dotnet rule: {e}") 67 | log.error("Do you have the YARA dotnet module compiled in?") 68 | sys.exit(1) 69 | 70 | def parse_manifest_resource(self, rsrc, rsrc_length): 71 | # We must have at least 12 bytes to start parsing... 72 | if rsrc_length < 12: 73 | return 74 | 75 | name_len = struct.unpack("= rsrc_length: 79 | return 80 | 81 | (num_rsrcs, num_types) = struct.unpack("= rsrc_length: 120 | break 121 | data_len = \ 122 | struct.unpack(" None: 128 | filename = f"{self.filename}.{_type}_bin_{len(self.results)}" 129 | self.results.append((filename, data)) 130 | 131 | def modules_callback(self, data): 132 | self.metadata = data 133 | for rsrc_dict in data.get("resources", []): 134 | rsrc_offset = rsrc_dict["offset"] 135 | rsrc_length = rsrc_dict["length"] 136 | 137 | # Check if this is a manifest resource, or if it's the variant that 138 | # is a generic encrypted, compressed blob. 139 | if rsrc_length < 4: 140 | log.info(f"Resource at {rsrc_offset:0x} too small to parse.") 141 | continue 142 | 143 | rsrc = self.data[rsrc_offset:rsrc_offset + rsrc_length] 144 | magic = struct.unpack("> key | data << (32 - key)) & 0xFFFFFFFF 34 | 35 | 36 | def gen_standard_hash(func, key): 37 | """ 38 | Perform standard hashing algorithm commonly observed in practice. 39 | 40 | :param bytearray func: Name of the function to hash. 41 | :param int key: Number for rotations to perform. 42 | 43 | :return: hash 44 | :rtype: int 45 | """ 46 | h = 0 47 | for c in func: 48 | h = c + ror(h, key) & 0xFFFFFFFF 49 | return h 50 | 51 | 52 | # Ref: 53 | # https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/hash.py 54 | def convert_unicode(string, uppercase=True): 55 | result = "" 56 | if uppercase: 57 | string = string.upper() 58 | for c in string: 59 | result += c + "\x00" 60 | return result 61 | 62 | 63 | # Ref: 64 | # https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/hash.py 65 | def gen_metasploit_hash(lib, func, key): 66 | """ 67 | Perform Metasploit's hashing algorithm. 68 | 69 | :param bytearray lib: Name of the library associated with function. 70 | Used in hash calculation. 71 | :param bytearray func: Name of the function to hash. 72 | :param int key: Number for rotations to perform. 73 | 74 | :return: hash 75 | :rtype: int 76 | """ 77 | module_hash = 0 78 | function_hash = 0 79 | for c in convert_unicode(lib + "\x00"): 80 | module_hash = ror(module_hash, key) 81 | module_hash += ord(c) 82 | for c in bytes(func + b'\x00'): 83 | function_hash = ror(function_hash, key) 84 | function_hash += c 85 | h = module_hash + function_hash & 0xFFFFFFFF 86 | return h 87 | 88 | 89 | def gen_crc32_hash(func): 90 | """ 91 | Perform a simple CRC32 computation of the supplied function name. 92 | 93 | :param str func: Name of the function to hash. 94 | 95 | :return: hash 96 | :rtype: int 97 | """ 98 | h = binascii.crc32(func) 99 | if h > 0x7FFFFFFF: 100 | h -= 0x100000000 & 0xffffffff 101 | return h 102 | 103 | 104 | def gen_js_hash(func): 105 | """ 106 | Perform JSHash computation of the supplied function name. 107 | 108 | :param bytearray func: Name of the function to hash. 109 | 110 | :return: hash 111 | :rtype: int 112 | """ 113 | h = 1315423911 114 | for c in func: 115 | h ^= ((h << 5) + c + (h >> 2) & 0xFFFFFFFF) 116 | return h 117 | 118 | 119 | def gen_carberp_hash(func): 120 | """ 121 | Perform hash computation of function name using Carberp algorithm. 122 | 123 | :param bytearray func: Name of the function to hash. 124 | 125 | :return: hash 126 | :rtype: int 127 | """ 128 | h = 0 129 | for c in func: 130 | h = ((h << 7) & 0xFFFFFFFE) | (h >> (32 - 7)) 131 | h = h ^ c 132 | return h 133 | 134 | 135 | def initialize_parser(): 136 | parser = argparse.ArgumentParser( 137 | description='Generate an sqlite database of API hashes using ' 138 | 'algorithms commonly observed in shellcode. Input files' 139 | ' must be valid Windows DLLs.') 140 | parser.add_argument('dll', metavar='FILE', nargs='*', 141 | help='Full path to the DLL(s) to be processed.') 142 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 143 | help='Output additional information when ' 144 | 'processing (mostly for debugging purposes).') 145 | parser.add_argument('-db', '--database-name', type=str, 146 | default='apihashes.db', 147 | help='Name of database file to be generated.' 148 | ' (default: apihashes.db)') 149 | 150 | return parser 151 | 152 | 153 | def main(): 154 | p = initialize_parser() 155 | args = p.parse_args() 156 | 157 | root = logging.getLogger() 158 | logging.basicConfig() 159 | if args.verbose: 160 | root.setLevel(logging.DEBUG) 161 | else: 162 | root.setLevel(logging.WARNING) 163 | 164 | if len(args.dll) == 0: 165 | log.error('No files were supplied. Please provide a DLL for hash ' 166 | 'generation.') 167 | p.print_help() 168 | sys.exit(2) 169 | 170 | for filename in args.dll: 171 | 172 | basename = os.path.basename(filename) 173 | log.info('Generating hashes for %s...' % basename) 174 | 175 | if not os.path.isfile(filename): 176 | log.warning('Failed to find file %s, skipping...' % filename) 177 | continue 178 | 179 | f = open(filename, 'rb') 180 | stream = f.read() 181 | 182 | symbols = [] 183 | try: 184 | pe = pefile.PE(data=stream) 185 | symbols = pe.DIRECTORY_ENTRY_EXPORT.symbols 186 | except pefile.PEFormatError: 187 | log.error('%s not a pe, skipping...' % basename) 188 | continue 189 | 190 | # Generate hashes as a list of tuples 191 | entries = [] 192 | for exp in symbols: 193 | 194 | if exp.name is None: 195 | continue 196 | 197 | entries.append(('Standard ROR 0x7', basename, 198 | bytes(exp.name).decode('ascii'), 199 | gen_standard_hash(exp.name, 0x7))) 200 | entries.append(('Standard ROR 0xd', basename, 201 | bytes(exp.name).decode('ascii'), 202 | gen_standard_hash(exp.name, 0xd))) 203 | entries.append(('Metasploit ROR 0xd', basename, 204 | bytes(exp.name).decode('ascii'), 205 | gen_metasploit_hash(basename, exp.name, 0xd))) 206 | entries.append(('CRC32', basename, bytes(exp.name).decode('ascii'), 207 | gen_crc32_hash(exp.name))) 208 | # I've seen this twist as well where the null byte 209 | # is part of the computation for crc32 210 | entries.append(('CRC32', basename, 211 | (bytes(exp.name + b'\\x00')).decode('ascii'), 212 | gen_crc32_hash(bytes(exp.name + b'\x00')))) 213 | entries.append(('JSHash', basename, 214 | bytes(exp.name).decode('ascii'), 215 | gen_js_hash(exp.name))) 216 | entries.append(('Carberp', basename, 217 | bytes(exp.name).decode('ascii'), 218 | gen_carberp_hash(exp.name))) 219 | 220 | if len(entries) == 0: 221 | log.info('No export entries were found') 222 | continue 223 | 224 | log.info('Found %s export entries...' % len(symbols)) 225 | log.info('Adding %s hashes to database...' % len(entries)) 226 | 227 | start = datetime.datetime.now() 228 | 229 | conn = sqlite3.connect(args.database_name) 230 | cursor = conn.cursor() 231 | cursor.execute("""CREATE TABLE IF NOT EXISTS apihashes 232 | (Algorithm text, Module text, Function text, Hash int) 233 | """) 234 | cursor.executemany("INSERT INTO apihashes (Algorithm, Module, " 235 | "Function, Hash) VALUES (?, ?, ?, ?)", 236 | entries) 237 | conn.commit() 238 | cursor.close() 239 | conn.close() 240 | end = datetime.datetime.now() 241 | 242 | log.info('Inserted %s new entries in %s...' % 243 | (len(entries), end - start)) 244 | 245 | print('Complete! Any computed hashes saved to %s' % args.database_name) 246 | 247 | 248 | if __name__ == '__main__': 249 | main() 250 | -------------------------------------------------------------------------------- /malchive/utilities/pepdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright(c) 2021 The MITRE Corporation. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # 8 | # You may obtain a copy of the License at: 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import sys 19 | import logging 20 | import argparse 21 | import pefile 22 | import operator 23 | import binascii 24 | import struct 25 | from tabulate import tabulate 26 | from datetime import datetime 27 | 28 | __version__ = "1.1.0" 29 | __author__ = "Jason Batchelor" 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | class PDBInfo: 35 | """ 36 | Gets specific info from PE DIRECTORY_ENTRY_DEBUG directory 37 | 38 | :ivar list results: List of tuples representing debug info in 39 | the following order; timestamp, signature, age, pdb path, guid. 40 | """ 41 | 42 | def __init__(self, buff): 43 | """ 44 | Initialize PDBInfo. 45 | 46 | :param bytes buff: The stream of bytes to be processed. Must be a PE. 47 | """ 48 | 49 | self.results = [] 50 | self.buff = buff 51 | 52 | try: 53 | pe = pefile.PE(data=self.buff) 54 | except pefile.PEFormatError: 55 | log.debug('Supplied file must be a valid PE!') 56 | return 57 | 58 | if not hasattr(pe, 'DIRECTORY_ENTRY_DEBUG'): 59 | log.debug('No debug info present.') 60 | return 61 | 62 | self.get_debug_dir_info(pe) 63 | 64 | def get_debug_dir_info(self, pe): 65 | """ 66 | Retrieves debug information from the Debug directory. 67 | 68 | :param pefile.PE pe: pefile object. 69 | """ 70 | 71 | for debugdata in pe.DIRECTORY_ENTRY_DEBUG: 72 | 73 | timestamp = getattr(debugdata.struct, 'TimeDateStamp', None) 74 | if timestamp is not None: 75 | timestamp = datetime.utcfromtimestamp(timestamp).__str__() 76 | else: 77 | log.debug('No timestamp entry was present.') 78 | 79 | dbg_type = getattr(debugdata.struct, 'Type', 0) 80 | # Reference: 81 | # https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PE-COFF.md#codeview-debug-directory-entry-type-2 82 | if dbg_type != 2: 83 | log.debug('Type is not a CodeView Debug Directory Entry') 84 | continue 85 | 86 | if debugdata.entry is None: 87 | log.debug('No debug entry data found.') 88 | continue 89 | 90 | entry = debugdata.entry 91 | signature, age, pdb_path, guid = self.get_entry_info(entry) 92 | self.results.append((timestamp, signature, age, pdb_path, guid)) 93 | 94 | @staticmethod 95 | def get_entry_info(entry): 96 | """ 97 | Process an entry from the debug directory and return metadata. 98 | """ 99 | 100 | age = getattr(entry, 'Age', None) 101 | if age is None: 102 | log.debug('Age entry not found.') 103 | 104 | pdb_path = getattr(entry, 'PdbFileName', None) 105 | if pdb_path is not None: 106 | try: 107 | pdb_path = entry.PdbFileName.rstrip(b'\x00').decode('utf-8') 108 | except UnicodeDecodeError: 109 | pdb_path = None 110 | log.warning('There was an error decoding unicode ' 111 | 'information from debug data.') 112 | else: 113 | log.debug('No PdbFileName entry found.') 114 | 115 | signature = getattr(entry, 'CvSignature', None) 116 | if signature is not None: 117 | signature = entry.CvSignature.decode('ascii') 118 | else: 119 | log.debug('No signature entry found.') 120 | 121 | try: 122 | guid = '{' + \ 123 | binascii.hexlify( 124 | struct.pack('>I', entry.Signature_Data1) 125 | ).decode('ascii') + \ 126 | '-' + \ 127 | binascii.hexlify( 128 | struct.pack('>H', entry.Signature_Data2) 129 | ).decode('ascii') + \ 130 | '-' + \ 131 | binascii.hexlify( 132 | struct.pack('>H', entry.Signature_Data3) 133 | ).decode('ascii') + \ 134 | '-' + \ 135 | binascii.hexlify( 136 | struct.pack('B', entry.Signature_Data4) 137 | ).decode('ascii') + \ 138 | binascii.hexlify( 139 | struct.pack('B', entry.Signature_Data5) 140 | ).decode('ascii') + \ 141 | '-' + \ 142 | binascii.hexlify( 143 | entry.Signature_Data6 144 | ).decode('ascii') + \ 145 | '}' 146 | 147 | except UnicodeDecodeError as e: 148 | guid = '' 149 | log.warning('Error while getting GUID information. ' 150 | 'Exception: %s', e) 151 | 152 | return signature, age, pdb_path, guid 153 | 154 | 155 | def initialize_parser(): 156 | parser = argparse.ArgumentParser( 157 | description='Take a series of provided files and print the extracted' 158 | ' PDB CodeView info from them.') 159 | parser.add_argument('infile', metavar='FILE', nargs='*', 160 | help='Full path to the file to be processed.') 161 | parser.add_argument('-t', '--table', action='store_true', 162 | help='Show results in table format instead of JSON.') 163 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 164 | help='Output additional information when processing ' 165 | '(mostly for debugging purposes).') 166 | 167 | return parser 168 | 169 | 170 | def main(): 171 | 172 | import json 173 | 174 | p = initialize_parser() 175 | args = p.parse_args() 176 | 177 | root = logging.getLogger() 178 | logging.basicConfig() 179 | if args.verbose: 180 | root.setLevel(logging.DEBUG) 181 | else: 182 | root.setLevel(logging.WARNING) 183 | 184 | if len(args.infile) == 0: 185 | p.print_help() 186 | sys.exit(2) 187 | 188 | results = [] 189 | for fname in args.infile: 190 | 191 | basename = os.path.basename(fname) 192 | 193 | log.info('Processing file %s' % basename) 194 | 195 | if not os.path.isfile(fname): 196 | log.warning('Failed to find file %s. Skipping...' % fname) 197 | continue 198 | 199 | with open(fname, 'rb') as f: 200 | stream = f.read() 201 | 202 | try: 203 | pefile.PE(data=stream) 204 | except pefile.PEFormatError: 205 | log.warning('%s not a pe, skipping...' % basename) 206 | continue 207 | 208 | try: 209 | dbg = PDBInfo(stream) 210 | except Exception as e: 211 | log.warning('Could not process %s. Make sure you are running the ' 212 | 'latest version of pefile. ' 213 | 'Exception %s' % (basename, e)) 214 | continue 215 | 216 | for d in dbg.results: 217 | results.append(((basename,) + d)) 218 | 219 | if args.table and len(results) > 0: 220 | results = sorted(results, key=operator.itemgetter(1, 0)) 221 | 222 | table_header = ["Filename", "Debug Timestamp", "Signature", 223 | "Revisions", "Path", "GUID"] 224 | 225 | print(tabulate(results, headers=table_header, tablefmt="grid")) 226 | elif len(results) > 0: 227 | 228 | for fname, debug_time, sig, rev, path, guid in results: 229 | j = { 230 | 'Filename': fname, 231 | 'Debug Timestamp': debug_time, 232 | 'Signature': sig, 233 | 'Revisions': rev, 234 | 'Path': path, 235 | 'GUID': guid, 236 | } 237 | try: 238 | print(json.dumps(j, indent=4, sort_keys=False)) 239 | except UnicodeDecodeError: 240 | log.warning('There was a Unicode decoding error when processing %s' 241 | % fname) 242 | continue 243 | 244 | else: 245 | print('No debug information found!') 246 | 247 | 248 | if __name__ == '__main__': 249 | main() 250 | --------------------------------------------------------------------------------