sandbox-cli 0.2.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sandbox_cli/__main__.py +28 -0
- sandbox_cli/cli/__init__.py +46 -0
- sandbox_cli/cli/downloader.py +308 -0
- sandbox_cli/cli/images.py +45 -0
- sandbox_cli/cli/reporter.py +146 -0
- sandbox_cli/cli/rules/__init__.py +83 -0
- sandbox_cli/cli/scanner/__init__.py +681 -0
- sandbox_cli/cli/unpack.py +50 -0
- sandbox_cli/console.py +24 -0
- sandbox_cli/internal/__init__.py +0 -0
- sandbox_cli/internal/config.py +143 -0
- sandbox_cli/internal/helpers.py +32 -0
- sandbox_cli/models/__init__.py +0 -0
- sandbox_cli/models/detections.py +91 -0
- sandbox_cli/utils/__init__.py +0 -0
- sandbox_cli/utils/compiler/__init__.py +60 -0
- sandbox_cli/utils/compiler/abc.py +51 -0
- sandbox_cli/utils/compiler/docker.py +136 -0
- sandbox_cli/utils/compiler/ssh.py +203 -0
- sandbox_cli/utils/downloader/__init__.py +178 -0
- sandbox_cli/utils/extractors.py +66 -0
- sandbox_cli/utils/merge_dll_hooks.py +84 -0
- sandbox_cli/utils/scanner/__init__.py +310 -0
- sandbox_cli/utils/scanner/advanced.py +360 -0
- sandbox_cli/utils/scanner/rescan.py +258 -0
- sandbox_cli/utils/unpack/__init__.py +96 -0
- sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
- sandbox_cli/utils/unpack/plugins/abc.py +18 -0
- sandbox_cli/utils/unpack/plugins/correlation.py +52 -0
- sandbox_cli/utils/unpack/plugins/sort_by_plugins.py +30 -0
- sandbox_cli-0.2.26.dist-info/METADATA +139 -0
- sandbox_cli-0.2.26.dist-info/RECORD +36 -0
- sandbox_cli-0.2.26.dist-info/WHEEL +4 -0
- sandbox_cli-0.2.26.dist-info/entry_points.txt +2 -0
- sandbox_cli-0.2.26.dist-info/licenses/LICENSE +21 -0
- sandbox_cli-0.2.26.dist-info/licenses/NOTICE +322 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import shutil
|
|
4
|
+
import string
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from asyncssh import (
|
|
10
|
+
HostKeyNotVerifiable,
|
|
11
|
+
SSHClientConnection,
|
|
12
|
+
SSHClientConnectionOptions,
|
|
13
|
+
SSHClientProcess,
|
|
14
|
+
SSHCompletedProcess,
|
|
15
|
+
SSHReader,
|
|
16
|
+
connect,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from sandbox_cli.console import console
|
|
20
|
+
from sandbox_cli.internal.config import settings
|
|
21
|
+
from sandbox_cli.utils.compiler.abc import AbstractCompiler
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RemoteCompiler(AbstractCompiler):
|
|
25
|
+
host: str
|
|
26
|
+
username: str
|
|
27
|
+
password: str
|
|
28
|
+
client: SSHClientConnection | None = None
|
|
29
|
+
tmp_directory: PurePosixPath
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.host = settings.sandbox[0].host
|
|
34
|
+
self.username = settings.sandbox[0].ssh.username
|
|
35
|
+
self.password = settings.sandbox[0].ssh.password
|
|
36
|
+
|
|
37
|
+
def _generate_random_string(self) -> str:
|
|
38
|
+
return "".join(random.choice(string.ascii_lowercase) for _ in range(8))
|
|
39
|
+
|
|
40
|
+
async def _init_ssh_client(self) -> None:
|
|
41
|
+
if not self.client:
|
|
42
|
+
try:
|
|
43
|
+
self.client = await connect(
|
|
44
|
+
host=self.host,
|
|
45
|
+
username=self.username,
|
|
46
|
+
password=self.username,
|
|
47
|
+
options=SSHClientConnectionOptions(public_key_auth=False),
|
|
48
|
+
)
|
|
49
|
+
except HostKeyNotVerifiable:
|
|
50
|
+
console.error(f"Can't verify ssh-key. Execute 'ssh {self.username}@{self.host}' and type 'yes'")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
async def _run_command(self, command: str, is_sudo: bool = False) -> SSHCompletedProcess:
|
|
54
|
+
await self._init_ssh_client()
|
|
55
|
+
assert self.client is not None
|
|
56
|
+
|
|
57
|
+
if is_sudo:
|
|
58
|
+
command = f"echo {self.password} | sudo -k -S {command}"
|
|
59
|
+
|
|
60
|
+
return await self.client.run(command)
|
|
61
|
+
|
|
62
|
+
async def _run_stream_command(self, command: str, is_sudo: bool = False) -> SSHClientProcess[Any]:
|
|
63
|
+
await self._init_ssh_client()
|
|
64
|
+
assert self.client is not None
|
|
65
|
+
|
|
66
|
+
if is_sudo:
|
|
67
|
+
command = f"echo {self.password} | sudo -k -S {command}"
|
|
68
|
+
|
|
69
|
+
async with self.client.create_process(command) as process:
|
|
70
|
+
async for line in cast(SSHReader[bytes], process.stdout):
|
|
71
|
+
print(f"{line.decode() if isinstance(line, bytes) else line}", end="")
|
|
72
|
+
|
|
73
|
+
return process
|
|
74
|
+
|
|
75
|
+
async def _create_tmp_directory(self) -> None:
|
|
76
|
+
self.tmp_directory = PurePosixPath("/tmp/sandbox-cli") / self._generate_random_string()
|
|
77
|
+
await self._run_command(f"mkdir -p {self.tmp_directory}")
|
|
78
|
+
|
|
79
|
+
async def _upload_rules(self, rules_dir: Path) -> None:
|
|
80
|
+
assert self.client is not None
|
|
81
|
+
|
|
82
|
+
arcname = shutil.make_archive("rules", "zip", rules_dir)
|
|
83
|
+
async with self.client.start_sftp_client() as ftp:
|
|
84
|
+
await ftp.put(arcname, f"{self.tmp_directory}")
|
|
85
|
+
os.remove(arcname)
|
|
86
|
+
|
|
87
|
+
await self._run_command(
|
|
88
|
+
f"""
|
|
89
|
+
mkdir -p {self.tmp_directory / "rules"} &&
|
|
90
|
+
mkdir -p {self.tmp_directory / "compiled-rules"} &&
|
|
91
|
+
unzip -d {self.tmp_directory / "rules"} {self.tmp_directory / "rules.zip"} >/dev/null
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def _cleanup(self, rules_dir: Path) -> None:
|
|
96
|
+
await self._run_command(f"rm -rf {self.tmp_directory}")
|
|
97
|
+
shutil.rmtree(rules_dir / "taxonomy")
|
|
98
|
+
|
|
99
|
+
async def _compile_rules_on_server(self) -> None:
|
|
100
|
+
# generate random container name for avoid collision with several peoples
|
|
101
|
+
result = await self._run_command(
|
|
102
|
+
command=f'ctr run --rm --memory-limit=1000000000 \
|
|
103
|
+
--mount type=bind,src={self.tmp_directory / "compiled-rules"},dst=/compiled-rules,options=rbind:rw \
|
|
104
|
+
--mount type=bind,src={self.tmp_directory / "rules"},dst=/rules,options=rbind:rw \
|
|
105
|
+
"{settings.docker.path}:{settings.docker.image_tag}" {self._generate_random_string()} package-builder correlation:compile -r /rules -c /compiled-rules',
|
|
106
|
+
is_sudo=True,
|
|
107
|
+
)
|
|
108
|
+
if result.exit_status:
|
|
109
|
+
console.log("failed to compile rules", style="bold red")
|
|
110
|
+
console.print(result.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
async def _download_compiled_rules(self) -> bytes:
|
|
114
|
+
assert self.client is not None
|
|
115
|
+
|
|
116
|
+
await self._run_command(
|
|
117
|
+
f"tar -C {self.tmp_directory / 'compiled-rules'} -czvf {self.tmp_directory / 'compiled-rules.tar.gz'} event_normalization_graph.json event_correlation_graph.json"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async with self.client.start_sftp_client() as ftp:
|
|
121
|
+
fd = await ftp.open(path=self.tmp_directory / "compiled-rules.tar.gz", pflags_or_mode="rb")
|
|
122
|
+
data: bytes = await fd.read()
|
|
123
|
+
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
async def pull_image(self) -> None:
|
|
127
|
+
"""Update image on remote server"""
|
|
128
|
+
|
|
129
|
+
process = await self._run_command(
|
|
130
|
+
f'ctr image pull --user "{settings.docker.username}:{settings.docker.token}" "{settings.docker.path}:{settings.docker.image_tag}"',
|
|
131
|
+
is_sudo=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if process.exit_status != 0:
|
|
135
|
+
console.error("Failed to update docker image on server")
|
|
136
|
+
|
|
137
|
+
if process.stdout:
|
|
138
|
+
value = process.stdout
|
|
139
|
+
console.log(f"{value.decode() if isinstance(value, bytes) else value}")
|
|
140
|
+
if process.stderr:
|
|
141
|
+
value = process.stderr
|
|
142
|
+
console.log(f"{value.decode() if isinstance(value, bytes) else value}")
|
|
143
|
+
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
console.info("Docker image successfully updated on server")
|
|
147
|
+
|
|
148
|
+
async def compile_rules(self, rules_dir: Path, compiled_rules_dir: Path | None) -> bytes | None:
|
|
149
|
+
"""
|
|
150
|
+
Compile rules on remote server
|
|
151
|
+
:params rules_dir path with rules
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
rules_dir = rules_dir.expanduser().resolve()
|
|
155
|
+
if not rules_dir.is_dir():
|
|
156
|
+
console.error("Invalid rules directory: {rules_dir}")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
|
|
159
|
+
# always take new version of taxonomy
|
|
160
|
+
shutil.copytree(rules_dir.parent / "taxonomy", rules_dir / "taxonomy", dirs_exist_ok=True)
|
|
161
|
+
|
|
162
|
+
compiled_rules: bytes | None = None
|
|
163
|
+
try:
|
|
164
|
+
# prepare folder
|
|
165
|
+
await self._create_tmp_directory()
|
|
166
|
+
|
|
167
|
+
# upload rules for compilation
|
|
168
|
+
await self._upload_rules(rules_dir)
|
|
169
|
+
|
|
170
|
+
await self._compile_rules_on_server()
|
|
171
|
+
compiled_rules = await self._download_compiled_rules()
|
|
172
|
+
finally:
|
|
173
|
+
# cleanup
|
|
174
|
+
await self._cleanup(rules_dir)
|
|
175
|
+
|
|
176
|
+
return compiled_rules
|
|
177
|
+
|
|
178
|
+
async def test_rules(self, root_rules_dir: Path, container_rules_dir: Path) -> bool:
|
|
179
|
+
# always take new version of taxonomy
|
|
180
|
+
shutil.copytree(root_rules_dir.parent / "taxonomy", root_rules_dir / "taxonomy", dirs_exist_ok=True)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# prepare folder
|
|
184
|
+
await self._create_tmp_directory()
|
|
185
|
+
|
|
186
|
+
# upload rules for compilation
|
|
187
|
+
await self._upload_rules(root_rules_dir)
|
|
188
|
+
|
|
189
|
+
await self._compile_rules_on_server()
|
|
190
|
+
|
|
191
|
+
# generate random container name for avoid collision with several peoples
|
|
192
|
+
process = await self._run_stream_command(
|
|
193
|
+
f'ctr run --rm --memory-limit=1000000000 \
|
|
194
|
+
--mount type=bind,src={self.tmp_directory / "compiled-rules"},dst=/compiled-rules,options=rbind:rw \
|
|
195
|
+
--mount type=bind,src={self.tmp_directory / "rules" / container_rules_dir},dst=/rules,options=rbind:rw \
|
|
196
|
+
"{settings.docker.path}:{settings.docker.image_tag}" {self._generate_random_string()} package-builder correlation:test -r /rules -c /compiled-rules',
|
|
197
|
+
is_sudo=True,
|
|
198
|
+
)
|
|
199
|
+
finally:
|
|
200
|
+
# cleanup
|
|
201
|
+
await self._cleanup(root_rules_dir)
|
|
202
|
+
|
|
203
|
+
return process.exit_status == 0
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Coroutine
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import aiofiles
|
|
9
|
+
import zstandard
|
|
10
|
+
from ptsandbox import Sandbox
|
|
11
|
+
from ptsandbox.models import (
|
|
12
|
+
ArtifactType,
|
|
13
|
+
LogType,
|
|
14
|
+
SandboxBaseTaskResponse,
|
|
15
|
+
SandboxFileNotFoundException,
|
|
16
|
+
)
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
from rich.progress import Progress, TaskID
|
|
19
|
+
|
|
20
|
+
from sandbox_cli.console import console
|
|
21
|
+
|
|
22
|
+
semaphore = asyncio.Semaphore(value=12)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def _save_artifact(
|
|
26
|
+
scan_id: UUID,
|
|
27
|
+
sandbox: Sandbox,
|
|
28
|
+
path: Path,
|
|
29
|
+
file_uri: str,
|
|
30
|
+
decompress: bool = False,
|
|
31
|
+
progress: Progress | None = None,
|
|
32
|
+
idx: str | None = None,
|
|
33
|
+
image: str | None = None,
|
|
34
|
+
link: str | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
_, uri = file_uri.split(":")
|
|
37
|
+
if not uri:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# sanitize path
|
|
41
|
+
path = Path(str(path).replace(" ", "_"))
|
|
42
|
+
task_id: TaskID = None # type: ignore
|
|
43
|
+
|
|
44
|
+
async with semaphore:
|
|
45
|
+
if progress:
|
|
46
|
+
if idx and image and link:
|
|
47
|
+
task_id = progress.add_task(
|
|
48
|
+
description=f"Download [green]{escape(path.name)}[/]",
|
|
49
|
+
idx=idx,
|
|
50
|
+
image=image,
|
|
51
|
+
url=link,
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
task_id = progress.add_task(rf"\[[green1]{scan_id}[/]] {escape(path.name)}")
|
|
55
|
+
|
|
56
|
+
path.parent.mkdir(exist_ok=True, parents=True)
|
|
57
|
+
try:
|
|
58
|
+
downloaded_data = await sandbox.get_file(uri)
|
|
59
|
+
except SandboxFileNotFoundException:
|
|
60
|
+
console.warning(f"File {path.name} not found in storage: {uri=} {scan_id=}")
|
|
61
|
+
if progress:
|
|
62
|
+
progress.stop_task(task_id)
|
|
63
|
+
progress.update(task_id=task_id, visible=False)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if decompress:
|
|
67
|
+
try:
|
|
68
|
+
dctx = zstandard.ZstdDecompressor()
|
|
69
|
+
with open(path, "wb") as output:
|
|
70
|
+
input_fd = BytesIO(downloaded_data)
|
|
71
|
+
dctx.copy_stream(input_fd, output)
|
|
72
|
+
except zstandard.ZstdError as e:
|
|
73
|
+
console.warning(f"Can't decompress [yellow]{path.name}[/]. {e}")
|
|
74
|
+
else:
|
|
75
|
+
async with aiofiles.open(path, "wb") as fd:
|
|
76
|
+
await fd.write(downloaded_data)
|
|
77
|
+
|
|
78
|
+
if progress:
|
|
79
|
+
progress.stop_task(task_id)
|
|
80
|
+
progress.update(task_id=task_id, visible=False)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def download(
|
|
84
|
+
report: SandboxBaseTaskResponse.LongReport,
|
|
85
|
+
sandbox: Sandbox,
|
|
86
|
+
out_dir: Path,
|
|
87
|
+
all: bool = False,
|
|
88
|
+
artifacts: bool = False,
|
|
89
|
+
crashdumps: bool = False,
|
|
90
|
+
debug: bool = False,
|
|
91
|
+
decompress: bool = False,
|
|
92
|
+
files: bool = False,
|
|
93
|
+
logs: bool = False,
|
|
94
|
+
procdumps: bool = False,
|
|
95
|
+
video: bool = False,
|
|
96
|
+
progress: Progress | None = None,
|
|
97
|
+
idx: str | None = None,
|
|
98
|
+
image: str | None = None,
|
|
99
|
+
link: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
tasks: list[Coroutine[Any, Any, None]] = []
|
|
102
|
+
|
|
103
|
+
def add_task(path: Path, file_uri: str, decompress: bool = False) -> None:
|
|
104
|
+
tasks.append(
|
|
105
|
+
_save_artifact(
|
|
106
|
+
scan_id=report.scan_id,
|
|
107
|
+
sandbox=sandbox,
|
|
108
|
+
path=path,
|
|
109
|
+
file_uri=file_uri,
|
|
110
|
+
progress=progress,
|
|
111
|
+
idx=idx,
|
|
112
|
+
image=image,
|
|
113
|
+
link=link,
|
|
114
|
+
decompress=decompress,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for artifact in report.artifacts:
|
|
119
|
+
sandbox_result = artifact.find_sandbox_result()
|
|
120
|
+
if sandbox_result is None:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if sandbox_result.details is None:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
if sandbox_result.details.sandbox is None:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
for log in sandbox_result.details.sandbox.logs:
|
|
130
|
+
# download default logs by default
|
|
131
|
+
if (all or logs) and log.type in {
|
|
132
|
+
LogType.EVENT_CORRELATED,
|
|
133
|
+
LogType.EVENT_NORMALIZED,
|
|
134
|
+
LogType.EVENT_RAW,
|
|
135
|
+
LogType.NETWORK,
|
|
136
|
+
}:
|
|
137
|
+
add_task(out_dir / log.file_name, log.file_uri)
|
|
138
|
+
|
|
139
|
+
if (all or video) and log.type == LogType.SCREENSHOT:
|
|
140
|
+
add_task(out_dir / log.file_name, log.file_uri)
|
|
141
|
+
|
|
142
|
+
if (all or crashdumps) and log.file_name in {"crashdump.bin", "crashdump.metadata"}:
|
|
143
|
+
add_task(out_dir / "crashdumps" / log.file_name, log.file_uri)
|
|
144
|
+
|
|
145
|
+
if (all or debug) and log.type in {LogType.DEBUG, LogType.GRAPH}:
|
|
146
|
+
add_task(out_dir / "debug" / log.file_name, log.file_uri)
|
|
147
|
+
|
|
148
|
+
if artifacts or files or procdumps or all:
|
|
149
|
+
if not sandbox_result.details:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if not sandbox_result.details.sandbox:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if not sandbox_result.details.sandbox.artifacts:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
for artifact in sandbox_result.details.sandbox.artifacts:
|
|
159
|
+
if not artifact.file_info:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if artifact.type == ArtifactType.FILE and (files or artifacts or all):
|
|
163
|
+
add_task(
|
|
164
|
+
out_dir / "artifacts" / artifact.file_info.file_path.removeprefix("/"),
|
|
165
|
+
artifact.file_info.file_uri,
|
|
166
|
+
)
|
|
167
|
+
if artifact.type == ArtifactType.PROCESS_DUMP and (procdumps or artifacts or all):
|
|
168
|
+
add_task(
|
|
169
|
+
out_dir
|
|
170
|
+
/ "process_dump"
|
|
171
|
+
/ artifact.file_info.details.process_dump.process_name.removeprefix("/"), # type: ignore
|
|
172
|
+
artifact.file_info.file_uri,
|
|
173
|
+
decompress=decompress,
|
|
174
|
+
)
|
|
175
|
+
if not tasks:
|
|
176
|
+
console.info(f"Nothing to download from {report.scan_id}")
|
|
177
|
+
|
|
178
|
+
await asyncio.gather(*tasks)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import orjson
|
|
2
|
+
from ptsandbox.models import ArtifactType, EngineSubsystem, SandboxBaseTaskResponse
|
|
3
|
+
|
|
4
|
+
from sandbox_cli.models.detections import Detections
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_verdict_from_trace(trace: bytes) -> set[str]:
|
|
8
|
+
non_generics: set[str] = set()
|
|
9
|
+
generics: set[str] = set()
|
|
10
|
+
|
|
11
|
+
d = Detections(trace)
|
|
12
|
+
for detect in d.malware:
|
|
13
|
+
if detect.name.split(".")[-2] == "Generic":
|
|
14
|
+
generics.add(detect.name)
|
|
15
|
+
else:
|
|
16
|
+
non_generics.add(detect.name)
|
|
17
|
+
|
|
18
|
+
return non_generics if len(non_generics) != 0 else generics
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extract_network_from_trace(trace: bytes) -> set[str]:
|
|
22
|
+
detects: set[str] = set()
|
|
23
|
+
|
|
24
|
+
for line in trace.decode().splitlines(keepends=False):
|
|
25
|
+
event = orjson.loads(line)
|
|
26
|
+
if event.get("event.name") == "Auxiliary.ObtainNetworkAlert":
|
|
27
|
+
detects.add(event.get("s_msg"))
|
|
28
|
+
|
|
29
|
+
return detects
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_static(report: SandboxBaseTaskResponse.LongReport) -> list[str]:
|
|
33
|
+
sandbox_result = report.artifacts[0].find_sandbox_result()
|
|
34
|
+
|
|
35
|
+
ret: set[str] = set()
|
|
36
|
+
|
|
37
|
+
if sandbox_result:
|
|
38
|
+
for artifact in report.artifacts:
|
|
39
|
+
if artifact.type == ArtifactType.PROCESS_DUMP:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if artifact.engine_results is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
for artifact_result in artifact.engine_results:
|
|
46
|
+
if artifact_result.engine_subsystem not in {EngineSubsystem.STATIC, EngineSubsystem.AV}:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
ret |= {f"{artifact_result.engine_code_name}: {x.detect}" for x in artifact_result.detections}
|
|
50
|
+
|
|
51
|
+
return sorted(ret)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def extract_memory(report: SandboxBaseTaskResponse.LongReport) -> set[str]:
|
|
55
|
+
ret: set[str] = set()
|
|
56
|
+
|
|
57
|
+
sandbox_result = report.artifacts[0].find_sandbox_result()
|
|
58
|
+
|
|
59
|
+
if sandbox_result:
|
|
60
|
+
for artifact in sandbox_result.details.sandbox.artifacts: # type: ignore
|
|
61
|
+
if artifact.type == ArtifactType.PROCESS_DUMP:
|
|
62
|
+
artifact_result = artifact.find_static_result()
|
|
63
|
+
if artifact_result:
|
|
64
|
+
ret |= {x.detect for x in artifact_result.detections}
|
|
65
|
+
|
|
66
|
+
return ret
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from pathlib import Path, PurePath
|
|
2
|
+
|
|
3
|
+
slow_dll_methods = {
|
|
4
|
+
"RtlAllocateHeap",
|
|
5
|
+
"GetModuleFileNameA",
|
|
6
|
+
"GetModuleFileNameW",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __validate_hook(hook: str) -> None:
|
|
11
|
+
args = hook.split(",")
|
|
12
|
+
|
|
13
|
+
if args[0] in ("System32", "SysWOW64"):
|
|
14
|
+
args = args[1:]
|
|
15
|
+
|
|
16
|
+
if len(args) < 1 or not args[0]:
|
|
17
|
+
raise RuntimeError("Empty function name")
|
|
18
|
+
|
|
19
|
+
func_name = args[0]
|
|
20
|
+
if func_name in slow_dll_methods:
|
|
21
|
+
raise RuntimeError(f'Function "{func_name}" considered slow')
|
|
22
|
+
|
|
23
|
+
if len(args) < 2 or not args[1]:
|
|
24
|
+
raise RuntimeError("Empty strategy")
|
|
25
|
+
|
|
26
|
+
strategy: str
|
|
27
|
+
function_args: list[str]
|
|
28
|
+
|
|
29
|
+
if args[1] == "no-retval":
|
|
30
|
+
strategy = args[2]
|
|
31
|
+
function_args = args[3:]
|
|
32
|
+
else:
|
|
33
|
+
strategy = args[1]
|
|
34
|
+
function_args = args[2:]
|
|
35
|
+
|
|
36
|
+
if strategy not in ("log", "log+stack"):
|
|
37
|
+
raise RuntimeError("Bad strategy")
|
|
38
|
+
|
|
39
|
+
for arg in function_args:
|
|
40
|
+
if not arg:
|
|
41
|
+
raise RuntimeError(f"Extra comma in function argument list of {func_name}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __merge_dll_hooks(
|
|
45
|
+
dll_hooks_file: Path,
|
|
46
|
+
name: PurePath,
|
|
47
|
+
dll_hooks_cache: set[str],
|
|
48
|
+
) -> bytes:
|
|
49
|
+
file_hooks = b""
|
|
50
|
+
with dll_hooks_file.open() as file:
|
|
51
|
+
for hook in file:
|
|
52
|
+
hook = hook.strip()
|
|
53
|
+
if hook == "" or hook.startswith("#"):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
__validate_hook(hook)
|
|
57
|
+
prefix = ""
|
|
58
|
+
if hook.startswith("SysWOW64"):
|
|
59
|
+
prefix = "SysWOW64\\"
|
|
60
|
+
elif hook.startswith("System32"):
|
|
61
|
+
prefix = "System32\\"
|
|
62
|
+
|
|
63
|
+
hook = f"{prefix}{name},{hook}"
|
|
64
|
+
|
|
65
|
+
if hook in dll_hooks_cache:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
file_hooks += f"{hook}\n".encode()
|
|
69
|
+
dll_hooks_cache.add(hook)
|
|
70
|
+
return file_hooks
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def merge_dll_hooks(dll_hooks_dir: Path) -> bytes:
|
|
74
|
+
dll_hooks_cache: set[str] = set()
|
|
75
|
+
|
|
76
|
+
merged_hooks = b""
|
|
77
|
+
for hooks_file in dll_hooks_dir.rglob("*.txt"):
|
|
78
|
+
name = PurePath(hooks_file.relative_to(dll_hooks_dir)).with_suffix(".dll")
|
|
79
|
+
merged_hooks += __merge_dll_hooks(hooks_file, name, dll_hooks_cache)
|
|
80
|
+
|
|
81
|
+
if not merged_hooks:
|
|
82
|
+
raise RuntimeError("No dll hooks has been found")
|
|
83
|
+
|
|
84
|
+
return merged_hooks
|