|
13 | 13 | from dissect.util.compression import xz |
14 | 14 |
|
15 | 15 | from dissect.target.filesystems.tar import TarFilesystem |
| 16 | +from dissect.target.helpers import keychain |
16 | 17 | from dissect.target.helpers.fsutil import open_decompress |
17 | 18 | from dissect.target.helpers.record import TargetRecordDescriptor, UnixUserRecord |
18 | 19 | from dissect.target.plugin import OperatingSystem, export |
@@ -117,9 +118,37 @@ def create(cls, target: Target, sysvol: Filesystem) -> Self: |
117 | 118 | try: |
118 | 119 | kernel_hash = get_kernel_hash(sysvol) |
119 | 120 | target.log.info("Kernel hash: %s", kernel_hash) |
120 | | - key = key_iv_for_kernel_hash(kernel_hash) |
121 | | - target.log.info("Trying to decrypt_rootfs using key: %r", key) |
122 | | - rfs_fh = decrypt_rootfs(rootfs.open(), key) |
| 121 | + |
| 122 | + keys: list[ChaCha20Key | AesKey] = [] |
| 123 | + |
| 124 | + # Loads known key from built-in map |
| 125 | + try: |
| 126 | + keys.append(key_iv_for_kernel_hash(kernel_hash)) |
| 127 | + except ValueError: |
| 128 | + target.log.warning("No known decryption key for kernel hash (%s)", kernel_hash) |
| 129 | + |
| 130 | + # Load keys from keychain |
| 131 | + keychain_keys = key_iv_from_keychain(target, kernel_hash=kernel_hash) |
| 132 | + keys.extend(keychain_keys) |
| 133 | + target.log.warning("Found %d key(s) in keychain", len(keychain_keys)) |
| 134 | + |
| 135 | + if not keys: |
| 136 | + raise ValueError("No decryption keys available") # noqa: TRY301 |
| 137 | + |
| 138 | + # Try to decrypt with all available keys |
| 139 | + rfs_fh = None |
| 140 | + for key in keys: |
| 141 | + target.log.warning("Trying to decrypt_rootfs using key: %r", key) |
| 142 | + try: |
| 143 | + rfs_fh = decrypt_rootfs(rootfs.open(), key) |
| 144 | + break |
| 145 | + except ValueError: |
| 146 | + target.log.info("Decryption with key %r failed", key) |
| 147 | + continue |
| 148 | + |
| 149 | + if not rfs_fh: |
| 150 | + raise ValueError("All decryption attempts failed") # noqa: TRY301 |
| 151 | + |
123 | 152 | target.log.info("Decrypted fh: %r", rfs_fh) |
124 | 153 | vfs = create_tar_filesystem(rfs_fh) |
125 | 154 | except RuntimeError: |
@@ -528,6 +557,50 @@ def key_iv_for_kernel_hash(kernel_hash: str) -> AesKey | ChaCha20Key: |
528 | 557 | raise ValueError(f"No known decryption keys for kernel hash: {kernel_hash}") |
529 | 558 |
|
530 | 559 |
|
| 560 | +def key_iv_from_keychain(target: Target, kernel_hash: str) -> list[ChaCha20Key | AesKey]: |
| 561 | + """Return list of ChaCha20Key and AesKey from keychain. |
| 562 | +
|
| 563 | + When using the ``--keychain-file`` option, the CSV format is: |
| 564 | +
|
| 565 | + provider,key_type,identifier,value |
| 566 | + fortios-chacha20seed,recovery_key,<kernel_hash>,<chacha20_seed> |
| 567 | + fortios-chacha20key,recovery_key,<kernel_hash>,<chacha20_key>:<chacha20_iv> |
| 568 | + fortios-aeskey,recovery_key,<kernel_hash>,<aes_key>:<aes_iv> |
| 569 | +
|
| 570 | + When using the ``--keychain-value`` option, multiple keys are returned due to missing provider. |
| 571 | +
|
| 572 | + Args: |
| 573 | + target: Target instance. |
| 574 | + kernel_hash: SHA256 hash of the kernel file used to match key identifiers in the keychain. |
| 575 | +
|
| 576 | + Returns: |
| 577 | + List of ChaCha20Key or AesKey. |
| 578 | + """ |
| 579 | + keys: list[ChaCha20Key | AesKey] = [] |
| 580 | + keychain_keys = keychain.get_all_keys() |
| 581 | + |
| 582 | + # We prioritize keys with matching identifier (kernel hash) |
| 583 | + identifier_keys = [key for key in keychain_keys if key.identifier == kernel_hash] |
| 584 | + other_keys = [key for key in keychain_keys if key.identifier is None] |
| 585 | + |
| 586 | + for key in identifier_keys + other_keys: |
| 587 | + if not isinstance(key.value, str): |
| 588 | + continue |
| 589 | + if key.provider in ("fortios-chacha20seed", None) and len(key.value) == 64: |
| 590 | + # 32 bytes hex string |
| 591 | + key_data, key_iv = _kdf_7_4_x(key.value) |
| 592 | + keys.append(ChaCha20Key(key_data, key_iv)) |
| 593 | + elif key.provider in ("fortios-aeskey", None) and len(key.value) == 97 and ":" in key.value: |
| 594 | + # 48 bytes hex string with colon separator |
| 595 | + key_data, _, key_iv = key.value.partition(":") |
| 596 | + keys.append(AesKey(key_data, key_iv)) |
| 597 | + elif key.provider in ("fortios-chacha20key", None) and len(key.value) == 97 and ":" in key.value: |
| 598 | + # 48 bytes hex string with colon separator |
| 599 | + key_data, _, key_iv = key.value.partition(":") |
| 600 | + keys.append(ChaCha20Key(key_data, key_iv)) |
| 601 | + return keys |
| 602 | + |
| 603 | + |
531 | 604 | def chacha20_decrypt(fh: BinaryIO, key: ChaCha20Key) -> bytes: |
532 | 605 | """Decrypt file using ChaCha20 with given ChaCha20Key. |
533 | 606 |
|
@@ -636,7 +709,8 @@ def decrypt_rootfs(fh: BinaryIO, key: ChaCha20Key | AesKey) -> BinaryIO: |
636 | 709 | result = chacha20_decrypt(fh, key) |
637 | 710 | elif isinstance(key, AesKey): |
638 | 711 | result = aes_decrypt(fh, key) |
639 | | - result = result[:-256] # strip off the 256 byte footer |
| 712 | + if len(result) > 256: |
| 713 | + result = result[:-256] # strip off the 256 byte footer |
640 | 714 |
|
641 | 715 | if result[0:2] != b"\x1f\x8b": |
642 | 716 | raise ValueError("Failed to decrypt: No gzip magic header found.") |
|
0 commit comments