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)