Skip to content

Commit 3c0d8ef

Browse files
authored
Add keychain support for decrypting FortiGate firmware files (#1469)
1 parent b805239 commit 3c0d8ef

File tree

2 files changed

+160
-4
lines changed
  • dissect/target/plugins/os/unix/linux/fortios
  • tests/plugins/os/unix/linux/fortios

2 files changed

+160
-4
lines changed

dissect/target/plugins/os/unix/linux/fortios/_os.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from dissect.util.compression import xz
1414

1515
from dissect.target.filesystems.tar import TarFilesystem
16+
from dissect.target.helpers import keychain
1617
from dissect.target.helpers.fsutil import open_decompress
1718
from dissect.target.helpers.record import TargetRecordDescriptor, UnixUserRecord
1819
from dissect.target.plugin import OperatingSystem, export
@@ -117,9 +118,37 @@ def create(cls, target: Target, sysvol: Filesystem) -> Self:
117118
try:
118119
kernel_hash = get_kernel_hash(sysvol)
119120
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+
123152
target.log.info("Decrypted fh: %r", rfs_fh)
124153
vfs = create_tar_filesystem(rfs_fh)
125154
except RuntimeError:
@@ -528,6 +557,50 @@ def key_iv_for_kernel_hash(kernel_hash: str) -> AesKey | ChaCha20Key:
528557
raise ValueError(f"No known decryption keys for kernel hash: {kernel_hash}")
529558

530559

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+
531604
def chacha20_decrypt(fh: BinaryIO, key: ChaCha20Key) -> bytes:
532605
"""Decrypt file using ChaCha20 with given ChaCha20Key.
533606
@@ -636,7 +709,8 @@ def decrypt_rootfs(fh: BinaryIO, key: ChaCha20Key | AesKey) -> BinaryIO:
636709
result = chacha20_decrypt(fh, key)
637710
elif isinstance(key, AesKey):
638711
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
640714

641715
if result[0:2] != b"\x1f\x8b":
642716
raise ValueError("Failed to decrypt: No gzip magic header found.")

tests/plugins/os/unix/linux/fortios/test_keys.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import re
44
from io import BytesIO
5+
from typing import TYPE_CHECKING
56

67
import pytest
78

9+
from dissect.target.helpers import keychain
810
from dissect.target.plugins.os.unix.linux.fortios._keys import (
911
KERNEL_KEY_MAP,
1012
AesKey,
@@ -15,8 +17,14 @@
1517
aes_decrypt,
1618
decrypt_rootfs,
1719
key_iv_for_kernel_hash,
20+
key_iv_from_keychain,
1821
)
1922

23+
if TYPE_CHECKING:
24+
from pathlib import Path
25+
26+
from dissect.target.target import Target
27+
2028

2129
def test_kernel_key_map() -> None:
2230
# Ensure that the kernel key map is valid
@@ -84,3 +92,77 @@ def test_aes_decrypt() -> None:
8492
assert isinstance(key, AesKey)
8593
data = aes_decrypt(BytesIO(encrypted_rootfs_header), key)
8694
assert data == decrypted_rootfs_header
95+
96+
97+
def test_decrypt_rootfs_from_keychain_file(target_unix: Target, tmp_path: Path) -> None:
98+
# encrypted FGT_1000D-v7.6.4.F-build3596-FORTINET.out/rootfs.gz
99+
kernel_hash = "8e7fb3af9fe68d69af224857164347cee271264308c8ba86e9ad036e405ac6c8"
100+
encrypted_rootfs_header = bytes.fromhex(
101+
"""
102+
d739 ba66 6d65 ca64 4295 b7e4 3c48 7165
103+
49ab e60c fc39 ef48 30b0 06cd f32c 37f2
104+
"""
105+
)
106+
decrypted_rootfs_header = bytes.fromhex(
107+
"""
108+
1f8b 0800 4d07 a668 0003 a4d3 5390 2ed0
109+
d226 e8c2 aeaf 6cdb b66d dbb6 6d57 edb2
110+
"""
111+
)
112+
113+
keys = key_iv_from_keychain(target_unix, kernel_hash=kernel_hash)
114+
assert not keys, "Keys found in keychain when none were expected"
115+
116+
keychain_file = tmp_path / "fortios_keychain.csv"
117+
keychain_file.write_text(
118+
"fortios-aeskey,recovery_key,"
119+
"8e7fb3af9fe68d69af224857164347cee271264308c8ba86e9ad036e405ac6c8,"
120+
"5adbbe614bcde31c3e05ba2e261c1a2410f0900ed340689835520a0612fc612b:e4973d6eff0412b4dbf4fe43c4d3136d"
121+
)
122+
keychain.register_keychain_file(keychain_file)
123+
124+
keys = key_iv_from_keychain(target_unix, kernel_hash=kernel_hash)
125+
assert keys, "No keys found in keychain for testing"
126+
127+
for key in keys:
128+
assert isinstance(key, AesKey)
129+
assert key.key == bytes.fromhex("5adbbe614bcde31c3e05ba2e261c1a2410f0900ed340689835520a0612fc612b")
130+
assert key.iv == bytes.fromhex("e4973d6eff0412b4dbf4fe43c4d3136d")
131+
132+
fh = decrypt_rootfs(BytesIO(encrypted_rootfs_header), key)
133+
assert fh.read() == decrypted_rootfs_header
134+
135+
136+
def test_decrypt_rootfs_from_keychain_value(target_unix: Target) -> None:
137+
# encrypted FGT_1000D-v7.6.4.F-build3596-FORTINET.out/rootfs.gz
138+
kernel_hash = "8e7fb3af9fe68d69af224857164347cee271264308c8ba86e9ad036e405ac6c8"
139+
encrypted_rootfs_header = bytes.fromhex(
140+
"""
141+
d739 ba66 6d65 ca64 4295 b7e4 3c48 7165
142+
49ab e60c fc39 ef48 30b0 06cd f32c 37f2
143+
"""
144+
)
145+
decrypted_rootfs_header = bytes.fromhex(
146+
"""
147+
1f8b 0800 4d07 a668 0003 a4d3 5390 2ed0
148+
d226 e8c2 aeaf 6cdb b66d dbb6 6d57 edb2
149+
"""
150+
)
151+
152+
keys = key_iv_from_keychain(target_unix, kernel_hash=kernel_hash)
153+
assert not keys, "Keys found in keychain when none were expected"
154+
155+
keychain.register_wildcard_value(
156+
"5adbbe614bcde31c3e05ba2e261c1a2410f0900ed340689835520a0612fc612b:e4973d6eff0412b4dbf4fe43c4d3136d"
157+
)
158+
159+
keys = key_iv_from_keychain(target_unix, kernel_hash=kernel_hash)
160+
assert keys, "No keys found in keychain for testing"
161+
162+
for key in keys:
163+
assert isinstance(key, AesKey)
164+
assert key.key == bytes.fromhex("5adbbe614bcde31c3e05ba2e261c1a2410f0900ed340689835520a0612fc612b")
165+
assert key.iv == bytes.fromhex("e4973d6eff0412b4dbf4fe43c4d3136d")
166+
167+
fh = decrypt_rootfs(BytesIO(encrypted_rootfs_header), key)
168+
assert fh.read() == decrypted_rootfs_header

0 commit comments

Comments
 (0)