From 65c8e22f754ac01b33f6e1e0f30a8b0b544b6fb7 Mon Sep 17 00:00:00 2001 From: dKtKRVn <93963533+dKtKRVn@users.noreply.github.com> Date: Tue, 9 Nov 2021 12:15:22 +0800 Subject: [PATCH 1/8] private key export support for macOS Monterey private key export support for macOS Monterey 12.0.1 --- chainbreaker.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chainbreaker.py b/chainbreaker.py index d68ae7c..9af73b6 100755 --- a/chainbreaker.py +++ b/chainbreaker.py @@ -149,9 +149,13 @@ def dump_private_keys(self): try: table_meta, private_key_list = self._get_table_from_type(CSSM_DL_DB_RECORD_PRIVATE_KEY) for i, private_key_offset in enumerate(private_key_list, 1): + try: + print "private_key_offset", self._get_private_key_record(private_key_offset) entries.append( self._get_private_key_record(private_key_offset)) + except Exception as e: + self.logger.warning(e) except KeyError: self.logger.warning('[!] Private Key Table is not available') return entries @@ -550,6 +554,10 @@ def _get_generic_password_record(self, record_offset): return record 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 From 55ecd6fe1e84f9f625e610a1f84a4aaa35dc8254 Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 17:42:47 +0900 Subject: [PATCH 2/8] * Support the Python3 * Todo : Bug fix *KEYERROR* issue on Public/Private Key Table --- README.md | 2 +- chainbreaker/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2edc30..3a7763b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,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: diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index 6f1e215..2ce714d 100644 --- a/chainbreaker/__init__.py +++ b/chainbreaker/__init__.py @@ -687,7 +687,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) From ecb9b4822e33e24278f6d5668cc5209cb4040d11 Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 19:50:24 +0900 Subject: [PATCH 3/8] Public/Private Key Table bug fixed. * Issues : write_to_disk error, "write to console" error --- chainbreaker/__init__.py | 12 ++++++------ chainbreaker/args_control.py | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index 2ce714d..715aca1 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: @@ -347,7 +347,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 '' @@ -451,7 +451,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 +474,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 +512,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] @@ -759,7 +759,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: diff --git a/chainbreaker/args_control.py b/chainbreaker/args_control.py index 510efea..7e64a85 100644 --- a/chainbreaker/args_control.py +++ b/chainbreaker/args_control.py @@ -120,6 +120,7 @@ def set_output_dir(args): 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() From d9659b02d7e63bf771cea990eef6df14f23199cf Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 21:54:42 +0900 Subject: [PATCH 4/8] keychain testset : OS X Lion, macOS 13(Ventura) --- chainbreaker/__init__.py | 17 ++++++++++++----- chainbreaker/args_control.py | 4 ++-- chainbreaker/results.py | 5 +++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index 715aca1..5fabfc8 100644 --- a/chainbreaker/__init__.py +++ b/chainbreaker/__init__.py @@ -863,7 +863,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 +916,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 +1160,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 +1181,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 7e64a85..1ac1fd1 100644 --- a/chainbreaker/args_control.py +++ b/chainbreaker/args_control.py @@ -110,12 +110,12 @@ 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) 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, From 5209bfde6dc328168f455c47f2e890d098437018 Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 22:33:48 +0900 Subject: [PATCH 5/8] Update README.md --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3a7763b..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 @@ -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. From 7c4a51cfa8a6b4fedbefae869d8c60b7586b6c71 Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 22:46:20 +0900 Subject: [PATCH 6/8] keychain testset : OS X Lion, macOS 13(Ventura) --- chainbreaker/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index 8cdfab9..c7d9197 100644 --- a/chainbreaker/__init__.py +++ b/chainbreaker/__init__.py @@ -184,13 +184,8 @@ def dump_private_keys(self): try: table_meta, private_key_list = self._get_table_from_type(CSSM_DL_DB_RECORD_PRIVATE_KEY) for i, private_key_offset in enumerate(private_key_list, 1): - try: - print "private_key_offset", self._get_private_key_record(private_key_offset) entries.append( self._get_private_key_record(private_key_offset)) - - except Exception as e: - self.logger.warning(e) except KeyError: self.logger.warning('[!] Private Key Table is not available') return entries From d60da914442bf7d99d3f51f250848984dec4ad36 Mon Sep 17 00:00:00 2001 From: n0fate Date: Thu, 29 Dec 2022 22:48:41 +0900 Subject: [PATCH 7/8] remove debug code. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 801196c1ef05326a63403529bcc0535538a8661b Mon Sep 17 00:00:00 2001 From: Matthew Gabeler-Lee Date: Wed, 22 Feb 2023 14:43:05 -0500 Subject: [PATCH 8/8] restore private key handling broken since #21 --- chainbreaker/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/chainbreaker/__init__.py b/chainbreaker/__init__.py index c7d9197..0ab8c7b 100644 --- a/chainbreaker/__init__.py +++ b/chainbreaker/__init__.py @@ -374,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:]