maco-extractor 1.2.18__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.
- maco/__init__.py +0 -0
- maco/exceptions.py +33 -0
- maco/extractor.py +70 -0
- maco/model/__init__.py +1 -0
- maco/model/model.py +606 -0
- maco/yara.py +129 -0
- maco_extractor-1.2.18.dist-info/METADATA +283 -0
- maco_extractor-1.2.18.dist-info/RECORD +11 -0
- maco_extractor-1.2.18.dist-info/WHEEL +5 -0
- maco_extractor-1.2.18.dist-info/licenses/LICENSE.md +11 -0
- maco_extractor-1.2.18.dist-info/top_level.txt +1 -0
maco/__init__.py
ADDED
|
File without changes
|
maco/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Exception classes for extractors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Can be raised by extractors to abort analysis of a sample
|
|
5
|
+
# ie. Can abort if preliminary checks at start of run indicate the file shouldn't be analyzed by extractor
|
|
6
|
+
class AnalysisAbortedException(Exception):
|
|
7
|
+
"""Raised when extractors voluntarily abort analysis of a sample."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtractorLoadError(Exception):
|
|
13
|
+
"""Raised when extractors cannot be loaded."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidExtractor(ValueError):
|
|
19
|
+
"""Raised when an extractor is invalid."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NoHitException(Exception):
|
|
25
|
+
"""Raised when the YARA rule of an extractor doesn't hit."""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SyntaxError(Exception):
|
|
31
|
+
"""Raised when there's a syntax error in the YARA rule."""
|
|
32
|
+
|
|
33
|
+
pass
|
maco/extractor.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Base class for an extractor script."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import textwrap
|
|
5
|
+
from typing import BinaryIO, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from maco import model, yara
|
|
8
|
+
from maco.exceptions import InvalidExtractor
|
|
9
|
+
|
|
10
|
+
DEFAULT_YARA_RULE = """
|
|
11
|
+
rule {name}
|
|
12
|
+
{{
|
|
13
|
+
condition:
|
|
14
|
+
true
|
|
15
|
+
}}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Extractor:
|
|
20
|
+
"""Base class for an analysis extractor with common entrypoint and metadata.
|
|
21
|
+
|
|
22
|
+
Override this docstring with a good description of your extractor.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
family: Union[str, List[str]] = None # family or families of malware that is detected by the extractor
|
|
26
|
+
author: str = None # author of the extractor (name@organisation)
|
|
27
|
+
last_modified: str = None # last modified date (YYYY-MM-DD)
|
|
28
|
+
sharing: str = "TLP:WHITE" # who can this be shared with?
|
|
29
|
+
yara_rule: str = None # yara rule that we filter inputs with
|
|
30
|
+
reference: str = None # link to malware report or other reference information
|
|
31
|
+
logger: logging.Logger = None # logger for use when debugging
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""Initialise the extractor.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
InvalidExtractor: When the extractor is invalid.
|
|
38
|
+
"""
|
|
39
|
+
self.name = name = type(self).__name__
|
|
40
|
+
self.logger = logging.getLogger(f"maco.extractor.{name}")
|
|
41
|
+
self.logger.debug(f"initialise '{name}'")
|
|
42
|
+
if not self.family or not self.author or not self.last_modified:
|
|
43
|
+
raise InvalidExtractor("must set family, author, last_modified")
|
|
44
|
+
# if author does not set a yara rule, match on everything
|
|
45
|
+
if not self.yara_rule:
|
|
46
|
+
self.yara_rule = DEFAULT_YARA_RULE.format(name=name)
|
|
47
|
+
# unindent the yara rule from triple quoted string
|
|
48
|
+
# this is for friendly printing, yara handles the rule ok either way
|
|
49
|
+
self.yara_rule = textwrap.dedent(self.yara_rule)
|
|
50
|
+
# check yara rules conform to expected structure
|
|
51
|
+
# we throw away these compiled rules as we need all rules in system compiled together
|
|
52
|
+
try:
|
|
53
|
+
self.yara_compiled = yara.compile(source=self.yara_rule)
|
|
54
|
+
except yara.SyntaxError as e:
|
|
55
|
+
raise InvalidExtractor(f"{self.name} - invalid yara rule") from e
|
|
56
|
+
# need to track which plugin owns the rules
|
|
57
|
+
self.yara_rule_names = [x.identifier for x in self.yara_compiled]
|
|
58
|
+
if not len(list(self.yara_compiled)):
|
|
59
|
+
raise InvalidExtractor(f"{name} must define at least one yara rule")
|
|
60
|
+
for x in self.yara_compiled:
|
|
61
|
+
if x.is_global:
|
|
62
|
+
raise InvalidExtractor(f"{x.identifier} yara rule must not be global")
|
|
63
|
+
|
|
64
|
+
def run(self, stream: BinaryIO, matches: List[yara.Match]) -> Optional[model.ExtractorModel]:
|
|
65
|
+
"""Run the analysis process and return dict matching.
|
|
66
|
+
|
|
67
|
+
:param stream: file object from disk/network/memory.
|
|
68
|
+
:param match: yara rule match information contains locations of strings.
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError()
|
maco/model/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from maco.model.model import * # noqa: F403
|
maco/model/model.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"""Malware config extractor output model."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ForbidModel(BaseModel):
|
|
10
|
+
"""We want to forbid extra properties, so that the 'other' field is used instead."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="forbid", use_enum_values=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConnUsageEnum(str, Enum):
|
|
16
|
+
"""Purpose of the connection."""
|
|
17
|
+
|
|
18
|
+
c2 = "c2" # issue commands to malware
|
|
19
|
+
upload = "upload" # get data out of the network
|
|
20
|
+
download = "download" # fetch dynamic config, second stage, etc
|
|
21
|
+
propagate = "propagate" # spread through the network
|
|
22
|
+
tunnel = "tunnel" # communicate through the network
|
|
23
|
+
ransom = "ransom" # payment
|
|
24
|
+
decoy = "decoy" # Decoy connections to obfuscate malicious
|
|
25
|
+
other = "other"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Encryption(ForbidModel):
|
|
29
|
+
"""Encryption usage."""
|
|
30
|
+
|
|
31
|
+
class UsageEnum(str, Enum):
|
|
32
|
+
"""Purpose of the encryption."""
|
|
33
|
+
|
|
34
|
+
config = "config"
|
|
35
|
+
communication = "communication"
|
|
36
|
+
binary = "binary"
|
|
37
|
+
ransom = "ransom"
|
|
38
|
+
other = "other"
|
|
39
|
+
|
|
40
|
+
algorithm: Optional[str] = None
|
|
41
|
+
public_key: Optional[str] = None
|
|
42
|
+
key: Optional[str] = None # private key or symmetric key
|
|
43
|
+
provider: Optional[str] = None # encryption library used. openssl, homebrew, etc.
|
|
44
|
+
|
|
45
|
+
mode: Optional[str] = None # block vs stream
|
|
46
|
+
# base 64'd binary data for these details?
|
|
47
|
+
# TODO to confirm usage of these different properties
|
|
48
|
+
iv: Optional[str] = None # initialisation vector
|
|
49
|
+
seed: Optional[str] = None
|
|
50
|
+
nonce: Optional[str] = None
|
|
51
|
+
constants: List[str] = []
|
|
52
|
+
|
|
53
|
+
usage: Optional[UsageEnum] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CategoryEnum(str, Enum):
|
|
57
|
+
"""Category of the malware."""
|
|
58
|
+
|
|
59
|
+
# Software that shows you extra promotions that you cannot control as you use your PC.
|
|
60
|
+
# You wouldn't see the extra ads if you didn't have adware installed.
|
|
61
|
+
adware = "adware"
|
|
62
|
+
|
|
63
|
+
# Malware related to an Advanced Persistent Threat (APT) group.
|
|
64
|
+
apt = "apt"
|
|
65
|
+
|
|
66
|
+
# A backdoor Trojan gives malicious users remote control over the infected computer.
|
|
67
|
+
# They enable the author to do anything they wish on the infected computer including
|
|
68
|
+
# sending, receiving, launching and deleting files, displaying data and rebooting the computer.
|
|
69
|
+
# Backdoor Trojans are often used to unite a group of victim computers to form a botnet or
|
|
70
|
+
# zombie network that can be used for criminal purposes.
|
|
71
|
+
backdoor = "backdoor"
|
|
72
|
+
|
|
73
|
+
# Trojan Banker programs are designed to steal your account data for online banking systems,
|
|
74
|
+
# e-payment systems and credit or debit cards.
|
|
75
|
+
banker = "banker"
|
|
76
|
+
|
|
77
|
+
# A malware variant that modifies the boot sectors of a hard drive, including the Master Boot Record (MBR)
|
|
78
|
+
# and Volume Boot Record (VBR).
|
|
79
|
+
bootkit = "bootkit"
|
|
80
|
+
|
|
81
|
+
# A malicious bot is self-propagating malware designed to infect a host and connect back to a central server
|
|
82
|
+
# or servers that act as a command and control (C&C) center for an entire network of compromised devices,
|
|
83
|
+
# or botnet.
|
|
84
|
+
bot = "bot"
|
|
85
|
+
|
|
86
|
+
# A browser hijacker is defined as a form of unwanted software that modifies a web browser's settings without
|
|
87
|
+
# the user's permission. The result is the placement of unwanted advertising into the browser,
|
|
88
|
+
# and possibly the replacement of an existing home page or search page with the hijacker page.
|
|
89
|
+
browser_hijacker = "browser_hijacker"
|
|
90
|
+
|
|
91
|
+
# Trojan bruteforcer are trying to brute force website in order to achieve something else
|
|
92
|
+
# (EX: Finding WordPress websites with default credentials).
|
|
93
|
+
bruteforcer = "bruteforcer"
|
|
94
|
+
|
|
95
|
+
# A type of trojan that can use your PC to 'click' on websites or applications.
|
|
96
|
+
# They are usually used to make money for a malicious hacker by clicking on online advertisements
|
|
97
|
+
# and making it look like the website gets more traffic than it does.
|
|
98
|
+
# They can also be used to skew online polls, install programs on your PC, or make unwanted software
|
|
99
|
+
# appear more popular than it is.
|
|
100
|
+
clickfraud = "clickfraud"
|
|
101
|
+
|
|
102
|
+
# Cryptocurrency mining malware.
|
|
103
|
+
cryptominer = "cryptominer"
|
|
104
|
+
|
|
105
|
+
# These programs conduct DoS (Denial of Service) attacks against a targeted web address.
|
|
106
|
+
# By sending multiple requests from your computer and several other infected computers,
|
|
107
|
+
# the attack can overwhelm the target address leading to a denial of service.
|
|
108
|
+
ddos = "ddos"
|
|
109
|
+
|
|
110
|
+
# Trojan Downloaders can download and install new versions of malicious programs in the target system.
|
|
111
|
+
downloader = "downloader"
|
|
112
|
+
|
|
113
|
+
# These programs are used by hackers in order to install malware or to prevent the detection of malicious programs.
|
|
114
|
+
dropper = "dropper"
|
|
115
|
+
|
|
116
|
+
# Exploit kits are programs that contain data or code that takes advantage of a vulnerability
|
|
117
|
+
# within an application that is running in the target system.
|
|
118
|
+
exploitkit = "exploitkit"
|
|
119
|
+
|
|
120
|
+
# Trojan FakeAV programs simulate the activity of antivirus software.
|
|
121
|
+
# They are designed to extort money in return for the detection and removal of threat, even though the
|
|
122
|
+
# threats that they report are actually non-existent.
|
|
123
|
+
fakeav = "fakeav"
|
|
124
|
+
|
|
125
|
+
# A type of tool that can be used to allow and maintain unauthorized access to your PC.
|
|
126
|
+
hacktool = "hacktool"
|
|
127
|
+
|
|
128
|
+
# A program that collects your personal information, such as your browsing history,
|
|
129
|
+
# and uses it without adequate consent.
|
|
130
|
+
infostealer = "infostealer"
|
|
131
|
+
|
|
132
|
+
# A keylogger monitors and logs every keystroke it can identify.
|
|
133
|
+
# Once installed, the virus either keeps track of all the keys and stores the information locally,
|
|
134
|
+
# after which the hacker needs physical access to the computer to retrieve the information,
|
|
135
|
+
# or the logs are sent over the internet back to the hacker.
|
|
136
|
+
keylogger = "keylogger"
|
|
137
|
+
|
|
138
|
+
# A program that loads another application / memory space.
|
|
139
|
+
loader = "loader"
|
|
140
|
+
|
|
141
|
+
# A type of malware that hides its code and purpose to make it more difficult for
|
|
142
|
+
# security software to detect or remove it.
|
|
143
|
+
obfuscator = "obfuscator"
|
|
144
|
+
|
|
145
|
+
# Point-of-sale malware is usually a type of malware that is used by cybercriminals to target point of sale (POS)
|
|
146
|
+
# and payment terminals with the intent to obtain credit card and debit card information.
|
|
147
|
+
pos = "pos"
|
|
148
|
+
|
|
149
|
+
# This type of trojan allows unauthorized parties to use the infected computer as a proxy server
|
|
150
|
+
# to access the Internet anonymously.
|
|
151
|
+
proxy = "proxy"
|
|
152
|
+
|
|
153
|
+
# A program that can be used by a remote hacker to gain access and control of an infected machine.
|
|
154
|
+
rat = "rat"
|
|
155
|
+
|
|
156
|
+
# This type of malware can modify data in the target computer so the operating system
|
|
157
|
+
# will stop running correctly or the data is no longer accessible.
|
|
158
|
+
# The criminal will only restore the computer state or data after a ransom is paid to them
|
|
159
|
+
# (mostly using cryptocurrency).
|
|
160
|
+
ransomware = "ransomware"
|
|
161
|
+
|
|
162
|
+
# A reverse proxy is a server that receives requests from the internet and forwards them to a small set of servers.
|
|
163
|
+
reverse_proxy = "reverse_proxy"
|
|
164
|
+
|
|
165
|
+
# Rootkits are designed to conceal certain objects or activities in the system.
|
|
166
|
+
# Often their main purpose is to prevent malicious programs being detected
|
|
167
|
+
# in order to extend the period in which programs can run on an infected computer.
|
|
168
|
+
rootkit = "rootkit"
|
|
169
|
+
|
|
170
|
+
# This type of malware scan the internet / network(s) / system(s) / service(s) to collect information.
|
|
171
|
+
# That information could be used later to perpetuate an cyber attack.
|
|
172
|
+
scanner = "scanner"
|
|
173
|
+
|
|
174
|
+
# Scareware is a form of malware which uses social engineering to cause shock, anxiety,
|
|
175
|
+
# or the perception of a threat in order to manipulate users into buying unwanted software.
|
|
176
|
+
scareware = "scareware"
|
|
177
|
+
|
|
178
|
+
# Malware that is sending spam.
|
|
179
|
+
spammer = "spammer"
|
|
180
|
+
|
|
181
|
+
# Generic or Unknown Trojan
|
|
182
|
+
trojan = "trojan"
|
|
183
|
+
|
|
184
|
+
# A generic computer virus
|
|
185
|
+
virus = "virus"
|
|
186
|
+
|
|
187
|
+
# A type of malware that destroy the data.
|
|
188
|
+
wiper = "wiper"
|
|
189
|
+
|
|
190
|
+
# A web shell is a script that can be uploaded to a web server to enable remote administration of the machine.
|
|
191
|
+
webshell = "webshell"
|
|
192
|
+
|
|
193
|
+
# A type of malware that spreads to other PCs.
|
|
194
|
+
worm = "worm"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ExtractorModel(ForbidModel):
|
|
198
|
+
r"""Captured config/iocs, unpacked binaries and other malware properties from a robo-analyst.
|
|
199
|
+
|
|
200
|
+
This model defines common fields for output of a script targeting a specific malware family.
|
|
201
|
+
Usage of this model will allow for easier sharing of scripts between different authors and systems.
|
|
202
|
+
The model will not define fields for all data that can be extracted from a binary, only the most common.
|
|
203
|
+
This is to make it easier for authors to understand and use the model.
|
|
204
|
+
|
|
205
|
+
This model can have new fields added in the future if they become more common,
|
|
206
|
+
but the intent is to avoid removing or modifying existing fields, for backwards compatibility.
|
|
207
|
+
|
|
208
|
+
Where data does not fit with the current model, the 'others' field should be used.
|
|
209
|
+
Contents in this field is not defined by the model and verification/normalisation is up to
|
|
210
|
+
the author and whatever systems run the scripts.
|
|
211
|
+
If many decoders define similar data in the 'others' field, that field should be migrated to this model.
|
|
212
|
+
|
|
213
|
+
The model must be kept relatively flat, with nested lists of dictionaries to be avoided.
|
|
214
|
+
This is to make queries simpler to write in sql, elasticsearch and other storage systems.
|
|
215
|
+
|
|
216
|
+
Malware and systems that investigate malware can do pretty much anything.
|
|
217
|
+
This model needs to be simple and flexible to make sharing easy.
|
|
218
|
+
Some things should be out of scope for this model.
|
|
219
|
+
Responsibility for these things are up to authors and systems that use this model.
|
|
220
|
+
|
|
221
|
+
Out of scope
|
|
222
|
+
* Verifying anything in the 'others' dict, including that it is json-compatible.
|
|
223
|
+
* We don't know anything about the structure
|
|
224
|
+
* checking is json compatible requires dumping to json string, which can be slow
|
|
225
|
+
* Connecting specific config items to malware behaviour catalog
|
|
226
|
+
* i.e. "Persistence::Modify Registry" with 'registry' item from model (SYSTEM\ControlSet001\Services\)
|
|
227
|
+
* due to complexity and normalisation difficulties
|
|
228
|
+
* much malware behaviour is not related to specific config items
|
|
229
|
+
* Normalisation/verification of individual properties
|
|
230
|
+
* i.e. lowercase filepaths - some filesystems are case sensitive
|
|
231
|
+
* i.e. checking registry hives match known - not enough SME and too complex for a simple model
|
|
232
|
+
* generally, this quickly becomes complex (validating a fully defined http item)
|
|
233
|
+
* calling systems are probably performing their own validation anyway
|
|
234
|
+
* requiring specific properties to be set
|
|
235
|
+
* i.e. if http item is defined, requiring hostname to be set
|
|
236
|
+
* Some use cases always seem to exist where a property should not be set
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
family: Union[str, List[str]] # family or families of malware that was detected
|
|
240
|
+
version: Optional[str] = None # version/variant of malware
|
|
241
|
+
category: List[CategoryEnum] = [] # capability/purpose of the malware
|
|
242
|
+
attack: List[str] = [] # mitre att&ck reference ids, e.g. 'T1129'
|
|
243
|
+
|
|
244
|
+
#
|
|
245
|
+
# simple config properties
|
|
246
|
+
#
|
|
247
|
+
|
|
248
|
+
# capabilities of the malware enabled/disabled in config
|
|
249
|
+
# note these are probably malware-specific capabilities so no attempt to normalise has been made
|
|
250
|
+
# note - av/sandbox detection should be noted by 'detect_<product>'
|
|
251
|
+
capability_enabled: List[str] = []
|
|
252
|
+
capability_disabled: List[str] = []
|
|
253
|
+
|
|
254
|
+
campaign_id: List[str] = [] # Server/Campaign Id for malware
|
|
255
|
+
identifier: List[str] = [] # UUID/Identifiers for deployed instance
|
|
256
|
+
decoded_strings: List[str] = [] # decoded strings from within malware
|
|
257
|
+
password: List[str] = [] # Any password extracted from the binary
|
|
258
|
+
mutex: List[str] = [] # mutex to prevent multiple instances
|
|
259
|
+
pipe: List[str] = [] # pipe name used for communication
|
|
260
|
+
sleep_delay: Optional[int] = None # time to sleep/delay execution (milliseconds)
|
|
261
|
+
# additional time applied to sleep_delay (milliseconds).
|
|
262
|
+
# Jitter implementations can vary but usually it is a value from which a random number is generated and
|
|
263
|
+
# added/subtracted to the sleep_delay to make behaviour more unpredictable
|
|
264
|
+
sleep_delay_jitter: Optional[int] = None
|
|
265
|
+
inject_exe: List[str] = [] # name of executable to inject into
|
|
266
|
+
|
|
267
|
+
# configuration or clustering/research data that doesnt fit the other fields
|
|
268
|
+
# * rarely used by decoders or specific to one decoder
|
|
269
|
+
# to prevent key explosion, the keys must not be dynamically generated
|
|
270
|
+
# e.g. api_imports, api_checksums, num_imports, import_hash + many more
|
|
271
|
+
# data stored here must always be JSON-serialisable
|
|
272
|
+
other: Dict[str, Any] = {}
|
|
273
|
+
|
|
274
|
+
#
|
|
275
|
+
# embedded binary data
|
|
276
|
+
#
|
|
277
|
+
class Binary(ForbidModel):
|
|
278
|
+
"""Binary data extracted by decoder."""
|
|
279
|
+
|
|
280
|
+
class TypeEnum(str, Enum):
|
|
281
|
+
"""Type of binary data."""
|
|
282
|
+
|
|
283
|
+
payload = "payload" # contained within the original file
|
|
284
|
+
config = "config" # sometimes malware uses json/formatted text for config
|
|
285
|
+
other = "other"
|
|
286
|
+
|
|
287
|
+
datatype: Optional[TypeEnum] = None # what the binary data is used for
|
|
288
|
+
data: bytes # binary data, not json compatible
|
|
289
|
+
|
|
290
|
+
# other information for the extracted binary rather than the config
|
|
291
|
+
# data stored here must always be JSON-serialisable
|
|
292
|
+
# e.g. filename, extension, relationship label
|
|
293
|
+
other: Dict[str, Any] = {}
|
|
294
|
+
|
|
295
|
+
# convenience for ret.encryption.append(ret.Encryption(*properties))
|
|
296
|
+
# Define as class as only way to allow for this to be accessed and not have pydantic try to parse it.
|
|
297
|
+
class Encryption(Encryption):
|
|
298
|
+
"""Encryption usage."""
|
|
299
|
+
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
encryption: Union[List[Encryption], Encryption, None] = None # encryption information for the binary
|
|
303
|
+
|
|
304
|
+
binaries: List[Binary] = []
|
|
305
|
+
|
|
306
|
+
#
|
|
307
|
+
# communication protocols
|
|
308
|
+
#
|
|
309
|
+
class FTP(ForbidModel):
|
|
310
|
+
"""Usage of FTP connection."""
|
|
311
|
+
|
|
312
|
+
username: Optional[str] = None
|
|
313
|
+
password: Optional[str] = None
|
|
314
|
+
hostname: Optional[str] = None
|
|
315
|
+
port: Optional[int] = None
|
|
316
|
+
|
|
317
|
+
path: Optional[str] = None
|
|
318
|
+
|
|
319
|
+
usage: Optional[ConnUsageEnum] = None
|
|
320
|
+
|
|
321
|
+
ftp: List[FTP] = []
|
|
322
|
+
|
|
323
|
+
class SMTP(ForbidModel):
|
|
324
|
+
"""Usage of SMTP."""
|
|
325
|
+
|
|
326
|
+
# credentials and location of server
|
|
327
|
+
username: Optional[str] = None
|
|
328
|
+
password: Optional[str] = None
|
|
329
|
+
hostname: Optional[str] = None
|
|
330
|
+
port: Optional[int] = None
|
|
331
|
+
|
|
332
|
+
mail_to: List[str] = [] # receivers
|
|
333
|
+
mail_from: Optional[str] = None # sender
|
|
334
|
+
subject: Optional[str] = None
|
|
335
|
+
|
|
336
|
+
usage: Optional[ConnUsageEnum] = None
|
|
337
|
+
|
|
338
|
+
smtp: List[SMTP] = [] # SMTP server for malware
|
|
339
|
+
|
|
340
|
+
class Http(ForbidModel):
|
|
341
|
+
"""Usage of HTTP connection."""
|
|
342
|
+
|
|
343
|
+
# malware sometimes does weird stuff with uris so we don't want to force
|
|
344
|
+
# authors to break the uri into username, hostname, path, etc.
|
|
345
|
+
# as we lose that information.
|
|
346
|
+
# e.g. extra '?' or '/' when unnecessary.
|
|
347
|
+
# or something that is technically an invalid uri but still works
|
|
348
|
+
uri: Optional[str] = None
|
|
349
|
+
|
|
350
|
+
# on the other hand we might not have enough info to construct a uri
|
|
351
|
+
protocol: Optional[str] = None # http,https
|
|
352
|
+
username: Optional[str] = None
|
|
353
|
+
password: Optional[str] = None
|
|
354
|
+
hostname: Optional[str] = None # (A host/hostname can be an IP, domain or hostname)
|
|
355
|
+
port: Optional[int] = None
|
|
356
|
+
path: Optional[str] = None
|
|
357
|
+
query: Optional[str] = None
|
|
358
|
+
fragment: Optional[str] = None
|
|
359
|
+
|
|
360
|
+
user_agent: Optional[str] = None # user agent sent by malware
|
|
361
|
+
method: Optional[str] = None # get put delete etc
|
|
362
|
+
headers: Optional[Dict[str, str]] = None # custom/additional HTTP headers
|
|
363
|
+
max_size: Optional[int] = None
|
|
364
|
+
|
|
365
|
+
usage: Optional[ConnUsageEnum] = None
|
|
366
|
+
|
|
367
|
+
http: List[Http] = []
|
|
368
|
+
|
|
369
|
+
class SSH(ForbidModel):
|
|
370
|
+
"""Usage of ssh connection."""
|
|
371
|
+
|
|
372
|
+
username: Optional[str] = None
|
|
373
|
+
password: Optional[str] = None
|
|
374
|
+
hostname: Optional[str] = None
|
|
375
|
+
port: Optional[int] = None
|
|
376
|
+
|
|
377
|
+
usage: Optional[ConnUsageEnum] = None
|
|
378
|
+
|
|
379
|
+
ssh: List[SSH] = []
|
|
380
|
+
|
|
381
|
+
class Proxy(ForbidModel):
|
|
382
|
+
"""Usage of proxy connection."""
|
|
383
|
+
|
|
384
|
+
protocol: Optional[str] = None # socks5,http
|
|
385
|
+
username: Optional[str] = None
|
|
386
|
+
password: Optional[str] = None
|
|
387
|
+
hostname: Optional[str] = None
|
|
388
|
+
port: Optional[int] = None
|
|
389
|
+
|
|
390
|
+
usage: Optional[ConnUsageEnum] = None
|
|
391
|
+
|
|
392
|
+
proxy: List[Proxy] = []
|
|
393
|
+
|
|
394
|
+
class ICMP(ForbidModel):
|
|
395
|
+
"""Usage of ICMP."""
|
|
396
|
+
|
|
397
|
+
type: Optional[int] = None
|
|
398
|
+
code: Optional[int] = None
|
|
399
|
+
header: Optional[str] = None # Some malware uses non-standard header fields
|
|
400
|
+
hostname: Optional[str] = None
|
|
401
|
+
|
|
402
|
+
usage: Optional[ConnUsageEnum] = None
|
|
403
|
+
|
|
404
|
+
icmp: List[ICMP] = []
|
|
405
|
+
|
|
406
|
+
#
|
|
407
|
+
# inter process communication (IPC)
|
|
408
|
+
#
|
|
409
|
+
class IPC(ForbidModel):
|
|
410
|
+
"""Usage of named pipe communications."""
|
|
411
|
+
|
|
412
|
+
# A record stored on disk, or a record synthesized on demand by a file
|
|
413
|
+
# server, which can be accessed by multiple processes.
|
|
414
|
+
file: Optional[List[str]] = None
|
|
415
|
+
# Data sent over a network interface, either to a different process on
|
|
416
|
+
# the same computer or to another computer on the network. Stream
|
|
417
|
+
# oriented (TCP; data written through a socket requires formatting to
|
|
418
|
+
# preserve message boundaries) or more rarely message-oriented (UDP,
|
|
419
|
+
# SCTP).
|
|
420
|
+
socket: Optional[List[str]] = None
|
|
421
|
+
# Similar to an internet socket, but all communication occurs within
|
|
422
|
+
# the kernel. Domain sockets use the file system as their address
|
|
423
|
+
# space. Processes reference a domain socket as an inode, and multiple
|
|
424
|
+
# processes can communicate with one socket.
|
|
425
|
+
unix_domain_socket: Optional[List[str]] = None
|
|
426
|
+
# A file mapped to RAM and can be modified by changing memory
|
|
427
|
+
# addresses directly instead of outputting to a stream. This shares
|
|
428
|
+
# the same benefits as a standard file.
|
|
429
|
+
memory_mapped_file: Optional[Union[bytes, List[str]]] = None
|
|
430
|
+
# A data stream similar to a socket, but which usually preserves
|
|
431
|
+
# message boundaries. Typically implemented by the operating system,
|
|
432
|
+
# they allow multiple processes to read and write to the message queue
|
|
433
|
+
# without being directly connected to each other.
|
|
434
|
+
message_queue: Optional[List[str]] = None
|
|
435
|
+
# A unidirectional data channel using standard input and output. Data
|
|
436
|
+
# written to the write-end of the pipe is buffered by the operating
|
|
437
|
+
# system until it is read from the read-end of the pipe. Two-way
|
|
438
|
+
# communication between processes can be achieved by using two pipes
|
|
439
|
+
# in opposite "directions".
|
|
440
|
+
anonymous_pipe: Optional[List[str]] = None
|
|
441
|
+
# A pipe that is treated like a file. Instead of using standard input
|
|
442
|
+
# and output as with an anonymous pipe, processes write to and read
|
|
443
|
+
# from a named pipe, as if it were a regular file.
|
|
444
|
+
named_pipe: Optional[List[str]] = None
|
|
445
|
+
# The process names involved in the IPC communication
|
|
446
|
+
process_names: Optional[List[str]] = None
|
|
447
|
+
# Multiple processes are given access to the same block of memory,
|
|
448
|
+
# which creates a shared buffer for the processes to communicate with
|
|
449
|
+
# each other.
|
|
450
|
+
shared_memory: Optional[bytes] = None
|
|
451
|
+
usage: Optional[ConnUsageEnum] = None
|
|
452
|
+
|
|
453
|
+
ipc: List[IPC] = [] # Inter-Process Communications (similar to 'pipe' but more detailed)
|
|
454
|
+
|
|
455
|
+
class DNS(ForbidModel):
|
|
456
|
+
"""Direct usage of DNS."""
|
|
457
|
+
|
|
458
|
+
class RecordTypeEnum(str, Enum):
|
|
459
|
+
"""DNS record types."""
|
|
460
|
+
|
|
461
|
+
A = "A"
|
|
462
|
+
AAAA = "AAAA"
|
|
463
|
+
AFSDB = "AFSDB"
|
|
464
|
+
APL = "APL"
|
|
465
|
+
CAA = "CAA"
|
|
466
|
+
CDNSKEY = "CDNSKEY"
|
|
467
|
+
CDS = "CDS"
|
|
468
|
+
CERT = "CERT"
|
|
469
|
+
CNAME = "CNAME"
|
|
470
|
+
CSYNC = "CSYNC"
|
|
471
|
+
DHCID = "DHCID"
|
|
472
|
+
DLV = "DLV"
|
|
473
|
+
DNAME = "DNAME"
|
|
474
|
+
DNSKEY = "DNSKEY"
|
|
475
|
+
DS = "DS"
|
|
476
|
+
EUI48 = "EUI48"
|
|
477
|
+
EUI64 = "EUI64"
|
|
478
|
+
HINFO = "HINFO"
|
|
479
|
+
HIP = "HIP"
|
|
480
|
+
HTTPS = "HTTPS"
|
|
481
|
+
IPSECKEY = "IPSECKEY"
|
|
482
|
+
KEY = "KEY"
|
|
483
|
+
KX = "KX"
|
|
484
|
+
LOC = "LOC"
|
|
485
|
+
MX = "MX"
|
|
486
|
+
NAPTR = "NAPTR"
|
|
487
|
+
NS = "NS"
|
|
488
|
+
NSEC = "NSEC"
|
|
489
|
+
NSEC3 = "NSEC3"
|
|
490
|
+
NSEC3PARAM = "NSEC3PARAM"
|
|
491
|
+
OPENPGPKEY = "OPENPGPKEY"
|
|
492
|
+
PTR = "PTR"
|
|
493
|
+
RRSIG = "RRSIG"
|
|
494
|
+
RP = "RP"
|
|
495
|
+
SIG = "SIG"
|
|
496
|
+
SMIMEA = "SMIMEA"
|
|
497
|
+
SOA = "SOA"
|
|
498
|
+
SRV = "SRV"
|
|
499
|
+
SSHFP = "SSHFP"
|
|
500
|
+
SVCB = "SVCB"
|
|
501
|
+
TA = "TA"
|
|
502
|
+
TKEY = "TKEY"
|
|
503
|
+
TLSA = "TLSA"
|
|
504
|
+
TSIG = "TSIG"
|
|
505
|
+
TXT = "TXT"
|
|
506
|
+
URI = "URI"
|
|
507
|
+
ZONEMD = "ZONEMD"
|
|
508
|
+
|
|
509
|
+
ip: Optional[str] = None
|
|
510
|
+
port: Optional[int] = None # The default value is 53
|
|
511
|
+
hostname: Optional[str] = None # This is the query hostname
|
|
512
|
+
record_type: Optional[RecordTypeEnum] = None # The DNS record type that is queried
|
|
513
|
+
usage: Optional[ConnUsageEnum] = None
|
|
514
|
+
|
|
515
|
+
dns: List[DNS] = [] # custom DNS address to use for name resolution
|
|
516
|
+
|
|
517
|
+
class Connection(ForbidModel):
|
|
518
|
+
"""Generic TCP/UDP usage."""
|
|
519
|
+
|
|
520
|
+
client_ip: Optional[str] = None
|
|
521
|
+
client_port: Optional[int] = None
|
|
522
|
+
server_ip: Optional[str] = None
|
|
523
|
+
server_domain: Optional[str] = None
|
|
524
|
+
server_port: Optional[int] = None
|
|
525
|
+
|
|
526
|
+
usage: Optional[ConnUsageEnum] = None
|
|
527
|
+
|
|
528
|
+
tcp: List[Connection] = []
|
|
529
|
+
udp: List[Connection] = []
|
|
530
|
+
|
|
531
|
+
#
|
|
532
|
+
# complex configuration properties
|
|
533
|
+
#
|
|
534
|
+
# convenience for ret.encryption.append(ret.Encryption(*properties))
|
|
535
|
+
# Define as class as only way to allow for this to be accessed and not have pydantic try to parse it.
|
|
536
|
+
class Encryption(Encryption):
|
|
537
|
+
"""Encryption usage."""
|
|
538
|
+
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
encryption: List[Encryption] = []
|
|
542
|
+
|
|
543
|
+
class Service(ForbidModel):
|
|
544
|
+
"""OS service usage by malware."""
|
|
545
|
+
|
|
546
|
+
dll: Optional[str] = None # dll that the service is loaded from
|
|
547
|
+
name: Optional[str] = None # service/driver name for persistence
|
|
548
|
+
display_name: Optional[str] = None # display name for service
|
|
549
|
+
description: Optional[str] = None # description for service
|
|
550
|
+
|
|
551
|
+
service: List[Service] = []
|
|
552
|
+
|
|
553
|
+
class Cryptocurrency(ForbidModel):
|
|
554
|
+
"""Cryptocoin usage (ransomware/miner)."""
|
|
555
|
+
|
|
556
|
+
class UsageEnum(str, Enum):
|
|
557
|
+
"""Cryptocoin usage."""
|
|
558
|
+
|
|
559
|
+
ransomware = "ransomware" # request money to unlock
|
|
560
|
+
miner = "miner" # use gpu/cpu to mint coins
|
|
561
|
+
other = "other"
|
|
562
|
+
|
|
563
|
+
coin: Optional[str] = None # BTC,ETH,USDT,BNB, etc
|
|
564
|
+
address: Optional[str] = None
|
|
565
|
+
ransom_amount: Optional[float] = None # number of coins required (if hardcoded)
|
|
566
|
+
|
|
567
|
+
usage: UsageEnum
|
|
568
|
+
|
|
569
|
+
cryptocurrency: List[Cryptocurrency] = []
|
|
570
|
+
|
|
571
|
+
class Path(ForbidModel):
|
|
572
|
+
"""Path used by malware."""
|
|
573
|
+
|
|
574
|
+
class UsageEnum(str, Enum):
|
|
575
|
+
"""Purpose of the path."""
|
|
576
|
+
|
|
577
|
+
c2 = "c2" # file/folder issues commands to malware
|
|
578
|
+
config = "config" # config is loaded from this path
|
|
579
|
+
install = "install" # install directory/filename for malware
|
|
580
|
+
plugins = "plugins" # load new capability from this directory
|
|
581
|
+
logs = "logs" # location to log activity
|
|
582
|
+
storage = "storage" # location to store/backup copied files
|
|
583
|
+
other = "other"
|
|
584
|
+
|
|
585
|
+
# C:\User\tmp\whatever.txt or /some/unix/folder/path
|
|
586
|
+
path: str
|
|
587
|
+
usage: Optional[UsageEnum] = None
|
|
588
|
+
|
|
589
|
+
paths: List[Path] = [] # files/directories used by malware
|
|
590
|
+
|
|
591
|
+
class Registry(ForbidModel):
|
|
592
|
+
"""Registry usage by malware."""
|
|
593
|
+
|
|
594
|
+
class UsageEnum(str, Enum):
|
|
595
|
+
"""Registry usage."""
|
|
596
|
+
|
|
597
|
+
persistence = "persistence" # stay alive
|
|
598
|
+
store_data = "store_data" # generated encryption keys or config
|
|
599
|
+
store_payload = "store_payload" # malware hidden in registry key
|
|
600
|
+
read = "read" # read system registry keys
|
|
601
|
+
other = "other"
|
|
602
|
+
|
|
603
|
+
key: str
|
|
604
|
+
usage: Optional[UsageEnum] = None
|
|
605
|
+
|
|
606
|
+
registry: List[Registry] = []
|
maco/yara.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""yara-python facade that uses yara-x."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from itertools import cycle
|
|
6
|
+
from typing import Dict, List, Union
|
|
7
|
+
|
|
8
|
+
import yara_x
|
|
9
|
+
|
|
10
|
+
from maco.exceptions import SyntaxError
|
|
11
|
+
|
|
12
|
+
RULE_ID_RE = re.compile("(\w+)? ?rule (\w+)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Create interfaces that resembles yara-python (but is running yara-x under the hood)
|
|
16
|
+
class StringMatchInstance:
|
|
17
|
+
"""Instance of a string match."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, match: yara_x.Match, file_content: bytes):
|
|
20
|
+
"""Initializes StringMatchInstance."""
|
|
21
|
+
self.matched_data = file_content[match.offset : match.offset + match.length]
|
|
22
|
+
self.matched_length = match.length
|
|
23
|
+
self.offset = match.offset
|
|
24
|
+
self.xor_key = match.xor_key
|
|
25
|
+
|
|
26
|
+
def plaintext(self) -> bytes:
|
|
27
|
+
"""Plaintext of the matched data.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
(bytes): Plaintext of the matched cipher text
|
|
31
|
+
"""
|
|
32
|
+
if not self.xor_key:
|
|
33
|
+
# No need to XOR the matched data
|
|
34
|
+
return self.matched_data
|
|
35
|
+
else:
|
|
36
|
+
return bytes(c ^ k for c, k in zip(self.matched_data, cycle(self.xor_key)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StringMatch:
|
|
40
|
+
"""String match."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, pattern: yara_x.Pattern, file_content: bytes):
|
|
43
|
+
"""Initializes StringMatch."""
|
|
44
|
+
self.identifier = pattern.identifier
|
|
45
|
+
self.instances = [StringMatchInstance(match, file_content) for match in pattern.matches]
|
|
46
|
+
self._is_xor = any([match.xor_key for match in pattern.matches])
|
|
47
|
+
|
|
48
|
+
def is_xor(self):
|
|
49
|
+
"""Checks if string match is xor'd.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
(bool): True if match is xor'd
|
|
53
|
+
"""
|
|
54
|
+
return self._is_xor
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Match:
|
|
58
|
+
"""Match."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, rule: yara_x.Rule, file_content: bytes):
|
|
61
|
+
"""Initializes Match."""
|
|
62
|
+
self.rule = rule.identifier
|
|
63
|
+
self.namespace = rule.namespace
|
|
64
|
+
self.tags = list(rule.tags) or []
|
|
65
|
+
self.meta = dict()
|
|
66
|
+
# Ensure metadata doesn't get overwritten
|
|
67
|
+
for k, v in rule.metadata:
|
|
68
|
+
self.meta.setdefault(k, []).append(v)
|
|
69
|
+
self.strings = [StringMatch(pattern, file_content) for pattern in rule.patterns]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Rules:
|
|
73
|
+
"""Rules."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, source: str = None, sources: Dict[str, str] = None):
|
|
76
|
+
"""Initializes Rules.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
SyntaxError: Raised when there's a syntax error in the YARA rule.
|
|
80
|
+
"""
|
|
81
|
+
Rule = namedtuple("Rule", "identifier namespace is_global")
|
|
82
|
+
if source:
|
|
83
|
+
sources = {"default": source}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
self._rules = []
|
|
87
|
+
compiler = yara_x.Compiler(relaxed_re_syntax=True)
|
|
88
|
+
for namespace, source in sources.items():
|
|
89
|
+
compiler.new_namespace(namespace)
|
|
90
|
+
for rule_type, id in RULE_ID_RE.findall(source):
|
|
91
|
+
is_global = True if rule_type == "global" else False
|
|
92
|
+
self._rules.append(Rule(namespace=namespace, identifier=id, is_global=is_global))
|
|
93
|
+
compiler.add_source(source)
|
|
94
|
+
self.scanner = yara_x.Scanner(compiler.build())
|
|
95
|
+
except yara_x.CompileError as e:
|
|
96
|
+
raise SyntaxError(e)
|
|
97
|
+
|
|
98
|
+
def __iter__(self):
|
|
99
|
+
"""Iterate over rules.
|
|
100
|
+
|
|
101
|
+
Yields:
|
|
102
|
+
YARA rules
|
|
103
|
+
"""
|
|
104
|
+
for rule in self._rules:
|
|
105
|
+
yield rule
|
|
106
|
+
|
|
107
|
+
def match(self, filepath: str = None, data: Union[bytes, bytearray] = None) -> List[Match]:
|
|
108
|
+
"""Performs a scan to check for YARA rules matches based on the file, either given by path or buffer.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
(List[Match]): A list of YARA matches.
|
|
112
|
+
"""
|
|
113
|
+
if filepath:
|
|
114
|
+
with open(filepath, "rb") as fp:
|
|
115
|
+
data = fp.read()
|
|
116
|
+
|
|
117
|
+
if isinstance(data, bytearray):
|
|
118
|
+
data = bytes(data)
|
|
119
|
+
|
|
120
|
+
return [Match(m, data) for m in self.scanner.scan(data).matching_rules]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def compile(source: str = None, sources: Dict[str, str] = None) -> Rules:
|
|
124
|
+
"""Compiles YARA rules from source or from sources.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
(Rules): a Rules object
|
|
128
|
+
"""
|
|
129
|
+
return Rules(source, sources)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maco-extractor
|
|
3
|
+
Version: 1.2.18
|
|
4
|
+
Summary: This package contains the essentials for creating Maco extractors and using them at runtime.
|
|
5
|
+
Author: sl-govau
|
|
6
|
+
Maintainer: cccs-rs
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2022 Crown Copyright, Government of Canada (Canadian Centre for Cyber Security / Communications Security Establishment) and Government of Australia (Australian Cyber Security Centre / Australian Signals Directorate)
|
|
10
|
+
|
|
11
|
+
Copyright title to all 3rd party software distributed with maco is held by the respective copyright holders as noted in those files. Users are asked to read the 3rd Party Licenses referenced with those assets.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
18
|
+
|
|
19
|
+
Project-URL: Repository, https://github.com/CybercentreCanada/Maco
|
|
20
|
+
Project-URL: Issues, https://github.com/CybercentreCanada/Maco/issues
|
|
21
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
30
|
+
Requires-Python: >=3.8
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE.md
|
|
33
|
+
Requires-Dist: pydantic>=2.0.0
|
|
34
|
+
Requires-Dist: yara-x
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Maco - Malware config extractor framework
|
|
38
|
+
|
|
39
|
+
## Maco is a framework for <ins>ma</ins>lware <ins>co</ins>nfig extractors.
|
|
40
|
+
|
|
41
|
+
It aims to solve two problems:
|
|
42
|
+
|
|
43
|
+
- Define a standardize ontology (or model) for extractor output. This greatly helps for databasing extracted values.
|
|
44
|
+
- Provide a standard way of identifying which parsers to run and how to execute them.
|
|
45
|
+
|
|
46
|
+
## Maco components
|
|
47
|
+
|
|
48
|
+
- `model.py`
|
|
49
|
+
- A data model for the common output of an extractor
|
|
50
|
+
- `extractor.py`
|
|
51
|
+
- Base class for extractors to implement
|
|
52
|
+
- `collector.py`
|
|
53
|
+
- Utilities for loading and running extractors
|
|
54
|
+
- `cli.py`
|
|
55
|
+
- A CLI tool `maco` to assist with running your extractors locally
|
|
56
|
+
- `base_test.py`
|
|
57
|
+
- Assist with writing unit tests for your extractors
|
|
58
|
+
|
|
59
|
+
**Note: If you're interested in using only the model in your project, you can `pip install maco-model` which is a smaller package containing only the model definition**
|
|
60
|
+
|
|
61
|
+
## Project Integrations 🛠️
|
|
62
|
+
|
|
63
|
+
This framework is actively being used by:
|
|
64
|
+
|
|
65
|
+
| Project | Description | License |
|
|
66
|
+
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
|
67
|
+
| <a href="https://cybercentrecanada.github.io/assemblyline4_docs/"><img src="https://images.weserv.nl/?url=cybercentrecanada.github.io/assemblyline4_docs/images/crane.png?v=4&h=100&w=100&fit=cover&maxage=7d"></a> | A malware analysis platform that uses the MACO model to export malware configuration extractions into a parseable, machine-friendly format | [](https://github.com/CybercentreCanada/assemblyline/blob/main/LICENSE.md) |
|
|
68
|
+
| [configextractor-py](https://github.com/CybercentreCanada/configextractor-py) | A tool designed to run extractors from multiple frameworks and uses the MACO model for output harmonization | [](https://github.com/CybercentreCanada/configextractor-py/blob/main/LICENSE.md) |
|
|
69
|
+
| <a href="https://github.com/jeFF0Falltrades/rat_king_parser"><img src="https://images.weserv.nl/?url=raw.githubusercontent.com/jeFF0Falltrades/rat_king_parser/master/.github/logo.png?v=4&h=100&w=100&fit=cover&maxage=7d"/> </a> | A robust, multiprocessing-capable, multi-family RAT config parser/extractor that is compatible with MACO | [](https://github.com/jeFF0Falltrades/rat_king_parser/blob/master/LICENSE) |
|
|
70
|
+
| <a href="https://github.com/CAPESandbox/community"><img src="https://images.weserv.nl/?url=github.com/CAPESandbox.png?v=4&h=100&w=100&fit=cover&maxage=7d0&mask=circle"/> </a> | A parser/extractor repository containing MACO extractors that's authored by the CAPE community but is integrated in [CAPE](https://github.com/kevoreilly/CAPEv2) deployments.<br>**Note: These MACO extractors wrap and parse the original CAPE extractors.** | [](https://github.com/kevoreilly/CAPEv2/blob/master/LICENSE) |
|
|
71
|
+
|
|
72
|
+
## Model Example
|
|
73
|
+
|
|
74
|
+
See [the model definition](https://github.com/CybercentreCanada/Maco/blob/0f447a66de5e5ce8770ef3fe2325aec002842e63/maco/model.py#L127) for all the supported fields.
|
|
75
|
+
You can use the model independently of the rest of the framework.
|
|
76
|
+
This is still useful for compatibility between systems!
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from maco import model
|
|
80
|
+
# 'family' is the only required property on the model
|
|
81
|
+
output = model.ExtractorModel(family="wanabee")
|
|
82
|
+
output.version = "2019" # variant first found in 2019
|
|
83
|
+
output.category.extend([model.CategoryEnum.cryptominer, model.CategoryEnum.clickfraud])
|
|
84
|
+
output.http.append(model.ExtractorModel.Http(protocol="https",
|
|
85
|
+
uri="https://bad-domain.com/c2_payload",
|
|
86
|
+
usage="c2"))
|
|
87
|
+
output.tcp.append(model.ExtractorModel.Connection(server_ip="127.0.0.1",
|
|
88
|
+
usage="ransom"))
|
|
89
|
+
output.campaign_id.append("859186-3224-9284")
|
|
90
|
+
output.inject_exe.append("explorer.exe")
|
|
91
|
+
output.binaries.append(
|
|
92
|
+
output.Binary(
|
|
93
|
+
data=b"sam I am",
|
|
94
|
+
datatype=output.Binary.TypeEnum.config,
|
|
95
|
+
encryption=output.Binary.Encryption(
|
|
96
|
+
algorithm="rot26",
|
|
97
|
+
mode="block",
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
# data about the malware that doesn't fit the model
|
|
102
|
+
output.other["author_lunch"] = "green eggs and ham"
|
|
103
|
+
output.other["author_lunch_time"] = "3pm"
|
|
104
|
+
print(output.model_dump(exclude_defaults=True))
|
|
105
|
+
|
|
106
|
+
# Generated model
|
|
107
|
+
{
|
|
108
|
+
'family': 'wanabee',
|
|
109
|
+
'version': '2019',
|
|
110
|
+
'category': ['cryptominer', 'clickfraud'],
|
|
111
|
+
'campaign_id': ['859186-3224-9284'],
|
|
112
|
+
'inject_exe': ['explorer.exe'],
|
|
113
|
+
'other': {'author_lunch': 'green eggs and ham', 'author_lunch_time': '3pm'},
|
|
114
|
+
'http': [{'uri': 'https://bad-domain.com/c2_payload', 'usage': 'c2', 'protocol': 'https'}],
|
|
115
|
+
'tcp': [{'server_ip': '127.0.0.1', 'usage': 'ransom'}],
|
|
116
|
+
'binaries': [{
|
|
117
|
+
'datatype': 'config', 'data': b'sam I am',
|
|
118
|
+
'encryption': {'algorithm': 'rot26', 'mode': 'block'}
|
|
119
|
+
}]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
And you can create model instances from dictionaries:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from maco import model
|
|
127
|
+
output = {
|
|
128
|
+
"family": "wanabee2",
|
|
129
|
+
"version": "2022",
|
|
130
|
+
"ssh": [
|
|
131
|
+
{
|
|
132
|
+
"username": "wanna",
|
|
133
|
+
"password": "bee2",
|
|
134
|
+
"hostname": "10.1.10.100",
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
print(model.ExtractorModel(**output))
|
|
139
|
+
|
|
140
|
+
# Generated model
|
|
141
|
+
family='wanabee2' version='2022' category=[] attack=[] capability_enabled=[]
|
|
142
|
+
capability_disabled=[] campaign_id=[] identifier=[] decoded_strings=[]
|
|
143
|
+
password=[] mutex=[] pipe=[] sleep_delay=None inject_exe=[] other={}
|
|
144
|
+
binaries=[] ftp=[] smtp=[] http=[]
|
|
145
|
+
ssh=[SSH(username='wanna', password='bee2', hostname='10.1.10.100', port=None, usage=None)]
|
|
146
|
+
proxy=[] dns=[] tcp=[] udp=[] encryption=[] service=[] cryptocurrency=[]
|
|
147
|
+
paths=[] registry=[]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Extractor Example
|
|
151
|
+
|
|
152
|
+
The following extractor will trigger on any file with more than 50 ELF sections,
|
|
153
|
+
and set some properties in the model.
|
|
154
|
+
|
|
155
|
+
Your extractors will do a better job of finding useful information than this one!
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
class Elfy(extractor.Extractor):
|
|
159
|
+
"""Check basic elf property."""
|
|
160
|
+
|
|
161
|
+
family = "elfy"
|
|
162
|
+
author = "blue"
|
|
163
|
+
last_modified = "2022-06-14"
|
|
164
|
+
yara_rule = """
|
|
165
|
+
import "elf"
|
|
166
|
+
|
|
167
|
+
rule Elfy
|
|
168
|
+
{
|
|
169
|
+
condition:
|
|
170
|
+
elf.number_of_sections > 50
|
|
171
|
+
}
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def run(
|
|
175
|
+
self, stream: BytesIO, matches: List[yara.Match]
|
|
176
|
+
) -> Optional[model.ExtractorModel]:
|
|
177
|
+
# return config model formatted results
|
|
178
|
+
ret = model.ExtractorModel(family=self.family)
|
|
179
|
+
# the list for campaign_id already exists and is empty, so we just add an item
|
|
180
|
+
ret.campaign_id.append(str(len(stream.read())))
|
|
181
|
+
return ret
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Writing Extractors
|
|
185
|
+
|
|
186
|
+
There are several examples that use Maco in the '`demo_extractors`' folder.
|
|
187
|
+
|
|
188
|
+
Some things to keep in mind:
|
|
189
|
+
|
|
190
|
+
- The Yara rule names must be prefixed with the extractor class name.
|
|
191
|
+
- e.g. Class 'MyScript' has Yara rules named 'MyScriptDetect1' and 'MyScriptDetect2', not 'Detect1'
|
|
192
|
+
- You can load other scripts contained within the same folder via a Python relative import
|
|
193
|
+
- See `complex.py` for details
|
|
194
|
+
- You can standardise your usage of the '`other`' dict
|
|
195
|
+
- This is optional, see `limit_other.py` for details
|
|
196
|
+
- Consider instead making a PR with the properties you are frequently using
|
|
197
|
+
|
|
198
|
+
# Requirements
|
|
199
|
+
|
|
200
|
+
Python 3.8+.
|
|
201
|
+
|
|
202
|
+
Install this package with `pip install maco`.
|
|
203
|
+
|
|
204
|
+
All required Python packages are in the `requirements.txt`.
|
|
205
|
+
|
|
206
|
+
# CLI Usage
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
> maco --help
|
|
210
|
+
usage: maco [-h] [-v] [--pretty] [--base64] [--logfile LOGFILE] [--include INCLUDE] [--exclude EXCLUDE] [-f] [--create_venv] extractors samples
|
|
211
|
+
|
|
212
|
+
Run extractors over samples.
|
|
213
|
+
|
|
214
|
+
positional arguments:
|
|
215
|
+
extractors path to extractors
|
|
216
|
+
samples path to samples
|
|
217
|
+
|
|
218
|
+
optional arguments:
|
|
219
|
+
-h, --help show this help message and exit
|
|
220
|
+
-v, --verbose print debug logging. -v extractor info, -vv extractor debug, -vvv cli debug
|
|
221
|
+
--pretty pretty print json output
|
|
222
|
+
--base64 Include base64 encoded binary data in output (can be large, consider printing to file rather than console)
|
|
223
|
+
--logfile LOGFILE file to log output
|
|
224
|
+
--include INCLUDE comma separated extractors to run
|
|
225
|
+
--exclude EXCLUDE comma separated extractors to not run
|
|
226
|
+
-f, --force ignore yara rules and execute all extractors
|
|
227
|
+
--create_venv Creates venvs for every requirements.txt found (only applies when extractor path is a directory)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## CLI output example
|
|
231
|
+
|
|
232
|
+
The CLI is helpful for using your extractors in a standalone system, such as in a reverse engineering environment.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
> maco demo_extractors/ /usr/lib --include Complex
|
|
236
|
+
extractors loaded: ['Complex']
|
|
237
|
+
|
|
238
|
+
complex by blue 2022-06-14 TLP:WHITE
|
|
239
|
+
This script has multiple yara rules and coverage of the data model.
|
|
240
|
+
|
|
241
|
+
path: /usr/lib/udev/hwdb.bin
|
|
242
|
+
run Complex extractor from rules ['ComplexAlt']
|
|
243
|
+
{"family": "complex", "version": "5", "decoded_strings": ["Paradise"],
|
|
244
|
+
"binaries": [{"datatype": "payload", "size": 9, "hex_sample": "736F6D652064617461", "sha256": "1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee",
|
|
245
|
+
"encryption": {"algorithm": "something"}}],
|
|
246
|
+
"http": [{"protocol": "https", "hostname": "blarg5.com", "path": "/malz/9956330", "usage": "c2"}],
|
|
247
|
+
"encryption": [{"algorithm": "sha256"}]}
|
|
248
|
+
|
|
249
|
+
path: /usr/lib/udev/hwdb.d/20-OUI.hwdb
|
|
250
|
+
run Complex extractor from rules ['ComplexAlt']
|
|
251
|
+
{"family": "complex", "version": "5", "decoded_strings": ["Paradise"],
|
|
252
|
+
"binaries": [{"datatype": "payload", "size": 9, "hex_sample": "736F6D652064617461", "sha256": "1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee",
|
|
253
|
+
"encryption": {"algorithm": "something"}}],
|
|
254
|
+
"http": [{"protocol": "https", "hostname": "blarg5.com", "path": "/malz/1986908", "usage": "c2"}],
|
|
255
|
+
"encryption": [{"algorithm": "sha256"}]}
|
|
256
|
+
|
|
257
|
+
path: /usr/lib/udev/hwdb.d/20-usb-vendor-model.hwdb
|
|
258
|
+
run Complex extractor from rules ['ComplexAlt']
|
|
259
|
+
{"family": "complex", "version": "5", "decoded_strings": ["Paradise"],
|
|
260
|
+
"binaries": [{"datatype": "payload", "size": 9, "hex_sample": "736F6D652064617461", "sha256": "1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee",
|
|
261
|
+
"encryption": {"algorithm": "something"}}],
|
|
262
|
+
"http": [{"protocol": "https", "hostname": "blarg5.com", "path": "/malz/1257481", "usage": "c2"}],
|
|
263
|
+
"encryption": [{"algorithm": "sha256"}]}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
15884 analysed, 3 hits, 3 extracted
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The demo extractors are designed to trigger when run over the '`demo_extractors`' folder.
|
|
270
|
+
|
|
271
|
+
e.g. `maco demo_extractors demo_extractors`
|
|
272
|
+
|
|
273
|
+
# Contributions
|
|
274
|
+
|
|
275
|
+
Please use ruff to format and lint PRs. This may be the cause of PR test failures.
|
|
276
|
+
|
|
277
|
+
Ruff will attempt to fix most issues, but some may require manual resolution.
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
pip install ruff
|
|
281
|
+
ruff format
|
|
282
|
+
ruff check --fix
|
|
283
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
maco/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
maco/exceptions.py,sha256=XBHUrs1kr1ZayPI9B_W-WejKgVmC8sWL_o4RL0b4DQE,745
|
|
3
|
+
maco/extractor.py,sha256=s36aGcsXSc-9iCik6iihVt5G1a1DZUA7TquvWYQNwdE,2912
|
|
4
|
+
maco/yara.py,sha256=y141t8NqDDXHY23uE1d6BDPeNmSuUuohR6Yr_LKa7GI,4067
|
|
5
|
+
maco/model/__init__.py,sha256=ULdyHx8R5D2ICHZo3VoCk1YTlewTok36TYIpwx__pNY,45
|
|
6
|
+
maco/model/model.py,sha256=DBHTmZXMzjpVq0s2mzZv3VCzPhwPnv7sH6u_QZCTcA4,24484
|
|
7
|
+
maco_extractor-1.2.18.dist-info/licenses/LICENSE.md,sha256=gMSjshPhXvV_F1qxmeNkKdBqGWkd__fEJf4glS504bM,1478
|
|
8
|
+
maco_extractor-1.2.18.dist-info/METADATA,sha256=-Hfk91VNhYm_ZIbqs8ke1qffq5eX4_nPeSvw5COYLAo,15208
|
|
9
|
+
maco_extractor-1.2.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
maco_extractor-1.2.18.dist-info/top_level.txt,sha256=JTYRldTIdoZJHXQU2LH0AKgD6Hm_azz5f_kOLuBorFU,5
|
|
11
|
+
maco_extractor-1.2.18.dist-info/RECORD,,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Crown Copyright, Government of Canada (Canadian Centre for Cyber Security / Communications Security Establishment) and Government of Australia (Australian Cyber Security Centre / Australian Signals Directorate)
|
|
4
|
+
|
|
5
|
+
Copyright title to all 3rd party software distributed with maco is held by the respective copyright holders as noted in those files. Users are asked to read the 3rd Party Licenses referenced with those assets.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
8
|
+
|
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maco
|