├── .gitignore ├── LICENSE ├── README.md ├── cli.py ├── entdb ├── __init__.py ├── db.py ├── finder.py ├── magic.py └── parser.py ├── init.sql ├── query.py └── rules ├── iPhoneOS.txt └── macOS.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 codecolorist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # entdb 2 | 3 | Self-hosted entitlement database, on macOS only. No 3rd-party dependency except `codesign` from macOS. 4 | 5 | `cli.py` to create database. See `query.py` for making queries. 6 | 7 | ## Usage 8 | 9 | `python3 cli.py --init` 10 | 11 | When `--init` is used, previous data will be cleared. 12 | 13 | ## Example 14 | 15 | Get [ipsw](https://github.com/blacktop/ipsw) tool to boost your research! 16 | 17 | ```bash 18 | ipsw mount fs ~/Downloads/iPhone100,0_100.0_AAAAA_Restore.ipsw 19 | # • Mounted fs DMG 000-00000-000.dmg 20 | # • Press Ctrl+C to unmount '/tmp/000-00000-000.dmg.mount' 21 | 22 | # another terminal 23 | python3 cli.py /tmp/000-00000-000.dmg.mount 24 | # tweak query.py for your needs 25 | python3 query.py 26 | ``` 27 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | import json 3 | import os 4 | from pathlib import Path 5 | 6 | from entdb.magic import is_macho 7 | from entdb.db import connect, init 8 | from entdb.finder import PathFinder 9 | from entdb.parser import xml 10 | 11 | 12 | class Visitor: 13 | def __init__(self, finder: PathFinder, root='/'): 14 | self.finder = finder 15 | self.root = Path(root).resolve() 16 | 17 | def run(self): 18 | for item in self.finder.entries(): 19 | yield from self.visit(self.root / item) 20 | 21 | def visit(self, path: Path): 22 | if not os.access(path, os.R_OK): 23 | return 24 | 25 | if not path.exists(): 26 | return 27 | 28 | path = path.resolve() 29 | 30 | if path.is_file(): 31 | if is_macho(path): 32 | yield path 33 | return 34 | 35 | if not path.is_dir(): 36 | return 37 | 38 | try: 39 | relative = path.relative_to(self.root) 40 | except ValueError: 41 | return 42 | 43 | if self.finder.is_excluded(str(relative)): 44 | return 45 | 46 | for child in path.iterdir(): 47 | yield from self.visit(child) 48 | 49 | 50 | def main(root: Path, db: str): 51 | conn = connect(db) 52 | 53 | with open(root / 'System/Library/CoreServices/SystemVersion.plist', 'rb') as fp: 54 | info = plistlib.load(fp) 55 | 56 | product = info['ProductName'] 57 | if product == 'iPhone OS': 58 | rule_file = 'iPhoneOS.txt' 59 | else: 60 | rule_file = 'macOS.txt' 61 | if product != 'macOS': 62 | logging.warning('unknown product name: %s', product) 63 | 64 | finder = PathFinder(Path(__file__).parent / 'rules' / rule_file) 65 | 66 | name = info['ProductName'] 67 | build = info['ProductBuildVersion'] 68 | ver = info['ProductVersion'] 69 | cur = conn.execute( 70 | 'INSERT INTO os(name, ver, build, path) values(?, ?, ?, ?)', (name, ver, build, str(root))) 71 | os_id = cur.lastrowid 72 | 73 | conn.commit() 74 | 75 | v = Visitor(finder, str(root)) 76 | known = set() 77 | for item in v.run(): 78 | if not is_macho(item): 79 | continue 80 | 81 | try: 82 | path = '/%s' % item.resolve().relative_to(root) 83 | except ValueError: 84 | continue 85 | 86 | if path in known: 87 | continue 88 | 89 | known.add(path) 90 | 91 | x = xml(str(item)) 92 | if len(x): 93 | d = plistlib.loads(x) 94 | j = json.dumps(d) 95 | else: 96 | d = {} 97 | j = '' 98 | 99 | cur = conn.execute( 100 | 'INSERT INTO bin(os_id, path, xml, json) VALUES (?, ?, ?, ?)', (os_id, path, x, j)) 101 | bin_id = cur.lastrowid 102 | 103 | for key, val in d.items(): 104 | conn.execute( 105 | 'INSERT INTO pair(binary_id, key, value) VALUES (?, ?, ?)', (bin_id, key, json.dumps( 106 | val)) 107 | ) 108 | 109 | conn.commit() 110 | 111 | conn.close() 112 | 113 | 114 | if __name__ == '__main__': 115 | import logging 116 | logging.basicConfig(level=logging.INFO) 117 | 118 | import argparse 119 | parser = argparse.ArgumentParser() 120 | parser.add_argument('root', help='root directory', default='/') 121 | parser.add_argument('database', help='database file', 122 | default='data.db', nargs='?') 123 | parser.add_argument('--init', action='store_true', 124 | help='initialize database') 125 | 126 | args = parser.parse_args() 127 | root = Path(args.root).resolve() 128 | 129 | if args.init: 130 | init(args.database) 131 | 132 | main(root, args.database) 133 | -------------------------------------------------------------------------------- /entdb/__init__.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | os = platform.system() 4 | 5 | if os != 'Darwin': 6 | raise RuntimeError('Unsupported OS %s' % os) 7 | -------------------------------------------------------------------------------- /entdb/db.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sqlite3 3 | 4 | cwd = Path(__file__).parent.parent 5 | 6 | 7 | def connect(name: str = 'data.db') -> sqlite3.Connection: 8 | return sqlite3.connect(name) 9 | 10 | 11 | def init(name: Path | None): 12 | sql = cwd / 'init.sql' 13 | conn = connect(name) 14 | with sql.open() as fp: 15 | conn.executescript(fp.read()) 16 | -------------------------------------------------------------------------------- /entdb/finder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def strip_slash(path: str): 5 | if path.startswith('/'): 6 | path = path[1:] 7 | 8 | if path.endswith('/'): 9 | path = path[:-1] 10 | 11 | return path 12 | 13 | 14 | class PathFinder: 15 | def __init__(self, rule_file: Path): 16 | self.rule_file = rule_file 17 | self.exclude_tree = {} 18 | self.includes = set() 19 | self.parse() 20 | 21 | def parse(self): 22 | with self.rule_file.open() as fp: 23 | for line in fp: 24 | rule = line.strip() 25 | 26 | if not len(rule) or rule.startswith('#'): 27 | continue 28 | 29 | self.add(rule) 30 | 31 | def entries(self): 32 | return self.includes 33 | 34 | def add(self, rule: str): 35 | if rule.startswith('!'): 36 | self.exclude(rule[1:]) 37 | else: 38 | self.includes.add(strip_slash(rule)) # strip / 39 | 40 | def exclude(self, path: str): 41 | leaf = self.exclude_tree 42 | for name in strip_slash(path).split('/'): 43 | if name not in leaf: 44 | leaf[name] = {} 45 | 46 | leaf = leaf[name] 47 | 48 | def is_excluded(self, relative: str): 49 | leaf = self.exclude_tree 50 | for name in strip_slash(relative).split('/'): 51 | leaf = leaf.get(name) 52 | if leaf is None: 53 | return False 54 | return leaf == {} 55 | -------------------------------------------------------------------------------- /entdb/magic.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from enum import IntEnum 3 | from pathlib import Path 4 | 5 | 6 | class Headers(IntEnum): 7 | FAT_MAGIC = 0xcafebabe 8 | FAT_CIGAM = 0xbebafeca 9 | MH_MAGIC = 0xfeedface 10 | MH_CIGAM = 0xcefaedfe 11 | MH_MAGIC_64 = 0xfeedfacf 12 | MH_CIGAM_64 = 0xcffaedfe 13 | 14 | 15 | class FileType(IntEnum): 16 | MH_OBJECT = 0x1 17 | MH_EXECUTE = 0x2 18 | MH_FVMLIB = 0x3 19 | MH_CORE = 0x4 20 | MH_PRELOAD = 0x5 21 | MH_DYLIB = 0x6 22 | MH_DYLINKER = 0x7 23 | MH_BUNDLE = 0x8 24 | MH_DYLIB_STUB = 0x9 25 | MH_DSYM = 0xa 26 | MH_KEXT_BUNDLE = 0xb 27 | 28 | 29 | all_values = set(val for val in Headers) 30 | 31 | 32 | def test(value): 33 | return value in all_values 34 | 35 | 36 | def is_macho(path_or_str: Path | str): 37 | try: 38 | with Path(path_or_str).open('rb') as fp: 39 | buf = fp.read(16) 40 | if len(buf) < 16: 41 | return False 42 | except (PermissionError, FileNotFoundError) as _: 43 | return False 44 | 45 | magic, cputype, cpusubtyp, filetype = struct.unpack('>IIII', buf) 46 | return test(magic) # and filetype == FileType.MH_EXECUTE 47 | 48 | -------------------------------------------------------------------------------- /entdb/parser.py: -------------------------------------------------------------------------------- 1 | import mmap 2 | import shutil 3 | import subprocess 4 | 5 | cli = shutil.which('codesign') 6 | needle = b'Specifying \':\' in the path is deprecated and will not work in a future release' 7 | cmd = ['codesign', '-d'] 8 | 9 | with open(cli, 'rb') as fp: 10 | mm = mmap.mmap(fp.fileno(), 0, prot=mmap.PROT_READ) 11 | idx = mm.find(needle) 12 | if idx > -1: 13 | cmd = cmd + ['--xml', '--entitlements', '-'] 14 | else: 15 | cmd = cmd + ['--entitlements', ':-'] 16 | mm.close() 17 | 18 | 19 | def xml(path: str): 20 | try: 21 | return subprocess.check_output(cmd + [path]).strip(b'\x00') 22 | except subprocess.CalledProcessError: 23 | return b'' 24 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "os"; 2 | DROP TABLE IF EXISTS "bin"; 3 | DROP TABLE IF EXISTS "bin_os_id"; 4 | DROP TABLE IF EXISTS "pair"; 5 | DROP TABLE IF EXISTS "pair_binary_id"; 6 | 7 | CREATE TABLE "os" ("id" INTEGER NOT NULL PRIMARY KEY, "build" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL, "ver" VARCHAR(255) NOT NULL, "path" TEXT NOT NULL); 8 | CREATE TABLE "bin" ("id" INTEGER NOT NULL PRIMARY KEY, "os_id" INTEGER NOT NULL, "path" TEXT NOT NULL, "xml" TEXT NOT NULL, "json" TEXT NOT NULL, FOREIGN KEY ("os_id") REFERENCES "os" ("id")); 9 | CREATE INDEX "bin_os_id" ON "bin" ("os_id"); 10 | CREATE TABLE "pair" ("id" INTEGER NOT NULL PRIMARY KEY, "binary_id" INTEGER NOT NULL, "key" VARCHAR(255) NOT NULL, "value" TEXT NOT NULL, FOREIGN KEY ("binary_id") REFERENCES "bin" ("id")); 11 | CREATE INDEX "pair_binary_id" ON "pair" ("binary_id"); -------------------------------------------------------------------------------- /query.py: -------------------------------------------------------------------------------- 1 | from sqlite3 import Connection 2 | from typing import Iterator 3 | 4 | from entdb.db import connect 5 | 6 | 7 | def find_by_key(conn: Connection, key: str) -> Iterator[str]: 8 | sql = '''SELECT bin.path FROM pair, bin WHERE pair.key == ? AND bin.id = pair.binary_id;''' 9 | for row in conn.execute(sql, [key]): 10 | path, = row 11 | yield path 12 | 13 | 14 | if __name__ == '__main__': 15 | import argparse 16 | 17 | parser = argparse.ArgumentParser() 18 | parser.add_argument('db', help='database file', default='data.db', nargs='?') 19 | args = parser.parse_args() 20 | conn = connect(args.db) 21 | 22 | print('library validation disabled') 23 | for path in find_by_key(conn, 'com.apple.security.cs.disable-library-validation'): 24 | print(path) 25 | 26 | print('allows debug') 27 | for path in find_by_key(conn, 'com.apple.security.get-task-allow'): 28 | print(path) 29 | 30 | print('all possible entitlements') 31 | for row in conn.execute('SELECT DISTINCT key FROM pair ORDER BY key'): 32 | key, = row 33 | print(key) 34 | 35 | conn.close() -------------------------------------------------------------------------------- /rules/iPhoneOS.txt: -------------------------------------------------------------------------------- 1 | /System/Library 2 | /Applications 3 | /var/staged_system_apps 4 | 5 | /bin 6 | /sbin 7 | /usr 8 | -------------------------------------------------------------------------------- /rules/macOS.txt: -------------------------------------------------------------------------------- 1 | # include 2 | 3 | /sbin 4 | /usr/sbin 5 | /usr/bin 6 | /usr/libexec 7 | 8 | /var/staged_system_apps/ 9 | 10 | # /System/Library/Filesystems 11 | /System/Library/CoreServices 12 | /System/Library/Frameworks 13 | /System/Library/PrivateFrameworks 14 | /System/Library/ExtensionKit/Extensions/ 15 | /System/Library/AppRemovalServices 16 | /System/Library/TextInput 17 | 18 | #/System/Library/AccessibilityBundles 19 | #/System/Library/PreferenceManifests 20 | #/System/Library/KerberosPlugins 21 | #/System/Library/UserEventPlugins 22 | #/System/Library/UserNotifications 23 | #/System/Library/MediaStreamPlugins 24 | 25 | 26 | /System/Applications 27 | /System/DriverKit 28 | 29 | # exclude 30 | !/System/Library/Frameworks/Ruby.framework/ 31 | !/usr/libexec/apache2 32 | !/usr/libexec/postfix/ 33 | !/usr/libexec/cups/ 34 | !/System/Library/CoreServices/Menu Extras/ 35 | !/usr/local/ 36 | 37 | !/usr/lib/zsh/ 38 | !/usr/lib/sasl2/ 39 | 40 | !/Library/Java/ 41 | !/Library/Developer/ --------------------------------------------------------------------------------