github.com/iDigitalFlame/xmt@v0.5.1/tools/sentinel.py (about)

     1  #!/usr/bin/python3
     2  # Copyright (C) 2020 - 2023 iDigitalFlame
     3  #
     4  # This program is free software: you can redistribute it and/or modify
     5  # it under the terms of the GNU General Public License as published by
     6  # the Free Software Foundation, either version 3 of the License, or
     7  # any later version.
     8  #
     9  # This program is distributed in the hope that it will be useful,
    10  # but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  # GNU General Public License for more details.
    13  #
    14  # You should have received a copy of the GNU General Public License
    15  # along with this program.  If not, see <https://www.gnu.org/licenses/>.
    16  #
    17  
    18  from io import BytesIO
    19  from shlex import split
    20  from json import dumps, loads
    21  from struct import unpack, pack
    22  from secrets import token_bytes
    23  from traceback import format_exc
    24  from base64 import b64decode, b64encode
    25  from sys import argv, exit, stderr, stdin, stdout
    26  from os.path import expanduser, expandvars, isfile
    27  from argparse import ArgumentParser, BooleanOptionalAction
    28  
    29  HELP_TEXT = """XMT man.Sentinel Builder v1 Release
    30  
    31  Builds or reads a Sentinel file based on the supplied arguments.
    32  Files can be converted from and to JSON.
    33  
    34  NOTE: JSON Files are NOT supported by XMT directly. They are only
    35  to be used for generation.
    36  
    37  Usage: {binary} <options>
    38  
    39  BASIC ARGUMENTS:
    40    -h                              Show this help message and exit.
    41    --help
    42  
    43  INPUT/OUTPUT ARGUMENTS:
    44    -f                <file>        Input/Output file path. Use '-' for
    45    --file                            stdin/stdout.
    46    -S                              Force saving the file. Disables JSON
    47    --save                            and Print. Used mainly for updating the
    48                                      Filter settings, which may not be
    49                                      automatically detected.
    50    -k                              Provide a key string value to encrypt or
    51    --key                             decrypt the Sentinel output with XOR CFB.
    52                                      Only valid when reading/writing from a
    53                                      binary file.
    54    -K                              Provide a base64 encoded key string value
    55    --key-b64                         to encrypt or decrypt the Sentinel output
    56                                      with XOR CFB. Only valid when reading/writing
    57                                      from a binary file.
    58    -y                              Provide a path to a file that contains the
    59    --key-file                        binary data for the key to be used to encrypt
    60                                      or decrypt the Sentinel output with XOR CFB.
    61                                      Only valid when reading/writing from a binary
    62                                      file.
    63    -j                              Output in JSON format. Omit for raw
    64    --json                            binary. (Or base64 when output to
    65                                      stdout.)
    66    -I                              Accept stdin input as commands. Each
    67    --stdin                           line from stdin will be treated as a
    68                                      'append' line to the supplied config.
    69                                      Input and Output are ignored and are
    70                                      only set via the command line.
    71                                      This option disables using stdin for
    72                                      Sentinel data.
    73  
    74  OPERATION ARGUMENTS:
    75    -p
    76    --print                         List values contained in the file
    77                                      input. Fails if no input is found or
    78                                      invalid. Output format can be modified
    79                                      using -j/-p.
    80  
    81  SENTINEL ARGUMENTS:
    82    -d                <path>        Supply a path for a file to be used as a DLL
    83    --dll                             Sentinel entry. The path is not expanded
    84                                      until the Sentinel is ran.
    85    --s               <path>        Supply a path for a file to be used as an
    86    --asm                             Assembly Sentinel entry. The path is not
    87                                      expanded until the Sentinel is ran.
    88    -z                <path>        Supply a path for a Assembly or DLL file to
    89    --zombie                          be used as a Zombie (Hallowed) Sentinel entry.
    90                                      DLLs will be converted to Assembly by the
    91                                      Sentinel (if enabled). This requires at
    92                                      least ONE fake command to be added with '-F'
    93                                      or '--fake'.
    94    -c                <command>     Supply a command to be used as a Sentinel entry.
    95    --command                         Any environment variables will not be expanded
    96                                      until the Sentinel is ran.
    97    -u                <url>         Supply a URL to be used as a Sentinel entry.
    98    --url                             The downloaded target will be executed depending
    99    --download                        on the resulting 'Content-Type' header.
   100                                      The '-A' or '--agent' value can be specified
   101                                      to change the 'User-Agent' header to be used
   102                                      when downloading.
   103    -A                <user-agent>  Sets or adds a 'User-Agent' string that can be
   104    --agent                           used when downloading a URL path. This argument
   105                                      may be used multiple times to add more User-Agents.
   106                                      When multiple are present, one is selected at
   107                                      random. Supports the Text matcher verbs in the
   108                                      'text' package. See the 'ADDITIONAL RESOURCES'
   109                                      section for more info.
   110    -F                <fake-cmd>    Sets or adds the 'Fake' commands line args used
   111    --fake                            when a Zombie process is started. The first
   112                                      argument (the target binary) MUST exist. This
   113                                      argument may be used multiple times to add more
   114                                      command lines. When multiple are present, one
   115                                      is selected at random.
   116  
   117  FILTER ARGUMENTS:
   118    -n                <pid>         Specify the PID to use for the Parent Filter.
   119    --pid                             Takes priority over all other options.
   120    -i                <name1,nameX> Specify a (comma|space) seperated list of process
   121    --include                         names to INCLUDE in the Filter search process.
   122                                      This may be used more than one time in the command.
   123    -x                <name1,nameX> Specify a (comma|space) seperated list of process
   124    --exclude                         names to EXCLUDE from the Filter search process.
   125                                      This may be used more than one time in the command.
   126    -v                              Enable the search to only ALLOW FOREGROUND or
   127    --desktop                         DESKTOP processes to be used. Default is don't
   128                                      care. Takes priority over any disable arguments.
   129    -V                              Enable the search to only ALLOW BACKGROUND or
   130    --no-desktop                      SERVICE processes to be used. Default is don't
   131                                      care.
   132    -e                              Enable the search to only ALLOW ELEVATED processes
   133    --admin                           to be used. Default is don't care. Takes priority
   134    --elevated                        over any disable arguments.
   135    -E                              Enable the search to only ALLOW NON-ELEVATED
   136    --no-admin                        processes to be used. Default is don't care.
   137    --no-elevated
   138    -r                              Enable the Filter to fallback if no suitable
   139    --fallback                        processes were found during the first run and
   140                                      run again with less restrictive settings.
   141    -R                              Disable the Filter's ability to fallback if no
   142    --no-fallback                     suitable processes were found during the first
   143                                      run.
   144  
   145  ADDITIONAL RESOURCES:
   146    Text Matcher Guide
   147      https://pkg.go.dev/github.com/iDigitalFlame/xmt@v0.3.3/util/text#Matcher
   148  """
   149  
   150  
   151  def _copy(d, s, p=0):
   152      n, i = 0, p
   153      for x in range(0, len(s)):
   154          if i >= len(d):
   155              break
   156          d[i] = s[x]
   157          i += 1
   158          n += 1
   159      return n
   160  
   161  
   162  def _join_split(r, v):
   163      if "," not in v:
   164          s = v.strip()
   165          if len(s) == 0:
   166              return
   167          return r.append(s)
   168      for i in v.split(","):
   169          if len(i) == 0:
   170              continue
   171          r.append(i.strip())
   172  
   173  
   174  def _join(a, split=False):
   175      if not isinstance(a, list) or len(a) == 0:
   176          return None
   177      r = list()
   178      for i in a:
   179          if isinstance(i, str):
   180              if split:
   181                  _join_split(r, i)
   182                  continue
   183              v = i.strip()
   184              if len(v) == 0:
   185                  continue
   186              r.append(v)
   187              del v
   188              continue
   189          if not isinstance(i, list):
   190              continue
   191          for x in i:
   192              if split:
   193                  _join_split(r, x)
   194                  continue
   195              v = x.strip()
   196              if len(v) == 0:
   197                  continue
   198              r.append(v)
   199              del v
   200      return r
   201  
   202  
   203  def _read_file_input(v, k):
   204      if v.strip() == "-" and not stdin.isatty():
   205          if hasattr(stdin, "buffer"):
   206              b = stdin.buffer.read()
   207          else:
   208              b = stdin.read()
   209          stdin.close()
   210      else:
   211          p = expandvars(expandvars(v))
   212          if not isfile(p):
   213              return Sentinel(), False
   214          with open(p, "rb") as f:
   215              b = f.read()
   216          del p
   217      if len(b) == 0:
   218          raise ValueError("input: empty input data")
   219      return Sentinel(raw=b, key=k), True
   220  
   221  
   222  def _nes(s, min=0, max=-1):
   223      if max > min:
   224          return isinstance(s, str) and len(s) < max and len(s) > min
   225      return isinstance(s, str) and len(s) > min
   226  
   227  
   228  def _xor(dst, src, key, o=0):
   229      n = len(key)
   230      if n > len(src):
   231          n = len(src)
   232      for i in range(0, n):
   233          dst[i + o] = src[i] ^ key[i]
   234      return n
   235  
   236  
   237  def _write_out(s, key, v, pretty, json):
   238      f = stdout
   239      if _nes(v) and v != "-":
   240          if not pretty and not json:
   241              f = open(v, "wb")
   242          else:
   243              f = open(v, "w")
   244      try:
   245          if pretty or json:
   246              return print(
   247                  dumps(s.to_json(), sort_keys=False, indent=(4 if pretty else None)),
   248                  file=f,
   249              )
   250          b = s.save(None, key)
   251          if f == stdout and not f.isatty():
   252              return f.buffer.write(b)
   253          if f.mode == "wb":
   254              return f.write(b)
   255          f.write(b64encode(b).decode("UTF-8"))
   256          del b
   257      finally:
   258          if f == stdout:
   259              print(end="")
   260          else:
   261              f.close()
   262          del f
   263  
   264  
   265  class CFBXor(object):
   266      __slots__ = ("key", "used", "out", "next", "decrypt")
   267  
   268      def __init__(self, iv, key, decrypt):
   269          self.key = key
   270          self.used = len(key)
   271          self.decrypt = decrypt
   272          self.out = bytearray(len(key))
   273          self.next = bytearray(len(key))
   274          _copy(self.next, iv)
   275  
   276      def xor(self, dst, src):
   277          if len(dst) < len(src):
   278              raise ValueError("output smaller than input")
   279          x, y = 0, 0
   280          while x < len(src):
   281              if self.used == len(self.out):
   282                  _xor(self.out, self.next, self.key)
   283                  self.used = 0
   284              if self.decrypt:
   285                  _copy(self.next, src[x:], self.used)
   286              n = _xor(dst, src[x:], self.out[self.used :], y)
   287              if not self.decrypt:
   288                  _copy(self.next, dst[y:], self.used)
   289              x += n
   290              y += n
   291              self.used += n
   292          del x, y
   293  
   294  
   295  class Reader(object):
   296      __slots__ = ("r",)
   297  
   298      def __init__(self, r):
   299          self.r = r
   300  
   301      def read_str(self):
   302          return self.read_bytes().decode("UTF-8")
   303  
   304      def read_bool(self):
   305          return self.read_uint8() == 1
   306  
   307      def read_bytes(self):
   308          t = self.read_uint8()
   309          if t == 0:
   310              return bytearray(0)
   311          n = 0
   312          if t == 1:
   313              n = self.read_uint8()
   314          elif t == 3:
   315              n = self.read_uint16()
   316          elif t == 5:
   317              n = self.read_uint32()
   318          elif t == 7:
   319              n = self.read_uint64()
   320          else:
   321              raise ValueError("read_bytes: invalid buffer type")
   322          if n < 0:
   323              raise ValueError("read_bytes: invalid buffer size")
   324          b = self.r.read(n)
   325          del t, n
   326          return b
   327  
   328      def read_uint8(self):
   329          return unpack(">B", self.r.read(1))[0]
   330  
   331      def read_uint16(self):
   332          return unpack(">H", self.r.read(2))[0]
   333  
   334      def read_uint32(self):
   335          return unpack(">I", self.r.read(4))[0]
   336  
   337      def read_uint64(self):
   338          return unpack(">Q", self.r.read(8))[0]
   339  
   340      def read_str_list(self):
   341          t = self.read_uint8()
   342          if t == 0:
   343              return list()
   344          n = 0
   345          if t == 1:
   346              n = self.read_uint8()
   347          elif t == 3:
   348              n = self.read_uint16()
   349          elif t == 5:
   350              n = self.read_uint32()
   351          elif t == 7:
   352              n = self.read_uint64()
   353          else:
   354              raise ValueError("invalid buffer type")
   355          if n < 0:
   356              raise ValueError("invalid list size")
   357          r = list()
   358          for _ in range(0, n):
   359              r.append(self.read_str())
   360          del t, n
   361          return r
   362  
   363  
   364  class Writer(object):
   365      __slots__ = ("w",)
   366  
   367      def __init__(self, w):
   368          self.w = w
   369  
   370      def write_str(self, v):
   371          if v is None:
   372              return self.write_bytes(None)
   373          if not isinstance(v, str):
   374              raise ValueError("write: not a string")
   375          self.write_bytes(v.encode("UTF-8"))
   376  
   377      def write_bool(self, v):
   378          self.write_uint8(1 if v else 0)
   379  
   380      def write_bytes(self, v):
   381          if v is None or len(v) == 0:
   382              return self.write_uint8(0)
   383          if not isinstance(v, (bytes, bytearray)):
   384              raise ValueError("write: not a bytes type")
   385          n = len(v)
   386          if n < 0xFF:
   387              self.write_uint8(1)
   388              self.write_uint8(n)
   389          elif n < 0xFFFF:
   390              self.write_uint8(3)
   391              self.write_uint16(n)
   392          elif n < 0xFFFFFFFF:
   393              self.write_uint8(5)
   394              self.write_uint32(n)
   395          else:
   396              self.write_uint8(7)
   397              self.write_uint64(n)
   398          self.w.write(v)
   399          del n
   400  
   401      def write_uint8(self, v):
   402          if not isinstance(v, int):
   403              raise ValueError("write: not a number")
   404          self.w.write(pack(">B", v))
   405  
   406      def write_uint16(self, v):
   407          if not isinstance(v, int):
   408              raise ValueError("write: not a number")
   409          self.w.write(pack(">H", v))
   410  
   411      def write_uint32(self, v):
   412          if not isinstance(v, int):
   413              raise ValueError("write: not a number")
   414          self.w.write(pack(">I", v))
   415  
   416      def write_uint64(self, v):
   417          if not isinstance(v, int):
   418              raise ValueError("write: not a number")
   419          self.w.write(pack(">Q", v))
   420  
   421      def write_str_list(self, v):
   422          if v is None or len(v) == 0:
   423              return self.write_uint8(0)
   424          if not isinstance(v, list):
   425              raise ValueError("not a list")
   426          n = len(v)
   427          if n < 0xFF:
   428              self.write_uint8(1)
   429              self.write_uint8(n)
   430          elif n < 0xFFFF:
   431              self.write_uint8(3)
   432              self.write_uint16(n)
   433          elif n < 0xFFFFFFFF:
   434              self.write_uint8(5)
   435              self.write_uint32(n)
   436          else:
   437              self.write_uint8(7)
   438              self.write_uint64(n)
   439          del n
   440          for i in v:
   441              self.write_str(i)
   442  
   443  
   444  class ReadCFB(object):
   445      __slots__ = ("r", "cfb")
   446  
   447      def __init__(self, cfb, r):
   448          self.r = r
   449          self.cfb = cfb
   450  
   451      def read(self, n):
   452          r = self.r.read(n)
   453          if r is None:
   454              return None
   455          b = bytearray(len(r))
   456          self.cfb.xor(b, r)
   457          del r
   458          return b
   459  
   460  
   461  class WriteCFB(object):
   462      __slots__ = ("w", "cfb")
   463  
   464      def __init__(self, cfb, w):
   465          self.w = w
   466          self.cfb = cfb
   467  
   468      def write(self, b):
   469          if not isinstance(b, (bytes, bytearray)):
   470              raise ValueError("write: not a bytes type")
   471          r = bytearray(len(b))
   472          self.cfb.xor(r, b)
   473          self.w.write(r)
   474          del r
   475  
   476  
   477  class Filter(object):
   478      __slots__ = ("pid", "session", "exclude", "include", "elevated", "fallback")
   479  
   480      def __init__(self, json=None):
   481          self.pid = 0
   482          self.session = None
   483          self.exclude = None
   484          self.include = None
   485          self.elevated = None
   486          self.fallback = False
   487          if not isinstance(json, dict):
   488              return
   489          self.from_json(json)
   490  
   491      def to_json(self):
   492          r = dict()
   493          if isinstance(self.session, bool):
   494              r["session"] = self.session
   495          if isinstance(self.elevated, bool):
   496              r["elevated"] = self.elevated
   497          if isinstance(self.fallback, bool):
   498              r["fallback"] = self.fallback
   499          if isinstance(self.pid, int) and self.pid > 0:
   500              r["pid"] = self.pid
   501          if isinstance(self.exclude, list) and len(self.exclude) > 0:
   502              r["exclude"] = self.exclude
   503          if isinstance(self.include, list) and len(self.include) > 0:
   504              r["include"] = self.include
   505          return r
   506  
   507      def read(self, r):
   508          if not r.read_bool():
   509              return
   510          self.pid = r.read_uint32()
   511          self.fallback = r.read_bool()
   512          b = r.read_uint8()
   513          if b == 0:
   514              self.session = None
   515          elif b == 1:
   516              self.session = False
   517          elif b == 2:
   518              self.session = True
   519          b = r.read_uint8()
   520          if b == 0:
   521              self.elevated = None
   522          elif b == 1:
   523              self.elevated = False
   524          elif b == 2:
   525              self.elevated = True
   526          self.exclude = r.read_str_list()
   527          self.include = r.read_str_list()
   528  
   529      def is_empty(self):
   530          if isinstance(self.session, bool):
   531              return False
   532          if isinstance(self.elevated, bool):
   533              return False
   534          if isinstance(self.pid, int) and self.pid > 0:
   535              return False
   536          if isinstance(self.exclude, list) and len(self.exclude) > 0:
   537              return False
   538          if isinstance(self.include, list) and len(self.include) > 0:
   539              return False
   540          return True
   541  
   542      def write(self, w):
   543          if self is None or self.is_empty():
   544              return w.write_bool(False)
   545          w.write_bool(True)
   546          w.write_uint32(self.pid)
   547          w.write_bool(self.fallback)
   548          if self.session is True:
   549              w.write_uint8(2)
   550          elif self.session is False:
   551              w.write_uint8(1)
   552          else:
   553              w.write_uint8(0)
   554          if self.elevated is True:
   555              w.write_uint8(2)
   556          elif self.elevated is False:
   557              w.write_uint8(1)
   558          else:
   559              w.write_uint8(0)
   560          w.write_str_list(self.exclude)
   561          w.write_str_list(self.include)
   562  
   563      def from_json(self, d):
   564          if not isinstance(d, dict):
   565              raise ValueError("from_json: value provided was not a dict")
   566          if "session" in d and isinstance(d["session"], bool):
   567              self.session = d["session"]
   568          if "elevated" in d and isinstance(d["elevated"], bool):
   569              self.elevated = d["elevated"]
   570          if "fallback" in d and isinstance(d["fallback"], bool):
   571              self.fallback = d["fallback"]
   572          if "pid" in d and isinstance(d["pid"], int) and d["pid"] > 0:
   573              self.pid = d["pid"]
   574          if "exclude" in d and isinstance(d["exclude"], list) and len(d["exclude"]) > 0:
   575              self.exclude = d["exclude"]
   576              for i in self.exclude:
   577                  if isinstance(i, str) and len(i) > 0:
   578                      continue
   579                  raise ValueError('from_json: empty or non-string value in "exclude"')
   580          if "include" in d and isinstance(d["include"], list) and len(d["include"]) > 0:
   581              self.include = d["include"]
   582              for i in self.include:
   583                  if isinstance(i, str) and len(i) > 0:
   584                      continue
   585                  raise ValueError('from_json: empty or non-string value in "include"')
   586  
   587  
   588  class Sentinel(object):
   589      __slots__ = ("paths", "filter")
   590  
   591      def __init__(self, raw=None, file=None, key=None, json=None):
   592          self.paths = None
   593          self.filter = Filter()
   594          if _nes(file):
   595              return self.load(file, key)
   596          if isinstance(raw, (str, bytes, bytearray)):
   597              return self.from_raw(raw, key)
   598          if not isinstance(json, dict):
   599              return
   600          self.from_json(json)
   601  
   602      def to_json(self):
   603          return {
   604              "filter": self.filter.to_json(),
   605              "paths": [i.to_json() for i in self.paths],
   606          }
   607  
   608      def read(self, r):
   609          self.filter.read(r)
   610          n = r.read_uint16()
   611          if n < 0:
   612              raise ValueError("invalid entry size")
   613          self.paths = list()
   614          for _ in range(0, n):
   615              self.paths.append(SentinelPath(reader=r))
   616          del n
   617  
   618      def write(self, w):
   619          self.filter.write(w)
   620          if not isinstance(self.paths, list) or len(self.paths) == 0:
   621              return w.write_uint16(0)
   622          w.write_uint16(len(self.paths))
   623          for x in range(0, min(len(self.paths), 0xFFFFFFFF)):
   624              self.paths[x].write(w)
   625  
   626      def from_json(self, j):
   627          if not isinstance(j, dict):
   628              raise ValueError("from_json: value provided was not a dict")
   629          if "filter" in j:
   630              self.filter.from_json(j["filter"])
   631          if "paths" not in j or not isinstance(j["paths"], list) or len(j["paths"]) == 0:
   632              return
   633          self.paths = list()
   634          for i in j["paths"]:
   635              self.paths.append(SentinelPath.from_json(i))
   636  
   637      def add_dll(self, path):
   638          if not _nes(path):
   639              raise ValueError('add_dll: "path" must be a non-empty string')
   640          if self.paths is None:
   641              self.paths = list()
   642          self.paths.append(SentinelPath(type=SentinelPath.DLL, path=path))
   643  
   644      def add_asm(self, path):
   645          if not _nes(path):
   646              raise ValueError('add_asm: "path" must be a non-empty string')
   647          if self.paths is None:
   648              self.paths = list()
   649          self.paths.append(SentinelPath(type=SentinelPath.ASM, path=path))
   650  
   651      def add_execute(self, cmd):
   652          if not _nes(cmd):
   653              raise ValueError('add_execute: "path" must be a non-empty string')
   654          if self.paths is None:
   655              self.paths = list()
   656          self.paths.append(SentinelPath(type=SentinelPath.EXECUTE, path=cmd))
   657  
   658      def save(self, path, key=None):
   659          k = key
   660          if _nes(key):
   661              k = key.encode("UTF-8")
   662          elif key is not None and not isinstance(key, (bytes, bytearray)):
   663              raise ValueError("save: key must be a string or bytes type")
   664          b = BytesIO()
   665          if k is not None:
   666              i = token_bytes(len(k))
   667              b.write(i)
   668              o = WriteCFB(CFBXor(i, k, False), b)
   669              del i
   670          else:
   671              o = b
   672          w = Writer(o)
   673          del o
   674          self.write(w)
   675          del w
   676          r = b.getvalue()
   677          b.close()
   678          del b
   679          if not _nes(path):
   680              return r
   681          with open(expanduser(expandvars(path)), "wb") as f:
   682              f.write(r)
   683          del r
   684  
   685      def add_zombie(self, path, fakes):
   686          if not _nes(path):
   687              raise ValueError('add_zombie: "path" must be a non-empty string')
   688          if not isinstance(fakes, (str, list)) or len(fakes) == 0:
   689              raise ValueError(
   690                  'add_zombie: "fakes" must be a non-empty string or string list'
   691              )
   692          if self.paths is None:
   693              self.paths = list()
   694          if isinstance(fakes, str):
   695              fakes = [fakes]
   696          self.paths.append(
   697              SentinelPath(type=SentinelPath.ZOMBIE, path=path, extra=fakes)
   698          )
   699  
   700      def from_raw(self, data, key=None):
   701          if isinstance(data, str) and len(data) > 0:
   702              if data[0] == "{" and data[-1].strip() == "}":
   703                  return self.from_json(loads(data))
   704              return self.load(None, key=key, buf=b64decode(data, validate=True))
   705          if isinstance(data, (bytes, bytearray)) and len(data) > 0:
   706              if data[0] == 91 and data.decode("UTF-8", "ignore").strip()[-1] == "]":
   707                  return self.from_json(loads(data.decode("UTF-8")))
   708              return self.load(None, key=key, buf=data)
   709          raise ValueError("from_raw: a bytes or string type is required")
   710  
   711      def load(self, path, key=None, buf=None):
   712          k = key
   713          if _nes(key):
   714              k = key.encode("UTF-8")
   715          elif key is not None and not isinstance(key, (bytes, bytearray)):
   716              raise ValueError("load: key must be a string or bytes type")
   717          if isinstance(buf, (bytes, bytearray)):
   718              b = BytesIO(buf)
   719          elif isinstance(buf, BytesIO):
   720              b = buf
   721          else:
   722              b = open(expanduser(expandvars(path)), "rb")
   723          if k is not None:
   724              i = b.read(len(k))
   725              o = ReadCFB(CFBXor(i, k, True), b)
   726              del i
   727          else:
   728              o = b
   729          r = Reader(o)
   730          del o
   731          try:
   732              self.read(r)
   733          finally:
   734              b.close()
   735              del b
   736          del r
   737  
   738      def add_download(self, url, agents=None):
   739          if not _nes(url):
   740              raise ValueError('add_download: "url" must be a non-empty string')
   741          if agents is not None and not isinstance(agents, (str, list)):
   742              raise ValueError('add_download: "agents" must be a string or string list')
   743          if self.paths is None:
   744              self.paths = list()
   745          if isinstance(agents, str):
   746              agents = [agents]
   747          self.paths.append(
   748              SentinelPath(type=SentinelPath.DOWNLOAD, path=url, extra=agents)
   749          )
   750  
   751  
   752  class SentinelPath(object):
   753      EXECUTE = 0
   754      DLL = 1
   755      ASM = 2
   756      DOWNLOAD = 3
   757      ZOMBIE = 4
   758  
   759      __slots__ = ("type", "path", "extra")
   760  
   761      def __init__(self, reader=None, type=None, path=None, extra=None):
   762          self.type = type
   763          self.path = path
   764          self.extra = extra
   765          if not isinstance(reader, Reader):
   766              return
   767          self.read(reader)
   768  
   769      def valid(self):
   770          if self.type > SentinelPath.ZOMBIE or not self.path:
   771              return False
   772          if self.type > SentinelPath.DOWNLOAD and not self.extra:
   773              return False
   774          return True
   775  
   776      @staticmethod
   777      def from_json(d):
   778          if not isinstance(d, dict):
   779              raise ValueError("from_json: value provided was not a dict")
   780          if "type" not in d or "path" not in d:
   781              raise ValueError("from_json: invalid JSON data")
   782          t = d["type"]
   783          if not _nes(t):
   784              raise ValueError('from_json: invalid "type" value')
   785          v = t.lower()
   786          del t
   787          p = d["path"]
   788          if not _nes(p):
   789              raise ValueError('from_json: invalid "path" value')
   790          s = SentinelPath()
   791          s.path = p
   792          del p
   793          if v == "execute":
   794              s.type = SentinelPath.EXECUTE
   795          elif v == "dll":
   796              s.type = SentinelPath.DLL
   797          elif v == "asm":
   798              s.type = SentinelPath.ASM
   799          elif v == "download":
   800              s.type = SentinelPath.DOWNLOAD
   801          elif v == "zombie":
   802              s.type = SentinelPath.ZOMBIE
   803          else:
   804              raise ValueError('from_json: unknown "type" value')
   805          del v
   806          if s.type < SentinelPath.DOWNLOAD:
   807              return s
   808          if "extra" not in d:
   809              if s.type == SentinelPath.DOWNLOAD:
   810                  return s
   811              raise ValueError('from_json: missing "extra" value')
   812          e = d["extra"]
   813          if not isinstance(e, list) or len(e) == 0:
   814              if s.type == SentinelPath.DOWNLOAD:
   815                  return s
   816              raise ValueError('from_json: invalid "extra" value')
   817          for i in e:
   818              if _nes(i):
   819                  continue
   820              raise ValueError('from_json: invalid "extra" sub-value')
   821          s.extra = e
   822          del e
   823          return s
   824  
   825      def to_json(self):
   826          if self.type > SentinelPath.ZOMBIE:
   827              raise ValueError("to_json: invalid path type")
   828          if self.type < SentinelPath.DOWNLOAD or (
   829              not isinstance(self.extra, list) and not self.extra
   830          ):
   831              return {"type": self.typename(), "path": self.path}
   832          return {"type": self.typename(), "path": self.path, "extra": self.extra}
   833  
   834      def read(self, r):
   835          self.type = r.read_uint8()
   836          self.path = r.read_str()
   837          if self.type < SentinelPath.DOWNLOAD:
   838              return
   839          self.extra = r.read_str_list()
   840  
   841      def __str__(self):
   842          if self.type == SentinelPath.EXECUTE:
   843              return f"Execute: {self.path}"
   844          if self.type == SentinelPath.DLL:
   845              return f"DLL: {self.path}"
   846          if self.type == SentinelPath.ASM:
   847              return f"ASM: {self.path}"
   848          if self.type == SentinelPath.DOWNLOAD:
   849              if isinstance(self.extra, list) and len(self.extra) > 0:
   850                  return f'Download: {self.path} (Agents: {", ".join(self.extra)})'
   851              return f"Download: {self.path}"
   852          if self.type == SentinelPath.ZOMBIE:
   853              if isinstance(self.extra, list) and len(self.extra) > 0:
   854                  return f'Zombie: {self.path} (Fakes: {", ".join(self.extra)})'
   855              return f"Zombie: {self.path}"
   856          return "Unknown"
   857  
   858      def write(self, w):
   859          w.write_uint8(self.type)
   860          w.write_str(self.path)
   861          if self.type < SentinelPath.DOWNLOAD:
   862              return
   863          w.write_str_list(self.extra)
   864  
   865      def typename(self):
   866          if self.type == SentinelPath.EXECUTE:
   867              return "execute"
   868          if self.type == SentinelPath.DLL:
   869              return "dll"
   870          if self.type == SentinelPath.ASM:
   871              return "asm"
   872          if self.type == SentinelPath.DOWNLOAD:
   873              return "download"
   874          if self.type == SentinelPath.ZOMBIE:
   875              return "zombie"
   876          return "invalid"
   877  
   878  
   879  class _Builder(ArgumentParser):
   880      def __init__(self):
   881          ArgumentParser.__init__(self, description="XMT man.Sentinel Tool")
   882          self.add_argument("-j", "--json", dest="json", action="store_true")
   883          self.add_argument("-p", "--print", dest="print", action="store_true")
   884          self.add_argument("-I", "--stdin", dest="stdin", action="store_true")
   885          self.add_argument("-f", "--file", type=str, dest="file")
   886          self.add_argument("-k", "--key", type=str, dest="key")
   887          self.add_argument("-y", "--key-file", type=str, dest="key_file")
   888          self.add_argument("-K", "--key-b64", type=str, dest="key_base64")
   889          self.add_argument("-d", "--dll", dest="dll", action="store_true")
   890          self.add_argument("-s", "--asm", dest="asm", action="store_true")
   891          self.add_argument("-S", "--save", dest="save", action="store_true")
   892          self.add_argument("-z", "--zombie", dest="zombie", action="store_true")
   893          self.add_argument("-c", "--command", dest="command", action="store_true")
   894          self.add_argument(
   895              "-u", "--url", "--download", dest="download", action="store_true"
   896          )
   897          self.add_argument(nargs="*", type=str, dest="path")
   898          self.add_argument(
   899              "-A",
   900              "-F",
   901              "--fake",
   902              "--agent",
   903              nargs="*",
   904              type=str,
   905              dest="extra",
   906              action="append",
   907          )
   908          self.add_argument("-n", "--pid", type=int, dest="pid")
   909          self.add_argument("-V", dest="no_desktop", action="store_false")
   910          self.add_argument(
   911              "-v", "--desktop", dest="desktop", action=BooleanOptionalAction
   912          )
   913          self.add_argument("-R", dest="no_fallback", action="store_false")
   914          self.add_argument(
   915              "-r", "--fallback", dest="fallback", action=BooleanOptionalAction
   916          )
   917          self.add_argument("-E", dest="no_admin", action="store_false")
   918          self.add_argument(
   919              "-e",
   920              "--admin",
   921              "--elevated",
   922              dest="admin",
   923              action=BooleanOptionalAction,
   924          )
   925          self.add_argument(
   926              "-x",
   927              "--exclude",
   928              nargs="*",
   929              type=str,
   930              dest="exclude",
   931              action="append",
   932          )
   933          self.add_argument(
   934              "-i",
   935              "--include",
   936              nargs="*",
   937              type=str,
   938              dest="include",
   939              action="append",
   940          )
   941  
   942      def run(self):
   943          a, k = self.parse_args(), None
   944          if _nes(a.key_file):
   945              with open(expanduser(expandvars(a.key_file)), "rb") as f:
   946                  k = f.read()
   947          elif _nes(a.key_base64):
   948              k = b64decode(a.key_base64, validate=True)
   949          elif _nes(a.key):
   950              k = a.key.encode("UTF-8")
   951          if a.file:
   952              s, z = _read_file_input(a.file, k)
   953          else:
   954              s, z = Sentinel(), False
   955          if a.stdin and a.file != "-":
   956              if stdin.isatty():
   957                  raise ValueError("stdin: no input found")
   958              if hasattr(stdin, "buffer"):
   959                  b = stdin.buffer.read().decode("UTF-8")
   960              else:
   961                  b = stdin.read()
   962              stdin.close()
   963              for v in b.split("\n"):
   964                  _Builder.build(s, super(__class__, self).parse_args(split(v)))
   965          elif (
   966              isinstance(a.path, list)
   967              and len(a.path) > 0
   968              and (not a.print or (a.print and not a.file))
   969          ):
   970              _Builder.build(s, a)
   971          elif not z:
   972              raise ValueError("no paths added to an empty Sentinel")
   973          elif not a.save and (a.print or a.json):
   974              a.file = None
   975          elif a.save:
   976              _Builder._parse_filter(s.filter, a)
   977          if not isinstance(s.paths, list) or len(s.paths) == 0:
   978              return
   979          if a.save:
   980              a.print, a.json = False, False
   981          _write_out(s, k, a.file, a.print, a.json)
   982          del s, z, a, k
   983  
   984      @staticmethod
   985      def build(s, a):
   986          _Builder._parse_filter(s.filter, a)
   987          if not isinstance(a.path, list) or len(a.path) == 0:
   988              return
   989          if a.command:
   990              s.add_execute(" ".join(a.path))
   991          elif a.dll:
   992              s.add_dll(" ".join(a.path))
   993          elif a.asm:
   994              s.add_asm(" ".join(a.path))
   995          elif a.download:
   996              s.add_download(" ".join(a.path), agents=_join(a.extra))
   997          elif a.zombie:
   998              s.add_zombie(" ".join(a.path), _join(a.extra))
   999          else:
  1000              s.add_execute(" ".join(a.path))
  1001          _Builder._parse_filter(s.filter, a)
  1002  
  1003      def parse_args(self):
  1004          if len(argv) <= 1:
  1005              return self.print_help()
  1006          return super(__class__, self).parse_args()
  1007  
  1008      @staticmethod
  1009      def _parse_filter(f, a):
  1010          if isinstance(a.pid, int):
  1011              if a.pid <= 0:
  1012                  f.pid = None
  1013              else:
  1014                  f.pid = a.pid
  1015          if a.exclude is not None and len(a.exclude) > 0:
  1016              f.exclude = _join(a.exclude, True)
  1017          if a.include is not None and len(a.include) > 0:
  1018              f.include = _join(a.include, True)
  1019          if not a.no_admin or a.admin is not None:
  1020              f.elevated = (a.admin is None and a.no_admin) or (
  1021                  a.admin is True and a.no_admin
  1022              )
  1023          if not a.no_desktop or a.desktop is not None:
  1024              f.session = (a.desktop is None and a.no_desktop) or (
  1025                  a.desktop is True and a.no_desktop
  1026              )
  1027          if not a.no_fallback or a.fallback is not None:
  1028              f.fallback = (a.fallback is None and a.no_fallback) or (
  1029                  a.fallback is True and a.no_fallback
  1030              )
  1031  
  1032      def print_help(self, file=None):
  1033          print(HELP_TEXT.format(binary=argv[0]), file=file)
  1034          exit(2)
  1035  
  1036  
  1037  if __name__ == "__main__":
  1038      try:
  1039          _Builder().run()
  1040      except Exception as err:
  1041          print(f"Error: {err}\n{format_exc(3)}", file=stderr)
  1042          exit(1)