Skip to content

Commit

Permalink
T1620 macOS w/Formatting (#33)
Browse files Browse the repository at this point in the history
* T1620 macOS Reflective Loading

- More content to come for Red Canary's 2024 Threat Detection Report (TDR)!

* T1620 macOS: Python formatting w/Black
  • Loading branch information
Brandon7CC committed Jan 16, 2024
1 parent 96d94f1 commit 6b00e8f
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 33 deletions.
81 changes: 57 additions & 24 deletions posix/src/posixath/tests/macos/test_T1620.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ def source_code_path(pytestconfig: pytest.Config):
@pytest.mark.macos
class TestReflectiveLoading:
# Path to the library directory containing supporting resources for this AtomicTestHarness execution.
__LIB_DIR = Path(abspath(getsourcefile(lambda: 0))).parent.joinpath("library", "T1620")
__LIB_DIR = Path(abspath(getsourcefile(lambda: 0))).parent.joinpath(
"library", "T1620"
)
__CHMOD_744: int = 0o744

def test_nscreateobjectfileimagefrommemory(self, bundle_path: Path=None, source_code_path: Path=None) -> None:
def test_nscreateobjectfileimagefrommemory(
self, bundle_path: Path = None, source_code_path: Path = None
) -> None:
"""
Tests T1620: Reflective Code loading by using `NSCreateObjectFileImageFromMemory` to reflectively load a bundle. A default implementation is provided, but you may also provide your own C source code or your own bundle to reflectively load.
Expand All @@ -54,60 +58,89 @@ def test_nscreateobjectfileimagefrommemory(self, bundle_path: Path=None, source_
libHello_path = bundle_path
else:
options: list = ["-bundle"]
c_compile_result: StandardizedCompletedProcess = DarwinSTDLib.gcc_compile_with_options(default_dylib_code, libHello_path, options)
c_compile_result: StandardizedCompletedProcess = (
DarwinSTDLib.gcc_compile_with_options(
default_dylib_code, libHello_path, options
)
)
# Check to make sure compilation was successful
returncode: int = c_compile_result.return_code
stderr: str = c_compile_result.stderr
stdout: str = c_compile_result.stdout
if returncode != 0:
assert False, f"Error compiling C source file: {stdout}\n{stderr}"

# Now we have our dylib. Let's compile our `nscreateobjectfileimagefrommemory.c` code.
nscreateobjectfileimagefrommemory_reflector_code: Path = self.__LIB_DIR.joinpath("nscreateobjectfileimagefrommemory.c").resolve()
nscreateobjectfileimagefrommemory_path: Path = self.__LIB_DIR.joinpath("nscreateobjectfileimagefrommemory").resolve()
c_compile_result: StandardizedCompletedProcess = DarwinSTDLib.gcc_compile_with_options(nscreateobjectfileimagefrommemory_reflector_code, nscreateobjectfileimagefrommemory_path)
nscreateobjectfileimagefrommemory_reflector_code: Path = (
self.__LIB_DIR.joinpath("nscreateobjectfileimagefrommemory.c").resolve()
)
nscreateobjectfileimagefrommemory_path: Path = self.__LIB_DIR.joinpath(
"nscreateobjectfileimagefrommemory"
).resolve()
c_compile_result: StandardizedCompletedProcess = (
DarwinSTDLib.gcc_compile_with_options(
nscreateobjectfileimagefrommemory_reflector_code,
nscreateobjectfileimagefrommemory_path,
)
)
# Check to make sure compilation was successful
returncode: int = c_compile_result.return_code
stderr: str = c_compile_result.stderr
stdout: str = c_compile_result.stdout
if returncode != 0:
assert False, f"Error compiling C source file: {stdout}\n{stderr}"
# Check to ensure that this binary is executable...
executable: bool = os.access(nscreateobjectfileimagefrommemory_path, self.__CHMOD_744)
executable: bool = os.access(
nscreateobjectfileimagefrommemory_path, self.__CHMOD_744
)
if not executable:
# Set the executable bit for the user
if not DarwinSTDLib.make_file_executable(nscreateobjectfileimagefrommemory_path):
assert False, "Failed to make the nscreateobjectfileimagefrommemory.c executable!"
if not DarwinSTDLib.make_file_executable(
nscreateobjectfileimagefrommemory_path
):
assert (
False
), "Failed to make the nscreateobjectfileimagefrommemory.c executable!"

# Record how long this takes as we'll use it for lookback context
start_time = time.time()
arguments = [str(nscreateobjectfileimagefrommemory_path.resolve()), str(libHello_path)]
execution_result = DarwinSTDLib.default_commandline_executer(arguments)
arguments = [
str(nscreateobjectfileimagefrommemory_path.resolve()),
str(libHello_path),
]
execution_result = DarwinSTDLib.default_commandline_executer(arguments)
elapsed_time = math.ceil(time.time() - start_time)
elapsed_time = 2 if elapsed_time < 1 else elapsed_time

# Query the log for the NSLinkModule writeback path
options = ['--style', 'compact']
options = ["--style", "compact"]
predicate = "eventMessage CONTAINS 'NSCreateObjectFileImageFromMemory-'"
logs = DarwinSTDLib.query_aul(options, predicate, look_back_s=elapsed_time)
filtered_logs = [log for log in logs if "kernel" in log and "(AppleMobileFileIntegrity) AMFI" in log]
filtered_logs = [
log
for log in logs
if "kernel" in log and "(AppleMobileFileIntegrity) AMFI" in log
]
extracted_path: str = "NOT FOUND"
if filtered_logs is not None and len(filtered_logs) > 0:
path_pattern = re.compile(r"(/private/var/folders/.+?/NSCreateObjectFileImageFromMemory-[^\s']+)")
path_pattern = re.compile(
r"(/private/var/folders/.+?/NSCreateObjectFileImageFromMemory-[^\s']+)"
)
most_recent_log = filtered_logs[-1]
extracted_path = path_pattern.search(most_recent_log).group(1) if path_pattern.search(most_recent_log) else None


extracted_path = (
path_pattern.search(most_recent_log).group(1)
if path_pattern.search(most_recent_log)
else None
)

# Inject our context and results
results_json = execution_result.to_json(sort_keys=True, indent=4)
results_dict: dict = json.loads(results_json)
results_dict["attack_id"] = "T1620"
results_dict["nslinkmodule_writeback_path"] = extracted_path
results_json = json.dumps(results_dict, sort_keys=True, indent=4)

rich.print_json(
results_json
)


rich.print_json(results_json)

# Was it a success?
assert results_dict is not None and results_dict["result"] == "success"
assert results_dict is not None and results_dict["result"] == "success"
51 changes: 42 additions & 9 deletions posix/src/posixath/utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,11 @@ def compile_swift(
response_model.return_code = returncode

return response_model

@staticmethod
def gcc_compile_with_options(source_code_path: Path, output_file_path: Path, options: list=None) -> StandardizedCompletedProcess:
def gcc_compile_with_options(
source_code_path: Path, output_file_path: Path, options: list = None
) -> StandardizedCompletedProcess:
"""
gcc_compile_with_options("hello.c", "libhello.bundle", ["-bundle"])
We expect a C source code file at the provided path. We'll compile it into a bundle and output it to the
Expand All @@ -405,10 +407,15 @@ def gcc_compile_with_options(source_code_path: Path, output_file_path: Path, opt
assert False, "Unable to install Xcode command line tools!"
gcc_bin_path: str = "/usr/bin/gcc"
if options is not None:
cmdl: list = [gcc_bin_path, "-o", output_file_path, source_code_path] + options
cmdl: list = [
gcc_bin_path,
"-o",
output_file_path,
source_code_path,
] + options
else:
cmdl: list = [gcc_bin_path, "-o", output_file_path, source_code_path]

gcc_compile_proc = subprocess.run(cmdl, capture_output=True)
returncode: int = gcc_compile_proc.returncode
stdout: str = gcc_compile_proc.stdout.decode("utf-8")
Expand Down Expand Up @@ -524,21 +531,47 @@ def archives_supported_by_archive_utility() -> [str]:
"""
Returns the list of file types supported by `Archive Utility.app`.
"""
archive_utility_path: Path = Path("/System/Library/CoreServices/Applications/Archive Utility.app")
archive_utility_path: Path = Path(
"/System/Library/CoreServices/Applications/Archive Utility.app"
)

# Get the plist and attempt to read the `CFBundleDocumentTypes` key:
archive_utility_plist = DarwinSTDLib.get_app_plist(app_path=archive_utility_path)
archive_utility_plist = DarwinSTDLib.get_app_plist(
app_path=archive_utility_path
)
if "CFBundleDocumentTypes" in archive_utility_plist:
return list(filter(lambda item: item is not None, map(lambda plist_dict: DarwinSTDLib.get_supported_file_type(plist_dict), archive_utility_plist["CFBundleDocumentTypes"])))
return list(
filter(
lambda item: item is not None,
map(
lambda plist_dict: DarwinSTDLib.get_supported_file_type(
plist_dict
),
archive_utility_plist["CFBundleDocumentTypes"],
),
)
)

@staticmethod
def query_aul(options: list, predicate: str, look_back_s: int=10) -> [str]:
def query_aul(options: list, predicate: str, look_back_s: int = 10) -> [str]:
"""
Queries the Apple Unified Logging by the predicate specified and returns the logs as a list of strings.
"""
result = subprocess.run(['/usr/bin/log', 'show', *options, '--last', f'{look_back_s}s', '--predicate', f'{predicate}'], capture_output=True)
result = subprocess.run(
[
"/usr/bin/log",
"show",
*options,
"--last",
f"{look_back_s}s",
"--predicate",
f"{predicate}",
],
capture_output=True,
)
return result.stdout.decode("utf-8").splitlines()


class LinuxSTDLib(STDLib):
def __init__(self):
super().__init__()

0 comments on commit 6b00e8f

Please sign in to comment.