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.
@@ -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 KERNEL_KEY_MAP
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
- key, iv = key_iv_for_kernel_hash(kernel_hash)
99
- rfs_fh = decrypt_rootfs(rootfs.open(), key, iv)
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) -> tuple[bytes, bytes]:
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 = bytes.fromhex(KERNEL_KEY_MAP.get(kernel_hash, ""))
490
- if len(key) == 32:
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
- return _kdf_7_4_x(key)
493
- elif len(key) == 48:
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[:32], key[32:]
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 decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
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: ChaCha20 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
- # First 8 bytes = counter, last 8 bytes = nonce
529
- # PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
530
- # We're interested in updating the position in the ChaCha20 internal state, so to make
531
- # PyCryptodome "OpenSSL-compatible" we have to multiply the counter by 64
532
- cipher = ChaCha20.new(key=key, nonce=iv[8:])
533
- cipher.seek(int.from_bytes(iv[:8], "little") * 64)
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[4:32] + key_data[:4]).digest()
552
- iv = hashlib.sha256(key_data[5:32] + key_data[:5]).digest()[:16]
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.dev15
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=PMOYIaA_Bn-WHXBKgl2xwwNFwMc0TxWBmLfxCSkSFVs,215099
256
- dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=7ZIwWFEfYwE924IvGfuinv1mEP6Uh28pl8VHSmsGKmM,20152
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.dev15.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
387
- dissect.target-3.21.dev15.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
388
- dissect.target-3.21.dev15.dist-info/METADATA,sha256=p2xaB0NEJpuBOXNn5kO1O5JudBJu7GqmzZZlFIYwO3A,13187
389
- dissect.target-3.21.dev15.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
390
- dissect.target-3.21.dev15.dist-info/entry_points.txt,sha256=yQwLCWUuzHgS6-sfCcRk66gAfoCfqXdCjqKjvhnQW8o,537
391
- dissect.target-3.21.dev15.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
392
- dissect.target-3.21.dev15.dist-info/RECORD,,
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,,