dissect.target 3.21.dev15__py3-none-any.whl → 3.21.dev16__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dissect/target/plugins/os/unix/linux/fortios/_keys.py +7919 -1951
- dissect/target/plugins/os/unix/linux/fortios/_os.py +109 -22
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/METADATA +1 -1
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/RECORD +9 -9
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/LICENSE +0 -0
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/WHEEL +0 -0
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import gzip
|
4
4
|
import hashlib
|
5
|
+
import struct
|
5
6
|
from base64 import b64decode
|
6
7
|
from datetime import datetime
|
7
8
|
from io import BytesIO
|
@@ -17,11 +18,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
|
19
20
|
from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
|
20
|
-
from dissect.target.plugins.os.unix.linux.fortios._keys import
|
21
|
+
from dissect.target.plugins.os.unix.linux.fortios._keys import (
|
22
|
+
KERNEL_KEY_MAP,
|
23
|
+
AesKey,
|
24
|
+
ChaCha20Key,
|
25
|
+
ChaCha20Seed,
|
26
|
+
)
|
21
27
|
from dissect.target.target import Target
|
22
28
|
|
23
29
|
try:
|
24
30
|
from Crypto.Cipher import AES, ChaCha20
|
31
|
+
from Crypto.Util import Counter
|
25
32
|
|
26
33
|
HAS_CRYPTO = True
|
27
34
|
except ImportError:
|
@@ -95,8 +102,11 @@ class FortiOSPlugin(LinuxPlugin):
|
|
95
102
|
# The rootfs.gz file could be encrypted.
|
96
103
|
try:
|
97
104
|
kernel_hash = get_kernel_hash(sysvol)
|
98
|
-
|
99
|
-
|
105
|
+
target.log.info("Kernel hash: %s", kernel_hash)
|
106
|
+
key = key_iv_for_kernel_hash(kernel_hash)
|
107
|
+
target.log.info("Trying to decrypt_rootfs using key: %r", key)
|
108
|
+
rfs_fh = decrypt_rootfs(rootfs.open(), key)
|
109
|
+
target.log.info("Decrypted fh: %r", rfs_fh)
|
100
110
|
vfs = TarFilesystem(rfs_fh, tarinfo=cpio.CpioInfo)
|
101
111
|
except RuntimeError:
|
102
112
|
target.log.warning("Could not decrypt rootfs.gz. Missing `pycryptodome` dependency.")
|
@@ -471,7 +481,7 @@ def decrypt_password(input: str) -> str:
|
|
471
481
|
return "ENC:" + input
|
472
482
|
|
473
483
|
|
474
|
-
def key_iv_for_kernel_hash(kernel_hash: str) ->
|
484
|
+
def key_iv_for_kernel_hash(kernel_hash: str) -> AesKey | ChaCha20Key:
|
475
485
|
"""Return decryption key and IV for a specific sha256 kernel hash.
|
476
486
|
|
477
487
|
The decryption key and IV are used to decrypt the ``rootfs.gz`` file.
|
@@ -486,17 +496,96 @@ def key_iv_for_kernel_hash(kernel_hash: str) -> tuple[bytes, bytes]:
|
|
486
496
|
ValueError: When no decryption keys are available for the given kernel hash.
|
487
497
|
"""
|
488
498
|
|
489
|
-
key =
|
490
|
-
if
|
499
|
+
key = KERNEL_KEY_MAP.get(kernel_hash)
|
500
|
+
if isinstance(key, ChaCha20Seed):
|
491
501
|
# FortiOS 7.4.x uses a KDF to derive the key and IV
|
492
|
-
|
493
|
-
|
502
|
+
key, iv = _kdf_7_4_x(key.key)
|
503
|
+
return ChaCha20Key(key, iv)
|
504
|
+
elif isinstance(key, ChaCha20Key):
|
494
505
|
# FortiOS 7.0.13 and 7.0.14 uses a static key and IV
|
495
|
-
return key
|
506
|
+
return key
|
507
|
+
elif isinstance(key, AesKey):
|
508
|
+
# FortiOS 7.0.16, 7.2.9, 7.4.4, 7.6.0 and higher uses AES-CTR with a custom CTR increment
|
509
|
+
return key
|
496
510
|
raise ValueError(f"No known decryption keys for kernel hash: {kernel_hash}")
|
497
511
|
|
498
512
|
|
499
|
-
def
|
513
|
+
def chacha20_decrypt(fh: BinaryIO, key: ChaCha20Key) -> bytes:
|
514
|
+
"""Decrypt file using ChaCha20 with given ChaCha20Key.
|
515
|
+
|
516
|
+
Args:
|
517
|
+
fh: File-like object to the encrypted rootfs.gz file.
|
518
|
+
key: ChaCha20Key.
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
Decrypted bytes.
|
522
|
+
"""
|
523
|
+
|
524
|
+
# First 8 bytes = counter, last 8 bytes = nonce
|
525
|
+
# PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
|
526
|
+
# We're interested in updating the position in the ChaCha20 internal state, so to make
|
527
|
+
# PyCryptodome "OpenSSL-compatible" we have to multiply the counter by 64
|
528
|
+
cipher = ChaCha20.new(key=key.key, nonce=key.iv[8:])
|
529
|
+
cipher.seek(int.from_bytes(key.iv[:8], "little") * 64)
|
530
|
+
return cipher.decrypt(fh.read())
|
531
|
+
|
532
|
+
|
533
|
+
def calculate_counter_increment(iv: bytes) -> int:
|
534
|
+
"""Calculate the custom FortiGate CTR increment from IV.
|
535
|
+
|
536
|
+
Args:
|
537
|
+
iv: 16 bytes IV.
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
Custom CTR increment.
|
541
|
+
"""
|
542
|
+
increment = 0
|
543
|
+
for i in range(16):
|
544
|
+
increment ^= (iv[i] & 15) ^ ((iv[i] >> 4) & 0xFF)
|
545
|
+
return max(increment, 1)
|
546
|
+
|
547
|
+
|
548
|
+
def aes_decrypt(fh: BinaryIO, key: AesKey) -> bytes:
|
549
|
+
"""Decrypt file using a custom AES CTR increment with given AesKey.
|
550
|
+
|
551
|
+
Args:
|
552
|
+
fh: File-like object to the encrypted rootfs.gz file.
|
553
|
+
key: AesKey.
|
554
|
+
|
555
|
+
Returns:
|
556
|
+
Decrypted bytes.
|
557
|
+
"""
|
558
|
+
|
559
|
+
data = bytearray(fh.read())
|
560
|
+
|
561
|
+
# Calculate custom CTR increment from IV
|
562
|
+
increment = calculate_counter_increment(key.iv)
|
563
|
+
advance_block = (b"\x69" * 16) * (increment - 1)
|
564
|
+
|
565
|
+
# AES counter is little-endian and has a prefix
|
566
|
+
prefix, counter = struct.unpack("<8sQ", key.iv)
|
567
|
+
ctr = Counter.new(
|
568
|
+
64,
|
569
|
+
prefix=prefix,
|
570
|
+
initial_value=counter,
|
571
|
+
little_endian=True,
|
572
|
+
allow_wraparound=True,
|
573
|
+
)
|
574
|
+
cipher = AES.new(key.key, mode=AES.MODE_CTR, counter=ctr)
|
575
|
+
|
576
|
+
nblocks, nleft = divmod(len(data), 16)
|
577
|
+
for i in range(nblocks):
|
578
|
+
offset = i * 16
|
579
|
+
data[offset : offset + 16] = cipher.decrypt(data[offset : offset + 16])
|
580
|
+
cipher.decrypt(advance_block) # custom advance the counter
|
581
|
+
|
582
|
+
if nleft:
|
583
|
+
data[nblocks * 16 :] = cipher.decrypt(data[nblocks * 16 :])
|
584
|
+
|
585
|
+
return data
|
586
|
+
|
587
|
+
|
588
|
+
def decrypt_rootfs(fh: BinaryIO, key: ChaCha20Key | AesKey) -> BinaryIO:
|
500
589
|
"""Attempt to decrypt an encrypted ``rootfs.gz`` file with given key and IV.
|
501
590
|
|
502
591
|
FortiOS releases as of 7.4.1 / 2023-08-31, have ChaCha20 encrypted ``rootfs.gz`` files.
|
@@ -511,8 +600,7 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
|
|
511
600
|
|
512
601
|
Args:
|
513
602
|
fh: File-like object to the encrypted rootfs.gz file.
|
514
|
-
key:
|
515
|
-
iv: ChaCha20 iv.
|
603
|
+
key: ChaCha20Key or AesKey.
|
516
604
|
|
517
605
|
Returns:
|
518
606
|
File-like object to the decrypted rootfs.gz file.
|
@@ -525,13 +613,12 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
|
|
525
613
|
if not HAS_CRYPTO:
|
526
614
|
raise RuntimeError("Missing pycryptodome dependency")
|
527
615
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
result = cipher.decrypt(fh.read())
|
616
|
+
result = b""
|
617
|
+
if isinstance(key, ChaCha20Key):
|
618
|
+
result = chacha20_decrypt(fh, key)
|
619
|
+
elif isinstance(key, AesKey):
|
620
|
+
result = aes_decrypt(fh, key)
|
621
|
+
result = result[:-256] # strip off the 256 byte footer
|
535
622
|
|
536
623
|
if result[0:2] != b"\x1f\x8b":
|
537
624
|
raise ValueError("Failed to decrypt: No gzip magic header found.")
|
@@ -539,7 +626,7 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
|
|
539
626
|
return BytesIO(result)
|
540
627
|
|
541
628
|
|
542
|
-
def _kdf_7_4_x(key_data: str | bytes) -> tuple[bytes, bytes]:
|
629
|
+
def _kdf_7_4_x(key_data: str | bytes, offset_key: int = 4, offset_iv: int = 5) -> tuple[bytes, bytes]:
|
543
630
|
"""Derive 32 byte key and 16 byte IV from 32 byte seed.
|
544
631
|
|
545
632
|
As the IV needs to be 16 bytes, we return the first 16 bytes of the sha256 hash.
|
@@ -548,8 +635,8 @@ def _kdf_7_4_x(key_data: str | bytes) -> tuple[bytes, bytes]:
|
|
548
635
|
if isinstance(key_data, str):
|
549
636
|
key_data = bytes.fromhex(key_data)
|
550
637
|
|
551
|
-
key = hashlib.sha256(key_data[
|
552
|
-
iv = hashlib.sha256(key_data[
|
638
|
+
key = hashlib.sha256(key_data[offset_key:32] + key_data[:offset_key]).digest()
|
639
|
+
iv = hashlib.sha256(key_data[offset_iv:32] + key_data[:offset_iv]).digest()[:16]
|
553
640
|
return key, iv
|
554
641
|
|
555
642
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.21.
|
3
|
+
Version: 3.21.dev16
|
4
4
|
Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
6
6
|
License: Affero General Public License v3
|
@@ -252,8 +252,8 @@ dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py,sha256=Z5IDA5Oot4P2hh6
|
|
252
252
|
dissect/target/plugins/os/unix/linux/debian/vyos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
253
253
|
dissect/target/plugins/os/unix/linux/debian/vyos/_os.py,sha256=TPjcfv1n68RCe3Er4aCVQwQDCZwJT-NLvje3kPjDfhk,1744
|
254
254
|
dissect/target/plugins/os/unix/linux/fortios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
255
|
-
dissect/target/plugins/os/unix/linux/fortios/_keys.py,sha256=
|
256
|
-
dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=
|
255
|
+
dissect/target/plugins/os/unix/linux/fortios/_keys.py,sha256=XWB-a_x7TBCi12u6tLENbD3G5dIZNnChWsLwH9AyyvI,480147
|
256
|
+
dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=VpG8IBEMluNOWug6X7q8TIP_bYjTEfBCGHluDPgx03Y,22669
|
257
257
|
dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=dc6YTDLV-VZq9k8IWmY_PE0sTGkkp3yamR-cYNUCtes,1265
|
258
258
|
dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=Pe7Bdj8UemCiktLeQnQ50TpY_skARAzRJA0ewAB4710,5243
|
259
259
|
dissect/target/plugins/os/unix/linux/redhat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -383,10 +383,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
|
|
383
383
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
384
384
|
dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
|
385
385
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
386
|
-
dissect.target-3.21.
|
387
|
-
dissect.target-3.21.
|
388
|
-
dissect.target-3.21.
|
389
|
-
dissect.target-3.21.
|
390
|
-
dissect.target-3.21.
|
391
|
-
dissect.target-3.21.
|
392
|
-
dissect.target-3.21.
|
386
|
+
dissect.target-3.21.dev16.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
387
|
+
dissect.target-3.21.dev16.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
388
|
+
dissect.target-3.21.dev16.dist-info/METADATA,sha256=icMKSQBO0aOVzKaJDGemuCo9oGVLiGloPTThqJ0Us4M,13187
|
389
|
+
dissect.target-3.21.dev16.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
390
|
+
dissect.target-3.21.dev16.dist-info/entry_points.txt,sha256=yQwLCWUuzHgS6-sfCcRk66gAfoCfqXdCjqKjvhnQW8o,537
|
391
|
+
dissect.target-3.21.dev16.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
392
|
+
dissect.target-3.21.dev16.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.21.dev15.dist-info → dissect.target-3.21.dev16.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|