implement PDTCRYPT header scanning
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Tue, 15 Aug 2017 15:37:12 +0000 (17:37 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Mon, 2 Apr 2018 11:34:09 +0000 (13:34 +0200)
First phase: collect all possible header start locations.

Adds a CLI subcommand “scan” to crypto.py for analyzing files.

deltatar/crypto.py

index 25cf8d9..bef3ced 100755 (executable)
@@ -132,6 +132,7 @@ import bisect
 import ctypes
 import io
 from functools import reduce, partial
+import mmap
 import os
 import struct
 import sys
@@ -508,6 +509,40 @@ def iv_fmt (iv):
 
 
 ###############################################################################
+## restoration
+###############################################################################
+
+class Location (object):
+    n = 0
+    offset = 0
+
+def restore_loc_fmt (loc):
+    return "%d off:%d" \
+        % (loc.n, loc.offset)
+
+def locate_hdr_candidates (fd):
+    """
+    Walk over instances of the magic string in the payload, collecting their
+    positions. If the offset of the first found instance is not zero, the file
+    begins with leading garbage.
+
+    :return:    The list of offsets in the file.
+    """
+    cands = []
+
+    mm = mmap.mmap(fd, 0, mmap.MAP_SHARED, mmap.PROT_READ)
+    pos = 0
+    while True:
+        pos = mm.find (PDTCRYPT_HDR_MAGIC, pos)
+        if pos == -1:
+            break
+        cands.append (pos)
+        pos += 1
+
+    return cands
+
+
+###############################################################################
 ## passthrough / null encryption
 ###############################################################################
 
@@ -1236,10 +1271,12 @@ _testing_set_PDTCRYPT_MAX_OBJ_SIZE = \
 
 PDTCRYPT_SUB_PROCESS = 0
 PDTCRYPT_SUB_SCRYPT  = 1
+PDTCRYPT_SUB_SCAN    = 2
 
 PDTCRYPT_SUB = \
         { "process" : PDTCRYPT_SUB_PROCESS
-        , "scrypt"  : PDTCRYPT_SUB_SCRYPT }
+        , "scrypt"  : PDTCRYPT_SUB_SCRYPT
+        , "scan"    : PDTCRYPT_SUB_SCAN }
 
 PDTCRYPT_SECRET_PW   = 0
 PDTCRYPT_SECRET_KEY  = 1
@@ -1565,6 +1602,30 @@ def mode_scrypt (pw, ins=None, nacl=None, fmt=PDTCRYPT_SCRYPT_INTRANATOR):
     print (out)
 
 
+def mode_scan (pw, fname, nacl=None):
+    """
+    Dissect a binary file, looking for PDTCRYPT headers and objects.
+    """
+    try:
+        fd = os.open (fname, os.O_RDONLY)
+    except FileNotFoundError:
+        noise ("PDT: failed to open %s readonly" % fname)
+        noise ("")
+        usage (err=True)
+
+    try:
+        if PDTCRYPT_VERBOSE is True:
+            noise ("PDT: scan for potential sync points")
+        cands = locate_hdr_candidates (fd)
+        if len (cands) == 0:
+            noise ("PDT: scan complete: input does not contain potential PDT "
+                   "headers; giving up.")
+            return -1
+        if PDTCRYPT_VERBOSE is True:
+            noise ("PDT: scan complete: found %d candidates" % len (cands))
+    finally:
+        os.close (fd)
+
 
 def usage (err=False):
     out = print
@@ -1694,6 +1755,8 @@ def parse_argv (argv):
                         noise ("PDT: scrypt output format “%s”" % scrypt_format)
                 else:
                     bail ("ERROR: unexpected positional argument “%s”" % arg)
+            elif subcommand == PDTCRYPT_SUB_SCAN:
+                pass
 
     if secret is None:
         if PDTCRYPT_VERBOSE is True:
@@ -1715,6 +1778,13 @@ def parse_argv (argv):
         elif mode & PDTCRYPT_DECRYPT:
             bail ("ERROR: encryption requested but no password given")
 
+    if subcommand == PDTCRYPT_SUB_SCAN:
+        if insspec is None:
+            bail ("ERROR: please supply an input file for scanning")
+        if insspec == '-':
+            bail ("ERROR: input must be seekable; please specify a file")
+        return True, partial (mode_scan, secret [1].encode (), insspec, nacl)
+
     if subcommand == PDTCRYPT_SUB_SCRYPT:
         if secret [0] == PDTCRYPT_SECRET_KEY:
             bail ("ERROR: scrypt mode requires a password")