Skip to content

Commit

Permalink
Merge branch 'master' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
doomedraven committed Sep 29, 2024
2 parents c56fd02 + 4d96d6a commit a02f6fd
Show file tree
Hide file tree
Showing 36 changed files with 3,189 additions and 2,548 deletions.
30 changes: 28 additions & 2 deletions agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
import multiprocessing
import os
import platform
import random
import shlex
import shutil
import socket
import socketserver
import stat
import string
import subprocess
import sys
import tempfile
import time
import traceback
from io import StringIO
from threading import Lock
from typing import Iterable
from zipfile import ZipFile

Expand All @@ -41,7 +44,7 @@
if sys.maxsize > 2**32 and sys.platform == "win32":
sys.exit("You should install python3 x86! not x64")

AGENT_VERSION = "0.17"
AGENT_VERSION = "0.18"
AGENT_FEATURES = [
"execpy",
"execute",
Expand All @@ -54,6 +57,7 @@

if sys.platform == "win32":
AGENT_FEATURES.append("mutex")
AGENT_FEATURES.append("browser_extension")
MUTEX_TIMEOUT_MS = 500
from ctypes import WinError, windll

Expand Down Expand Up @@ -89,6 +93,8 @@ def _missing_(cls, value):
return None


AGENT_BROWSER_EXT_PATH = ""
AGENT_BROWSER_LOCK = Lock()
ANALYZER_FOLDER = ""
agent_mutexes = {}
"""Holds handles of mutexes held by the agent."""
Expand Down Expand Up @@ -196,7 +202,7 @@ def handle(self, obj):
if "client_ip" in state and request.client_ip != state["client_ip"]:
if request.client_ip != "127.0.0.1":
return
if obj.path != "/status" or request.method != "POST":
if obj.path not in ["/status", "/browser_extension"] or request.method != "POST":
return

for route, fn in self.routes[obj.command]:
Expand Down Expand Up @@ -753,6 +759,26 @@ def do_execpy():
return json_exception(f"Error executing Python command: {ex}")


@app.route("/browser_extension", methods=["POST"])
def do_browser_ext():
global AGENT_BROWSER_EXT_PATH
AGENT_BROWSER_LOCK.acquire()
if not AGENT_BROWSER_EXT_PATH:
try:
ext_tmpdir = tempfile.mkdtemp(prefix="tmp")
except Exception:
AGENT_BROWSER_LOCK.release()
return json_exception("Error creating temporary directory")
ext_filepath = "bext_" + "".join(random.choice(string.ascii_letters) for _ in range(11)) + ".json"
AGENT_BROWSER_EXT_PATH = os.path.join(ext_tmpdir, ext_filepath)
network_data = request.form.get("networkData")
if network_data:
with open(AGENT_BROWSER_EXT_PATH, "w") as ext_fd:
ext_fd.write(network_data)
AGENT_BROWSER_LOCK.release()
return json_success("OK")


@app.route("/pinning")
def do_pinning():
if "client_ip" in state:
Expand Down
74 changes: 74 additions & 0 deletions analyzer/windows/modules/auxiliary/browsermonitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (C) 2024 [email protected]
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
# See the file 'docs/LICENSE' for copying permission.
import logging
import os
import subprocess
import tempfile
import time
from threading import Thread

from lib.common.abstracts import Auxiliary
from lib.common.results import upload_to_host

log = logging.getLogger(__name__)


class Browsermonitor(Auxiliary, Thread):
"""Monitors Browser Extension request logs."""

def __init__(self, options=None, config=None):
if options is None:
options = {}
Auxiliary.__init__(self, options, config)
Thread.__init__(self)
self.do_run = False
self.enabled = config.browsermonitor
self.startupinfo = subprocess.STARTUPINFO()
self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.browser_logfile = ""
self.last_modification = 0.0
self._is_first_save = True

def _find_browser_extension(self):
temp_dir = tempfile.gettempdir()
while not self.browser_logfile and self.do_run:
temp_dir_list = os.listdir(temp_dir)
for directory in temp_dir_list:
tmp_directory_path = os.path.join(temp_dir, directory)
if not os.path.isdir(tmp_directory_path):
continue
if not directory.startswith("tmp"):
continue
tmp_dir_files = os.listdir(tmp_directory_path)
for file in tmp_dir_files:
if file.startswith("bext_") and file.endswith(".json"):
self.browser_logfile = os.path.join(temp_dir, directory, file)
log.debug(f"Found extension logs: {self.browser_logfile}")
break
time.sleep(1)

def _collect_browser_logs(self):
if not self._is_first_save and self.last_modification != os.path.getmtime(self.browser_logfile):
return
self.last_modification = os.path.getmtime(self.browser_logfile)
upload_to_host(self.browser_logfile, "browser/requests.log")
self._is_first_save = False

def run(self):
self.do_run = True
if self.enabled:
self._find_browser_extension()
self.last_modification = os.path.getmtime(self.browser_logfile)
while self.do_run:
self._collect_browser_logs()
time.sleep(1)
return True
return False

def stop(self):
if self.enabled:
self.do_run = False
if self.browser_logfile:
self._collect_browser_logs()
return True
25 changes: 25 additions & 0 deletions analyzer/windows/modules/packages/chromium_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (C) 2024 [email protected]
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
# See the file 'docs/LICENSE' for copying permission.

import time
import webbrowser

from lib.common.abstracts import Package


class ChromiumExt(Package):
"""Chromium extension analysis package."""

PATHS = [
("LOCALAPPDATA", "Chromium", "chrome.exe"),
]
summary = "Opens the URL in Chromium with loaded extension."
description = """Runs Chromium preloaded with a custom extensios."""

def start(self, url):
webbrowser.register("chromium", None, webbrowser.BackgroundBrowser(self.get_path("chrome.exe")))
chromium = webbrowser.get("chromium")
chromium.open("about:blank")
time.sleep(10)
return chromium.open(url)
24 changes: 24 additions & 0 deletions analyzer/windows/modules/packages/firefox_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (C) 2024 [email protected]
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
# See the file 'docs/LICENSE' for copying permission.
import time
import webbrowser

from lib.common.abstracts import Package


class Firefox_Ext(Package):
"""Firefox analysis package (with extension)."""

PATHS = [
("ProgramFiles", "Mozilla Firefox", "firefox.exe"),
]
summary = "Opens the URL in firefox."
description = """Spawns firefox.exe and opens the supplied URL."""

def start(self, url):
webbrowser.register("firefox", None, webbrowser.BackgroundBrowser(self.get_path("firefox.exe")))
firefox = webbrowser.get("firefox")
firefox.open("about:blank")
time.sleep(7) # Rough estimate, change based on your setup times.
return firefox.open(url)
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### [26.09.2024] Browser monitoring
* [Browser extension details](https://github.com/kevoreilly/CAPEv2/tree/master/extra/browser_extension/README.md). For code details see [PR](https://github.com/kevoreilly/CAPEv2/pull/2330)


### [23.09.2024]
* Monitor update: Fix size bug with unpacking embedded PEs
* .NET loader 'SlowLoader' detonation shim for slower cpus (race condition)
Expand Down
2 changes: 2 additions & 0 deletions conf/default/auxiliary.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ filecollector = yes
windows_static_route = no
tracee_linux = no
sslkeylogfile = no
# Requires setting up browser extension, check extra/browser_extension
browsermonitor = no

[AzSniffer]
# Enable or disable the use of Azure Network Watcher packet capture feature, disable standard sniffer if this is in use to not create concurrent .pcap files
Expand Down
4 changes: 4 additions & 0 deletions conf/default/az.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ monitor_rate = 300
# normal instances
spot_instances = false

quota_name = MyQuota
# This value is used to set the number of machines that will be reserved outside the scale set (deducted from the quota).
quota_machine_exclusion = 5

# This boolean value is used to indicate if we want to wait for each VM to have its agent running before we
# start pulling tasks off of the stack
wait_for_agent_before_starting = true
Expand Down
2 changes: 2 additions & 0 deletions conf/default/processing.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ platform = linux

[debug]
enabled = yes
# Amount of text (bytes)
buffer = 8192

[detections]
enabled = yes
Expand Down
3 changes: 3 additions & 0 deletions conf/default/reporting.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,6 @@ enabled = no
# Community
[malheur]
enabled = no

[browserext]
enabled = no
14 changes: 7 additions & 7 deletions data/yara/CAPE/Quickbind.yar
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ rule Quickbind
description = "Quickbind"
cape_type = "Quickbind Payload"
strings:
$anti_appdirs = {E8 [4] 83 F8 0? 7? ?? E8}
$anti_procs_ram = {E8 [4] 83 F8 0? 7? ?? E8 [4] 3D (FF 0E | 00 0F) 00 00}
$anti_ram = {E8 [4] 3D (FF 1F | 00 20 | 00 17) 00 00}
$mutex_1 = {FF [1-5] 3D B7 00 00 00 74 [7-10] 25 89 00 00 00}
$mutex_2 = {FF 15 [4] 4? 89 C? 4? 85 C? 74 ?? FF 15 [4] 3D B7 00 00 00}
$mutex_3 = {FF 15 [4] 4? 89 44 24 ?? 4? 83 7C 24 ?? 00 74 ?? FF 15 [4] 3D B7 00 00 00}
$sleep = {B9 64 00 00 00 [0-7] FF}
$anti_appdirs = {E8 [4] 83 F8 0? 7? ?? E8}
$anti_procs_ram = {E8 [4] 83 F8 0? 7? ?? E8 [4] 3D (FF 0E | 00 0F | FF 16) 00 00}
$anti_ram = {E8 [4] 3D (FF 1F | 00 20 | 00 17 | FF 0E | FF 16 | FF 2F) 00 00}

Check warning on line 10 in data/yara/CAPE/Quickbind.yar

View check run for this annotation

VirusTotal YARA-CI / Rules Analysis

data/yara/CAPE/Quickbind.yar#L10

rule "Quickbind": string "$anti_ram" may slow down scanning
$mutex_1 = {FF [1-5] 3D B7 00 00 00 74 [7-10] 25 89 00 00 00}
$mutex_2 = {FF 15 [4] 4? 89 C? 4? 85 C? 74 ?? FF 15 [4] 3D B7 00 00 00}
$mutex_3 = {FF 15 [4] 4? 89 44 24 ?? 4? 83 7C 24 ?? 00 74 ?? FF 15 [4] 3D B7 00 00 00}
$sleep = {B9 64 00 00 00 [0-7] FF}
condition:
all of ($anti_*) and 1 of ($mutex_*) and $sleep
}
28 changes: 28 additions & 0 deletions extra/browser_extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
This browser extension intercepts requests done within a browser and sends them
back to agent.py.

Extension setup differs on a browser basis:


==== Firefox ====

Firefox requires having a signed extension. Head to
https://addons.mozilla.org/en-US/developers/addons and sign your extension
(adjust details on manifest.json) and install it in your browser.

Alternatively it's possible to install an unsigned extension temporarily by
heading to about:debugging and pointing to your compressed (.zip) extension but
this is not supported.


==== Chromium ====

Download latest build from https://download-chromium.appspot.com/ -- Enable
developer mode and load unpacked extension. Then, close the browser. Once you
open chromium again, it will complain about the extension being unsafe and
Chromium auto-disables it. Head back to the extensions page and give it
permissions back. Then, the extension is permantently loaded. Tested on version
131.0.X

The default path for the `chromium_ext` package is %LOCALAPPDATA%/Chromium/chrome.exe,
change the path in .py if needed.
76 changes: 76 additions & 0 deletions extra/browser_extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
let networkData = [];


function onRequestEvent(details) {
if (details.url.includes("/browser_extension")) {
return;
}
const requestEvent = networkData.find(entry => entry.url === details.url);
if (requestEvent) {
return
}
const info = {
url: details.url,
method: details.method,
timeStamp: details.timeStamp,
requestHeaders: details.requestHeaders,
};
networkData.push(info);
}

function onResponseEvent(details) {
const requestEvent = networkData.find(entry => entry.url === details.url);
if (requestEvent) {
if (requestEvent.ip) {
return;
}
requestEvent.responseHeaders = details.responseHeaders;
requestEvent.type = details.type;
requestEvent.ip = details.ip;
requestEvent.originUrl = details.originUrl;
sendEvents();
}
}

function sendEvents() {
const form = new FormData();
form.append('networkData', JSON.stringify(networkData));

fetch('http://localhost:8000/browser_extension', {
method: 'POST',
body: form
})
.then(response => response.json())
.catch(error => {
console.error('Error posting data to endpoint:', error);
});
}


browser.webRequest.onBeforeSendHeaders.addListener(
onRequestEvent,
{urls: ["<all_urls>"]},
["requestHeaders"]
);

browser.webRequest.onCompleted.addListener(
onResponseEvent,
{urls: ["<all_urls>"]},
["responseHeaders"]
);

browser.downloads.onCreated.addListener(function(downloadItem) {
const downloadId = downloadItem.id;
browser.downloads.onChanged.addListener(function(delta) {
if (delta.id === downloadId && delta.state && delta.state.current === "complete") {
const requestEvent = networkData.find(entry => entry.url === downloadItem.url);
if (requestEvent) {
requestEvent.filePath = downloadItem.filename;
}
}
});
});

browser.runtime.onStartup.addListener(function () {
networkData = [];
});
Loading

0 comments on commit a02f6fd

Please sign in to comment.