diff --git a/README.md b/README.md index a2edc30..637e354 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ -Chainbreaker2 - python 3 +Chainbreaker ============ -An updated version of the Chainbreaker2 repository, by making chainbreaker2 compatible for python 3. - Chainbreaker can be used to extract the following types of information from an OSX keychain in a forensically sound manner: * Hashed Keychain password, suitable for cracking with [hashcat](https://hashcat.net/hashcat/) or @@ -46,7 +44,7 @@ And this returns a keychain object which you can use in your script. ## Supported OS's -Snow Leopard, Lion, Mountain Lion, Mavericks, Yosemite, El Capitan, (High) Sierra, Mojave, Catalina +OS X Snow Leopard(10.6) to macOS Ventura(13) ## Target Keychain file Any valid .keychain or .keychain-db can be supplied. Common Keychain locations include: @@ -60,7 +58,7 @@ Any valid .keychain or .keychain-db can be supplied. Common Keychain locations i ## Help: ``` -$ python ./chainbreaker.py --help +$ python -m chainbreaker --help usage: chainbreaker.py [-h] [--dump-all] [--dump-keychain-password-hash] [--dump-generic-passwords] [--dump-internet-passwords] [--dump-appleshare-passwords] [--dump-private-keys] @@ -146,12 +144,9 @@ Output Options: ## Example Usage ``` -./chainbreaker.py --password=TestPassword -a test_keychain.keychain +python -m chainbreaker -pa test_keychain.keychain -o output 2020-11-12 15:58:18,925 - INFO - -ChainBreaker 2 - https://github.com/gaddie-3/chainbreaker - -2020-11-12 15:58:18,925 - INFO - Runtime Command: chainbreaker.py --password=TestPassword -a test_keychain.keychain 2020-11-12 15:58:18,925 - INFO - Keychain: test_keychain.keychain 2020-11-12 15:58:18,925 - INFO - Keychain MD5: eb3abc06c22afa388ca522ea5aa032fc 2020-11-12 15:58:18,925 - INFO - Keychain 256: 2d76f564ac24fa6a8a22adb6d5cb9b430032785b1ba3effa8ddea38222008441 @@ -328,4 +323,4 @@ During the refactor, additional functionality was added including: ## TODO * Better commenting of code. -* Better documentation of the keychain format. \ No newline at end of file +* Better documentation of the keychain format. diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index 6f1e215..0ab8c7b 100644 --- a/chainbreaker/__init__.py +++ b/chainbreaker/__init__.py @@ -126,7 +126,7 @@ def dump_generic_passwords(self): return entries - # Returns a list of InterertPasswordRecord objects extracted from the Keychain + # Returns a list of InternetPasswordRecord objects extracted from the Keychain def dump_internet_passwords(self): entries = [] try: @@ -186,7 +186,6 @@ def dump_private_keys(self): for i, private_key_offset in enumerate(private_key_list, 1): entries.append( self._get_private_key_record(private_key_offset)) - except KeyError: self.logger.warning('[!] Private Key Table is not available') return entries @@ -347,7 +346,7 @@ def _get_four_char_code(self, base_addr, pcol): else: return _FOUR_CHAR_CODE(self.kc_buffer[base_addr + pcol:base_addr + pcol + 4]).Value - # Get an lv from the keychain buffer + # Get a lv from the keychain buffer def _get_lv(self, base_addr, pcol): if pcol <= 0: return '' @@ -375,11 +374,9 @@ def _private_key_decryption(self, encryptedblob, iv): if len(plain) == 0: return '', '' - # now we handle the unwrapping. we need to take the first 32 bytes, - # and reverse them. - revplain = bytes(reversed(plain[0:32])) - # now the real key gets found. */ - plain = Chainbreaker._kcdecrypt(self.db_key, iv, revplain) + # reverse the plaintext before decrypting again + plain = bytes(reversed(plain)) + plain = Chainbreaker._kcdecrypt(self.db_key, iv, plain) keyname = plain[:12] # Copied Buffer when user click on right and copy a key on Keychain Access keyblob = plain[12:] @@ -451,7 +448,7 @@ def _get_appleshare_record(self, record_offset): ) def _get_private_key_record(self, record_offset): - record = self._get_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PRIVATE_KEY), record_offset) + record = self._get_key_record(CSSM_DL_DB_RECORD_PRIVATE_KEY, record_offset) if not self.db_key: keyname = privatekey = Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE @@ -474,7 +471,7 @@ def _get_private_key_record(self, record_offset): ) def _get_public_key_record(self, record_offset): - record = self._get_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PUBLIC_KEY), record_offset) + record = self._get_key_record(CSSM_DL_DB_RECORD_PUBLIC_KEY, record_offset) return self.PublicKeyRecord( print_name=record[0], label=record[1], @@ -512,7 +509,7 @@ def _get_key_record(self, table_name, record_offset): self._get_int(base_addr, record_meta.EffectiveKeySize & 0xFFFFFFFE), self._get_int(base_addr, record_meta.Extractable & 0xFFFFFFFE), STD_APPLE_ADDIN_MODULE[ - str(self._get_lv(base_addr, record_meta.KeyCreator & 0xFFFFFFFE)).split('\x00')[0]], + self._get_lv(base_addr, record_meta.KeyCreator & 0xFFFFFFFE).decode('utf-8').split('\x00')[0]], iv, key] @@ -605,6 +602,10 @@ def _get_generic_password_record(self, record_offset): dbkey=dbkey) def _get_base_address(self, table_name, offset=None): + if table_name == 23972: + table_name = 16 + if table_name == 30912: + table_name = 16 base_address = _APPL_DB_HEADER.STRUCT.size + self._get_table_offset(table_name) if offset: base_address += offset @@ -687,7 +688,7 @@ def _kcdecrypt(key, iv, data): if len(data) % Chainbreaker.BLOCKSIZE != 0: return b'' - cipher = DES3.new(key, DES3.MODE_CBC, iv=bytearray(iv)) + cipher = DES3.new(key, DES3.MODE_CBC, IV=iv) plain = cipher.decrypt(data) @@ -759,7 +760,7 @@ def __init__(self): def write_to_disk(self, output_directory): # self.exportable contains the content we should write to disk. If it isn't implemented we can't - # then writing to disk via this method isn't currently supported. + # then can write to disk via this method isn't currently supported. try: export_content = self.exportable except NotImplementedError: @@ -863,7 +864,7 @@ def exportable(self): @property def file_name(self): - return "".join(x for x in self.PrintName if x.isalnum()) + return "PublicKey" @property def file_ext(self): @@ -916,7 +917,7 @@ def exportable(self): @property def file_name(self): - return "".join(x for x in self.PrintName if x.isalnum()) + return "PrivateKey" @property def file_ext(self): @@ -1160,7 +1161,7 @@ def __str__(self): def check_input_args(args): # Check various input arguments args = args_control.args_prompt_input(args) - args.output = args_control.set_output_dir(args) + args.output = args_control.set_output_dir(args, logger) args = args_control.set_all_options_true(args) # Make sure we're actually doing something, exit if we're not. @@ -1181,8 +1182,15 @@ def main(): logging.info(f'Version - {__version__}') # Calculate the MD5 and SHA256 of the input keychain file. - keychain_md5 = md5(args.keychain.encode('utf-8')).hexdigest() - keychain_sha256 = sha256(args.keychain.encode('utf-8')).hexdigest() + try: + tmp = open(args.keychain, 'rb') + buf = tmp.read() + tmp.close() + except: + logging.critical(f'Failed to open the keychain file') + exit(1) + keychain_md5 = md5(buf).hexdigest() + keychain_sha256 = sha256(buf).hexdigest() # Print out some summary info before we actually start doing any work. summary_output = results.summary(args, keychain_md5, keychain_sha256) diff --git a/chainbreaker/args_control.py b/chainbreaker/args_control.py index 510efea..1ac1fd1 100644 --- a/chainbreaker/args_control.py +++ b/chainbreaker/args_control.py @@ -110,16 +110,17 @@ def setup_argsparse(): return arguments.parse_args() -def set_output_dir(args): +def set_output_dir(args, log): + logger = log if args.output: if not os.path.exists(args.output): try: os.makedirs(args.output) - return args.output except OSError: logger.critical("Unable to create output directory: %s" % args.output) exit(1) logger.addHandler(logging.FileHandler(os.path.join(args.output, 'output.log'), mode='w')) + return args.output else: return os.getcwd() diff --git a/chainbreaker/results.py b/chainbreaker/results.py index 3f442a3..a82a056 100644 --- a/chainbreaker/results.py +++ b/chainbreaker/results.py @@ -11,8 +11,9 @@ def summary(args, keychain_md5, keychain_sha256): # Collect summary of information summary_output = [ - "Credits: Forked from https://github.com/n0fate/chainbreaker", - "Credits: Thanks to https://github.com/gaddie-3/chainbreaker", + "Chainbreaker : https://github.com/n0fate/chainbreaker", + #"Credits: Forked from https://github.com/n0fate/chainbreaker", + #"Credits: Thanks to https://github.com/gaddie-3/chainbreaker", "Version: %s" % chainbreaker.__version__, "Runtime Command: %s" % ' '.join(sys.argv), "Keychain: %s" % args.keychain, diff --git a/requirements.txt b/requirements.txt index 512bb65..ceb88af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -setuptools~=44.1.1 +setuptools~=65.6.3 argparse~=1.2.1 pycryptodome~=3.10.1 \ No newline at end of file