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)