diff --git a/.gitignore b/.gitignore index c55189b..2eb5695 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,266 @@ *.pyc exported/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/README-keydump.txt b/README-keydump.txt deleted file mode 100644 index 3b33f98..0000000 --- a/README-keydump.txt +++ /dev/null @@ -1,104 +0,0 @@ -Here is info related to a quick hack of chainbreaker to dump non-exportable keys on High Sierra 10.13. -See README.md original README for chainbreaker. -Credits for coding go to Anton Brazovski. - -=== === === === === === === === === === === === === === === === === === === === === === === === - -Prerequisites: sudo pip install hexdump pycrypto - -$ ./chainbreaker.py -f ~/Library/Keychains/login.keychain-db -p "ooUlah..#" - [-] DB Key -[+] Symmetric Key Table: -[+] Generic Password Record -... -[+] Certificate -... -[+] Public Key Record -... -[+] Private Key Record -... - -All extracted data are saved under extracted/, in certs/ and keys/. -The tool also tries to match keys and certs and saves them under associated/. -There is a simple shell script which can be used to double-check matching - match.sh. - -$ ls -lR exported -total 0 -drwxr-xr-x 8 abb staff 256 Jun 11 12:57 associated -drwxr-xr-x 12 abb staff 384 Jun 11 12:57 certs -drwxr-xr-x 11 abb staff 352 Jun 11 12:57 keys - -exported/associated: -total 0 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 1 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 2 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 3 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 4 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 5 -drwxr-xr-x 4 abb staff 128 Jun 11 12:57 6 - -exported/associated/1: -total 16 --rw-r--r-- 1 abb staff 1384 Jun 11 12:57 1.crt --rw-r--r-- 1 abb staff 1217 Jun 11 12:57 4.key - -exported/associated/2: -total 16 --rw-r--r-- 1 abb staff 1384 Jun 11 12:57 3.crt --rw-r--r-- 1 abb staff 1216 Jun 11 12:57 3.key - -exported/associated/3: -total 16 --rw-r--r-- 1 abb staff 1219 Jun 11 12:57 5.key --rw-r--r-- 1 abb staff 1114 Jun 11 12:57 6.crt - -exported/associated/4: -total 16 --rw-r--r-- 1 abb staff 1218 Jun 11 12:57 6.key --rw-r--r-- 1 abb staff 1382 Jun 11 12:57 7.crt - -exported/associated/5: -total 16 --rw-r--r-- 1 abb staff 1218 Jun 11 12:57 2.key --rw-r--r-- 1 abb staff 1553 Jun 11 12:57 8.crt - -exported/associated/6: -total 16 --rw-r--r-- 1 abb staff 1216 Jun 11 12:57 1.key --rw-r--r-- 1 abb staff 1336 Jun 11 12:57 10.crt - -exported/certs: -total 80 --rw-r--r-- 1 abb staff 1384 Jun 11 12:57 1.crt --rw-r--r-- 1 abb staff 1336 Jun 11 12:57 10.crt --rw-r--r-- 1 abb staff 1058 Jun 11 12:57 2.crt --rw-r--r-- 1 abb staff 1384 Jun 11 12:57 3.crt --rw-r--r-- 1 abb staff 1114 Jun 11 12:57 4.crt --rw-r--r-- 1 abb staff 1382 Jun 11 12:57 5.crt --rw-r--r-- 1 abb staff 1514 Jun 11 12:57 6.crt --rw-r--r-- 1 abb staff 1500 Jun 11 12:57 7.crt --rw-r--r-- 1 abb staff 1553 Jun 11 12:57 8.crt --rw-r--r-- 1 abb staff 1340 Jun 11 12:57 9.crt - -exported/keys: -total 72 --rw-r--r-- 1 abb staff 1217 Jun 11 12:57 1.key --rw-r--r-- 1 abb staff 121 Jun 11 12:57 2.key --rw-r--r-- 1 abb staff 778 Jun 11 12:57 3.key --rw-r--r-- 1 abb staff 1217 Jun 11 12:57 4.key --rw-r--r-- 1 abb staff 1216 Jun 11 12:57 5.key --rw-r--r-- 1 abb staff 1219 Jun 11 12:57 6.key --rw-r--r-- 1 abb staff 1218 Jun 11 12:57 7.key --rw-r--r-- 1 abb staff 1218 Jun 11 12:57 8.key --rw-r--r-- 1 abb staff 1216 Jun 11 12:57 9.key - -All files are in DER format. To convert to PEM and export as P12: - -$ openssl -OpenSSL> x509 -inform DER -in exported/certs/4.crt -out secret.crt -OpenSSL> rsa -inform DER -in exported/keys/4.key -out secret.key -writing RSA key -OpenSSL> pkcs12 -export -out secret.p12 -inkey secret.key -in secret.crt -Enter Export Password: -Verifying - Enter Export Password: - diff --git a/README.md b/README.md index 935105a..681fe7e 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,234 @@ -chainbreaker +Chainbreaker2 ============ -The chainbreaker can extract user credential in a Keychain file with Master Key or user password in forensically sound manner. -Master Key candidates can be extracted from [volafox](https://github.com/n0fate/volafox) or [volatility](https://github.com/volatilityfoundation/volatility) keychaindump module. +Chainbreaker can be used to extract the following types of information from an OSX keychain in a forensically sound manner: -## Supported OS -Snow Leopard, Lion, Mountain Lion, Mavericks, Yosemite, El Capitan, (High) Sierra +* Hashed Keychain password, suitable for cracking with [hashcat](https://hashcat.net/hashcat/) or + [John the Ripper](https://www.openwall.com/john/) +* Internet Passwords +* Generic Passwords +* Private Keys +* Public Keys +* X509 Certificates +* Appleshare Passwords + +Given the keychain unlock password, a master key obtained using [volafox](https://github.com/n0fate/volafox) or +[volatility](https://github.com/volatilityfoundation/volatility), Chainbreaker will also provide plaintext passwords. +Without one of these methods of unlocking the Keychain, Chainbreaker will display all other available information. + +## Supported OS's +Snow Leopard, Lion, Mountain Lion, Mavericks, Yosemite, El Capitan, (High) Sierra, Mojave, Catalina ## Target Keychain file -* User Keychain(~/Users/[username]/Library/Keychains/login.keychain) : It has user id/password about installed application, ssh/vpn, mail, contacts, calendar and so on. It has key for call history decryption too. -* System Keychain(/Library/Keychains/System.keychain) : It has WiFi password registered by local machine and several certifications and public/private keys. (Detailed Info : http://forensic.n0fate.com/2014/09/system-keychain-analysis/) - -## How to use: - -If you have only keychain file and password, command as follow: - - $ python chainbreaker.py - usage: chainbreaker.py [-h] -f FILE (-k KEY | -p PASSWORD) - chainbreaker.py: error: argument -f/--file is required - - -If you have memory image, you can extract master key candidates using volafox project. The volafox, memory forensic toolit for Mac OS X has been written in Python as a cross platform open source project. Of course, you can dump it using volatility. - - $ python volafox.py -i [memory image] -o keychaindump - .... - .... - $ python chainbreaker.py -f [keychain file] -k [master key] - - -## Example - $ python vol.py -i ~/Desktop/show/macosxml.mem -o keychaindump - - [+] Find MALLOC_TINY heap range (guess) - [-] range 0x7fef03400000-0x7fef03500000 - [-] range 0x7fef03500000-0x7fef03600000 - [-] range 0x7fef03600000-0x7fef03700000 - [-] range 0x7fef04800000-0x7fef04900000 - [-] range 0x7fef04900000-0x7fef04a00000 - - [*] Search for keys in range 0x7fef03400000-0x7fef03500000 complete. master key candidates : 0 - [*] Search for keys in range 0x7fef03500000-0x7fef03600000 complete. master key candidates : 0 - [*] Search for keys in range 0x7fef03600000-0x7fef03700000 complete. master key candidates : 0 - [*] Search for keys in range 0x7fef04800000-0x7fef04900000 complete. master key candidates : 0 - [*] Search for keys in range 0x7fef04900000-0x7fef04a00000 complete. master key candidates : 6 - - [*] master key candidate: 78006A6CC504140E077D62D39F30DBBAFC5BDF5995039974 - [*] master key candidate: 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 - [*] master key candidate: 2DD97A4ED361F492C01FFF84962307D7B82343B94595726E - [*] master key candidate: 21BB87A2EB24FD663A0AC95E16BEEBF7728036994C0EEC19 - [*] master key candidate: 05556393141766259F62053793F62098D21176BAAA540927 - [*] master key candidate: 903C49F0FE0700C0133749F0FE0700404158544D00000000 - $ python chainbreaker.py -h - usage: chainbreaker.py [-h] -f FILE (-k KEY | -p PASSWORD) - - Tool for OS X Keychain Analysis by @n0fate - - optional arguments: - -h, --help show this help message and exit - -f FILE, --file FILE Keychain file(*.keychain) - -k KEY, --key KEY Masterkey candidate - -p PASSWORD, --password PASSWORD - User Password - $ python chainbreaker.py -f ~/Desktop/show/login.keychain -k 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 - [-] DB Key - 00000000: 05 55 63 93 14 17 66 25 9F 62 05 37 93 F6 20 98 .Uc...f%.b.7.. . - 00000010: D2 11 76 BA AA 54 09 27 ..v..T.' - [+] Symmetric Key Table: 0x00006488 - [+] Generic Password: 0x0000dea4 - [+] Generic Password Record - [-] RecordSize : 0x000000fc - [-] Record Number : 0x00000000 - [-] SECURE_STORAGE_GROUP(SSGP) Area : 0x0000004c - [-] Create DateTime: 20130318062355Z - [-] Last Modified DateTime: 20130318062355Z - [-] Description : - [-] Creator : - [-] Type : - [-] PrintName : ***********@gmail.com - [-] Alias : - [-] Account : 1688945386 - [-] Service : iCloud - [-] Password - 00000000: ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **************** - 00000010: 7A ** 69 ** 50 ** 51 36 ** ** ** 48 32 61 31 66 **************** - 00000020: ** 49 ** 73 ** 62 ** 79 79 41 6F 3D **********= - - - - [+] Internet Record - [-] RecordSize : 0x0000014c - [-] Record Number : 0x00000005 - [-] SECURE_STORAGE_GROUP(SSGP) Area : 0x0000002c - [-] Create DateTime: 20130318065146Z - [-] Last Modified DateTime: 20130318065146Z - [-] Description : Web form password - [-] Comment : default - [-] Creator : - [-] Type : - [-] PrintName : www.facebook.com (***********@gmail.com) - [-] Alias : - [-] Protected : - [-] Account : ***********@gmail.com - [-] SecurityDomain : - [-] Server : www.facebook.com - [-] Protocol Type : kSecProtocolTypeHTTPS - [-] Auth Type : kSecAuthenticationTypeHTMLForm - [-] Port : 0 - [-] Path : - [-] Password - 00000000: ** ** ** ** ** ** ** ** ** ** ** ** ************ - -If you have memory image only, you can dump a keychain file on it and decrypt keychain contents as [link](https://gist.github.com/n0fate/790428d408d54b910956) - - -## Contacts -chainbreaker was written by [n0fate](http://twitter.com/n0fate) -E-Mail address can be found from source code. +Any valid .keychain or .keychain-db can be supplied. Common Keychain locations include: +* User keychains, these can contain ID's, passwords, and other secure data pertaining to installed applications, ssh/vpn, mail, contacts, calendar + * /Users/[username]/Library/Keychains/login.keychain + * /Users/[username]/Library/Keychains/login.keychain-db + +* System Keychains, these can contain WiFi passwords registered by the local machine and several certifications and public/private keys. + * /Library/Keychains/System.keychain + * Note: The unlock file for this keychain is commonly located at /var/db/SystemKey + +## Help: +``` +$ ./chainbreaker.py --help +usage: chainbreaker.py [-h] [--dump-all] [--dump-keychain-password-hash] + [--dump-generic-passwords] [--dump-internet-passwords] + [--dump-appleshare-passwords] [--dump-private-keys] + [--dump-public-keys] [--dump-x509-certificates] + [--password-prompt] [--password PASSWORD] + [--key-prompt] [--key KEY] [--unlock-file UNLOCK_FILE] + [--output OUTPUT] [-q] [-d] + keychain + +Dump items stored in an OSX Keychain + +positional arguments: + keychain Location of the keychain file to parse + +optional arguments: + -h, --help show this help message and exit + +Available Actions: + --dump-all, -a Dump all keychain items + --dump-keychain-password-hash + Dump the keychain password hash in a format suitable + for hashcat or John The Ripper + --dump-generic-passwords + Dump all generic passwords + --dump-internet-passwords + Dump all internet passwords + --dump-appleshare-passwords + Dump all appleshare passwords + --dump-private-keys Dump all private keys + --dump-public-keys Dump all public keys + --dump-x509-certificates + Dump all X509 ertificates + +Unlock Options: + --password-prompt, -p + Prompt for a password to use in unlocking the keychain + --password PASSWORD Unlock the keychain with a password, provided on the + terminal.Caution: This is insecure and you should + likely use--password-prompt instead. + --key-prompt, -k Prompt for a key to use in unlocking the keychain + --key KEY Unlock the keychain with a key, provided via + argument.Caution: This is insecure and you should + likely use --key-prompt instead. + --unlock-file UNLOCK_FILE + Unlock the keychain with a key file + +Output Options: + --output OUTPUT, -o OUTPUT + Not currently implemented.Directory to output exported + records to. + -q, --quiet Suppress all output + -d, --debug Print debug information +``` + + +## Example Usage +``` +./chainbreaker.py -a --password TestPassword ./test_keychain.keychain +Keychain Password Hash + $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5 + + +1 Generic Passwords + [+] Generic Password Record + [-] Create DateTime: 2020-09-24 23:34:14 + [-] Last Modified DateTime: 2020-09-29 21:54:55 + [-] Description: + [-] Creator: + [-] Type: + [-] Print Name: Stored Test Password + [-] Alias: + [-] Account: TestUser + [-] Service: Stored Test Password + [-] Password: TestPasswordValue123! + + + +1 Internet Passwords + [+] Internet Record + [-] Create DateTime: 2020-09-29 22:21:51 + [-] Last Modified DateTime: 2020-09-29 22:21:51 + [-] Description: + [-] Comment: + [-] Creator: + [-] Type: + [-] PrintName: example.com + [-] Alias: + [-] Protected: + [-] Account: TestUsername + [-] SecurityDomain: + [-] Server: example.com + [-] Protocol Type: kSecProtocolTypeHTTPS + [-] Auth Type: kSecAuthenticationTypeDefault + [-] Port: 0 + [-] Path: + [-] Password: TestPassword123! + + + +0 Appleshare Passwords + + +0 Public Keys + + +0 Private Keys +``` + +##Cracking the Keychain Hash using hashcat +### Hash Extraction +The password used to encrypt a keychain can be dumped using the --dump-keychain-password-hash option. +``` +$ ./chainbreaker.py --dump-keychain-password-hash ./test_keychain.keychain +Keychain Password Hash + $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5 +``` +### Hash Cracking +After obtaining the keychain password hash, you can use a program such as [hashcat](https://hashcat.net/hashcat/) to attempt to crack it. + +``` + +> hashcat.exe -m 23100 hashes.txt dictionary.txt +hashcat (v6.1.1) starting... + +[...] + +Minimum password length supported by kernel: 0 +Maximum password length supported by kernel: 256 + +Hashes: 1 digests; 1 unique digests, 1 unique salts +Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates +Rules: 1 + +Applicable optimizers applied: +* Zero-Byte +* Single-Hash +* Single-Salt +* Slow-Hash-SIMD-LOOP + +[...] + +$keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5:TestPassword + +Session..........: hashcat +Status...........: Cracked +Hash.Name........: Apple Keychain +Hash.Target......: $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1...d067e5 +[...] + +``` + +## Extraction from memory images +Volofax can be used to extract Keychain files and master key candidates from memory images. + + +``` +$ python vol.py -i ~/Desktop/show/macosxml.mem -o keychaindump + +[+] Find MALLOC_TINY heap range (guess) + [-] range 0x7fef03400000-0x7fef03500000 + [...] + [-] range 0x7fef04900000-0x7fef04a00000 + +[*] Search for keys in range 0x7fef03400000-0x7fef03500000 complete. master key candidates : 0 +[...] +[*] Search for keys in range 0x7fef04900000-0x7fef04a00000 complete. master key candidates : 6 + +[*] master key candidate: 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 +[...] +[*] master key candidate: 903C49F0FE0700C0133749F0FE0700404158544D00000000 + +$ ./chainbreaker.py --key 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 ./test_keychain.keychain +``` + +Additional examples can be found in this [gist](https://gist.github.com/n0fate/790428d408d54b910956) by n0fate. + +## Why the rewrite? +Chainbreaker2 was forked to be heavily refactored and modified from the original [chainbreaker](https://github.com/n0fate/chainbreaker). + +The primary reason behind this fork is to add better support integration into third-party forensic platforms such as +[Autopsy](https://www.autopsy.com/). + +During the refactor, additional functionality was added including: +* Enhanced user control and options +* Extraction of the Keychain hash for use with third-party hash cracking software. +* Dumping all available information, regardless of the presence of an unlocking method + + +## Credits +* Chainbreaker2 has been significantly refactored and with accitional functionality added by [Luke Gaddie](luke@socially-inept.net) +* The original author of [chainbreaker](https://github.com/n0fate/chainbreaker) is [n0fate](http://twitter.com/n0fate) ## License [GNU GPL v2](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) + +## TODO +* Allow keys and certificates to be exported to file. +* Better export options (e.g. dump to CSV) +* Better commenting of code. +* Better documentation of the keychain format. \ No newline at end of file diff --git a/Schema.py b/Schema.py deleted file mode 100644 index 45863a0..0000000 --- a/Schema.py +++ /dev/null @@ -1,314 +0,0 @@ -# http://web.mit.edu/darwin/src/modules/Security/cdsa/cdsa/cssmtype.h -KEY_TYPE = { - 0x00+0x0F : 'CSSM_KEYCLASS_PUBLIC_KEY', - 0x01+0x0F : 'CSSM_KEYCLASS_PRIVATE_KEY', - 0x02+0x0F : 'CSSM_KEYCLASS_SESSION_KEY', - 0x03+0x0F : 'CSSM_KEYCLASS_SECRET_PART', - 0xFFFFFFFF : 'CSSM_KEYCLASS_OTHER' -} - -CSSM_ALGORITHMS = { - 0 : 'CSSM_ALGID_NONE', - 1 : 'CSSM_ALGID_CUSTOM', - 2 : 'CSSM_ALGID_DH', - 3 : 'CSSM_ALGID_PH', - 4 : 'CSSM_ALGID_KEA', - 5 : 'CSSM_ALGID_MD2', - 6 : 'CSSM_ALGID_MD4', - 7 : 'CSSM_ALGID_MD5', - 8 : 'CSSM_ALGID_SHA1', - 9 : 'CSSM_ALGID_NHASH', - 10 : 'CSSM_ALGID_HAVAL:', - 11 : 'CSSM_ALGID_RIPEMD', - 12 : 'CSSM_ALGID_IBCHASH', - 13 : 'CSSM_ALGID_RIPEMAC', - 14 : 'CSSM_ALGID_DES', - 15 : 'CSSM_ALGID_DESX', - 16 : 'CSSM_ALGID_RDES', - 17 : 'CSSM_ALGID_3DES_3KEY_EDE', - 18 : 'CSSM_ALGID_3DES_2KEY_EDE', - 19 : 'CSSM_ALGID_3DES_1KEY_EEE', - 20 : 'CSSM_ALGID_3DES_3KEY_EEE', - 21 : 'CSSM_ALGID_3DES_2KEY_EEE', - 22 : 'CSSM_ALGID_IDEA', - 23 : 'CSSM_ALGID_RC2', - 24 : 'CSSM_ALGID_RC5', - 25 : 'CSSM_ALGID_RC4', - 26 : 'CSSM_ALGID_SEAL', - 27 : 'CSSM_ALGID_CAST', - 28 : 'CSSM_ALGID_BLOWFISH', - 29 : 'CSSM_ALGID_SKIPJACK', - 30 : 'CSSM_ALGID_LUCIFER', - 31 : 'CSSM_ALGID_MADRYGA', - 32 : 'CSSM_ALGID_FEAL', - 33 : 'CSSM_ALGID_REDOC', - 34 : 'CSSM_ALGID_REDOC3', - 35 : 'CSSM_ALGID_LOKI', - 36 : 'CSSM_ALGID_KHUFU', - 37 : 'CSSM_ALGID_KHAFRE', - 38 : 'CSSM_ALGID_MMB', - 39 : 'CSSM_ALGID_GOST', - 40 : 'CSSM_ALGID_SAFER', - 41 : 'CSSM_ALGID_CRAB', - 42 : 'CSSM_ALGID_RSA', - 43 : 'CSSM_ALGID_DSA', - 44 : 'CSSM_ALGID_MD5WithRSA', - 45 : 'CSSM_ALGID_MD2WithRSA', - 46 : 'CSSM_ALGID_ElGamal', - 47 : 'CSSM_ALGID_MD2Random', - 48 : 'CSSM_ALGID_MD5Random', - 49 : 'CSSM_ALGID_SHARandom', - 50 : 'CSSM_ALGID_DESRandom', - 51 : 'CSSM_ALGID_SHA1WithRSA', - 52 : 'CSSM_ALGID_CDMF', - 53 : 'CSSM_ALGID_CAST3', - 54 : 'CSSM_ALGID_CAST5', - 55 : 'CSSM_ALGID_GenericSecret', - 56 : 'CSSM_ALGID_ConcatBaseAndKey', - 57 : 'CSSM_ALGID_ConcatKeyAndBase', - 58 : 'CSSM_ALGID_ConcatBaseAndData', - 59 : 'CSSM_ALGID_ConcatDataAndBase', - 60 : 'CSSM_ALGID_XORBaseAndData', - 61 : 'CSSM_ALGID_ExtractFromKey', - 62 : 'CSSM_ALGID_SSL3PreMasterGen', - 63 : 'CSSM_ALGID_SSL3MasterDerive', - 64 : 'CSSM_ALGID_SSL3KeyAndMacDerive', - 65 : 'CSSM_ALGID_SSL3MD5_MAC', - 66 : 'CSSM_ALGID_SSL3SHA1_MAC', - 67 : 'CSSM_ALGID_PKCS5_PBKDF1_MD5', - 68 : 'CSSM_ALGID_PKCS5_PBKDF1_MD2', - 69 : 'CSSM_ALGID_PKCS5_PBKDF1_SHA1', - 70 : 'CSSM_ALGID_WrapLynks', - 71 : 'CSSM_ALGID_WrapSET_OAEP', - 72 : 'CSSM_ALGID_BATON', - 73 : 'CSSM_ALGID_ECDSA', - 74 : 'CSSM_ALGID_MAYFLY', - 75 : 'CSSM_ALGID_JUNIPER', - 76 : 'CSSM_ALGID_FASTHASH', - 77 : 'CSSM_ALGID_3DES', - 78 : 'CSSM_ALGID_SSL3MD5', - 79 : 'CSSM_ALGID_SSL3SHA1', - 80 : 'CSSM_ALGID_FortezzaTimestamp', - 81 : 'CSSM_ALGID_SHA1WithDSA', - 82 : 'CSSM_ALGID_SHA1WithECDSA', - 83 : 'CSSM_ALGID_DSA_BSAFE', - 84 : 'CSSM_ALGID_ECDH', - 85 : 'CSSM_ALGID_ECMQV', - 86 : 'CSSM_ALGID_PKCS12_SHA1_PBE', - 87 : 'CSSM_ALGID_ECNRA', - 88 : 'CSSM_ALGID_SHA1WithECNRA', - 89 : 'CSSM_ALGID_ECES', - 90 : 'CSSM_ALGID_ECAES', - 91 : 'CSSM_ALGID_SHA1HMAC', - 92 : 'CSSM_ALGID_FIPS186Random', - 93 : 'CSSM_ALGID_ECC', - 94 : 'CSSM_ALGID_MQV', - 95 : 'CSSM_ALGID_NRA', - 96 : 'CSSM_ALGID_IntelPlatformRandom', - 97 : 'CSSM_ALGID_UTC', - 98 : 'CSSM_ALGID_HAVAL3', - 99 : 'CSSM_ALGID_HAVAL4', - 100 : 'CSSM_ALGID_HAVAL5', - 101 : 'CSSM_ALGID_TIGER', - 102 : 'CSSM_ALGID_MD5HMAC', - 103 : 'CSSM_ALGID_PKCS5_PBKDF2', - 104 : 'CSSM_ALGID_RUNNING_COUNTER', - 0x7FFFFFFF : 'CSSM_ALGID_LAST' -} - -#CSSM TYPE -## http://www.opensource.apple.com/source/libsecurity_cssm/libsecurity_cssm-36064/lib/cssmtype.h - -########## CSSM_DB_RECORDTYPE ############# - -#/* Industry At Large Application Name Space Range Definition */ -#/* AppleFileDL record types. */ -CSSM_DB_RECORDTYPE_APP_DEFINED_START = 0x80000000 -CSSM_DL_DB_RECORD_GENERIC_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0 -CSSM_DL_DB_RECORD_INTERNET_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 1 -CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 2 -CSSM_DL_DB_RECORD_USER_TRUST = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 3 -CSSM_DL_DB_RECORD_X509_CRL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 4 -CSSM_DL_DB_RECORD_UNLOCK_REFERRAL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 5 -CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 6 - -CSSM_DL_DB_RECORD_X509_CERTIFICATE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x1000 -CSSM_DL_DB_RECORD_METADATA = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x8000 ## DBBlob -CSSM_DB_RECORDTYPE_APP_DEFINED_END = 0xffffffff - -#/* Record Types defined in the Schema Management Name Space */ -CSSM_DB_RECORDTYPE_SCHEMA_START = 0x00000000 -CSSM_DL_DB_SCHEMA_INFO = CSSM_DB_RECORDTYPE_SCHEMA_START + 0 -CSSM_DL_DB_SCHEMA_INDEXES = CSSM_DB_RECORDTYPE_SCHEMA_START + 1 -CSSM_DL_DB_SCHEMA_ATTRIBUTES = CSSM_DB_RECORDTYPE_SCHEMA_START + 2 -CSSM_DL_DB_SCHEMA_PARSING_MODULE = CSSM_DB_RECORDTYPE_SCHEMA_START + 3 -CSSM_DB_RECORDTYPE_SCHEMA_END = CSSM_DB_RECORDTYPE_SCHEMA_START + 4 - -#/* Record Types defined in the Open Group Application Name Space */ -#/* Open Group Application Name Space Range Definition*/ -CSSM_DB_RECORDTYPE_OPEN_GROUP_START = 0x0000000A -CSSM_DL_DB_RECORD_ANY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 0 -CSSM_DL_DB_RECORD_CERT = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 1 -CSSM_DL_DB_RECORD_CRL = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 2 -CSSM_DL_DB_RECORD_POLICY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 3 -CSSM_DL_DB_RECORD_GENERIC = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 4 -CSSM_DL_DB_RECORD_PUBLIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 5 -CSSM_DL_DB_RECORD_PRIVATE_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 6 -CSSM_DL_DB_RECORD_SYMMETRIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 7 -CSSM_DL_DB_RECORD_ALL_KEYS = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 -CSSM_DB_RECORDTYPE_OPEN_GROUP_END = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 -##################### - -######## KEYUSE ######### -CSSM_KEYUSE_ANY = 0x80000000 -CSSM_KEYUSE_ENCRYPT = 0x00000001 -CSSM_KEYUSE_DECRYPT = 0x00000002 -CSSM_KEYUSE_SIGN = 0x00000004 -CSSM_KEYUSE_VERIFY = 0x00000008 -CSSM_KEYUSE_SIGN_RECOVER = 0x00000010 -CSSM_KEYUSE_VERIFY_RECOVER = 0x00000020 -CSSM_KEYUSE_WRAP = 0x00000040 -CSSM_KEYUSE_UNWRAP = 0x00000080 -CSSM_KEYUSE_DERIVE = 0x00000100 -#################### - -############ CERT TYPE ############## -CERT_TYPE = { - 0x00 : 'CSSM_CERT_UNKNOWN', - 0x01 : 'CSSM_CERT_X_509v1', - 0x02 : 'CSSM_CERT_X_509v2', - 0x03 : 'CSSM_CERT_X_509v3', - 0x04 : 'CSSM_CERT_PGP', - 0x05 : 'CSSM_CERT_SPKI', - 0x06 : 'CSSM_CERT_SDSIv1', - 0x08 : 'CSSM_CERT_Intel', - 0x09 : 'CSSM_CERT_X_509_ATTRIBUTE', - 0x0A : 'CSSM_CERT_X9_ATTRIBUTE', - 0x0C : 'CSSM_CERT_ACL_ENTRY', - 0x7FFE: 'CSSM_CERT_MULTIPLE', - 0x7FFF : 'CSSM_CERT_LAST', - 0x8000 : 'CSSM_CL_CUSTOM_CERT_TYPE' -} -#################################### - -########### CERT ENCODING ############# -CERT_ENCODING = { - 0x00 : 'CSSM_CERT_ENCODING_UNKNOWN', - 0x01 : 'CSSM_CERT_ENCODING_CUSTOM', - 0x02 : 'CSSM_CERT_ENCODING_BER', - 0x03 : 'CSSM_CERT_ENCODING_DER', - 0x04 : 'CSSM_CERT_ENCODING_NDR', - 0x05 : 'CSSM_CERT_ENCODING_SEXPR', - 0x06 : 'CSSM_CERT_ENCODING_PGP', - 0x7FFE: 'CSSM_CERT_ENCODING_MULTIPLE', - 0x7FFF : 'CSSM_CERT_ENCODING_LAST' -} - -STD_APPLE_ADDIN_MODULE = { - '{87191ca0-0fc9-11d4-849a-000502b52122}': 'CSSM itself', - '{87191ca1-0fc9-11d4-849a-000502b52122}': 'File based DL (aka "Keychain DL")', - '{87191ca2-0fc9-11d4-849a-000502b52122}': 'Core CSP (local space)', - '{87191ca3-0fc9-11d4-849a-000502b52122}': 'Secure CSP/DL (aka "Keychain CSPDL")', - '{87191ca4-0fc9-11d4-849a-000502b52122}': 'X509 Certificate CL', - '{87191ca5-0fc9-11d4-849a-000502b52122}': 'X509 Certificate TP', - '{87191ca6-0fc9-11d4-849a-000502b52122}': 'DLAP/OpenDirectory access DL', - '{87191ca7-0fc9-11d4-849a-000502b52122}': 'TP for ".mac" related policies', - '{87191ca8-0fc9-11d4-849a-000502b52122}': 'Smartcard CSP/DL', - '{87191ca9-0fc9-11d4-849a-000502b52122}': 'DL for ".mac" certificate access' -} - -SECURE_STORAGE_GROUP = 'ssgp' - -# SecAuthenticationType -AUTH_TYPE = { - 'ntlm': 'kSecAuthenticationTypeNTLM', - 'msna': 'kSecAuthenticationTypeMSN', - 'dpaa': 'kSecAuthenticationTypeDPA', - 'rpaa': 'kSecAuthenticationTypeRPA', - 'http': 'kSecAuthenticationTypeHTTPBasic', - 'httd': 'kSecAuthenticationTypeHTTPDigest', - 'form': 'kSecAuthenticationTypeHTMLForm', - 'dflt': 'kSecAuthenticationTypeDefault', - '': 'kSecAuthenticationTypeAny', - '\x00\x00\x00\x00': 'kSecAuthenticationTypeAny' -} - -# SecProtocolType -PROTOCOL_TYPE = { - 'ftp ': 'kSecProtocolTypeFTP', - 'ftpa': 'kSecProtocolTypeFTPAccount', - 'http': 'kSecProtocolTypeHTTP', - 'irc ': 'kSecProtocolTypeIRC', - 'nntp': 'kSecProtocolTypeNNTP', - 'pop3': 'kSecProtocolTypePOP3', - 'smtp': 'kSecProtocolTypeSMTP', - 'sox ': 'kSecProtocolTypeSOCKS', - 'imap': 'kSecProtocolTypeIMAP', - 'ldap': 'kSecProtocolTypeLDAP', - 'atlk': 'kSecProtocolTypeAppleTalk', - 'afp ': 'kSecProtocolTypeAFP', - 'teln': 'kSecProtocolTypeTelnet', - 'ssh ': 'kSecProtocolTypeSSH', - 'ftps': 'kSecProtocolTypeFTPS', - 'htps': 'kSecProtocolTypeHTTPS', - 'htpx': 'kSecProtocolTypeHTTPProxy', - 'htsx': 'kSecProtocolTypeHTTPSProxy', - 'ftpx': 'kSecProtocolTypeFTPProxy', - 'cifs': 'kSecProtocolTypeCIFS', - 'smb ': 'kSecProtocolTypeSMB', - 'rtsp': 'kSecProtocolTypeRTSP', - 'rtsx': 'kSecProtocolTypeRTSPProxy', - 'daap': 'kSecProtocolTypeDAAP', - 'eppc': 'kSecProtocolTypeEPPC', - 'ipp ': 'kSecProtocolTypeIPP', - 'ntps': 'kSecProtocolTypeNNTPS', - 'ldps': 'kSecProtocolTypeLDAPS', - 'tels': 'kSecProtocolTypeTelnetS', - 'imps': 'kSecProtocolTypeIMAPS', - 'ircs': 'kSecProtocolTypeIRCS', - 'pops': 'kSecProtocolTypePOP3S', - 'cvsp': 'kSecProtocolTypeCVSpserver', - 'svn ': 'kSecProtocolTypeCVSpserver', - 'AdIM': 'kSecProtocolTypeAdiumMessenger', - '\x00\x00\x00\x00': 'kSecProtocolTypeAny' -} - -# This is somewhat gross: we define a bunch of module-level constants based on -# the SecKeychainItem.h defines (FourCharCodes) by passing them through -# struct.unpack and converting them to ctypes.c_long() since we'll never use -# them for non-native APIs - -CARBON_DEFINES = { - 'cdat': 'kSecCreationDateItemAttr', - 'mdat': 'kSecModDateItemAttr', - 'desc': 'kSecDescriptionItemAttr', - 'icmt': 'kSecCommentItemAttr', - 'crtr': 'kSecCreatorItemAttr', - 'type': 'kSecTypeItemAttr', - 'scrp': 'kSecScriptCodeItemAttr', - 'labl': 'kSecLabelItemAttr', - 'invi': 'kSecInvisibleItemAttr', - 'nega': 'kSecNegativeItemAttr', - 'cusi': 'kSecCustomIconItemAttr', - 'acct': 'kSecAccountItemAttr', - 'svce': 'kSecServiceItemAttr', - 'gena': 'kSecGenericItemAttr', - 'sdmn': 'kSecSecurityDomainItemAttr', - 'srvr': 'kSecServerItemAttr', - 'atyp': 'kSecAuthenticationTypeItemAttr', - 'port': 'kSecPortItemAttr', - 'path': 'kSecPathItemAttr', - 'vlme': 'kSecVolumeItemAttr', - 'addr': 'kSecAddressItemAttr', - 'ssig': 'kSecSignatureItemAttr', - 'ptcl': 'kSecProtocolItemAttr', - 'ctyp': 'kSecCertificateType', - 'cenc': 'kSecCertificateEncoding', - 'crtp': 'kSecCrlType', - 'crnc': 'kSecCrlEncoding', - 'alis': 'kSecAlias', - 'inet': 'kSecInternetPasswordItemClass', - 'genp': 'kSecGenericPasswordItemClass', - 'ashp': 'kSecAppleSharePasswordItemClass', - CSSM_DL_DB_RECORD_X509_CERTIFICATE: 'kSecCertificateItemClass' -} \ No newline at end of file diff --git a/chainbreaker.py b/chainbreaker.py index 7dd1795..20c8213 100755 --- a/chainbreaker.py +++ b/chainbreaker.py @@ -3,6 +3,8 @@ # Author : n0fate # E-Mail rapfer@gmail.com, n0fate@n0fate.com # +# 10/7/2020 - Significant changes made by luke@socially-inept.net +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or (at @@ -17,652 +19,312 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # - -import argparse -import os -from sys import exit import struct -from binascii import unhexlify -import datetime -#from hexdump import hexdump - from pbkdf2 import pbkdf2 - +from schema import * +from schema import _APPL_DB_HEADER, _APPL_DB_SCHEMA, _TABLE_HEADER, _DB_BLOB, _GENERIC_PW_HEADER, \ + _KEY_BLOB_REC_HEADER, _KEY_BLOB, _SSGP, _INTERNET_PW_HEADER, _APPLE_SHARE_HEADER, _X509_CERT_HEADER, _SECKEY_HEADER, \ + _UNLOCK_BLOB, _KEYCHAIN_TIME, _INT, _FOUR_CHAR_CODE, _LV, _TABLE_ID, _RECORD_OFFSET from pyDes import triple_des, CBC -from ctypes import * -from Schema import * - -from validator import Validator - -ATOM_SIZE = 4 -KEYCHAIN_SIGNATURE = "kych" -BLOCKSIZE = 8 -KEYLEN = 24 - - -# DATA CLASSES ################################################################# -class _APPL_DB_HEADER(BigEndianStructure): - _fields_ = [ - ("Signature", c_char * 4), - ("Version", c_int), - ("HeaderSize", c_int), - ("SchemaOffset", c_int), - ("AuthOffset", c_int) - ] - - -class _APPL_DB_SCHEMA(BigEndianStructure): - _fields_ = [ - ("SchemaSize", c_int), - ("TableCount", c_int) - ] - - -class _KEY_BLOB_REC_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordCount", c_uint), - ("Dummy", c_char * 0x7C), - ] - - -class _GENERIC_PW_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordNumber", c_uint), - ("Unknown2", c_uint), - ("Unknown3", c_uint), - ("SSGPArea", c_uint), - ("Unknown5", c_uint), - ("CreationDate", c_uint), - ("ModDate", c_uint), - ("Description", c_uint), - ("Comment", c_uint), - ("Creator", c_uint), - ("Type", c_uint), - ("ScriptCode", c_uint), - ("PrintName", c_uint), - ("Alias", c_uint), - ("Invisible", c_uint), - ("Negative", c_uint), - ("CustomIcon", c_uint), - ("Protected", c_uint), - ("Account", c_uint), - ("Service", c_uint), - ("Generic", c_uint) - ] - - -class _APPLE_SHARE_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordNumber", c_uint), - ("Unknown2", c_uint), - ("Unknown3", c_uint), - ("SSGPArea", c_uint), - ("Unknown5", c_uint), - ("CreationDate", c_uint), - ("ModDate", c_uint), - ("Description", c_uint), - ("Comment", c_uint), - ("Creator", c_uint), - ("Type", c_uint), - ("ScriptCode", c_uint), - ("PrintName", c_uint), - ("Alias", c_uint), - ("Invisible", c_uint), - ("Negative", c_uint), - ("CustomIcon", c_uint), - ("Protected", c_uint), - ("Account", c_uint), - ("Volume", c_uint), - ("Server", c_uint), - ("Protocol", c_uint), - ("AuthType", c_uint), - ("Address", c_uint), - ("Signature", c_uint) - ] - - -class _INTERNET_PW_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordNumber", c_uint), - ("Unknown2", c_uint), - ("Unknown3", c_uint), - ("SSGPArea", c_uint), - ("Unknown5", c_uint), - ("CreationDate", c_uint), - ("ModDate", c_uint), - ("Description", c_uint), - ("Comment", c_uint), - ("Creator", c_uint), - ("Type", c_uint), - ("ScriptCode", c_uint), - ("PrintName", c_uint), - ("Alias", c_uint), - ("Invisible", c_uint), - ("Negative", c_uint), - ("CustomIcon", c_uint), - ("Protected", c_uint), - ("Account", c_uint), - ("SecurityDomain", c_uint), - ("Server", c_uint), - ("Protocol", c_uint), - ("AuthType", c_uint), - ("Port", c_uint), - ("Path", c_uint) - ] - - -class _X509_CERT_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordNumber", c_uint), - ("Unknown1", c_uint), - ("Unknown2", c_uint), - ("CertSize", c_uint), - ("Unknown3", c_uint), - ("CertType", c_uint), - ("CertEncoding", c_uint), - ("PrintName", c_uint), - ("Alias", c_uint), - ("Subject", c_uint), - ("Issuer", c_uint), - ("SerialNumber", c_uint), - ("SubjectKeyIdentifier", c_uint), - ("PublicKeyHash", c_uint) - ] - - -# http://www.opensource.apple.com/source/Security/Security-55179.1/include/security_cdsa_utilities/KeySchema.h -# http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36940/lib/SecKey.h -class _SECKEY_HEADER(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint32), - ("RecordNumber", c_uint32), - ("Unknown1", c_uint32), - ("Unknown2", c_uint32), - ("BlobSize", c_uint32), - ("Unknown3", c_uint32), - ("KeyClass", c_uint32), - ("PrintName", c_uint32), - ("Alias", c_uint32), - ("Permanent", c_uint32), - ("Private", c_uint32), - ("Modifiable", c_uint32), - ("Label", c_uint32), - ("ApplicationTag", c_uint32), - ("KeyCreator", c_uint32), - ("KeyType", c_uint32), - ("KeySizeInBits", c_uint32), - ("EffectiveKeySize", c_uint32), - ("StartDate", c_uint32), - ("EndDate", c_uint32), - ("Sensitive", c_uint32), - ("AlwaysSensitive", c_uint32), - ("Extractable", c_uint32), - ("NeverExtractable", c_uint32), - ("Encrypt", c_uint32), - ("Decrypt", c_uint32), - ("Derive", c_uint32), - ("Sign", c_uint32), - ("Verify", c_uint32), - ("SignRecover", c_uint32), - ("VerifyRecover", c_uint32), - ("Wrap", c_uint32), - ("UnWrap", c_uint32) - ] - - -class _TABLE_HEADER(BigEndianStructure): - _fields_ = [ - ("TableSize", c_uint), - ("TableId", c_uint), - ("RecordCount", c_uint), - ("Records", c_uint), - ("IndexesOffset", c_uint), - ("FreeListHead", c_uint), - ("RecordNumbersCount", c_uint), - ] - - -""" -class _SCHEMA_INFO_RECORD(BigEndianStructure): - _fields_ = [ - ("RecordSize", c_uint), - ("RecordNumber", c_uint), - ("Unknown2", c_uint), - ("Unknown3", c_uint), - ("Unknown4", c_uint), - ("Unknown5", c_uint), - ("Unknown6", c_uint), - ("RecordType", c_uint), - ("DataSize", c_uint), - ("Data", c_uint) - ] -""" - - -class _COMMON_BLOB(BigEndianStructure): - _fields_ = [ - ("magic", c_uint32), - ("blobVersion", c_uint32) - ] - - -# _ENCRYPTED_BLOB_METADATA -class _KEY_BLOB(BigEndianStructure): - _fields_ = [ - ("CommonBlob", _COMMON_BLOB), - ("startCryptoBlob", c_uint32), - ("totalLength", c_uint32), - ("iv", c_ubyte * 8) - ] - - -class _DB_PARAMETERS(BigEndianStructure): - _fields_ = [ - ("idleTimeout", c_uint32), # uint32 - ("lockOnSleep", c_uint32) # uint8 - ] - - -class _DB_BLOB(BigEndianStructure): - _fields_ = [ - ("CommonBlob", _COMMON_BLOB), - ("startCryptoBlob", c_uint32), - ("totalLength", c_uint32), - ("randomSignature", c_ubyte * 16), - ("sequence", c_uint32), - ("params", _DB_PARAMETERS), - ("salt", c_ubyte * 20), - ("iv", c_ubyte * 8), - ("blobSignature", c_ubyte * 20) - ] - - -class _SSGP(BigEndianStructure): - _fields_ = [ - ("magic", c_char * 4), - ("label", c_ubyte * 16), - ("iv", c_ubyte * 8) - ] - -# /var/db/SystemKey contains the master key for /Library/Keychains/System.keychain -class _UNLOCK_BLOB(BigEndianStructure): - _fields_ = [ - ("CommonBlob", _COMMON_BLOB), - ("masterKey", c_char*24), - ("blobSignature", c_ubyte * 16) - ] - - -# UTILITY FUNCTIONS ############################################################ -def _memcpy(buf, fmt): - return cast(c_char_p(buf), POINTER(fmt)).contents - - -def add_file(directory, filename='default', key=None, cert=None): - # print 'into function key={}, cert={}'.format(key, cert) - target_path = BASEPATH + directory - if not os.path.exists(target_path): - os.makedirs(target_path) - if key is not None: - with open(target_path + '/{}.key'.format(filename), 'w+') as f: - f.write(key) - if cert is not None: - with open(target_path + '/{}.crt'.format(filename), 'w+') as f: - f.write(cert) - -# SOURCE : extractkeychain.py -def kcdecrypt(key, iv, data): - if len(data) == 0: - # print>>stderr, "FileSize is 0" - return '' - - if len(data) % BLOCKSIZE != 0: - return '' - - cipher = triple_des(key, CBC, str(bytearray(iv))) - - # the line below is for pycrypto instead - # cipher = DES3.new( key, DES3.MODE_CBC, iv ) - - plain = cipher.decrypt(data) - - # now check padding - pad = ord(plain[-1]) - if pad > 8: - # print>> stderr, "Bad padding byte. You probably have a wrong password" - return '' - - - for z in plain[-pad:]: - if ord(z) != pad: - # print>> stderr, "Bad padding. You probably have a wrong password" - return '' - - plain = plain[:-pad] - - return plain - +from binascii import unhexlify, hexlify +import logging +import base64 +import string + + +class Chainbreaker(object): + ATOM_SIZE = 4 + KEYCHAIN_SIGNATURE = "kych" + BLOCKSIZE = 8 + KEYLEN = 24 + MAGIC_CMS_IV = unhexlify('4adda22c79e82105') + KEYCHAIN_LOCKED_SIGNATURE = '[Invalid Password / Keychain Locked]' + KEYCHAIN_PASSWORD_HASH_FORMAT = "$keychain$*%s*%s*%s" + + def __init__(self, filepath, unlock_password=None, unlock_key=None, unlock_file=None): + self._filepath = None + self._unlock_password = None + self._unlock_key = None + self._unlock_file = None + self._db_key = None + + self.kc_buffer = '' + + self.header = None + self.schema_info = None + self.table_list = None + self.table_metadata = None + self.record_list = None + self.table_count = None + self.table_enum = None + self.symmetric_key_list = None + self.symmetric_key_offset = None + self.dbblob = None + self.unlocked = False + + self.logger = logging.getLogger('Chainbreaker') + + self.key_list = {} + + self.db_key = None -# KEYCHAIN CLASS ############################################################### -class KeyChain(): - def __init__(self, filepath): self.filepath = filepath - self.fbuf = '' - - def open(self): - try: - fhandle = open(self.filepath, 'rb') - except: - return False - self.fbuf = fhandle.read() - if len(self.fbuf): - fhandle.close() - return True - return False - - def checkValidKeychain(self): - if self.fbuf[0:4] != KEYCHAIN_SIGNATURE: - return False - return True - - ## get apple DB Header - def getHeader(self): - header = _memcpy(self.fbuf[:sizeof(_APPL_DB_HEADER)], _APPL_DB_HEADER) - - return header - - def getSchemaInfo(self, offset): - table_list = [] - # schema_info = struct.unpack(APPL_DB_SCHEMA, self.fbuf[offset:offset + APPL_DB_SCHEMA_SIZE]) - _schemainfo = _memcpy(self.fbuf[offset:offset + sizeof(_APPL_DB_SCHEMA)], _APPL_DB_SCHEMA) - for i in xrange(_schemainfo.TableCount): - BASE_ADDR = sizeof(_APPL_DB_HEADER) + sizeof(_APPL_DB_SCHEMA) - table_list.append( - struct.unpack('>I', self.fbuf[BASE_ADDR + (ATOM_SIZE * i):BASE_ADDR + (ATOM_SIZE * i) + ATOM_SIZE])[0]) - - return _schemainfo, table_list - - def getTable(self, offset): - record_list = [] - BASE_ADDR = sizeof(_APPL_DB_HEADER) + offset - - TableMetaData = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_TABLE_HEADER)], _TABLE_HEADER) - - RECORD_OFFSET_BASE = BASE_ADDR + sizeof(_TABLE_HEADER) - - record_count = 0 - offset = 0 - while TableMetaData.RecordCount != record_count: - RecordOffset = struct.unpack('>I', self.fbuf[ - RECORD_OFFSET_BASE + (ATOM_SIZE * offset):RECORD_OFFSET_BASE + ( - ATOM_SIZE * offset) + ATOM_SIZE])[0] - # if len(record_list) >= 1: - # if record_list[len(record_list)-1] >= RecordOffset: - # continue - if (RecordOffset != 0x00) and (RecordOffset % 4 == 0): - record_list.append(RecordOffset) - # print ' [-] Record Offset: 0x%.8x'%RecordOffset - record_count += 1 - offset += 1 - - return TableMetaData, record_list - - def getTablenametoList(self, recordList, tableList): - TableDic = {} - for count in xrange(len(recordList)): - tableMeta, GenericList = self.getTable(tableList[count]) - TableDic[tableMeta.TableId] = count # extract valid table list - - return len(recordList), TableDic - def getKeyblobRecord(self, base_addr, offset): + if not self._is_valid_keychain(): + self.logger.warning('Keychain signature does not match. are you sure this is a valid keychain file?') - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset + self.unlock_password = unlock_password + self.unlock_key = unlock_key + self.unlock_file = unlock_file - KeyBlobRecHeader = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_KEY_BLOB_REC_HEADER)], _KEY_BLOB_REC_HEADER) + def dump_generic_passwords(self): + entries = [] + try: + table_metadata, generic_pw_list = self._get_table( + self._get_table_offset(CSSM_DL_DB_RECORD_GENERIC_PASSWORD)) - record = self.fbuf[ - BASE_ADDR + sizeof(_KEY_BLOB_REC_HEADER):BASE_ADDR + KeyBlobRecHeader.RecordSize] # password data area + for generic_pw_id in generic_pw_list: + entries.append( + self._get_generic_password_record(self._get_table_offset(CSSM_DL_DB_RECORD_GENERIC_PASSWORD), + generic_pw_id)) - KeyBlobRecord = _memcpy(record[:+sizeof(_KEY_BLOB)], _KEY_BLOB) - # hexdump(KeyBlobRecord.iv) + except KeyError: + self.logger.warning('[!] Generic Password Table is not available') - if SECURE_STORAGE_GROUP != str(record[KeyBlobRecord.totalLength + 8:KeyBlobRecord.totalLength + 8 + 4]): - return '', '', '', 1 + return entries - CipherLen = KeyBlobRecord.totalLength - KeyBlobRecord.startCryptoBlob - if CipherLen % BLOCKSIZE != 0: - print "Bad ciphertext len" - return '', '', '', 1 + def dump_internet_passwords(self): + entries = [] + try: + table_metadata, internet_pw_list = self._get_table( + self._get_table_offset(CSSM_DL_DB_RECORD_INTERNET_PASSWORD)) - ciphertext = record[KeyBlobRecord.startCryptoBlob:KeyBlobRecord.totalLength] + for internet_pw_id in internet_pw_list: + entries.append(self._get_internet_password_record( + self._get_table_offset(CSSM_DL_DB_RECORD_INTERNET_PASSWORD), internet_pw_id)) - # match data, keyblob_ciphertext, Initial Vector, success - return record[KeyBlobRecord.totalLength + 8:KeyBlobRecord.totalLength + 8 + 20], ciphertext, KeyBlobRecord.iv, 0 + except KeyError: + self.logger.warning('[!] Internet Password Table is not available') + return entries - def getGenericPWRecord(self, base_addr, offset): - record = [] + def dump_appleshare_passwords(self): + entries = [] + try: + table_metadata, appleshare_pw_list = self._get_table( + self._get_table_offset(CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD)) - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset + for appleshare_pw_offset in appleshare_pw_list: + entries.append(self._get_appleshare_record( + self._get_table_offset(CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD), appleshare_pw_offset)) - RecordMeta = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_GENERIC_PW_HEADER)], _GENERIC_PW_HEADER) + except KeyError: + self.logger.warning('[!] Appleshare Records Table is not available') + return entries - Buffer = self.fbuf[BASE_ADDR + sizeof( - _GENERIC_PW_HEADER):BASE_ADDR + RecordMeta.RecordSize] # record_meta[0] => record size + def dump_x509_certificates(self): + entries = [] + try: + table_metadata, x509_cert_list = self._get_table(self._get_table_offset(CSSM_DL_DB_RECORD_X509_CERTIFICATE)) - if RecordMeta.SSGPArea != 0: - record.append(Buffer[:RecordMeta.SSGPArea]) - else: - record.append('') + for i, x509_cert_offset in enumerate(x509_cert_list, 1): + entries.append( + self._get_x_509_record(self._get_table_offset(CSSM_DL_DB_RECORD_X509_CERTIFICATE), + x509_cert_offset)) - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE)) - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE)) + except KeyError: + self.logger.warning('[!] Certificate Table is not available') - record.append(self.getLV(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE)) + return entries - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE)) - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE)) + def dump_public_keys(self): + entries = [] + try: + table_metadata, public_key_list = self._get_table(self._get_table_offset(CSSM_DL_DB_RECORD_PUBLIC_KEY)) + for public_key_offset in public_key_list: + entries.append( + self._get_public_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PUBLIC_KEY), + public_key_offset)) + except KeyError: + self.logger.warning('[!] Public Key Table is not available') + return entries + + def dump_private_keys(self): + entries = [] + try: + table_meta, private_key_list = self._get_table(self._get_table_offset(CSSM_DL_DB_RECORD_PRIVATE_KEY)) + for i, private_key_offset in enumerate(private_key_list, 1): + entries.append( + self._get_private_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PRIVATE_KEY), + private_key_offset)) - record.append(self.getLV(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Service & 0xFFFFFFFE)) + except KeyError: + self.logger.warning('[!] Private Key Table is not available') + return entries - return record + def _read_keychain_to_buffer(self): + try: + with open(self.filepath, 'rb') as fp: + self.kc_buffer = fp.read() - def getInternetPWRecord(self, base_addr, offset): - record = [] + if self.kc_buffer: + self.header = _APPL_DB_HEADER(self.kc_buffer[:_APPL_DB_HEADER.STRUCT.size]) + self.schema_info, self.table_list = self._get_schema_info(self.header.SchemaOffset) + self.table_metadata, self.record_list = self._get_table(self.table_list[0]) + self.table_count, self.table_enum = self._get_table_name_to_list(self.record_list, self.table_list) - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset + self.symmetric_key_offset = self.table_list[self.table_enum[CSSM_DL_DB_RECORD_METADATA]] - RecordMeta = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_INTERNET_PW_HEADER)], _INTERNET_PW_HEADER) + self.base_addr = _APPL_DB_HEADER.STRUCT.size + self.symmetric_key_offset + 0x38 + self.dbblob = _DB_BLOB(self.kc_buffer[self.base_addr:self.base_addr + _DB_BLOB.STRUCT.size]) - Buffer = self.fbuf[BASE_ADDR + sizeof(_INTERNET_PW_HEADER):BASE_ADDR + RecordMeta.RecordSize] - if RecordMeta.SSGPArea != 0: - record.append(Buffer[:RecordMeta.SSGPArea]) - else: - record.append('') + except Exception as e: + self.logger.critical("Unable to read keychain: %s" % (e)) - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE)) - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE)) + def _is_valid_keychain(self): + if self.kc_buffer[0:4] != Chainbreaker.KEYCHAIN_SIGNATURE: + return False + return True - record.append(self.getLV(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Comment & 0xFFFFFFFE)) + def _generate_key_list(self): + table_meta_data, symmetric_key_list = self._get_table(self._get_table_offset(CSSM_DL_DB_RECORD_SYMMETRIC_KEY)) - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE)) - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE)) + for symmetric_key_record in symmetric_key_list: + keyblob, ciphertext, iv, return_value = self._get_keyblob_record( + self._get_table_offset(CSSM_DL_DB_RECORD_SYMMETRIC_KEY), symmetric_key_record) + if return_value == 0: + password = self._keyblob_decryption(ciphertext, iv, self.db_key) + if password != '': + self.key_list[keyblob] = password - record.append(self.getLV(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Protected & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.SecurityDomain & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Server & 0xFFFFFFFE)) + def _get_schema_info(self, offset): + table_list = [] + _schema_info = _APPL_DB_SCHEMA(self.kc_buffer[offset:offset + _APPL_DB_SCHEMA.STRUCT.size]) - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Protocol & 0xFFFFFFFE)) + for i in xrange(_schema_info.TableCount): + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + _APPL_DB_SCHEMA.STRUCT.size + table_list.append(_TABLE_ID(self.kc_buffer[BASE_ADDR + (Chainbreaker.ATOM_SIZE * i):BASE_ADDR + ( + Chainbreaker.ATOM_SIZE * i) + Chainbreaker.ATOM_SIZE]).Value) - record.append(self.getLV(BASE_ADDR, RecordMeta.AuthType & 0xFFFFFFFE)) + return _schema_info, table_list - record.append(self.getInt(BASE_ADDR, RecordMeta.Port & 0xFFFFFFFE)) + def _get_table_offset(self, table_name): + return self.table_list[self.table_enum[table_name]] - record.append(self.getLV(BASE_ADDR, RecordMeta.Path & 0xFFFFFFFE)) + def _get_table(self, offset): + record_list = [] - return record + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + offset + table_metadata = _TABLE_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _TABLE_HEADER.STRUCT.size]) + RECORD_OFFSET_BASE = BASE_ADDR + _TABLE_HEADER.STRUCT.size - def getx509Record(self, base_addr, offset): - record = [] + record_count = 0 + offset = 0 + while table_metadata.RecordCount != record_count: + record_offset = _RECORD_OFFSET(self.kc_buffer[ + RECORD_OFFSET_BASE + (Chainbreaker.ATOM_SIZE * offset):RECORD_OFFSET_BASE + ( + Chainbreaker.ATOM_SIZE * offset) + Chainbreaker.ATOM_SIZE]).Value - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset + if (record_offset != 0x00) and (record_offset % 4 == 0): + record_list.append(record_offset) + record_count += 1 + offset += 1 - RecordMeta = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_X509_CERT_HEADER)], _X509_CERT_HEADER) + return table_metadata, record_list - x509Certificate = self.fbuf[BASE_ADDR + sizeof(_X509_CERT_HEADER):BASE_ADDR + sizeof( - _X509_CERT_HEADER) + RecordMeta.CertSize] + # + def _get_table_name_to_list(self, record_list, table_list): + table_dict = {} + for count in xrange(len(record_list)): + table_metadata, generic_list = self._get_table(table_list[count]) + table_dict[table_metadata.TableId] = count # extract valid table list - record.append(self.getInt(BASE_ADDR, RecordMeta.CertType & 0xFFFFFFFE)) # Cert Type - record.append(self.getInt(BASE_ADDR, RecordMeta.CertEncoding & 0xFFFFFFFE)) # Cert Encoding + return len(record_list), table_dict - record.append(self.getLV(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Subject & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Issuer & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.SerialNumber & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.SubjectKeyIdentifier & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.PublicKeyHash & 0xFFFFFFFE)) + def _get_keyblob_record(self, base_addr, offset): - record.append(x509Certificate) - return record + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset - def getKeyRecord(self, base_addr, offset): ## PUBLIC and PRIVATE KEY - record = [] + KeyBlobRecHeader = _KEY_BLOB_REC_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _KEY_BLOB_REC_HEADER.STRUCT.size]) - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset + record = self.kc_buffer[ + BASE_ADDR + _KEY_BLOB_REC_HEADER.STRUCT.size:BASE_ADDR + KeyBlobRecHeader.RecordSize] # password data area - RecordMeta = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_SECKEY_HEADER)], _SECKEY_HEADER) + KeyBlobRecord = _KEY_BLOB(record[:+_KEY_BLOB.STRUCT.size]) - KeyBlob = self.fbuf[BASE_ADDR + sizeof(_SECKEY_HEADER):BASE_ADDR + sizeof(_SECKEY_HEADER) + RecordMeta.BlobSize] + if SECURE_STORAGE_GROUP != str(record[KeyBlobRecord.TotalLength + 8:KeyBlobRecord.TotalLength + 8 + 4]): + return '', '', '', 1 - record.append(self.getLV(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Label & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.KeyClass & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.Private & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.KeyType & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.KeySizeInBits & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.EffectiveKeySize & 0xFFFFFFFE)) - record.append(self.getInt(BASE_ADDR, RecordMeta.Extractable & 0xFFFFFFFE)) - record.append(str(self.getLV(BASE_ADDR, RecordMeta.KeyCreator & 0xFFFFFFFE)).split('\x00')[0]) + CipherLen = KeyBlobRecord.TotalLength - KeyBlobRecord.StartCryptoBlob + if CipherLen % Chainbreaker.BLOCKSIZE != 0: + self.logger.debug("Bad ciphertext length.") + return '', '', '', 1 - IV, Key = self.getEncryptedDatainBlob(KeyBlob) - record.append(IV) - record.append(Key) + ciphertext = record[KeyBlobRecord.StartCryptoBlob:KeyBlobRecord.TotalLength] - return record + # match data, keyblob_ciphertext, Initial Vector, success + return record[KeyBlobRecord.TotalLength + 8:KeyBlobRecord.TotalLength + 8 + 20], ciphertext, KeyBlobRecord.IV, 0 - def getEncryptedDatainBlob(self, BlobBuf): - KeyBlob = _memcpy(BlobBuf[:sizeof(_KEY_BLOB)], _KEY_BLOB) + def _get_encrypted_data_in_blob(self, BlobBuf): + KeyBlob = _KEY_BLOB(BlobBuf[:_KEY_BLOB.STRUCT.size]) - if KeyBlob.CommonBlob.magic != 0xFADE0711: + if KeyBlob.CommonBlob.Magic != _KEY_BLOB.COMMON_BLOB_MAGIC: return '', '' - KeyData = BlobBuf[KeyBlob.startCryptoBlob:KeyBlob.totalLength] - return KeyBlob.iv, KeyData # IV, Encrypted Data + KeyData = BlobBuf[KeyBlob.StartCryptoBlob:KeyBlob.TotalLength] + return KeyBlob.IV, KeyData # IV, Encrypted Data - def getKeychainTime(self, BASE_ADDR, pCol): + def _get_keychain_time(self, BASE_ADDR, pCol): if pCol <= 0: return '' else: - data = str(struct.unpack('>16s', self.fbuf[BASE_ADDR + pCol:BASE_ADDR + pCol + struct.calcsize('>16s')])[0]) - return datetime.datetime.strptime(data.strip('\x00'), '%Y%m%d%H%M%SZ') + return _KEYCHAIN_TIME(self.kc_buffer[BASE_ADDR + pCol:BASE_ADDR + pCol + _KEYCHAIN_TIME.STRUCT.size]).Time - def getInt(self, BASE_ADDR, pCol): + def _get_int(self, BASE_ADDR, pCol): if pCol <= 0: return 0 else: - return struct.unpack('>I', self.fbuf[BASE_ADDR + pCol:BASE_ADDR + pCol + 4])[0] + return _INT(self.kc_buffer[BASE_ADDR + pCol:BASE_ADDR + pCol + 4]).Value - def getFourCharCode(self, BASE_ADDR, pCol): + def _get_four_char_code(self, BASE_ADDR, pCol): if pCol <= 0: return '' else: - return struct.unpack('>4s', self.fbuf[BASE_ADDR + pCol:BASE_ADDR + pCol + 4])[0] + return _FOUR_CHAR_CODE(self.kc_buffer[BASE_ADDR + pCol:BASE_ADDR + pCol + 4]).Value - def getLV(self, BASE_ADDR, pCol): + def _get_lv(self, BASE_ADDR, pCol): if pCol <= 0: return '' - str_length = struct.unpack('>I', self.fbuf[BASE_ADDR + pCol:BASE_ADDR + pCol + 4])[0] + str_length = _INT(self.kc_buffer[BASE_ADDR + pCol:BASE_ADDR + pCol + 4]).Value # 4byte arrangement if (str_length % 4) == 0: real_str_len = (str_length / 4) * 4 else: real_str_len = ((str_length / 4) + 1) * 4 - unpack_value = '>' + str(real_str_len) + 's' + try: - data = struct.unpack(unpack_value, self.fbuf[BASE_ADDR + pCol + 4:BASE_ADDR + pCol + 4 + real_str_len])[0] + data = _LV(self.kc_buffer[BASE_ADDR + pCol + 4:BASE_ADDR + pCol + 4 + real_str_len], real_str_len).Value except struct.error: - # print 'Length is too long : %d'%real_str_len + self.logger.debug('LV string length is too long.') return '' - return data - def getAppleshareRecord(self, base_addr, offset): - record = [] - - BASE_ADDR = sizeof(_APPL_DB_HEADER) + base_addr + offset - - RecordMeta = _memcpy(self.fbuf[BASE_ADDR:BASE_ADDR + sizeof(_APPLE_SHARE_HEADER)], _APPLE_SHARE_HEADER) - - Buffer = self.fbuf[BASE_ADDR + sizeof(_APPLE_SHARE_HEADER):BASE_ADDR + RecordMeta.RecordSize] - - if RecordMeta.SSGPArea != 0: - record.append(Buffer[:RecordMeta.SSGPArea]) - else: - record.append('') - - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE)) - record.append(self.getKeychainTime(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE)) - - record.append(self.getLV(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Comment & 0xFFFFFFFE)) - - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE)) - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE)) - - record.append(self.getLV(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Protected & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Volume & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Server & 0xFFFFFFFE)) - - record.append(self.getFourCharCode(BASE_ADDR, RecordMeta.Protocol & 0xFFFFFFFE)) - - record.append(self.getLV(BASE_ADDR, RecordMeta.Address & 0xFFFFFFFE)) - record.append(self.getLV(BASE_ADDR, RecordMeta.Signature & 0xFFFFFFFE)) - - return record + return data - ## decrypted dbblob area - ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT - ## http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36620/lib/StorageManager.cpp - def SSGPDecryption(self, ssgp, dbkey): - SSGP = _memcpy(ssgp, _SSGP) - plain = kcdecrypt(dbkey, SSGP.iv, ssgp[sizeof(_SSGP):]) - - return plain + # + # ## decrypted dbblob area + # ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT + # ## http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36620/lib/StorageManager.cpp + def _ssgp_decryption(self, ssgp, dbkey): + return Chainbreaker._kcdecrypt(dbkey, _SSGP(ssgp).IV, ssgp[_SSGP.STRUCT.size:]) # Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT # source : http://www.opensource.apple.com/source/libsecurity_cdsa_client/libsecurity_cdsa_client-36213/lib/securestorage.cpp # magicCmsIV : http://www.opensource.apple.com/source/Security/Security-28/AppleCSP/AppleCSP/wrapKeyCms.cpp - def KeyblobDecryption(self, encryptedblob, iv, dbkey): + def _keyblob_decryption(self, encryptedblob, iv, dbkey): - magicCmsIV = unhexlify('4adda22c79e82105') - plain = kcdecrypt(dbkey, magicCmsIV, encryptedblob) + # magicCmsIV = unhexlify('4adda22c79e82105') + plain = Chainbreaker._kcdecrypt(dbkey, Chainbreaker.MAGIC_CMS_IV, encryptedblob) if plain.__len__() == 0: return '' @@ -674,21 +336,20 @@ def KeyblobDecryption(self, encryptedblob, iv, dbkey): revplain += plain[31 - i] # now the real key gets found. */ - plain = kcdecrypt(dbkey, iv, revplain) + plain = Chainbreaker._kcdecrypt(dbkey, iv, revplain) keyblob = plain[4:] - if len(keyblob) != KEYLEN: - # raise "Bad decrypted keylen!" + if len(keyblob) != Chainbreaker.KEYLEN: + self.logger.debug("Decrypted key length is not valid") return '' return keyblob - # test code - # http://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55044/lib/KeyItem.cpp - def PrivateKeyDecryption(self, encryptedblob, iv, dbkey): - magicCmsIV = unhexlify('4adda22c79e82105') - plain = kcdecrypt(dbkey, magicCmsIV, encryptedblob) + # + # # http://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55044/lib/KeyItem.cpp + def _private_key_decryption(self, encryptedblob, iv): + plain = Chainbreaker._kcdecrypt(self.db_key, Chainbreaker.MAGIC_CMS_IV, encryptedblob) if plain.__len__() == 0: return '', '' @@ -700,323 +361,779 @@ def PrivateKeyDecryption(self, encryptedblob, iv, dbkey): revplain += plain[len(plain) - 1 - i] # now the real key gets found. */ - plain = kcdecrypt(dbkey, iv, revplain) + plain = Chainbreaker._kcdecrypt(self.db_key, iv, revplain) Keyname = plain[:12] # Copied Buffer when user click on right and copy a key on Keychain Access keyblob = plain[12:] return Keyname, keyblob - ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT - def generateMasterKey(self, pw, symmetrickey_offset): + # ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT + def _generate_master_key(self, pw): + return pbkdf2(pw, str(bytearray(self.dbblob.Salt)), 1000, Chainbreaker.KEYLEN) - base_addr = sizeof(_APPL_DB_HEADER) + symmetrickey_offset + 0x38 # header - dbblob = _memcpy(self.fbuf[base_addr:base_addr + sizeof(_DB_BLOB)], _DB_BLOB) + # ## find DBBlob and extract Wrapping key + def _find_wrapping_key(self, master): + # get cipher text area + ciphertext = self.kc_buffer[ + self.base_addr + self.dbblob.StartCryptoBlob:self.base_addr + self.dbblob.TotalLength] - masterkey = pbkdf2(pw, str(bytearray(dbblob.salt)), 1000, KEYLEN) - return masterkey + # decrypt the key + plain = Chainbreaker._kcdecrypt(master, self.dbblob.IV, ciphertext) - ## find DBBlob and extract Wrapping key - def findWrappingKey(self, master, symmetrickey_offset): + if plain.__len__() < Chainbreaker.KEYLEN: + return '' - base_addr = sizeof(_APPL_DB_HEADER) + symmetrickey_offset + 0x38 + dbkey = plain[:Chainbreaker.KEYLEN] - dbblob = _memcpy(self.fbuf[base_addr:base_addr + sizeof(_DB_BLOB)], _DB_BLOB) + # return encrypted wrapping key + return dbkey - # get cipher text area - ciphertext = self.fbuf[base_addr + dbblob.startCryptoBlob:base_addr + dbblob.totalLength] + def dump_keychain_password_hash(self): + cyphertext = hexlify( + self.kc_buffer[self.base_addr + self.dbblob.StartCryptoBlob:self.base_addr + self.dbblob.TotalLength]) + + iv = hexlify(self.dbblob.IV) + salt = hexlify(self.dbblob.Salt) + + return Chainbreaker.KEYCHAIN_PASSWORD_HASH_FORMAT % (salt, iv, cyphertext) + + def _get_appleshare_record(self, base_addr, offset): + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset + + RecordMeta = _APPLE_SHARE_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _APPLE_SHARE_HEADER.STRUCT.size]) + + Buffer = self.kc_buffer[BASE_ADDR + _APPLE_SHARE_HEADER.STRUCT.size:BASE_ADDR + RecordMeta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(RecordMeta, Buffer) + + return self.AppleshareRecord( + created=self._get_keychain_time(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE), + description=self._get_lv(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE), + comment=self._get_lv(BASE_ADDR, RecordMeta.Comment & 0xFFFFFFFE), + creator=self._get_four_char_code(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE), + print_name=self._get_lv(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE), + protected=self._get_lv(BASE_ADDR, RecordMeta.Protected & 0xFFFFFFFE), + account=self._get_lv(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE), + volume=self._get_lv(BASE_ADDR, RecordMeta.Volume & 0xFFFFFFFE), + server=self._get_lv(BASE_ADDR, RecordMeta.Server & 0xFFFFFFFE), + protocol_type=self._get_four_char_code(BASE_ADDR, RecordMeta.Protocol & 0xFFFFFFFE), + address=self._get_lv(BASE_ADDR, RecordMeta.Address & 0xFFFFFFFE), + signature=self._get_lv(BASE_ADDR, RecordMeta.Signature & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey + ) + + def _get_private_key_record(self, base_addr, offset): + record = self._get_key_record(base_addr, offset) + + if not self.db_key: + keyname = privatekey = Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + else: + keyname, privatekey = self._private_key_decryption(record[10], record[9]) + return self.PrivateKeyRecord( + print_name=record[0], + label=record[1], + key_class=KEY_TYPE[record[2]], + private=record[3], + key_type=record[4], + key_size=record[5], + effective_key_size=record[6], + extracted=record[7], + cssm_type=record[8], + iv=record[9], + key=record[10], + key_name=keyname, + private_key=privatekey, + ) + + def _get_public_key_record(self, base_addr, offset): + record = self._get_key_record(base_addr, offset) + return self.PublicKeyRecord( + print_name=record[0], + label=record[1], + key_class=KEY_TYPE[record[2]], + private=record[3], + key_type=record[4], + key_size=record[5], + effective_key_size=record[6], + extracted=record[7], + cssm_type=record[8], + iv=record[9], + public_key=record[10], + ) + + def _get_key_record(self, base_addr, offset): ## PUBLIC and PRIVATE KEY + + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset + + RecordMeta = _SECKEY_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _SECKEY_HEADER.STRUCT.size]) + + KeyBlob = self.kc_buffer[ + BASE_ADDR + _SECKEY_HEADER.STRUCT.size:BASE_ADDR + _SECKEY_HEADER.STRUCT.size + RecordMeta.BlobSize] + + IV, Key = self._get_encrypted_data_in_blob(KeyBlob) + + return [self._get_lv(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE), + self._get_lv(BASE_ADDR, RecordMeta.Label & 0xFFFFFFFE), + self._get_int(BASE_ADDR, RecordMeta.KeyClass & 0xFFFFFFFE), + self._get_int(BASE_ADDR, RecordMeta.Private & 0xFFFFFFFE), + CSSM_ALGORITHMS[self._get_int(BASE_ADDR, RecordMeta.KeyType & 0xFFFFFFFE)], + self._get_int(BASE_ADDR, RecordMeta.KeySizeInBits & 0xFFFFFFFE), + self._get_int(BASE_ADDR, RecordMeta.EffectiveKeySize & 0xFFFFFFFE), + self._get_int(BASE_ADDR, RecordMeta.Extractable & 0xFFFFFFFE), + STD_APPLE_ADDIN_MODULE[ + str(self._get_lv(BASE_ADDR, RecordMeta.KeyCreator & 0xFFFFFFFE)).split('\x00')[0]], + IV, + Key] + + def _get_x_509_record(self, base_addr, offset): + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset + RecordMeta = _X509_CERT_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _X509_CERT_HEADER.STRUCT.size]) + + return self.X509CertificateRecord( + type=self._get_int(BASE_ADDR, RecordMeta.CertType & 0xFFFFFFFE), + encoding=self._get_int(BASE_ADDR, RecordMeta.CertEncoding & 0xFFFFFFFE), + print_name=self._get_lv(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE), + subject=self._get_lv(BASE_ADDR, RecordMeta.Subject & 0xFFFFFFFE), + issuer=self._get_lv(BASE_ADDR, RecordMeta.Issuer & 0xFFFFFFFE), + serial_number=self._get_lv(BASE_ADDR, RecordMeta.SerialNumber & 0xFFFFFFFE), + subject_key_identifier=self._get_lv(BASE_ADDR, RecordMeta.SubjectKeyIdentifier & 0xFFFFFFFE), + public_key_hash=self._get_lv(BASE_ADDR, RecordMeta.PublicKeyHash & 0xFFFFFFFE), + certificate=self.kc_buffer[ + BASE_ADDR + _X509_CERT_HEADER.STRUCT.size:BASE_ADDR + _X509_CERT_HEADER.STRUCT.size + RecordMeta.CertSize] + ) + + def _extract_ssgp_and_dbkey(self, recordmeta, buffer): + ssgp = None + dbkey = None + + if recordmeta.SSGPArea != 0: + ssgp = _SSGP(buffer[:recordmeta.SSGPArea]) + dbkey_index = ssgp.Magic + ssgp.Label + + if dbkey_index in self.key_list: + dbkey = self.key_list[dbkey_index] + + return ssgp, dbkey + + def _get_internet_password_record(self, base_addr, offset): + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset + RecordMeta = _INTERNET_PW_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _INTERNET_PW_HEADER.STRUCT.size]) + + Buffer = self.kc_buffer[BASE_ADDR + _INTERNET_PW_HEADER.STRUCT.size:BASE_ADDR + RecordMeta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(RecordMeta, Buffer) + + return self.InternetPasswordRecord( + created=self._get_keychain_time(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE), + description=self._get_lv(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE), + comment=self._get_lv(BASE_ADDR, RecordMeta.Comment & 0xFFFFFFFE), + creator=self._get_four_char_code(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE), + print_name=self._get_lv(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE), + protected=self._get_lv(BASE_ADDR, RecordMeta.Protected & 0xFFFFFFFE), + account=self._get_lv(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE), + security_domain=self._get_lv(BASE_ADDR, RecordMeta.SecurityDomain & 0xFFFFFFFE), + server=self._get_lv(BASE_ADDR, RecordMeta.Server & 0xFFFFFFFE), + protocol_type=self._get_four_char_code(BASE_ADDR, RecordMeta.Protocol & 0xFFFFFFFE), + auth_type=self._get_lv(BASE_ADDR, RecordMeta.AuthType & 0xFFFFFFFE), + port=self._get_int(BASE_ADDR, RecordMeta.Port & 0xFFFFFFFE), + path=self._get_lv(BASE_ADDR, RecordMeta.Path & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey + ) + + def _get_generic_password_record(self, base_addr, offset): + BASE_ADDR = _APPL_DB_HEADER.STRUCT.size + base_addr + offset + + RecordMeta = _GENERIC_PW_HEADER(self.kc_buffer[BASE_ADDR:BASE_ADDR + _GENERIC_PW_HEADER.STRUCT.size]) + + Buffer = self.kc_buffer[ + BASE_ADDR + _GENERIC_PW_HEADER.STRUCT.size:BASE_ADDR + RecordMeta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(RecordMeta, Buffer) + + return self.GenericPasswordRecord( + created=self._get_keychain_time(BASE_ADDR, RecordMeta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(BASE_ADDR, RecordMeta.ModDate & 0xFFFFFFFE), + description=self._get_lv(BASE_ADDR, RecordMeta.Description & 0xFFFFFFFE), + creator=self._get_four_char_code(BASE_ADDR, RecordMeta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(BASE_ADDR, RecordMeta.Type & 0xFFFFFFFE), + print_name=self._get_lv(BASE_ADDR, RecordMeta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(BASE_ADDR, RecordMeta.Alias & 0xFFFFFFFE), + account=self._get_lv(BASE_ADDR, RecordMeta.Account & 0xFFFFFFFE), + service=self._get_lv(BASE_ADDR, RecordMeta.Service & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey) - # decrypt the key - plain = kcdecrypt(master, dbblob.iv, ciphertext) + return record - if plain.__len__() < KEYLEN: + # SOURCE : extractkeychain.py + @staticmethod + def _kcdecrypt(key, iv, data): + logger = logging.getLogger('Chainbreaker') + if len(data) == 0: + logger.debug("Encrypted data is 0.") return '' - dbkey = plain[:KEYLEN] + if len(data) % Chainbreaker.BLOCKSIZE != 0: + return '' - # return encrypted wrapping key - return dbkey + cipher = triple_des(key, CBC, str(bytearray(iv))) + plain = cipher.decrypt(data) -# DUMPERS ###################################################################### -def dump_generic_password_table(keychain, TableList, key_list): - try: - TableMetadata, genericpw_list = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_GENERIC_PASSWORD]]) + # now check padding + pad = ord(plain[-1]) + if pad > 8: + logger.debug("Bad padding byte. Keychain password bight be incorrect.") + return '' + + for z in plain[-pad:]: + if ord(z) != pad: + logger.debug("Bad padding byte. Keychain password might be incorrect.") + return '' + + plain = plain[:-pad] + + return plain - for genericpw in genericpw_list: - record = keychain.getGenericPWRecord(TableList[tableEnum[CSSM_DL_DB_RECORD_GENERIC_PASSWORD]], genericpw) - print '[+] Generic Password Record' + @property + def filepath(self): + return self._filepath + + @filepath.setter + def filepath(self, value): + self._filepath = value + if self._filepath: + self._read_keychain_to_buffer() + + @property + def unlock_password(self): + return self._unlock_password + + @unlock_password.setter + def unlock_password(self, unlock_password): + self._unlock_password = unlock_password + + if self._unlock_password: + masterkey = self._generate_master_key(self._unlock_password) + self.db_key = self._find_wrapping_key(masterkey) + # masterkey = self._generate_master_key(self._unlock_password, + # self.table_list[self.table_enum[CSSM_DL_DB_RECORD_METADATA]]) + # self.db_key = self._find_wrapping_key(masterkey, + # self.table_list[self.table_enum[CSSM_DL_DB_RECORD_METADATA]]) + + @property + def unlock_key(self): + return self._unlock_key + + @unlock_key.setter + def unlock_key(self, unlock_key): + self._unlock_key = unlock_key + + if self._unlock_key: + self.db_key = self._find_wrapping_key(unhexlify(self._unlock_key)) + # self.db_key = self._find_wrapping_key(unhexlify(self._unlock_key), + # self.table_list[self.table_enum[CSSM_DL_DB_RECORD_METADATA]]) + + @property + def unlock_file(self): + return self._unlock_file + + @unlock_file.setter + def unlock_file(self, file): + self._unlock_file = file + + if self._unlock_file: try: - real_key = key_list[record[0][0:20]] - passwd = keychain.SSGPDecryption(record[0], real_key) - except KeyError: - passwd = '' - print ' [-] Create DateTime: %s' % record[1] # 16byte string - print ' [-] Last Modified DateTime: %s' % record[2] # 16byte string - print ' [-] Description : %s' % record[3] - print ' [-] Creator : %s' % record[4] - print ' [-] Type : %s' % record[5] - print ' [-] PrintName : %s' % record[6] - print ' [-] Alias : %s' % record[7] - print ' [-] Account : %s' % record[8] - print ' [-] Service : %s' % record[9] - print ' [-] Password: {}'.format(passwd) - except KeyError: - print '[!] Generic Password Table is not available' - - -def dump_internet_passwords(keychain, TableList, key_list): - try: - TableMetadata, internetpw_list = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_INTERNET_PASSWORD]]) + with open(self._unlock_file, mode='rb') as uf: + filecontent = uf.read() + + unlockkeyblob = _UNLOCK_BLOB(filecontent) + self.db_key = self._find_wrapping_key(unlockkeyblob.MasterKey) + except: + logger.warning("Unable to read unlock file: %s" % self._unlock_file) - for internetpw in internetpw_list: - record = keychain.getInternetPWRecord(TableList[tableEnum[CSSM_DL_DB_RECORD_INTERNET_PASSWORD]], internetpw) - print '[+] Internet Record' + @property + def db_key(self): + return self._db_key + + @db_key.setter + def db_key(self, key): + self._db_key = key + + if self._db_key: + self._generate_key_list() + + class KeyRecord(object): + # TODO: Figure out how we want to dump out certificates and keys. + pass + + class PublicKeyRecord(KeyRecord): + def __init__(self, print_name=None, label=None, key_class=None, private=None, key_type=None, key_size=None, + effective_key_size=None, extracted=None, cssm_type=None, public_key=None, iv=None, key=None): + self.PrintName = print_name + self.Label = label + self.KeyClass = key_class + self.Private = private + self.KeyType = key_type + self.KeySize = key_size + self.EffectiveKeySize = effective_key_size + self.Extracted = extracted + self.CSSMType = cssm_type + self.PublicKey = public_key + self.IV = iv + self.Key = key + + def __str__(self): + output = '[+] Public Key\n' + output += ' [-] Print Name: %s\n' % self.PrintName + # output += ' [-] Label: %s\n' % self.Label + output += ' [-] Key Class: %s\n' % self.KeyClass + output += ' [-] Private: %s\n' % self.Private + output += ' [-] Key Type: %s\n' % self.KeyType + output += ' [-] Key Size: %s\n' % self.KeySize + output += ' [-] Effective Key Size: %s\n' % self.EffectiveKeySize + output += ' [-] Extracted: %s\n' % self.Extracted + output += ' [-] CSSM Type: %s\n' % self.CSSMType + output += ' [-] Base64 Encoded Public Key: %s\n' % base64.b64encode(self.PublicKey) + return output + + class PrivateKeyRecord(KeyRecord): + def __init__(self, print_name=None, label=None, key_class=None, private=None, key_type=None, key_size=None, + effective_key_size=None, extracted=None, cssm_type=None, key_name=None, private_key=None, iv=None, + key=None): + self.PrintName = print_name + self.Label = label + self.KeyClass = key_class + self.Private = private + self.KeyType = key_type + self.KeySize = key_size + self.EffectiveKeySize = effective_key_size + self.Extracted = extracted + self.CSSMType = cssm_type + self.KeyName = key_name + self.PrivateKey = private_key + self.IV = iv + self.Key = key + + def __str__(self): + output = '[+] Private Key\n' + output += ' [-] Print Name: %s\n' % self.PrintName + # output += ' [-] Label: %s\n' % self.Label + output += ' [-] Key Class: %s\n' % self.KeyClass + # output += ' [-] Private: %s\n' % self.Private + output += ' [-] Key Type: %s\n' % self.KeyType + output += ' [-] Key Size: %s\n' % self.KeySize + output += ' [-] Effective Key Size: %s\n' % self.EffectiveKeySize + # output += ' [-] Extracted: %s\n' % self.Extracted + output += ' [-] CSSM Type: %s\n' % self.CSSMType + # output += ' [-] KeyName: %s\n' % self.KeyName + + output += ' [-] Base64 Encoded PrivateKey: ' + if self.PrivateKey == Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE: + output += "%s\n" % Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + else: + output += "%s\n" % base64.b64encode(self.PrivateKey) + + return output + + class X509CertificateRecord(object): + def __init__(self, type=None, encoding=None, print_name=None, alias=None, subject=None, issuer=None, + serial_number=None, subject_key_identifier=None, public_key_hash=None, certificate=None): + self.Type = type + self.Encoding = encoding + self.Print_Name = print_name + self.Alias = alias + self.Subject = subject + self.Issuer = issuer + self.Serial_Number = serial_number + self.Subject_Key_Identifier = subject_key_identifier + self.Public_Key_Hash = public_key_hash + self.Certificate = certificate + + def __str__(self): + output = '[+] X509 Certificate\n' + # output += " [-] Type: %s\n" % self.Type + # output += " [-] Encoding: %s\n" % self.Encoding + output += " [-] Print Name: %s\n" % self.Print_Name + # output += " [-] Alias: %s\n" % self.Alias + # output += " [-] Subject: %s\n" % self.Subject + # output += " [-] Issuer: %s\n" % self.Issuer + # output += " [-] Serial Number: %s\n" % self.Serial_Number + # output += " [-] Subject Key Identifier: %s\n" % self.Subject_Key_Identifier + # output += " [-] Public Key Hash: %s\n" % self.Public_Key_Hash + output += " [-] Certificate: %s\n" % base64.b64encode(self.Certificate) + return output + + class SSGBEncryptedRecord(object): + def __init__(self): + self._password = None + self.locked = True + self.password_b64_encoded = False + + def decrypt_password(self): try: - real_key = key_list[record[0][0:20]] - passwd = keychain.SSGPDecryption(record[0], real_key) + if self.SSGP and self.DBKey: + self._password = Chainbreaker._kcdecrypt(self.DBKey, self.SSGP.IV, self.SSGP.EncryptedPassword) + if not all(c in string.printable for c in self._password): + self._password = base64.b64encode(self._password) + self.password_b64_encoded = True + self.locked = False except KeyError: - passwd = '' - print ' [-] Create DateTime: %s' % record[1] # 16byte string - print ' [-] Last Modified DateTime: %s' % record[2] # 16byte string - print ' [-] Description : %s' % record[3] - print ' [-] Comment : %s' % record[4] - print ' [-] Creator : %s' % record[5] - print ' [-] Type : %s' % record[6] - print ' [-] PrintName : %s' % record[7] - print ' [-] Alias : %s' % record[8] - print ' [-] Protected : %s' % record[9] - print ' [-] Account : %s' % record[10] - print ' [-] SecurityDomain : %s' % record[11] - print ' [-] Server : %s' % record[12] + if not self._password: + self.locked = True + self._password = None + return self._password + + def get_password_output_str(self): + password = self.Password + if self.password_b64_encoded: + return ' [-] Base64 Encoded Password: {}\n'.format(password) + else: + return ' [-] Password: {}\n'.format(password) + + @property + def Password(self): + if not self._password: + self.decrypt_password() + if self.locked: + self._password = Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + + return self._password + + class GenericPasswordRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, creator=None, type=None, print_name=None, + alias=None, account=None, service=None, key=None, ssgp=None, dbkey=None): + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Account = account + self.Service = service + self.Key = key + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] Generic Password Record\n' + output += ' [-] Create DateTime: %s\n' % self.Created # 16byte string + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified # 16byte string + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] Print Name: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Account: %s\n' % self.Account + output += ' [-] Service: %s\n' % self.Service + output += self.get_password_output_str() + + return output + + class InternetPasswordRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, comment=None, creator=None, type=None, + print_name=None, alias=None, protected=None, account=None, security_domain=None, server=None, + protocol_type=None, auth_type=None, port=None, path=None, ssgp=None, dbkey=None): + + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Comment = comment + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Protected = protected + self.Account = account + self.SecurityDomain = security_domain + self.Server = server + self.ProtocolType = protocol_type + self.AuthType = auth_type + self.Port = port + self.Path = path + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] Internet Record\n' + output += ' [-] Create DateTime: %s\n' % self.Created + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Comment: %s\n' % self.Comment + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] PrintName: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Protected: %s\n' % self.Protected + output += ' [-] Account: %s\n' % self.Account + output += ' [-] SecurityDomain: %s\n' % self.SecurityDomain + output += ' [-] Server: %s\n' % self.Server + try: - print ' [-] Protocol Type : %s' % PROTOCOL_TYPE[record[13]] + output += ' [-] Protocol Type: %s\n' % PROTOCOL_TYPE[self.ProtocolType] except KeyError: - print ' [-] Protocol Type : %s' % record[13] + output += ' [-] Protocol Type: %s\n' % self.ProtocolType + try: - print ' [-] Auth Type : %s' % AUTH_TYPE[record[14]] + output += ' [-] Auth Type: %s\n' % AUTH_TYPE[self.AuthType] except KeyError: - print ' [-] Auth Type : %s' % record[14] - print ' [-] Port : %d' % record[15] - print ' [-] Path : %s' % record[16] - print ' [-] Password: {}'.format(passwd) - except KeyError: - print '[!] Internet Password Table is not available' + output += ' [-] Auth Type: %s\n' % self.AuthType + + output += ' [-] Port: %d\n' % self.Port + output += ' [-] Path: %s\n' % self.Path + output += self.get_password_output_str() + + return output + + class AppleshareRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, comment=None, creator=None, type=None, + print_name=None, alias=None, protected=None, account=None, volume=None, server=None, + protocol_type=None, address=None, signature=None, dbkey=None, ssgp=None): + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Comment = comment + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Protected = protected + self.Account = account + self.Volume = volume + self.Server = server + self.Protocol_Type = protocol_type + self.Address = address + self.Signature = signature + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] AppleShare Record (no longer used in OS X)\n' + output += ' [-] Create DateTime: %s\n' % self.Created + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Comment: %s\n' % self.Comment + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] PrintName: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Protected: %s\n' % self.Protected + output += ' [-] Account: %s\n' % self.Account + output += ' [-] Volume: %s\n' % self.Volume + output += ' [-] Server: %s\n' % self.Server - -def dump_appleshare_records(keychain, TableList, key_list): - try: - TableMetadata, applesharepw_list = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD]]) - - for applesharepw in applesharepw_list: - record = keychain.getAppleshareRecord(TableList[tableEnum[CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD]], applesharepw) - print '[+] AppleShare Record (no more used OS X)' try: - real_key = key_list[record[0][0:20]] - passwd = keychain.SSGPDecryption(record[0], real_key) + output += ' [-] Protocol Type: %s\n' % PROTOCOL_TYPE[self.Protocol_Type] except KeyError: - passwd = '' - print '' - print ' [-] Create DateTime: %s' % record[1] # 16byte string - print ' [-] Last Modified DateTime: %s' % record[2] # 16byte string - print ' [-] Description : %s' % record[3] - print ' [-] Comment : %s' % record[4] - print ' [-] Creator : %s' % record[5] - print ' [-] Type : %s' % record[6] - print ' [-] PrintName : %s' % record[7] - print ' [-] Alias : %s' % record[8] - print ' [-] Protected : %s' % record[9] - print ' [-] Account : %s' % record[10] - print ' [-] Volume : %s' % record[11] - print ' [-] Server : %s' % record[12] + output += ' [-] Protocol Type: %s\n' % self.Protocol_Type + + output += ' [-] Address: %d\n' % self.Address + output += ' [-] Signature: %s\n' % self.Signature + output += self.get_password_output_str() + + return output + + +if __name__ == "__main__": + import argparse + import getpass + import sys + import os + + arguments = argparse.ArgumentParser(description='Dump items stored in an OSX Keychain') + + # General Arguments + arguments.add_argument('keychain', help='Location of the keychain file to parse') + + # Available actions + action_args = arguments.add_argument_group('Available Actions') + action_args.add_argument('--dump-all', '-a', help='Dump all keychain items', + action='store_const', dest='dump_all', const=True) + action_args.add_argument('--dump-keychain-password-hash', + help='Dump the keychain password hash in a format suitable for hashcat or John The Ripper', + action='store_const', dest='dump_keychain_password_hash', const=True) + action_args.add_argument('--dump-generic-passwords', help='Dump all generic passwords', + action='store_const', dest='dump_generic_passwords', const=True) + action_args.add_argument('--dump-internet-passwords', help='Dump all internet passwords', + action='store_const', dest='dump_internet_passwords', const=True) + action_args.add_argument('--dump-appleshare-passwords', help='Dump all appleshare passwords', + action='store_const', dest='dump_appleshare_passwords', const=True) + action_args.add_argument('--dump-private-keys', help='Dump all private keys', + action='store_const', dest='dump_private_keys', const=True) + action_args.add_argument('--dump-public-keys', help='Dump all public keys', + action='store_const', dest='dump_public_keys', const=True) + action_args.add_argument('--dump-x509-certificates', help='Dump all X509 certificates', + action='store_const', dest='dump_x509_certificates', const=True) + + # Keychain Unlocking Arguments + unlock_args = arguments.add_argument_group('Unlock Options') + unlock_args.add_argument('--password-prompt', '-p', help='Prompt for a password to use in unlocking the keychain', + action='store_const', dest='password_prompt', const=True) + unlock_args.add_argument('--password', help='Unlock the keychain with a password, provided on the terminal.' + 'Caution: This is insecure and you should likely use' + '--password-prompt instead.') + unlock_args.add_argument('--key-prompt', '-k', help='Prompt for a key to use in unlocking the keychain', + action='store_const', dest='key_prompt', const=True) + unlock_args.add_argument('--key', help='Unlock the keychain with a key, provided via argument.' + 'Caution: This is insecure and you should likely use --key-prompt instead.') + unlock_args.add_argument('--unlock-file', help='Unlock the keychain with a key file') + + # Output arguments + output_args = arguments.add_argument_group('Output Options') + output_args.add_argument('--output', '-o', help='Not currently implemented.' + 'Directory to output exported records to.') + output_args.add_argument('-q', '--quiet', help="Suppress all output", action="store_true", default=False) + output_args.add_argument('-d', '--debug', help="Print debug information", action="store_const", dest="loglevel", + const=logging.DEBUG) + + misc_args = arguments.add_argument_group('Miscellaneous') + + arguments.set_defaults( + loglevel=logging.INFO, + dump_all=False, + dump_keychain_password_hash=False, + dump_generic_passwords=False, + dump_internet_passwords=False, + dump_appleshare_passwords=False, + dump_public_keys=False, + dump_private_keys=False, + dump_x509_certificates=False, + password_prompt=False, + key_prompt=False, + password=None, + key=None, + unlock_file=None, + quiet=False + ) + + args = arguments.parse_args() + + if args.password_prompt: + args.password = getpass.getpass('Unlock Password: ') + + if args.key_prompt: + args.key = getpass.getpass('Unlock Key: ') + + # create logger + logger = logging.getLogger('Chainbreaker') + logger.setLevel(args.loglevel) + + if not args.quiet: + console_handler = logging.StreamHandler(stream=sys.stdout) + console_handler.setLevel(args.loglevel) + # console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + logger.addHandler(console_handler) + + if args.output: + if not os.path.exists(args.output): try: - print ' [-] Protocol Type : %s' % PROTOCOL_TYPE[record[13]] - except KeyError: - print ' [-] Protocol Type : %s' % record[13] - print ' [-] Address : %d' % record[14] - print ' [-] Signature : %s' % record[15] - print ' [-] Password: {}'.format(passwd) - except KeyError: - print '[!] AppleShare Table is not available' - - -def dump_x509_certificates(keychain, TableList, key_list): - try: - TableMetadata, x509CertList = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_X509_CERTIFICATE]]) - - for i, x509Cert in enumerate(x509CertList, 1): - record = keychain.getx509Record(TableList[tableEnum[CSSM_DL_DB_RECORD_X509_CERTIFICATE]], x509Cert) - print '[+] Certificate, dumped to disk' - # print ' [-] Cert Type: %s' % CERT_TYPE[record[0]] - # print ' [-] Cert Encoding: %s' % CERT_ENCODING[record[1]] - # print ' [-] PrintName : %s' % record[2] - # print ' [-] Alias : %s' % record[3] - # print ' [-] Subject' - # hexdump(record[4]) - # print ' [-] Issuer :' - # hexdump(record[5]) - # print ' [-] SerialNumber' - # hexdump(record[6]) - # print ' [-] SubjectKeyIdentifier' - # hexdump(record[7]) - # print ' [-] Public Key Hash' - # hexdump(record[8]) - # print ' [-] Certificate' - add_file(directory='certs', filename=str(i), cert=str(record[9])) - # hexdump(record[9]) - # print '' - except KeyError: - print '[!] Certification Table is not available' - - -def dump_public_keys(keychain, TableList, key_list): - try: - TableMetadata, PublicKeyList = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_PUBLIC_KEY]]) - for PublicKey in PublicKeyList: - record = keychain.getKeyRecord(TableList[tableEnum[CSSM_DL_DB_RECORD_PUBLIC_KEY]], PublicKey) - print '[+] Public Key Record' - # print ' [-] PrintName: %s' % record[0] - # print ' [-] Label' - # hexdump(record[1]) - # print ' [-] Key Class : %s' % KEY_TYPE[record[2]] - # print ' [-] Private : %d' % record[3] - # print ' [-] Key Type : %s' % CSSM_ALGORITHMS[record[4]] - # print ' [-] Key Size : %d bits' % record[5] - # print ' [-] Effective Key Size : %d bits' % record[6] - # print ' [-] Extracted : %d' % record[7] - # print ' [-] CSSM Type : %s' % STD_APPLE_ADDIN_MODULE[record[8]] - # print ' [-] Public Key' - # hexdump(record[10]) - # print '' - except KeyError: - print '[!] Public Key Table is not available' - pass + os.makedirs(args.output) + except OSError as e: + logger.critical("Unable to create output directory: %s" % args.output) + exit(1) + + output_handler = logging.FileHandler(os.path.join(args.output, 'output.txt')) + output_handler.setLevel(args.loglevel) + logger.addHandler(output_handler) + + if args.dump_all: + args.dump_keychain_password_hash = args.dump_generic_passwords = args.dump_internet_passwords = \ + args.dump_appleshare_passwords = args.dump_public_keys = args.dump_private_keys = \ + args.dump_x509_certificates = True + + if not (args.dump_keychain_password_hash or args.dump_generic_passwords or args.dump_internet_passwords \ + or args.dump_appleshare_passwords or args.dump_public_keys or args.dump_private_keys or \ + args.dump_x509_certificates or args.dump_all): + logger.critical("No action specified.") + exit(1) + + # Done parsing out input options, now actually do the work of fulfilling the users request. + keychain = Chainbreaker(args.keychain, unlock_password=args.password, unlock_key=args.key, + unlock_file=args.unlock_file) + + output = [] + + if args.dump_keychain_password_hash: + output.append( + { + 'header': 'Keychain Password Hash', + 'hash': keychain.dump_keychain_password_hash(), + } + ) + + if args.dump_generic_passwords: + output.append( + { + 'header': 'Generic Passwords', + 'records': keychain.dump_generic_passwords(), + } + ) + if args.dump_internet_passwords: + output.append( + { + 'header': 'Internet Passwords', + 'records': keychain.dump_internet_passwords(), + } + ) + if args.dump_appleshare_passwords: + output.append( + { + 'header': 'Appleshare Passwords', + 'records': keychain.dump_appleshare_passwords(), + } + ) + if args.dump_public_keys: + output.append( + { + 'header': 'Public Keys', + 'records': keychain.dump_public_keys(), + } + ) + if args.dump_private_keys: + output.append( + { + 'header': 'Private Keys', + 'records': keychain.dump_private_keys(), + } + ) + if args.dump_x509_certificates: + output.append( + { + 'header': 'x509 Certificates', + 'records': keychain.dump_x509_certificates(), + } + ) - -def dump_private_keys(keychain, TableList, key_list): try: - table_meta, PrivateKeyList = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_PRIVATE_KEY]]) - for i, PrivateKey in enumerate(PrivateKeyList, 1): - record = keychain.getKeyRecord(TableList[tableEnum[CSSM_DL_DB_RECORD_PRIVATE_KEY]], PrivateKey) - print '[+] Private Key Record, dumped to disk' - # print ' [-] PrintName: %s' % record[0] - # print ' [-] Label' - # hexdump(record[1]) - # print ' [-] Key Class : %s' % KEY_TYPE[record[2]] - # print ' [-] Private : %d' % record[3] - # print ' [-] Key Type : %s' % CSSM_ALGORITHMS[record[4]] - # print ' [-] Key Size : %d bits' % record[5] - # print ' [-] Effective Key Size : %d bits' % record[6] - # print ' [-] Extracted : %d' % record[7] - # print ' [-] CSSM Type : %s' % STD_APPLE_ADDIN_MODULE[record[8]] - keyname, privatekey = keychain.PrivateKeyDecryption(record[10], record[9], dbkey) - # print ' [-] Key Name' - # hexdump(keyname) - # print ' [-] Decrypted Private Key' - add_file(directory='keys', filename=str(i), key=str(privatekey)) - # hexdump(privatekey) - # print '' - except KeyError: - print '[!] Private Key Table is not available' - pass - - -# MAIN ######################################################################### -# Parse arguments -parser = argparse.ArgumentParser(description='Tool for OS X Keychain Analysis by @n0fate') -parser.add_argument('-f', '--file', nargs=1, help='Keychain file(*.keychain)', required=True) -group = parser.add_mutually_exclusive_group(required=True) -group.add_argument('-k', '--key', nargs='+', action='append', help='Keychain Masterkey', required=False) -group.add_argument('-u', '--unlockfile', nargs=1, help='System.keychain unlock file (/var/db/SystemKey)', required=False) -group.add_argument('-p', '--password', nargs=1, help='Keychain Password', required=False) -args = parser.parse_args() - -# Verify arguments -if os.path.exists(args.file[0]) is False: - print '[!] ERROR: Keychain does not exist' - parser.print_help() - exit() - -keychain = KeyChain(args.file[0]) -if keychain.open() is False: - print '[!] ERROR: %s Open Failed' % args.file[0] - parser.print_help() - exit() - -KeychainHeader = keychain.getHeader() -if KeychainHeader.Signature != KEYCHAIN_SIGNATURE: - print '[!] ERROR: Invalid Keychain Format' - parser.print_help() - exit() - -# Create export dirs -BASEPATH = os.getcwd() + '/exported/' -if not os.path.exists(BASEPATH): - os.makedirs(BASEPATH) - os.makedirs(BASEPATH + 'certs') - os.makedirs(BASEPATH + 'keys') - -# Get tables from the keychain file -SchemaInfo, TableList = keychain.getSchemaInfo(KeychainHeader.SchemaOffset) -TableMetadata, RecordList = keychain.getTable(TableList[0]) -tableCount, tableEnum = keychain.getTablenametoList(RecordList, TableList) - -# Generate database key -if args.password is not None: - masterkey = keychain.generateMasterKey(args.password[0], TableList[tableEnum[CSSM_DL_DB_RECORD_METADATA]]) - dbkey = keychain.findWrappingKey(masterkey, TableList[tableEnum[CSSM_DL_DB_RECORD_METADATA]]) -elif args.key is not None: - for key in args.key: - dbkey = keychain.findWrappingKey(unhexlify(key[0]), TableList[tableEnum[CSSM_DL_DB_RECORD_METADATA]]) - if len(dbkey): - print '[+] Found a key for the keychain: {}'.format(key[0]) - break -elif args.unlockfile is not None: - with open(args.unlockfile[0], mode='rb') as uf: - filecontent = uf.read() - unlockkeyblob = _memcpy(filecontent, _UNLOCK_BLOB) - dbkey = keychain.findWrappingKey(unlockkeyblob.masterKey, TableList[tableEnum[CSSM_DL_DB_RECORD_METADATA]]) -else: - print '[!] ERROR: password or master key candidate is invalid' - exit() - -if len(dbkey) == 0: - print '[!] ERROR: password or master key candidate is invalid' - exit() - -# Get symmetric key blob -key_list = {} # keyblob list -TableMetadata, symmetrickey_list = keychain.getTable(TableList[tableEnum[CSSM_DL_DB_RECORD_SYMMETRIC_KEY]]) -for symmetrickey_record in symmetrickey_list: - keyblob, ciphertext, iv, return_value = keychain.getKeyblobRecord( - TableList[tableEnum[CSSM_DL_DB_RECORD_SYMMETRIC_KEY]], - symmetrickey_record) - if return_value == 0: - passwd = keychain.KeyblobDecryption(ciphertext, iv, dbkey) - if passwd != '': - key_list[keyblob] = passwd - -# Dump contents of the keyfile -dump_generic_password_table(keychain, TableList, key_list) -dump_internet_passwords(keychain, TableList, key_list) -dump_appleshare_records(keychain, TableList, key_list) -dump_x509_certificates(keychain, TableList, key_list) -dump_public_keys(keychain, TableList, key_list) -dump_private_keys(keychain, TableList, key_list) - -# Save recovered data to disk -certs = os.listdir(BASEPATH + '/certs') -keys = os.listdir(BASEPATH + '/keys') -v = Validator() -k_path = BASEPATH + '/keys/{}' -c_path = BASEPATH + '/certs/{}' - -for i, c in enumerate(certs, 1): - for j, k in enumerate(keys, 1): - if v.validate_by_filenames(key_path=k_path.format(k), cert_path=c_path.format(c)): - try: - new_folder_name = len(os.listdir(BASEPATH + 'associated')) + 1 - except OSError: - new_folder_name = 1 - add_file('associated/{}'.format(new_folder_name), filename=str(i), cert=open(c_path.format(c)).read()) - add_file('associated/{}'.format(new_folder_name), filename=str(j), key=open(k_path.format(k)).read()) + for record_collection in output: + if 'records' in record_collection: + number_records = len(record_collection['records']) + logger.info("%s %s" % (len(record_collection['records']), record_collection['header'])) + for record in record_collection['records']: + for line in str(record).split('\n'): + logger.info("\t%s" % line) + logger.info('\n') + elif 'hash' in record_collection: + logger.info(record_collection['header']) + logger.info("\t%s\n\n" % record_collection['hash']) + except KeyboardInterrupt: + exit(0) + + exit(0) diff --git a/match.sh b/match.sh deleted file mode 100755 index 86a221d..0000000 --- a/match.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -for c in exported/certs/* ; do - echo "==================================" - echo "Certificate: $c" - openssl x509 -noout -text -inform DER -in $c | grep Subject: - for k in exported/keys/* ; do - if cmp --quiet <(openssl x509 -pubkey -inform DER -in $c -noout) <(openssl pkey -pubout -inform DER -in $k -outform PEM) ; then - echo "Key: $k" - fi - done -done - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d450e54..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pycrypto==2.6.1 -pyOpenSSL==19.1.0 diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..0021f4b --- /dev/null +++ b/schema.py @@ -0,0 +1,495 @@ +from struct import Struct +from datetime import datetime + +# http://web.mit.edu/darwin/src/modules/Security/cdsa/cdsa/cssmtype.h +KEY_TYPE = { + 0x00 + 0x0F: 'CSSM_KEYCLASS_PUBLIC_KEY', + 0x01 + 0x0F: 'CSSM_KEYCLASS_PRIVATE_KEY', + 0x02 + 0x0F: 'CSSM_KEYCLASS_SESSION_KEY', + 0x03 + 0x0F: 'CSSM_KEYCLASS_SECRET_PART', + 0xFFFFFFFF: 'CSSM_KEYCLASS_OTHER' +} + +CSSM_ALGORITHMS = { + 0: 'CSSM_ALGID_NONE', + 1: 'CSSM_ALGID_CUSTOM', + 2: 'CSSM_ALGID_DH', + 3: 'CSSM_ALGID_PH', + 4: 'CSSM_ALGID_KEA', + 5: 'CSSM_ALGID_MD2', + 6: 'CSSM_ALGID_MD4', + 7: 'CSSM_ALGID_MD5', + 8: 'CSSM_ALGID_SHA1', + 9: 'CSSM_ALGID_NHASH', + 10: 'CSSM_ALGID_HAVAL:', + 11: 'CSSM_ALGID_RIPEMD', + 12: 'CSSM_ALGID_IBCHASH', + 13: 'CSSM_ALGID_RIPEMAC', + 14: 'CSSM_ALGID_DES', + 15: 'CSSM_ALGID_DESX', + 16: 'CSSM_ALGID_RDES', + 17: 'CSSM_ALGID_3DES_3KEY_EDE', + 18: 'CSSM_ALGID_3DES_2KEY_EDE', + 19: 'CSSM_ALGID_3DES_1KEY_EEE', + 20: 'CSSM_ALGID_3DES_3KEY_EEE', + 21: 'CSSM_ALGID_3DES_2KEY_EEE', + 22: 'CSSM_ALGID_IDEA', + 23: 'CSSM_ALGID_RC2', + 24: 'CSSM_ALGID_RC5', + 25: 'CSSM_ALGID_RC4', + 26: 'CSSM_ALGID_SEAL', + 27: 'CSSM_ALGID_CAST', + 28: 'CSSM_ALGID_BLOWFISH', + 29: 'CSSM_ALGID_SKIPJACK', + 30: 'CSSM_ALGID_LUCIFER', + 31: 'CSSM_ALGID_MADRYGA', + 32: 'CSSM_ALGID_FEAL', + 33: 'CSSM_ALGID_REDOC', + 34: 'CSSM_ALGID_REDOC3', + 35: 'CSSM_ALGID_LOKI', + 36: 'CSSM_ALGID_KHUFU', + 37: 'CSSM_ALGID_KHAFRE', + 38: 'CSSM_ALGID_MMB', + 39: 'CSSM_ALGID_GOST', + 40: 'CSSM_ALGID_SAFER', + 41: 'CSSM_ALGID_CRAB', + 42: 'CSSM_ALGID_RSA', + 43: 'CSSM_ALGID_DSA', + 44: 'CSSM_ALGID_MD5WithRSA', + 45: 'CSSM_ALGID_MD2WithRSA', + 46: 'CSSM_ALGID_ElGamal', + 47: 'CSSM_ALGID_MD2Random', + 48: 'CSSM_ALGID_MD5Random', + 49: 'CSSM_ALGID_SHARandom', + 50: 'CSSM_ALGID_DESRandom', + 51: 'CSSM_ALGID_SHA1WithRSA', + 52: 'CSSM_ALGID_CDMF', + 53: 'CSSM_ALGID_CAST3', + 54: 'CSSM_ALGID_CAST5', + 55: 'CSSM_ALGID_GenericSecret', + 56: 'CSSM_ALGID_ConcatBaseAndKey', + 57: 'CSSM_ALGID_ConcatKeyAndBase', + 58: 'CSSM_ALGID_ConcatBaseAndData', + 59: 'CSSM_ALGID_ConcatDataAndBase', + 60: 'CSSM_ALGID_XORBaseAndData', + 61: 'CSSM_ALGID_ExtractFromKey', + 62: 'CSSM_ALGID_SSL3PreMasterGen', + 63: 'CSSM_ALGID_SSL3MasterDerive', + 64: 'CSSM_ALGID_SSL3KeyAndMacDerive', + 65: 'CSSM_ALGID_SSL3MD5_MAC', + 66: 'CSSM_ALGID_SSL3SHA1_MAC', + 67: 'CSSM_ALGID_PKCS5_PBKDF1_MD5', + 68: 'CSSM_ALGID_PKCS5_PBKDF1_MD2', + 69: 'CSSM_ALGID_PKCS5_PBKDF1_SHA1', + 70: 'CSSM_ALGID_WrapLynks', + 71: 'CSSM_ALGID_WrapSET_OAEP', + 72: 'CSSM_ALGID_BATON', + 73: 'CSSM_ALGID_ECDSA', + 74: 'CSSM_ALGID_MAYFLY', + 75: 'CSSM_ALGID_JUNIPER', + 76: 'CSSM_ALGID_FASTHASH', + 77: 'CSSM_ALGID_3DES', + 78: 'CSSM_ALGID_SSL3MD5', + 79: 'CSSM_ALGID_SSL3SHA1', + 80: 'CSSM_ALGID_FortezzaTimestamp', + 81: 'CSSM_ALGID_SHA1WithDSA', + 82: 'CSSM_ALGID_SHA1WithECDSA', + 83: 'CSSM_ALGID_DSA_BSAFE', + 84: 'CSSM_ALGID_ECDH', + 85: 'CSSM_ALGID_ECMQV', + 86: 'CSSM_ALGID_PKCS12_SHA1_PBE', + 87: 'CSSM_ALGID_ECNRA', + 88: 'CSSM_ALGID_SHA1WithECNRA', + 89: 'CSSM_ALGID_ECES', + 90: 'CSSM_ALGID_ECAES', + 91: 'CSSM_ALGID_SHA1HMAC', + 92: 'CSSM_ALGID_FIPS186Random', + 93: 'CSSM_ALGID_ECC', + 94: 'CSSM_ALGID_MQV', + 95: 'CSSM_ALGID_NRA', + 96: 'CSSM_ALGID_IntelPlatformRandom', + 97: 'CSSM_ALGID_UTC', + 98: 'CSSM_ALGID_HAVAL3', + 99: 'CSSM_ALGID_HAVAL4', + 100: 'CSSM_ALGID_HAVAL5', + 101: 'CSSM_ALGID_TIGER', + 102: 'CSSM_ALGID_MD5HMAC', + 103: 'CSSM_ALGID_PKCS5_PBKDF2', + 104: 'CSSM_ALGID_RUNNING_COUNTER', + 0x7FFFFFFF: 'CSSM_ALGID_LAST' +} + +# CSSM TYPE +## http://www.opensource.apple.com/source/libsecurity_cssm/libsecurity_cssm-36064/lib/cssmtype.h + +########## CSSM_DB_RECORDTYPE ############# + +# /* Industry At Large Application Name Space Range Definition */ +# /* AppleFileDL record types. */ +CSSM_DB_RECORDTYPE_APP_DEFINED_START = 0x80000000 +CSSM_DL_DB_RECORD_GENERIC_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0 +CSSM_DL_DB_RECORD_INTERNET_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 1 +CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 2 +CSSM_DL_DB_RECORD_USER_TRUST = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 3 +CSSM_DL_DB_RECORD_X509_CRL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 4 +CSSM_DL_DB_RECORD_UNLOCK_REFERRAL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 5 +CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 6 + +CSSM_DL_DB_RECORD_X509_CERTIFICATE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x1000 +CSSM_DL_DB_RECORD_METADATA = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x8000 ## DBBlob +CSSM_DB_RECORDTYPE_APP_DEFINED_END = 0xffffffff + +# /* Record Types defined in the Schema Management Name Space */ +CSSM_DB_RECORDTYPE_SCHEMA_START = 0x00000000 +CSSM_DL_DB_SCHEMA_INFO = CSSM_DB_RECORDTYPE_SCHEMA_START + 0 +CSSM_DL_DB_SCHEMA_INDEXES = CSSM_DB_RECORDTYPE_SCHEMA_START + 1 +CSSM_DL_DB_SCHEMA_ATTRIBUTES = CSSM_DB_RECORDTYPE_SCHEMA_START + 2 +CSSM_DL_DB_SCHEMA_PARSING_MODULE = CSSM_DB_RECORDTYPE_SCHEMA_START + 3 +CSSM_DB_RECORDTYPE_SCHEMA_END = CSSM_DB_RECORDTYPE_SCHEMA_START + 4 + +# /* Record Types defined in the Open Group Application Name Space */ +# /* Open Group Application Name Space Range Definition*/ +CSSM_DB_RECORDTYPE_OPEN_GROUP_START = 0x0000000A +CSSM_DL_DB_RECORD_ANY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 0 +CSSM_DL_DB_RECORD_CERT = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 1 +CSSM_DL_DB_RECORD_CRL = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 2 +CSSM_DL_DB_RECORD_POLICY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 3 +CSSM_DL_DB_RECORD_GENERIC = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 4 +CSSM_DL_DB_RECORD_PUBLIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 5 +CSSM_DL_DB_RECORD_PRIVATE_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 6 +CSSM_DL_DB_RECORD_SYMMETRIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 7 +CSSM_DL_DB_RECORD_ALL_KEYS = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 +CSSM_DB_RECORDTYPE_OPEN_GROUP_END = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 +##################### + +######## KEYUSE ######### +CSSM_KEYUSE_ANY = 0x80000000 +CSSM_KEYUSE_ENCRYPT = 0x00000001 +CSSM_KEYUSE_DECRYPT = 0x00000002 +CSSM_KEYUSE_SIGN = 0x00000004 +CSSM_KEYUSE_VERIFY = 0x00000008 +CSSM_KEYUSE_SIGN_RECOVER = 0x00000010 +CSSM_KEYUSE_VERIFY_RECOVER = 0x00000020 +CSSM_KEYUSE_WRAP = 0x00000040 +CSSM_KEYUSE_UNWRAP = 0x00000080 +CSSM_KEYUSE_DERIVE = 0x00000100 +#################### + +############ CERT TYPE ############## +CERT_TYPE = { + 0x00: 'CSSM_CERT_UNKNOWN', + 0x01: 'CSSM_CERT_X_509v1', + 0x02: 'CSSM_CERT_X_509v2', + 0x03: 'CSSM_CERT_X_509v3', + 0x04: 'CSSM_CERT_PGP', + 0x05: 'CSSM_CERT_SPKI', + 0x06: 'CSSM_CERT_SDSIv1', + 0x08: 'CSSM_CERT_Intel', + 0x09: 'CSSM_CERT_X_509_ATTRIBUTE', + 0x0A: 'CSSM_CERT_X9_ATTRIBUTE', + 0x0C: 'CSSM_CERT_ACL_ENTRY', + 0x7FFE: 'CSSM_CERT_MULTIPLE', + 0x7FFF: 'CSSM_CERT_LAST', + 0x8000: 'CSSM_CL_CUSTOM_CERT_TYPE' +} +#################################### + +########### CERT ENCODING ############# +CERT_ENCODING = { + 0x00: 'CSSM_CERT_ENCODING_UNKNOWN', + 0x01: 'CSSM_CERT_ENCODING_CUSTOM', + 0x02: 'CSSM_CERT_ENCODING_BER', + 0x03: 'CSSM_CERT_ENCODING_DER', + 0x04: 'CSSM_CERT_ENCODING_NDR', + 0x05: 'CSSM_CERT_ENCODING_SEXPR', + 0x06: 'CSSM_CERT_ENCODING_PGP', + 0x7FFE: 'CSSM_CERT_ENCODING_MULTIPLE', + 0x7FFF: 'CSSM_CERT_ENCODING_LAST' +} + +STD_APPLE_ADDIN_MODULE = { + '{87191ca0-0fc9-11d4-849a-000502b52122}': 'CSSM itself', + '{87191ca1-0fc9-11d4-849a-000502b52122}': 'File based DL (aka "Keychain DL")', + '{87191ca2-0fc9-11d4-849a-000502b52122}': 'Core CSP (local space)', + '{87191ca3-0fc9-11d4-849a-000502b52122}': 'Secure CSP/DL (aka "Keychain CSPDL")', + '{87191ca4-0fc9-11d4-849a-000502b52122}': 'X509 Certificate CL', + '{87191ca5-0fc9-11d4-849a-000502b52122}': 'X509 Certificate TP', + '{87191ca6-0fc9-11d4-849a-000502b52122}': 'DLAP/OpenDirectory access DL', + '{87191ca7-0fc9-11d4-849a-000502b52122}': 'TP for ".mac" related policies', + '{87191ca8-0fc9-11d4-849a-000502b52122}': 'Smartcard CSP/DL', + '{87191ca9-0fc9-11d4-849a-000502b52122}': 'DL for ".mac" certificate access' +} + +SECURE_STORAGE_GROUP = 'ssgp' + +# SecAuthenticationType +AUTH_TYPE = { + 'ntlm': 'kSecAuthenticationTypeNTLM', + 'msna': 'kSecAuthenticationTypeMSN', + 'dpaa': 'kSecAuthenticationTypeDPA', + 'rpaa': 'kSecAuthenticationTypeRPA', + 'http': 'kSecAuthenticationTypeHTTPBasic', + 'httd': 'kSecAuthenticationTypeHTTPDigest', + 'form': 'kSecAuthenticationTypeHTMLForm', + 'dflt': 'kSecAuthenticationTypeDefault', + '': 'kSecAuthenticationTypeAny', + '\x00\x00\x00\x00': 'kSecAuthenticationTypeAny' +} + +# SecProtocolType +PROTOCOL_TYPE = { + 'ftp ': 'kSecProtocolTypeFTP', + 'ftpa': 'kSecProtocolTypeFTPAccount', + 'http': 'kSecProtocolTypeHTTP', + 'irc ': 'kSecProtocolTypeIRC', + 'nntp': 'kSecProtocolTypeNNTP', + 'pop3': 'kSecProtocolTypePOP3', + 'smtp': 'kSecProtocolTypeSMTP', + 'sox ': 'kSecProtocolTypeSOCKS', + 'imap': 'kSecProtocolTypeIMAP', + 'ldap': 'kSecProtocolTypeLDAP', + 'atlk': 'kSecProtocolTypeAppleTalk', + 'afp ': 'kSecProtocolTypeAFP', + 'teln': 'kSecProtocolTypeTelnet', + 'ssh ': 'kSecProtocolTypeSSH', + 'ftps': 'kSecProtocolTypeFTPS', + 'htps': 'kSecProtocolTypeHTTPS', + 'htpx': 'kSecProtocolTypeHTTPProxy', + 'htsx': 'kSecProtocolTypeHTTPSProxy', + 'ftpx': 'kSecProtocolTypeFTPProxy', + 'cifs': 'kSecProtocolTypeCIFS', + 'smb ': 'kSecProtocolTypeSMB', + 'rtsp': 'kSecProtocolTypeRTSP', + 'rtsx': 'kSecProtocolTypeRTSPProxy', + 'daap': 'kSecProtocolTypeDAAP', + 'eppc': 'kSecProtocolTypeEPPC', + 'ipp ': 'kSecProtocolTypeIPP', + 'ntps': 'kSecProtocolTypeNNTPS', + 'ldps': 'kSecProtocolTypeLDAPS', + 'tels': 'kSecProtocolTypeTelnetS', + 'imps': 'kSecProtocolTypeIMAPS', + 'ircs': 'kSecProtocolTypeIRCS', + 'pops': 'kSecProtocolTypePOP3S', + 'cvsp': 'kSecProtocolTypeCVSpserver', + 'svn ': 'kSecProtocolTypeCVSpserver', + 'AdIM': 'kSecProtocolTypeAdiumMessenger', + '\x00\x00\x00\x00': 'kSecProtocolTypeAny' +} + +# This is somewhat gross: we define a bunch of module-level constants based on +# the SecKeychainItem.h defines (FourCharCodes) by passing them through +# struct.unpack and converting them to ctypes.c_long() since we'll never use +# them for non-native APIs + +CARBON_DEFINES = { + 'cdat': 'kSecCreationDateItemAttr', + 'mdat': 'kSecModDateItemAttr', + 'desc': 'kSecDescriptionItemAttr', + 'icmt': 'kSecCommentItemAttr', + 'crtr': 'kSecCreatorItemAttr', + 'type': 'kSecTypeItemAttr', + 'scrp': 'kSecScriptCodeItemAttr', + 'labl': 'kSecLabelItemAttr', + 'invi': 'kSecInvisibleItemAttr', + 'nega': 'kSecNegativeItemAttr', + 'cusi': 'kSecCustomIconItemAttr', + 'acct': 'kSecAccountItemAttr', + 'svce': 'kSecServiceItemAttr', + 'gena': 'kSecGenericItemAttr', + 'sdmn': 'kSecSecurityDomainItemAttr', + 'srvr': 'kSecServerItemAttr', + 'atyp': 'kSecAuthenticationTypeItemAttr', + 'port': 'kSecPortItemAttr', + 'path': 'kSecPathItemAttr', + 'vlme': 'kSecVolumeItemAttr', + 'addr': 'kSecAddressItemAttr', + 'ssig': 'kSecSignatureItemAttr', + 'ptcl': 'kSecProtocolItemAttr', + 'ctyp': 'kSecCertificateType', + 'cenc': 'kSecCertificateEncoding', + 'crtp': 'kSecCrlType', + 'crnc': 'kSecCrlEncoding', + 'alis': 'kSecAlias', + 'inet': 'kSecInternetPasswordItemClass', + 'genp': 'kSecGenericPasswordItemClass', + 'ashp': 'kSecAppleSharePasswordItemClass', + CSSM_DL_DB_RECORD_X509_CERTIFICATE: 'kSecCertificateItemClass' +} + +class _APPL_DB_HEADER(object): + STRUCT = Struct('> 4s i i i i') + + def __init__(self, buffer): + (self.Signature, self.Version, self.HeaderSize, self.SchemaOffset, + self.AuthOffset) = _APPL_DB_HEADER.STRUCT.unpack( + buffer) + + +class _APPL_DB_SCHEMA(object): + STRUCT = Struct('> i i') + + def __init__(self, buffer): + (self.SchemaSize, self.TableCount) = _APPL_DB_SCHEMA.STRUCT.unpack(buffer) + + +class _TABLE_HEADER(object): + STRUCT = Struct('> I I I I I I I') + + def __init__(self, buffer): + (self.TableSize, self.TableId, self.RecordCount, self.Records, self.IndexesOffset, self.FreeListHead, + self.RecordNumbersCount) = _TABLE_HEADER.STRUCT.unpack(buffer) + + +class _DB_BLOB(object): + STRUCT = Struct('> 8s I I 16s I 8s 20s 8s 20s') + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.StartCryptoBlob, self.TotalLength, self.RandomSignature, self.Sequence, + self.ParamsBuffer, self.Salt, self.IV, self.BlobSignature) = _DB_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + self.Params = _DB_PARAMETERS(self.ParamsBuffer) + + +class _COMMON_BLOB(object): + STRUCT = Struct('> L l') + + def __init__(self, buffer): + (self.Magic, self.BlobVersion) = _COMMON_BLOB.STRUCT.unpack(buffer) + + +class _DB_PARAMETERS(object): + STRUCT = Struct('> I I') + def __init__(self, buffer): + (self.IdleTimeout, self.LockOnSleep) = _DB_PARAMETERS.STRUCT.unpack(buffer) + + +class _GENERIC_PW_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.Service, self.Generic,) = _GENERIC_PW_HEADER.STRUCT.unpack(buffer) + + +class _KEY_BLOB_REC_HEADER(object): + STRUCT = Struct('> I I 124s ') + + def __init__(self, buffer): + (self.RecordSize, self.RecordCount, self.Dummy) = _KEY_BLOB_REC_HEADER.STRUCT.unpack(buffer) + + +class _KEY_BLOB(object): + STRUCT = Struct('> 8s I I 8s') + COMMON_BLOB_MAGIC = 0xFADE0711 + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.StartCryptoBlob, self.TotalLength, self.IV,) = _KEY_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + + +# First 28 bytes are structured - remainder is our encrypted password. +class _SSGP(object): + STRUCT = Struct('> 4s 16s 8s') + + def __init__(self, buffer): + (self.Magic, self.Label, self.IV,) = _SSGP.STRUCT.unpack(buffer[:28]) + self.EncryptedPassword = buffer[28:] + + +class _INTERNET_PW_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.SecurityDomain, self.Server, self.Protocol, self.AuthType, self.Port, + self.Path,) = _INTERNET_PW_HEADER.STRUCT.unpack(buffer) + + +class _APPLE_SHARE_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.Volume, self.Server, self.Protocol, self.AuthType, self.Address, + self.Signature,) = _APPLE_SHARE_HEADER.STRUCT.unpack(buffer) + + +class _X509_CERT_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown1, self.Unknown2, self.CertSize, self.Unknown3, self.CertType, + self.CertEncoding, self.PrintName, self.Alias, self.Subject, self.Issuer, self.SerialNumber, + self.SubjectKeyIdentifier, self.PublicKeyHash,) = _X509_CERT_HEADER.STRUCT.unpack(buffer) + + +# # http://www.opensource.apple.com/source/Security/Security-55179.1/include/security_cdsa_utilities/KeySchema.h +# # http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36940/lib/SecKey.h + +class _SECKEY_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown1, self.Unknown2, self.BlobSize, self.Unknown3, self.KeyClass, + self.PrintName, self.Alias, self.Permanent, self.Private, self.Modifiable, self.Label, self.ApplicationTag, + self.KeyCreator, self.KeyType, self.KeySizeInBits, self.EffectiveKeySize, self.StartDate, self.EndDate, + self.Sensitive, self.AlwaysSensitive, self.Extractable, self.NeverExtractable, self.Encrypt, self.Decrypt, + self.Derive, self.Sign, self.Verify, self.SignRecover, self.VerifyRecover, self.Wrap, + self.UnWrap,) = _SECKEY_HEADER.STRUCT.unpack(buffer) + + +class _UNLOCK_BLOB(object): + STRUCT = Struct('> 8s 24s 16s') + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.MasterKey, self.BlobSignature) = _UNLOCK_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + + +class _KEYCHAIN_TIME(object): + STRUCT = Struct('>16s') + STRPTIME_FORMAT = "%Y%m%d%H%M%SZ" + + def __init__(self, buffer): + self.Value = _KEYCHAIN_TIME.STRUCT.unpack(buffer)[0].strip('\x00') + self.Time = datetime.strptime(self.Value, _KEYCHAIN_TIME.STRPTIME_FORMAT) + + def __repr__(self): + return + + +class _INT(object): + STRUCT = Struct('>I') + + def __init__(self, buffer): + self.Value = _INT.STRUCT.unpack(buffer)[0] + + +class _FOUR_CHAR_CODE(object): + STRUCT = Struct('>4s') + + def __init__(self, buffer): + self.Value = _FOUR_CHAR_CODE.STRUCT.unpack(buffer)[0] + + +class _LV(object): + def __init__(self, buffer, length): + self.STRUCT = Struct(">" + str(length) + "s") + self.Value = self.STRUCT.unpack(buffer)[0].strip('\x00') + + +class _RECORD_OFFSET(_INT): + pass + + +class _TABLE_ID(_INT): + pass diff --git a/validator.py b/validator.py deleted file mode 100644 index f5fd3da..0000000 --- a/validator.py +++ /dev/null @@ -1,48 +0,0 @@ -import OpenSSL.crypto -from Crypto.Util import asn1 - -c = OpenSSL.crypto - - -class Validator: - def __init__(self): - pass - - def _get_key(self, key_path): - st_key = open(key_path, 'rt').read() - key = c.load_privatekey(c.FILETYPE_ASN1, st_key) - return key - - def _get_cert(self, cert_path): - st_cert = open(cert_path, 'rt').read() - cert = c.load_certificate(c.FILETYPE_ASN1, st_cert) - return cert - - def validate_by_filenames(self, key_path, cert_path): - key = self._get_key(key_path) - cert = self._get_cert(cert_path) - - pub = cert.get_pubkey() - - # Only works for RSA (I think) - # if pub.type() != c.TYPE_RSA or key.type() != c.TYPE_RSA: - # raise Exception('Can only handle RSA keys') - - # This seems to work with public as well - pub_asn1 = c.dump_privatekey(c.FILETYPE_ASN1, pub) - priv_asn1 = c.dump_privatekey(c.FILETYPE_ASN1, key) - - # Decode DER - pub_der = asn1.DerSequence() - pub_der.decode(pub_asn1) - priv_der = asn1.DerSequence() - priv_der.decode(priv_asn1) - - # Get the modulus - pub_modulus = pub_der[1] - priv_modulus = priv_der[1] - - if pub_modulus == priv_modulus: - return True - else: - return False