github.com/iDigitalFlame/xmt@v0.5.4/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 CTRXor(object):
   266      __slots__ = ("ctr", "key", "used", "out", "total")
   267  
   268      def __init__(self, iv, key):
   269          if len(iv) != len(key):
   270              raise ValueError("key and iv lengths must be equal")
   271          self.key = key
   272          self.used = 0
   273          self.total = 0
   274          self.ctr = bytearray(len(key))
   275          self.out = bytearray(len(key))
   276          _copy(self.ctr, iv)
   277  
   278      def refill(self):
   279          r = self.total - self.used
   280          if self.used > 0:
   281              _copy(self.out, self.out[self.used :])
   282          while r <= (len(self.out) - len(self.out)):
   283              _xor(self.out, self.ctr, self.key, r)
   284              r += len(self.out)
   285              for x in range(len(self.ctr) - 1, 0, -1):
   286                  if self.ctr[x] == 0xFF:
   287                      self.ctr[x] = 0
   288                  else:
   289                      self.ctr[x] += 1
   290                  if self.ctr[x] != 0:
   291                      break
   292          self.total, self.used = r, 0
   293  
   294      def xor(self, dst, src):
   295          if len(dst) < len(src):
   296              raise ValueError("output smaller than input")
   297          x, y = 0, 0
   298          while x < len(src):
   299              if self.used >= self.total - len(self.out):
   300                  self.refill()
   301              n = _xor(dst, src[x:], self.out[self.used :], y)
   302              x += n
   303              y += n
   304              self.used += n
   305          del x, y
   306  
   307  
   308  class Reader(object):
   309      __slots__ = ("r",)
   310  
   311      def __init__(self, r):
   312          self.r = r
   313  
   314      def read_str(self):
   315          return self.read_bytes().decode("UTF-8")
   316  
   317      def read_bool(self):
   318          return self.read_uint8() == 1
   319  
   320      def read_bytes(self):
   321          t = self.read_uint8()
   322          if t == 0:
   323              return bytearray(0)
   324          n = 0
   325          if t == 1:
   326              n = self.read_uint8()
   327          elif t == 3:
   328              n = self.read_uint16()
   329          elif t == 5:
   330              n = self.read_uint32()
   331          elif t == 7:
   332              n = self.read_uint64()
   333          else:
   334              raise ValueError("read_bytes: invalid buffer type")
   335          if n < 0:
   336              raise ValueError("read_bytes: invalid buffer size")
   337          b = self.r.read(n)
   338          del t, n
   339          return b
   340  
   341      def read_uint8(self):
   342          return unpack(">B", self.r.read(1))[0]
   343  
   344      def read_uint16(self):
   345          return unpack(">H", self.r.read(2))[0]
   346  
   347      def read_uint32(self):
   348          return unpack(">I", self.r.read(4))[0]
   349  
   350      def read_uint64(self):
   351          return unpack(">Q", self.r.read(8))[0]
   352  
   353      def read_str_list(self):
   354          t = self.read_uint8()
   355          if t == 0:
   356              return list()
   357          n = 0
   358          if t == 1:
   359              n = self.read_uint8()
   360          elif t == 3:
   361              n = self.read_uint16()
   362          elif t == 5:
   363              n = self.read_uint32()
   364          elif t == 7:
   365              n = self.read_uint64()
   366          else:
   367              raise ValueError("invalid buffer type")
   368          if n < 0:
   369              raise ValueError("invalid list size")
   370          r = list()
   371          for _ in range(0, n):
   372              r.append(self.read_str())
   373          del t, n
   374          return r
   375  
   376  
   377  class Writer(object):
   378      __slots__ = ("w",)
   379  
   380      def __init__(self, w):
   381          self.w = w
   382  
   383      def write_str(self, v):
   384          if v is None:
   385              return self.write_bytes(None)
   386          if not isinstance(v, str):
   387              raise ValueError("write: not a string")
   388          self.write_bytes(v.encode("UTF-8"))
   389  
   390      def write_bool(self, v):
   391          self.write_uint8(1 if v else 0)
   392  
   393      def write_bytes(self, v):
   394          if v is None or len(v) == 0:
   395              return self.write_uint8(0)
   396          if not isinstance(v, (bytes, bytearray)):
   397              raise ValueError("write: not a bytes type")
   398          n = len(v)
   399          if n < 0xFF:
   400              self.write_uint8(1)
   401              self.write_uint8(n)
   402          elif n < 0xFFFF:
   403              self.write_uint8(3)
   404              self.write_uint16(n)
   405          elif n < 0xFFFFFFFF:
   406              self.write_uint8(5)
   407              self.write_uint32(n)
   408          else:
   409              self.write_uint8(7)
   410              self.write_uint64(n)
   411          self.w.write(v)
   412          del n
   413  
   414      def write_uint8(self, v):
   415          if not isinstance(v, int):
   416              raise ValueError("write: not a number")
   417          self.w.write(pack(">B", v))
   418  
   419      def write_uint16(self, v):
   420          if not isinstance(v, int):
   421              raise ValueError("write: not a number")
   422          self.w.write(pack(">H", v))
   423  
   424      def write_uint32(self, v):
   425          if not isinstance(v, int):
   426              raise ValueError("write: not a number")
   427          self.w.write(pack(">I", v))
   428  
   429      def write_uint64(self, v):
   430          if not isinstance(v, int):
   431              raise ValueError("write: not a number")
   432          self.w.write(pack(">Q", v))
   433  
   434      def write_str_list(self, v):
   435          if v is None or len(v) == 0:
   436              return self.write_uint8(0)
   437          if not isinstance(v, list):
   438              raise ValueError("not a list")
   439          n = len(v)
   440          if n < 0xFF:
   441              self.write_uint8(1)
   442              self.write_uint8(n)
   443          elif n < 0xFFFF:
   444              self.write_uint8(3)
   445              self.write_uint16(n)
   446          elif n < 0xFFFFFFFF:
   447              self.write_uint8(5)
   448              self.write_uint32(n)
   449          else:
   450              self.write_uint8(7)
   451              self.write_uint64(n)
   452          del n
   453          for i in v:
   454              self.write_str(i)
   455  
   456  
   457  class ReadCTR(object):
   458      __slots__ = ("r", "ctr")
   459  
   460      def __init__(self, ctr, r):
   461          self.r = r
   462          self.ctr = ctr
   463  
   464      def read(self, n):
   465          r = self.r.read(n)
   466          if r is None:
   467              return None
   468          b = bytearray(len(r))
   469          self.ctr.xor(b, r)
   470          del r
   471          return b
   472  
   473  
   474  class WriteCTR(object):
   475      __slots__ = ("w", "ctr")
   476  
   477      def __init__(self, ctr, w):
   478          self.w = w
   479          self.ctr = ctr
   480  
   481      def write(self, b):
   482          if not isinstance(b, (bytes, bytearray)):
   483              raise ValueError("write: not a bytes type")
   484          r = bytearray(len(b))
   485          self.ctr.xor(r, b)
   486          self.w.write(r)
   487          del r
   488  
   489  
   490  class Filter(object):
   491      __slots__ = ("pid", "session", "exclude", "include", "elevated", "fallback")
   492  
   493      def __init__(self, json=None):
   494          self.pid = 0
   495          self.session = None
   496          self.exclude = None
   497          self.include = None
   498          self.elevated = None
   499          self.fallback = False
   500          if not isinstance(json, dict):
   501              return
   502          self.from_json(json)
   503  
   504      def to_json(self):
   505          r = dict()
   506          if isinstance(self.session, bool):
   507              r["session"] = self.session
   508          if isinstance(self.elevated, bool):
   509              r["elevated"] = self.elevated
   510          if isinstance(self.fallback, bool):
   511              r["fallback"] = self.fallback
   512          if isinstance(self.pid, int) and self.pid > 0:
   513              r["pid"] = self.pid
   514          if isinstance(self.exclude, list) and len(self.exclude) > 0:
   515              r["exclude"] = self.exclude
   516          if isinstance(self.include, list) and len(self.include) > 0:
   517              r["include"] = self.include
   518          return r
   519  
   520      def read(self, r):
   521          if not r.read_bool():
   522              return
   523          self.pid = r.read_uint32()
   524          self.fallback = r.read_bool()
   525          b = r.read_uint8()
   526          if b == 0:
   527              self.session = None
   528          elif b == 1:
   529              self.session = False
   530          elif b == 2:
   531              self.session = True
   532          b = r.read_uint8()
   533          if b == 0:
   534              self.elevated = None
   535          elif b == 1:
   536              self.elevated = False
   537          elif b == 2:
   538              self.elevated = True
   539          self.exclude = r.read_str_list()
   540          self.include = r.read_str_list()
   541  
   542      def is_empty(self):
   543          if isinstance(self.session, bool):
   544              return False
   545          if isinstance(self.elevated, bool):
   546              return False
   547          if isinstance(self.pid, int) and self.pid > 0:
   548              return False
   549          if isinstance(self.exclude, list) and len(self.exclude) > 0:
   550              return False
   551          if isinstance(self.include, list) and len(self.include) > 0:
   552              return False
   553          return True
   554  
   555      def write(self, w):
   556          if self is None or self.is_empty():
   557              return w.write_bool(False)
   558          w.write_bool(True)
   559          w.write_uint32(self.pid)
   560          w.write_bool(self.fallback)
   561          if self.session is True:
   562              w.write_uint8(2)
   563          elif self.session is False:
   564              w.write_uint8(1)
   565          else:
   566              w.write_uint8(0)
   567          if self.elevated is True:
   568              w.write_uint8(2)
   569          elif self.elevated is False:
   570              w.write_uint8(1)
   571          else:
   572              w.write_uint8(0)
   573          w.write_str_list(self.exclude)
   574          w.write_str_list(self.include)
   575  
   576      def from_json(self, d):
   577          if not isinstance(d, dict):
   578              raise ValueError("from_json: value provided was not a dict")
   579          if "session" in d and isinstance(d["session"], bool):
   580              self.session = d["session"]
   581          if "elevated" in d and isinstance(d["elevated"], bool):
   582              self.elevated = d["elevated"]
   583          if "fallback" in d and isinstance(d["fallback"], bool):
   584              self.fallback = d["fallback"]
   585          if "pid" in d and isinstance(d["pid"], int) and d["pid"] > 0:
   586              self.pid = d["pid"]
   587          if "exclude" in d and isinstance(d["exclude"], list) and len(d["exclude"]) > 0:
   588              self.exclude = d["exclude"]
   589              for i in self.exclude:
   590                  if isinstance(i, str) and len(i) > 0:
   591                      continue
   592                  raise ValueError('from_json: empty or non-string value in "exclude"')
   593          if "include" in d and isinstance(d["include"], list) and len(d["include"]) > 0:
   594              self.include = d["include"]
   595              for i in self.include:
   596                  if isinstance(i, str) and len(i) > 0:
   597                      continue
   598                  raise ValueError('from_json: empty or non-string value in "include"')
   599  
   600  
   601  class Sentinel(object):
   602      __slots__ = ("paths", "filter")
   603  
   604      def __init__(self, raw=None, file=None, key=None, json=None):
   605          self.paths = None
   606          self.filter = Filter()
   607          if _nes(file):
   608              return self.load(file, key)
   609          if isinstance(raw, (str, bytes, bytearray)):
   610              return self.from_raw(raw, key)
   611          if not isinstance(json, dict):
   612              return
   613          self.from_json(json)
   614  
   615      def to_json(self):
   616          return {
   617              "filter": self.filter.to_json(),
   618              "paths": [i.to_json() for i in self.paths],
   619          }
   620  
   621      def read(self, r):
   622          self.filter.read(r)
   623          n = r.read_uint16()
   624          if n < 0:
   625              raise ValueError("invalid entry size")
   626          self.paths = list()
   627          for _ in range(0, n):
   628              self.paths.append(SentinelPath(reader=r))
   629          del n
   630  
   631      def write(self, w):
   632          self.filter.write(w)
   633          if not isinstance(self.paths, list) or len(self.paths) == 0:
   634              return w.write_uint16(0)
   635          w.write_uint16(len(self.paths))
   636          for x in range(0, min(len(self.paths), 0xFFFFFFFF)):
   637              self.paths[x].write(w)
   638  
   639      def from_json(self, j):
   640          if not isinstance(j, dict):
   641              raise ValueError("from_json: value provided was not a dict")
   642          if "filter" in j:
   643              self.filter.from_json(j["filter"])
   644          if "paths" not in j or not isinstance(j["paths"], list) or len(j["paths"]) == 0:
   645              return
   646          self.paths = list()
   647          for i in j["paths"]:
   648              self.paths.append(SentinelPath.from_json(i))
   649  
   650      def add_dll(self, path):
   651          if not _nes(path):
   652              raise ValueError('add_dll: "path" must be a non-empty string')
   653          if self.paths is None:
   654              self.paths = list()
   655          self.paths.append(SentinelPath(type=SentinelPath.DLL, path=path))
   656  
   657      def add_asm(self, path):
   658          if not _nes(path):
   659              raise ValueError('add_asm: "path" must be a non-empty string')
   660          if self.paths is None:
   661              self.paths = list()
   662          self.paths.append(SentinelPath(type=SentinelPath.ASM, path=path))
   663  
   664      def add_execute(self, cmd):
   665          if not _nes(cmd):
   666              raise ValueError('add_execute: "path" must be a non-empty string')
   667          if self.paths is None:
   668              self.paths = list()
   669          self.paths.append(SentinelPath(type=SentinelPath.EXECUTE, path=cmd))
   670  
   671      def save(self, path, key=None):
   672          k = key
   673          if _nes(key):
   674              k = key.encode("UTF-8")
   675          elif key is not None and not isinstance(key, (bytes, bytearray)):
   676              raise ValueError("save: key must be a string or bytes type")
   677          b = BytesIO()
   678          if k is not None:
   679              i = token_bytes(len(k))
   680              b.write(i)
   681              o = WriteCTR(CTRXor(i, k), b)
   682              del i
   683          else:
   684              o = b
   685          w = Writer(o)
   686          del o
   687          self.write(w)
   688          del w
   689          r = b.getvalue()
   690          b.close()
   691          del b
   692          if not _nes(path):
   693              return r
   694          with open(expanduser(expandvars(path)), "wb") as f:
   695              f.write(r)
   696          del r
   697  
   698      def add_zombie(self, path, fakes):
   699          if not _nes(path):
   700              raise ValueError('add_zombie: "path" must be a non-empty string')
   701          if not isinstance(fakes, (str, list)) or len(fakes) == 0:
   702              raise ValueError(
   703                  'add_zombie: "fakes" must be a non-empty string or string list'
   704              )
   705          if self.paths is None:
   706              self.paths = list()
   707          if isinstance(fakes, str):
   708              fakes = [fakes]
   709          self.paths.append(
   710              SentinelPath(type=SentinelPath.ZOMBIE, path=path, extra=fakes)
   711          )
   712  
   713      def from_raw(self, data, key=None):
   714          if isinstance(data, str) and len(data) > 0:
   715              if data[0] == "{" and data[-1].strip() == "}":
   716                  return self.from_json(loads(data))
   717              return self.load(None, key=key, buf=b64decode(data, validate=True))
   718          if isinstance(data, (bytes, bytearray)) and len(data) > 0:
   719              if data[0] == 91 and data.decode("UTF-8", "ignore").strip()[-1] == "]":
   720                  return self.from_json(loads(data.decode("UTF-8")))
   721              return self.load(None, key=key, buf=data)
   722          raise ValueError("from_raw: a bytes or string type is required")
   723  
   724      def load(self, path, key=None, buf=None):
   725          k = key
   726          if _nes(key):
   727              k = key.encode("UTF-8")
   728          elif key is not None and not isinstance(key, (bytes, bytearray)):
   729              raise ValueError("load: key must be a string or bytes type")
   730          if isinstance(buf, (bytes, bytearray)):
   731              b = BytesIO(buf)
   732          elif isinstance(buf, BytesIO):
   733              b = buf
   734          else:
   735              b = open(expanduser(expandvars(path)), "rb")
   736          if k is not None:
   737              i = b.read(len(k))
   738              o = ReadCTR(CTRXor(i, k), b)
   739              del i
   740          else:
   741              o = b
   742          r = Reader(o)
   743          del o
   744          try:
   745              self.read(r)
   746          finally:
   747              b.close()
   748              del b
   749          del r
   750  
   751      def add_download(self, url, agents=None):
   752          if not _nes(url):
   753              raise ValueError('add_download: "url" must be a non-empty string')
   754          if agents is not None and not isinstance(agents, (str, list)):
   755              raise ValueError('add_download: "agents" must be a string or string list')
   756          if self.paths is None:
   757              self.paths = list()
   758          if isinstance(agents, str):
   759              agents = [agents]
   760          self.paths.append(
   761              SentinelPath(type=SentinelPath.DOWNLOAD, path=url, extra=agents)
   762          )
   763  
   764  
   765  class SentinelPath(object):
   766      EXECUTE = 0
   767      DLL = 1
   768      ASM = 2
   769      DOWNLOAD = 3
   770      ZOMBIE = 4
   771  
   772      __slots__ = ("type", "path", "extra")
   773  
   774      def __init__(self, reader=None, type=None, path=None, extra=None):
   775          self.type = type
   776          self.path = path
   777          self.extra = extra
   778          if not isinstance(reader, Reader):
   779              return
   780          self.read(reader)
   781  
   782      def valid(self):
   783          if self.type > SentinelPath.ZOMBIE or not self.path:
   784              return False
   785          if self.type > SentinelPath.DOWNLOAD and not self.extra:
   786              return False
   787          return True
   788  
   789      @staticmethod
   790      def from_json(d):
   791          if not isinstance(d, dict):
   792              raise ValueError("from_json: value provided was not a dict")
   793          if "type" not in d or "path" not in d:
   794              raise ValueError("from_json: invalid JSON data")
   795          t = d["type"]
   796          if not _nes(t):
   797              raise ValueError('from_json: invalid "type" value')
   798          v = t.lower()
   799          del t
   800          p = d["path"]
   801          if not _nes(p):
   802              raise ValueError('from_json: invalid "path" value')
   803          s = SentinelPath()
   804          s.path = p
   805          del p
   806          if v == "execute":
   807              s.type = SentinelPath.EXECUTE
   808          elif v == "dll":
   809              s.type = SentinelPath.DLL
   810          elif v == "asm":
   811              s.type = SentinelPath.ASM
   812          elif v == "download":
   813              s.type = SentinelPath.DOWNLOAD
   814          elif v == "zombie":
   815              s.type = SentinelPath.ZOMBIE
   816          else:
   817              raise ValueError('from_json: unknown "type" value')
   818          del v
   819          if s.type < SentinelPath.DOWNLOAD:
   820              return s
   821          if "extra" not in d:
   822              if s.type == SentinelPath.DOWNLOAD:
   823                  return s
   824              raise ValueError('from_json: missing "extra" value')
   825          e = d["extra"]
   826          if not isinstance(e, list) or len(e) == 0:
   827              if s.type == SentinelPath.DOWNLOAD:
   828                  return s
   829              raise ValueError('from_json: invalid "extra" value')
   830          for i in e:
   831              if _nes(i):
   832                  continue
   833              raise ValueError('from_json: invalid "extra" sub-value')
   834          s.extra = e
   835          del e
   836          return s
   837  
   838      def to_json(self):
   839          if self.type > SentinelPath.ZOMBIE:
   840              raise ValueError("to_json: invalid path type")
   841          if self.type < SentinelPath.DOWNLOAD or (
   842              not isinstance(self.extra, list) and not self.extra
   843          ):
   844              return {"type": self.typename(), "path": self.path}
   845          return {"type": self.typename(), "path": self.path, "extra": self.extra}
   846  
   847      def read(self, r):
   848          self.type = r.read_uint8()
   849          self.path = r.read_str()
   850          if self.type < SentinelPath.DOWNLOAD:
   851              return
   852          self.extra = r.read_str_list()
   853  
   854      def __str__(self):
   855          if self.type == SentinelPath.EXECUTE:
   856              return f"Execute: {self.path}"
   857          if self.type == SentinelPath.DLL:
   858              return f"DLL: {self.path}"
   859          if self.type == SentinelPath.ASM:
   860              return f"ASM: {self.path}"
   861          if self.type == SentinelPath.DOWNLOAD:
   862              if isinstance(self.extra, list) and len(self.extra) > 0:
   863                  return f'Download: {self.path} (Agents: {", ".join(self.extra)})'
   864              return f"Download: {self.path}"
   865          if self.type == SentinelPath.ZOMBIE:
   866              if isinstance(self.extra, list) and len(self.extra) > 0:
   867                  return f'Zombie: {self.path} (Fakes: {", ".join(self.extra)})'
   868              return f"Zombie: {self.path}"
   869          return "Unknown"
   870  
   871      def write(self, w):
   872          w.write_uint8(self.type)
   873          w.write_str(self.path)
   874          if self.type < SentinelPath.DOWNLOAD:
   875              return
   876          w.write_str_list(self.extra)
   877  
   878      def typename(self):
   879          if self.type == SentinelPath.EXECUTE:
   880              return "execute"
   881          if self.type == SentinelPath.DLL:
   882              return "dll"
   883          if self.type == SentinelPath.ASM:
   884              return "asm"
   885          if self.type == SentinelPath.DOWNLOAD:
   886              return "download"
   887          if self.type == SentinelPath.ZOMBIE:
   888              return "zombie"
   889          return "invalid"
   890  
   891  
   892  class _Builder(ArgumentParser):
   893      def __init__(self):
   894          ArgumentParser.__init__(self, description="XMT man.Sentinel Tool")
   895          self.add_argument("-j", "--json", dest="json", action="store_true")
   896          self.add_argument("-p", "--print", dest="print", action="store_true")
   897          self.add_argument("-I", "--stdin", dest="stdin", action="store_true")
   898          self.add_argument("-f", "--file", type=str, dest="file")
   899          self.add_argument("-k", "--key", type=str, dest="key")
   900          self.add_argument("-y", "--key-file", type=str, dest="key_file")
   901          self.add_argument("-K", "--key-b64", type=str, dest="key_base64")
   902          self.add_argument("-d", "--dll", dest="dll", action="store_true")
   903          self.add_argument("-s", "--asm", dest="asm", action="store_true")
   904          self.add_argument("-S", "--save", dest="save", action="store_true")
   905          self.add_argument("-z", "--zombie", dest="zombie", action="store_true")
   906          self.add_argument("-c", "--command", dest="command", action="store_true")
   907          self.add_argument(
   908              "-u", "--url", "--download", dest="download", action="store_true"
   909          )
   910          self.add_argument(nargs="*", type=str, dest="path")
   911          self.add_argument(
   912              "-A",
   913              "-F",
   914              "--fake",
   915              "--agent",
   916              nargs="*",
   917              type=str,
   918              dest="extra",
   919              action="append",
   920          )
   921          self.add_argument("-n", "--pid", type=int, dest="pid")
   922          self.add_argument("-V", dest="no_desktop", action="store_false")
   923          self.add_argument(
   924              "-v", "--desktop", dest="desktop", action=BooleanOptionalAction
   925          )
   926          self.add_argument("-R", dest="no_fallback", action="store_false")
   927          self.add_argument(
   928              "-r", "--fallback", dest="fallback", action=BooleanOptionalAction
   929          )
   930          self.add_argument("-E", dest="no_admin", action="store_false")
   931          self.add_argument(
   932              "-e",
   933              "--admin",
   934              "--elevated",
   935              dest="admin",
   936              action=BooleanOptionalAction,
   937          )
   938          self.add_argument(
   939              "-x",
   940              "--exclude",
   941              nargs="*",
   942              type=str,
   943              dest="exclude",
   944              action="append",
   945          )
   946          self.add_argument(
   947              "-i",
   948              "--include",
   949              nargs="*",
   950              type=str,
   951              dest="include",
   952              action="append",
   953          )
   954  
   955      def run(self):
   956          a, k = self.parse_args(), None
   957          if _nes(a.key_file):
   958              with open(expanduser(expandvars(a.key_file)), "rb") as f:
   959                  k = f.read()
   960          elif _nes(a.key_base64):
   961              k = b64decode(a.key_base64, validate=True)
   962          elif _nes(a.key):
   963              k = a.key.encode("UTF-8")
   964          if a.file:
   965              s, z = _read_file_input(a.file, k)
   966          else:
   967              s, z = Sentinel(), False
   968          if a.stdin and a.file != "-":
   969              if stdin.isatty():
   970                  raise ValueError("stdin: no input found")
   971              if hasattr(stdin, "buffer"):
   972                  b = stdin.buffer.read().decode("UTF-8")
   973              else:
   974                  b = stdin.read()
   975              stdin.close()
   976              for v in b.split("\n"):
   977                  _Builder.build(s, super(__class__, self).parse_args(split(v)))
   978          elif (
   979              isinstance(a.path, list)
   980              and len(a.path) > 0
   981              and (not a.print or (a.print and not a.file))
   982          ):
   983              _Builder.build(s, a)
   984          elif not z:
   985              raise ValueError("no paths added to an empty Sentinel")
   986          elif not a.save and (a.print or a.json):
   987              a.file = None
   988          elif a.save:
   989              _Builder._parse_filter(s.filter, a)
   990          if not isinstance(s.paths, list) or len(s.paths) == 0:
   991              return
   992          if a.save:
   993              a.print, a.json = False, False
   994          _write_out(s, k, a.file, a.print, a.json)
   995          del s, z, a, k
   996  
   997      @staticmethod
   998      def build(s, a):
   999          _Builder._parse_filter(s.filter, a)
  1000          if not isinstance(a.path, list) or len(a.path) == 0:
  1001              return
  1002          if a.command:
  1003              s.add_execute(" ".join(a.path))
  1004          elif a.dll:
  1005              s.add_dll(" ".join(a.path))
  1006          elif a.asm:
  1007              s.add_asm(" ".join(a.path))
  1008          elif a.download:
  1009              s.add_download(" ".join(a.path), agents=_join(a.extra))
  1010          elif a.zombie:
  1011              s.add_zombie(" ".join(a.path), _join(a.extra))
  1012          else:
  1013              s.add_execute(" ".join(a.path))
  1014          _Builder._parse_filter(s.filter, a)
  1015  
  1016      def parse_args(self):
  1017          if len(argv) <= 1:
  1018              return self.print_help()
  1019          return super(__class__, self).parse_args()
  1020  
  1021      @staticmethod
  1022      def _parse_filter(f, a):
  1023          if isinstance(a.pid, int):
  1024              if a.pid <= 0:
  1025                  f.pid = None
  1026              else:
  1027                  f.pid = a.pid
  1028          if a.exclude is not None and len(a.exclude) > 0:
  1029              f.exclude = _join(a.exclude, True)
  1030          if a.include is not None and len(a.include) > 0:
  1031              f.include = _join(a.include, True)
  1032          if not a.no_admin or a.admin is not None:
  1033              f.elevated = (a.admin is None and a.no_admin) or (
  1034                  a.admin is True and a.no_admin
  1035              )
  1036          if not a.no_desktop or a.desktop is not None:
  1037              f.session = (a.desktop is None and a.no_desktop) or (
  1038                  a.desktop is True and a.no_desktop
  1039              )
  1040          if not a.no_fallback or a.fallback is not None:
  1041              f.fallback = (a.fallback is None and a.no_fallback) or (
  1042                  a.fallback is True and a.no_fallback
  1043              )
  1044  
  1045      def print_help(self, file=None):
  1046          print(HELP_TEXT.format(binary=argv[0]), file=file)
  1047          exit(2)
  1048  
  1049  
  1050  if __name__ == "__main__":
  1051      try:
  1052          _Builder().run()
  1053      except Exception as err:
  1054          print(f"Error: {err}\n{format_exc(3)}", file=stderr)
  1055          exit(1)