Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -328,4 +323,4 @@ During the refactor, additional functionality was added including:

## TODO
* Better commenting of code.
* Better documentation of the keychain format.
* Better documentation of the keychain format.
44 changes: 26 additions & 18 deletions chainbreaker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ''
Expand Down Expand Up @@ -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:]
Expand Down Expand Up @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions chainbreaker/args_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
5 changes: 3 additions & 2 deletions chainbreaker/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
setuptools~=44.1.1
setuptools~=65.6.3
argparse~=1.2.1
pycryptodome~=3.10.1