Skip to content

Commit

Permalink
Merge branch 'kevoreilly:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dsecuma authored Sep 19, 2024
2 parents 6d799df + 6151764 commit 00e9667
Show file tree
Hide file tree
Showing 86 changed files with 3,194 additions and 676 deletions.
7 changes: 6 additions & 1 deletion analyzer/linux/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# See the file 'docs/LICENSE' for copying permission.

import datetime
import hashlib
import logging
import os
import pkgutil
Expand Down Expand Up @@ -324,12 +325,16 @@ def run(self):
pid_check = False

time_counter = 0

complete_folder = hashlib.md5(f"cape-{self.config.id}".encode()).hexdigest()
complete_analysis_pattern = os.path.join(os.environ.get("TMP", "/tmp"), complete_folder)
while True:
time_counter += 1
if time_counter > int(self.config.timeout):
log.info("Analysis timeout hit, terminating analysis")
break
if os.path.isdir(complete_analysis_pattern):
log.info("Analysis termination requested by user")
break

try:
# If the process monitor is enabled we start checking whether
Expand Down
2 changes: 2 additions & 0 deletions analyzer/linux/lib/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
"memory": os.path.join(ROOT, "memory"),
"drop": os.path.join(ROOT, "drop"),
}

OPT_CURDIR = "curdir"
42 changes: 39 additions & 3 deletions analyzer/linux/lib/core/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

import inspect
import logging
import shutil
import subprocess
import timeit
from os import environ, path, sys
from os import environ, makedirs, path, sys
from threading import Event, Thread

from lib.common.constants import OPT_CURDIR
from lib.common.results import NetlogFile, append_buffer_to_host

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,6 +75,18 @@ def _guess_package_name(file_type, file_name):
return None


def create_custom_folders(directory_path: str):
"""Create custom folders (recursively) given the full path."""
if path.exists(directory_path):
log.info("%s already exists, skipping creation", directory_path)
else:
try:
makedirs(directory_path)
log.info("%s created", directory_path)
except OSError:
log.error("Unable to create user-defined custom folder directory")


class Package:
"""Base analysis package"""

Expand Down Expand Up @@ -111,6 +125,25 @@ def prepare(self):
"""Preparation routine. Do anything you want here."""
pass

def move_curdir(self, filepath):
"""Move a file to the current working directory so it can be executed
from there.
@param filepath: the file to be moved
@return: the new filepath
"""
if OPT_CURDIR not in self.options:
return filepath

curdir = path.expandvars(self.options[OPT_CURDIR])
create_custom_folders(curdir)

if not path.exists(curdir):
return filepath

newpath = path.join(curdir, path.basename(filepath))
shutil.move(filepath, newpath)
return newpath

def start(self):
"""Runs an analysis process.
This function is a generator.
Expand All @@ -120,6 +153,7 @@ def start(self):
filepath = path.join(environ.get("TEMP", "/tmp"), target_name)
# Remove the trailing slash (if any)
self.target = filepath.rstrip("/")
self.target = self.move_curdir(self.target)
self.prepare()
self.nc.init("logs/strace.log", False)
self.thread = Thread(target=self.thread_send_strace_buffer, daemon=True)
Expand Down Expand Up @@ -170,12 +204,14 @@ def strace_analysis(self):
if "args" in kwargs:
target_cmd += f' {" ".join(kwargs["args"])}'

# eg: strace_args=-e trace=!recvfrom;epoll_pwait
strace_args = self.options.get("strace_args", "").replace(";", ",")
# Tricking strace into always showing PID on stderr output
# https://github.com/strace/strace/issues/278#issuecomment-1815914576
cmd = f"sudo strace -o /dev/stderr -s 800 -ttf {target_cmd}"
cmd = f"sudo strace -o /dev/stderr -s 800 {strace_args} -ttf {target_cmd}"
# If nohuman is set to yes, it's possible to interact with interactive scripts or programs via VNC.
if self.options.get("nohuman"):
cmd = f"sudo strace -o /dev/stderr -s 800 -ttf xterm -hold -e {target_cmd}"
cmd = f"sudo strace -o /dev/stderr -s 800 {strace_args} -ttf xterm -hold -e {target_cmd}"
log.info(cmd)
self.proc = subprocess.Popen(
cmd, env={"XAUTHORITY": "/root/.Xauthority", "DISPLAY": ":0"}, stderr=subprocess.PIPE, shell=True
Expand Down
2 changes: 1 addition & 1 deletion analyzer/linux/modules/auxiliary/filecollector.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def stop(self):

time.sleep(2) # wait a while to process stuff in the queue
self.do_run = False
self.thread.join()
self.thread.join(timeout=5)

def __init__(self, options, config):
Auxiliary.__init__(self, options, config)
Expand Down
2 changes: 1 addition & 1 deletion analyzer/windows/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ def analysis_loop(self, aux_modules):
# Tell all processes to complete their monitoring
if not kernel_analysis:
for pid in self.process_list.pids:
proc = Process(pid=pid)
proc = Process(options=self.options, config=self.config, pid=pid)
if proc.is_alive():
try:
proc.set_terminate_event()
Expand Down
12 changes: 12 additions & 0 deletions analyzer/windows/data/yara/Themida.yar
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
rule Themida
{
meta:
author = "kevoreilly"
description = "Themida detonation shim"
cape_options = "unhook-apis=NtSetInformationThread,force-sleepskip=0"
packed = "6337ff4cf413f56cc6c9a8e67f24b8d7f94f620eae06ac9f0b113b5ba82ea176"
strings:
$code = {FC 31 C9 49 89 CA 31 C0 31 DB AC 30 C8 88 E9 88 D5 88 F2 B6 08 66 D1 EB 66 D1 D8 73 09}
condition:
uint16(0) == 0x5A4D and all of them
}
Binary file modified analyzer/windows/dll/capemon.dll
Binary file not shown.
Binary file modified analyzer/windows/dll/capemon_x64.dll
Binary file not shown.
92 changes: 87 additions & 5 deletions analyzer/windows/lib/api/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
CAPEMON64_NAME,
LOADER32_NAME,
LOADER64_NAME,
TTD32_NAME,
TTD64_NAME,
LOGSERVER_PREFIX,
PATHS,
PIPE,
Expand Down Expand Up @@ -499,6 +501,44 @@ def resume(self):
log.error("Failed to resume %s", self)
return False

def ttd_stop(self):
"""Time Travel Debugging stop"""

if not self.pid:
return False

if self.is_64bit():
ttd_name = "bin\\TTD.exe"
bit_str = "64-bit"
else:
ttd_name = "bin\\wow64\\TTD.exe"
bit_str = "32-bit"

try:
result = subprocess.run(
[os.path.join(Path.cwd(), ttd_name), "-accepteula", "-stop", str(self.pid)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=1,
)
except subprocess.TimeoutExpired as e:
if e.stdout:
log.info(" ".join(e.stdout.split()))
if e.stderr:
log.error(" ".join(e.stderr.split()))
except Exception as e:
log.error("Exception attempting TTD stop for %s process with pid %d: %s", bit_str, self.pid, e)

if result.stdout:
log.info(" ".join(result.stdout.split()))
if result.stderr:
log.error(" ".join(result.stderr.split()))

log.info("Stopped TTD for %s process with pid %d: %s", bit_str, self.pid)

return True

def set_terminate_event(self):
"""Sets the termination event for the process."""
if self.h_process == 0:
Expand All @@ -525,6 +565,13 @@ def set_terminate_event(self):
log.info("Termination confirmed for %s", self)
KERNEL32.CloseHandle(self.terminate_event_handle)

try:
ttd = int(self.options.get("ttd", 0))
except (ValueError, TypeError):
ttd = 0
if ttd:
self.ttd_stop()

def terminate(self):
"""Terminate process.
@return: operation status.
Expand Down Expand Up @@ -604,6 +651,7 @@ def write_monitor_config(self, interest=None, nosleepskip=False):
"pre_script_args",
"pre_script_timeout",
"during_script_args",
"ttd",
]

for optname, option in self.options.items():
Expand All @@ -627,10 +675,12 @@ def inject(self, interest=None, nosleepskip=False):
return False

if self.is_64bit():
ttd_name = TTD64_NAME
bin_name = LOADER64_NAME
dll = CAPEMON64_NAME
bit_str = "64-bit"
else:
ttd_name = TTD32_NAME
bin_name = LOADER32_NAME
dll = CAPEMON32_NAME
bit_str = "32-bit"
Expand All @@ -647,24 +697,56 @@ def inject(self, interest=None, nosleepskip=False):
log.warning("invalid path %s for monitor DLL to be injected in %s, injection aborted", dll, self)
return False

try:
ttd = int(self.options.get("ttd", 0))
except (ValueError, TypeError):
ttd = 0
if ttd:
self.options["no-iat"] = 1

self.write_monitor_config(interest, nosleepskip)

log.info("%s DLL to inject is %s, loader %s", bit_str, dll, bin_name)

try:
ret = subprocess.run([bin_name, "inject", str(self.pid), str(thread_id), dll])

if ret.returncode == 0:
return True
elif ret.returncode == 1:
if ret.returncode == 1:
log.info("Injected into %s %s", bit_str, self)
else:
elif ret.returncode != 0:
log.error("Unable to inject into %s %s, error: %d", bit_str, self, ret.returncode)
return False
except Exception as e:
log.error("Error running process: %s", e)
return False

if not ttd:
return True

try:
ret = subprocess.run(
[
os.path.join(Path.cwd(), ttd_name),
"-accepteula",
"-out",
os.path.join(PATHS["TTD"], f"{self.pid}.run"),
"-attach",
str(self.pid),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=1,
)
except subprocess.TimeoutExpired as e:
if e.stdout:
log.info(" ".join(e.stdout.split()))
if e.stderr:
log.error(" ".join(e.stderr.split()))
except Exception as e:
log.error("Exception attempting TTD injection into %s process with pid %d: %s", bit_str, self.pid, e)

return True

def upload_memdump(self):
"""Upload process memory dump.
@return: operation status.
Expand Down
14 changes: 13 additions & 1 deletion analyzer/windows/lib/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@
"shots": os.path.join(ROOT, "shots"),
"memory": os.path.join(ROOT, "memory"),
"drop": os.path.join(ROOT, "drop"),
"TTD": os.path.join(ROOT, "TTD"),
}

PIPE = f"\\\\.\\PIPE\\{random_string(6, 10)}"
LOGSERVER_PREFIX = f"\\\\.\\PIPE\\{random_string(8, 12)}"
SHUTDOWN_MUTEX = f"Global\\{random_string(6, 10)}"
TERMINATE_EVENT = f"Global\\{random_string(6, 10)}"
CAPEMON32_NAME = f"dll\\{random_string(6, 8)}.dll"
CAPEMON64_NAME = f"dll\\{random_string(6, 8)}.dll"
LOADER32_NAME = f"bin\\{random_string(7)}.exe"
LOADER64_NAME = f"bin\\{random_string(8)}.exe"
LOGSERVER_PREFIX = f"\\\\.\\PIPE\\{random_string(8, 12)}"
TTD32_NAME = "bin\\wow64\\TTD.exe"
TTD64_NAME = "bin\\TTD.exe"

# Options
OPT_APPDATA = "appdata"
Expand All @@ -49,6 +52,7 @@

ARCHIVE_OPTIONS = (OPT_FILE, OPT_PASSWORD)
DLL_OPTIONS = (OPT_ARGUMENTS, OPT_DLLLOADER, OPT_FUNCTION)
SERVICE_OPTIONS = (OPT_SERVICENAME, OPT_SERVICEDESC, OPT_ARGUMENTS)


""" Excel, Word, and Powerpoint won't have macros enabled without interaction for
Expand All @@ -62,3 +66,11 @@
TRUSTED_PATH_TEXT = (
f"Use MS Office Trusted Path location {MSOFFICE_TRUSTED_PATH} unless the user has provided a '{OPT_CURDIR}' option."
)

DLL_OPTION_TEXT = f"""\
Use the '{OPT_DLLLOADER}' option to set the name of the process loading the DLL (defaults to rundll32.exe).
Use the '{OPT_ARGUMENTS}' option to set the arguments to be passed to the exported function(s).
Use the '{OPT_FUNCTION}' option to set the name of the exported function/ordinal to execute.
The default function is '#1'.
Can be multiple function/ordinals split by colon. Ex: function=#1:#3 or #2-4
"""
Loading

0 comments on commit 00e9667

Please sign in to comment.