2026 Lab work
This is part of multiple install guides that I have made as I built out a new personal lab environment.
The following diagram shows a high-level view of the pipeline being built out.
Click to expand diagram
MALWARE ANALYSIS LAB
=======================================================================================
MALWARE INTAKE
│
┌──────────────────┼──────────────────┐
│ │ │
Sample Hash API
│ │ │
└──────────────────┴──────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ ASSEMBLYLINE4 │
│ Intake / Triage / Orchestration │
│ │
│ ┌───────────┐ ┌────────────┐ ┌───────────────────┐ │
│ │ File Type │ -> │ Static │ -> │ YARA / PE / Other │ │
│ │ / Hashing │ │ Analysis │ │ Analysis Services │ │
│ └───────────┘ └────────────┘ └─────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ CAPE Service│ │
│ └──────┬──────┘ │
│ │ │
└────────────────────────────────────────────────┼──────────────┘
│
Submit Sample
│
▼
┌───────────────────────────────────────────────────────────────┐
│ CAPEv2 │
│ Automated Dynamic Analysis │
│ │
│ API / Scheduler │
│ │ │
│ ▼ │
│ Machine Selection │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Windows Sandbox VM │ │
│ │ │ │
│ │ CAPE Agent │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Malware Execution │ │
│ │ │ │ │
│ │ ├── Process Behavior │ │
│ │ ├── API Activity │ │
│ │ ├── Memory │ │
│ │ ├── Dropped Files │ │
│ │ ├── CAPEMON │ │
│ │ └── Network Traffic ───────────────┐ │
│ └──────────────────────────────┘ │ │
│ │ │
│ CAPE Parsers │ │
│ Config Extraction │ │
│ PCAP │ │
│ Behavioral Report │ │
│ │ │ │
└─────────────────┼────────────────────────────┼────────────────┘
│ │
│ ▼
│ ┌──────────────────────────────┐
│ │ INetSim │
│ │ Simulated Malware Network │
│ │ │
│ │ DNS │
│ │ HTTP / HTTPS │
│ │ FTP / SMTP │
│ │ Other Fake Services │
│ │ │
│ │ "Controlled Fake Internet" │
│ └──────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ CAPE RESULTS │
│ │
│ Behavior │ PCAP │ Extracted Files │ Config │ IOCs │ Dumps │
└──────────────────────────────┬────────────────────────────────┘
│
│
┌───────────────────┴─────────────────────┐
│ │
▼ ▼
Return Results to AL4 Escalate for Manual Analysis
│ │
▼ ▼
┌───────────────────────┐ ┌────────────────────────────────┐
│ ASSEMBLYLINE4 │ │ FLAREVM │
│ │ │ Manual Analysis / Reverse Eng. │
│ Aggregate Results │ │ │
│ Scoring │ │ Static Analysis │
│ Service Results │ │ │ │
│ Search / Hunting │ │ ├── IDA Pro │
│ │ │ ├── FLOSS │
└───────────┬───────────┘ │ ├── capa │
│ │ └── YARA │
│ │ │
│ │ Dynamic / Debug Analysis │
│ │ │ │
│ │ ├── x64dbg │
│ │ ├── API tracing │
│ │ └── Manual execution │
│ │ │
│ │ Reverse Engineering │
│ │ │ │
│ │ ├── Config extraction │
│ │ ├── C2 protocol │
│ │ ├── Crypto │
│ │ └── Capability analysis │
│ │ │
│ └───────────────┬────────────────┘
│ │
└───────────────────┬──────────────────┘
│
▼
┌─────────────────────────────────────┐
│ MALWARE INTELLIGENCE │
│ │
│ Malware Family │
│ Behavior / Capabilities │
│ Configuration │
│ C2 Infrastructure │
│ IOCs │
│ Detection Opportunities │
│ YARA / Signatures │
│ Reverse Engineering Findings │
└──────────────────┬──────────────────┘
│
▼
┌─────────────────────┐
│ REPORTING │
│ │
│ Malware Report │
│ Family Profile │
│ Threat Intel │
│ Detection Content │
└─────────────────────┘
Purpose
CAPE malware configuration parsers extract useful configuration data from malware samples and unpacked payloads, such as:
- Command-and-control servers
- Campaign IDs
- Bot IDs
- Mutexes
- Encryption keys
- User agents
- Installation paths
- Persistence values
- Ports
- Credentials
- Cryptocurrency wallets
- Version information
- Other family-specific configuration
For locally developed or private parsers, CAPE supports placing pure Python parser modules in:
1
/opt/CAPEv2/custom/parsers/
This is the preferred location for private parsers. CAPE’s built-in parsers were moved into the separate CAPE-parsers project in November 2024, but CAPE explicitly retains support for custom/parsers/. A custom parser with the same name as an installed CAPE parser can override that parser.
The default CAPE processing configuration currently points its CAPE extractor framework at:
1
2
3
4
5
[CAPE_extractors]
enabled = yes
modules_path = custom/parsers/
parsers = all
exclude=
1. How CAPE Config Extraction Works
The normal flow is roughly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Malware executes
|
v
CAPE captures original/unpacked/dumped files
|
v
CAPE YARA rules scan those files
|
v
A CAPE YARA rule identifies a malware family
|
v
CAPE determines the family/parser name
|
v
static_config_parsers()
|
v
custom/parsers/<family>.py
|
v
extract_config(data)
|
v
Extracted configuration added to CAPE report
CAPE’s processing code runs CAPE YARA rules against captured files. When a YARA hit provides a CAPE detection, CAPE derives the malware family name and calls static_config_parsers() for that family.
This means that writing the Python parser is only half of the job.
You normally need:
- A YARA rule capable of identifying the malware/config.
- A parser whose name corresponds to the family detected by CAPE.
- An
extract_config(data)function capable of extracting the configuration.
CAPE recommends its native parser framework specifically because a parser can simply expose:
1
def extract_config(data):
and CAPE will call it through its config parsing infrastructure.
2. Directory Layout
Your private parsers should live under:
1
/opt/CAPEv2/custom/parsers/
For example:
1
2
3
4
5
6
7
/opt/CAPEv2/
├── custom/
│ └── parsers/
│ ├── ExampleRAT.py
│ ├── QV02.py
│ ├── PrivateLoader.py
│ └── CompanySpecificMalware.py
Do not copy the entire CAPE-parsers repository into this directory.
The official CAPE parsers are installed as a Python dependency through Poetry. CAPE maintainers specifically recommend custom/parsers/ for private pure-Python parsers.
3. Minimal CAPE Parser
The simplest useful parser looks like this:
1
2
3
4
5
6
7
8
9
def extract_config(data):
config = {}
# Parse malware data here.
config["C2"] = ["https://example.com"]
config["Campaign ID"] = "example-campaign"
return config
data is the malware/configuration file supplied to the parser as bytes.
A slightly safer skeleton is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def extract_config(data):
config = {}
if not data:
return config
try:
# Perform extraction here.
pass
except Exception:
return {}
return config
For development, however, avoid swallowing every exception until the parser is working. Silent exception handling makes debugging unnecessarily difficult.
A better development version is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import logging
log = logging.getLogger(__name__)
def extract_config(data):
config = {}
if not data:
return config
try:
# Extraction logic.
pass
except Exception:
log.exception("Failed extracting ExampleRAT configuration")
return {}
return config
4. A Practical Example Parser
Imagine a malware family named ExampleRAT.
Assume its configuration appears in the binary as:
1
2
3
4
5
6
CFG|
c2=https://evil.example.com
port=443
campaign=ATTACK01
mutex=Global\ExampleMutex
|ENDCFG
Create:
1
2
3
cd /opt/CAPEv2
sudo -u cape vim custom/parsers/ExampleRAT.py
Then:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import logging
import re
log = logging.getLogger(__name__)
def extract_config(data):
config = {}
if not data:
return config
try:
match = re.search(
rb"CFG\|(.*?)\|ENDCFG",
data,
re.DOTALL,
)
if not match:
return {}
raw_config = match.group(1)
for line in raw_config.splitlines():
if b"=" not in line:
continue
key, value = line.split(b"=", 1)
key = key.decode("utf-8", errors="ignore").strip()
value = value.decode("utf-8", errors="ignore").strip()
if key == "c2":
config.setdefault("C2", []).append(value)
elif key == "port":
config["Port"] = value
elif key == "campaign":
config["Campaign ID"] = value
elif key == "mutex":
config["Mutex"] = value
return config
except Exception:
log.exception("ExampleRAT config extraction failed")
return {}
A successful return could look like:
1
2
3
4
5
6
7
8
{
"C2": [
"https://evil.example.com"
],
"Port": "443",
"Campaign ID": "ATTACK01",
"Mutex": "Global\\ExampleMutex",
}
5. Parser Naming Is Important
CAPE needs to associate the malware family identified by its CAPE YARA detection with the corresponding parser.
Conceptually:
1
2
3
4
5
6
7
YARA identifies:
ExampleRAT
↓
CAPE parser:
ExampleRAT.py
The safest practice is therefore:
1
YARA family name == parser module name
For example:
1
2
Family: ExampleRAT
Parser: custom/parsers/ExampleRAT.py
Avoid unnecessary differences such as:
1
2
3
4
Example-RAT
Example_RAT
examplerat
ExampleRatParser
unless you have verified how the particular CAPE YARA metadata resolves into the parser name.
CAPE’s processor derives the CAPE family name from its YARA hit and passes that name into static_config_parsers().
6. Creating the YARA Detection
A parser normally will not magically execute against every file CAPE encounters.
CAPE needs to know that the parser is relevant.
A simplified CAPE YARA rule might conceptually look like:
rule ExampleRAT
{
meta:
author = "Travis Mathison"
description = "Detect ExampleRAT"
cape_type = "ExampleRAT Payload"
strings:
$config_marker = "CFG|"
$end_marker = "|ENDCFG"
condition:
uint16(0) == 0x5A4D and
all of them
}
The important relationship is:
1
2
3
4
5
Detection
+
Family identification
+
Parser
CAPE scans captured files with its CAPE YARA rules and uses qualifying YARA hits to derive a cape_name, which then causes static_config_parsers() to execute.
Your exact CAPE YARA metadata should follow the conventions already used by the YARA rules installed on your CAPE server.
A good development workflow is to find an existing malware family that behaves similarly and model both its YARA rule and parser after that implementation.
7. Recommended Parser Return Structure
Use Python-native structures that serialize cleanly into JSON.
Good:
1
2
3
4
5
6
7
8
9
{
"C2": [
"https://one.example",
"https://two.example",
],
"Port": 443,
"Campaign ID": "ABC123",
"Mutex": "Global\\mutex",
}
Avoid returning:
1
2
3
{
"raw_object": SomeCustomPythonClass(),
}
or:
1
2
3
{
"binary": b"\x00\x01\x02"
}
unless you explicitly convert those values into something JSON-compatible.
For binary data, consider:
1
config["Key"] = key.hex()
instead of:
1
config["Key"] = key
8. Handling Multiple C2 Servers
For malware with multiple C2 servers:
1
config["C2"] = []
Then:
1
config["C2"].append(c2)
Better yet, deduplicate:
1
2
3
4
5
6
c2s = set()
for value in extracted_values:
c2s.add(value)
config["C2"] = sorted(c2s)
Example:
1
2
3
4
5
6
7
{
"C2": [
"https://c2-a.example",
"https://c2-b.example",
"185.100.10.20:443",
]
}
9. Handling Encoded Configuration
Most real malware requires more than string extraction.
Typical parser operations include:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Locate configuration
|
+-- PE section
+-- resource
+-- overlay
+-- marker
+-- RVA
+-- pattern
|
v
Extract encrypted blob
|
v
Decrypt / decode
|
+-- XOR
+-- RC4
+-- AES
+-- Base64
+-- custom algorithm
|
v
Deserialize
|
+-- JSON
+-- struct
+-- protobuf
+-- null-separated strings
+-- custom TLV
|
v
Return Python dictionary
For example:
1
2
3
4
5
def xor_decode(data, key):
return bytes(
byte ^ key[i % len(key)]
for i, byte in enumerate(data)
)
Then:
1
decoded = xor_decode(encrypted_config, key)
10. PE Parsing
For PE malware, pefile is often useful.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import io
import logging
import pefile
log = logging.getLogger(__name__)
def extract_config(data):
config = {}
try:
pe = pefile.PE(data=data, fast_load=False)
for section in pe.sections:
name = section.Name.rstrip(b"\x00").decode(
"ascii",
errors="ignore",
)
if name == ".data":
section_data = section.get_data()
# Parse config here.
return config
except pefile.PEFormatError:
return {}
except Exception:
log.exception("Failed parsing malware configuration")
return {}
Do not assume every file supplied to your parser will necessarily be a perfectly formed PE.
CAPE may be dealing with:
- Memory dumps
- Reconstructed executables
- Injected payloads
- Decompressed data
- Config blobs
- Truncated files
Design the parser accordingly.
11. Useful Helper Functions
For production parsers, small helper functions make maintenance much easier.
For example:
1
2
3
4
5
def decode_string(value):
if isinstance(value, bytes):
return value.decode("utf-8", errors="ignore").rstrip("\x00")
return str(value)
IPv4 conversion:
1
2
3
4
5
import socket
def decode_ipv4(raw):
return socket.inet_ntoa(raw)
Little-endian integer:
1
2
3
4
5
import struct
def uint32_le(data):
return struct.unpack("<I", data)[0]
Null-terminated string:
1
2
3
4
5
def read_cstring(data):
return data.split(b"\x00", 1)[0].decode(
"utf-8",
errors="ignore",
)
12. Keep Extraction Separate From Identification
Avoid writing parsers like:
1
2
3
4
5
def extract_config(data):
if b"ExampleRAT" not in data:
return {}
...
unless the marker is actually required to locate the config.
The YARA rule should generally handle:
1
Is this ExampleRAT?
The parser should handle:
1
2
3
Where is the ExampleRAT config,
how is it encoded,
and what fields does it contain?
This separation makes both components easier to maintain.
13. Local Parser Testing
Do not repeatedly submit malware to CAPE just to test minor parser changes.
You can test your parser directly.
Assume:
1
custom/parsers/ExampleRAT.py
and:
1
/tmp/example.exe
From /opt/CAPEv2:
1
2
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3
Then:
1
2
3
4
5
6
7
8
from custom.parsers.ExampleRAT import extract_config
with open("/tmp/example.exe", "rb") as f:
data = f.read()
config = extract_config(data)
print(config)
Or create a very small test script:
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python3
import pprint
from custom.parsers.ExampleRAT import extract_config
with open("/tmp/example.exe", "rb") as f:
data = f.read()
pprint.pp(extract_config(data))
Then:
1
2
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 /tmp/test_parser.py
This is usually the fastest parser-development loop.
14. Syntax Checking
Before restarting CAPE:
1
cd /opt/CAPEv2
Run:
1
2
3
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 -m py_compile \
custom/parsers/ExampleRAT.py
No output normally means the file compiled successfully.
You can also test importing it:
1
2
3
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 -c \
'from custom.parsers.ExampleRAT import extract_config; print(extract_config)'
15. File Ownership
Because CAPE runs under the cape account, keep ownership consistent:
1
sudo chown cape:cape /opt/CAPEv2/custom/parsers/ExampleRAT.py
Recommended permissions:
1
sudo chmod 0644 /opt/CAPEv2/custom/parsers/ExampleRAT.py
Verify:
1
ls -la /opt/CAPEv2/custom/parsers/
Example:
1
-rw-r--r-- 1 cape cape 2841 Aug 25 21:30 ExampleRAT.py
16. Restart CAPE Processing
Once the parser is installed or modified:
1
sudo systemctl restart cape-processor
The CAPE maintainers specifically recommend restarting cape-processor after parser installation/update.
Check it:
1
sudo systemctl status cape-processor --no-pager
And inspect logs:
1
sudo journalctl -u cape-processor -n 100 --no-pager
For live debugging:
1
sudo journalctl -u cape-processor -f
17. Debug Logging
Add:
1
2
3
import logging
log = logging.getLogger(__name__)
Then during development:
1
log.debug("ExampleRAT parser invoked")
or:
1
log.info("Found ExampleRAT configuration")
For errors:
1
log.exception("ExampleRAT parser failed")
Be careful not to permanently dump entire malware buffers into production logs.
Bad:
1
log.debug(data)
Better:
1
2
3
4
log.debug(
"Parsing ExampleRAT candidate of %d bytes",
len(data),
)
18. Verify That CAPE Is Actually Calling the Parser
A parser that works manually but never appears in CAPE reports usually means the problem is detection/dispatch rather than extraction.
Temporarily add:
1
log.warning("ExampleRAT PARSER EXECUTED")
Restart:
1
sudo systemctl restart cape-processor
Submit the malware.
Then:
1
sudo journalctl -u cape-processor -f
If the message never appears, investigate:
1
2
3
4
5
6
7
8
9
10
11
12
13
YARA rule
|
v
Did CAPE produce a CAPE YARA hit?
|
v
Did that hit resolve to ExampleRAT?
|
v
Does ExampleRAT.py exist?
|
v
Can the cape account import it?
19. A Better Production Parser Template
This is a good starting point for new private parsers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""
CAPE configuration parser for ExampleRAT.
Author:
Travis Mathison
Purpose:
Extract ExampleRAT malware configuration.
"""
import logging
import re
log = logging.getLogger(__name__)
CONFIG_START = b"CFG|"
CONFIG_END = b"|ENDCFG"
def _decode(value):
return value.decode(
"utf-8",
errors="ignore",
).strip().rstrip("\x00")
def _locate_config(data):
pattern = (
re.escape(CONFIG_START)
+ rb"(.*?)"
+ re.escape(CONFIG_END)
)
match = re.search(
pattern,
data,
re.DOTALL,
)
if not match:
return None
return match.group(1)
def _parse_config(raw):
config = {}
for line in raw.splitlines():
if b"=" not in line:
continue
key, value = line.split(b"=", 1)
key = _decode(key)
value = _decode(value)
if not value:
continue
if key == "c2":
config.setdefault("C2", []).append(value)
elif key == "port":
try:
config["Port"] = int(value)
except ValueError:
config["Port"] = value
elif key == "campaign":
config["Campaign ID"] = value
elif key == "mutex":
config["Mutex"] = value
elif key == "version":
config["Version"] = value
return config
def extract_config(data):
"""Extract ExampleRAT configuration."""
if not isinstance(data, (bytes, bytearray)):
return {}
if not data:
return {}
try:
raw_config = _locate_config(data)
if raw_config is None:
return {}
config = _parse_config(raw_config)
if config:
log.debug(
"Successfully extracted ExampleRAT configuration"
)
return config
except Exception:
log.exception(
"Unexpected error extracting ExampleRAT configuration"
)
return {}
The benefit of this layout is that each stage can be tested independently:
1
2
3
4
5
6
7
extract_config()
|
+-- _locate_config()
|
+-- decode/decrypt
|
+-- _parse_config()
20. Unit Testing Your Parser
For parsers your team expects to maintain, create tests.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from custom.parsers.ExampleRAT import extract_config
def test_example_rat():
sample = (
b"MZ"
+ b"\x00" * 100
+ b"CFG|\n"
+ b"c2=https://c2.example\n"
+ b"port=443\n"
+ b"campaign=TEST01\n"
+ b"|ENDCFG"
)
result = extract_config(sample)
assert result["C2"] == [
"https://c2.example"
]
assert result["Port"] == 443
assert result["Campaign ID"] == "TEST01"
This becomes especially valuable when a malware family changes configuration versions.
21. Supporting Multiple Malware Versions
Avoid creating:
1
2
3
ExampleRAT_v1.py
ExampleRAT_v2.py
ExampleRAT_v3.py
if they are clearly the same malware family.
Instead:
1
2
3
4
5
6
7
8
9
10
11
12
13
def extract_config(data):
version = identify_config_version(data)
if version == 1:
return parse_v1(data)
if version == 2:
return parse_v2(data)
if version == 3:
return parse_v3(data)
return {}
Example:
1
2
3
4
5
6
7
8
def identify_config_version(data):
if b"CFG1" in data:
return 1
if b"CFG2" in data:
return 2
return None
This keeps CAPE reporting the same malware family while allowing the parser to evolve.
22. Overriding an Official CAPE Parser
One particularly useful feature of custom/parsers/ is that a local parser can override an official CAPE parser with the same name. CAPE explicitly documents that custom parsers continue to load and overwrite a CAPE parser when the names match.
For example, suppose the official package contains:
1
Lumma.py
You could create:
1
/opt/CAPEv2/custom/parsers/Lumma.py
Your local implementation can then supersede the packaged implementation.
This is useful when:
- An upstream parser is temporarily broken.
- Your organization has support for a newer malware variant.
- You need private extraction logic.
- You need additional fields unavailable upstream.
- You are developing a fix before submitting it upstream.
It also means you should be careful with parser names, because accidentally naming a private parser after an official parser can override it.
23. Private vs Upstream Parsers
A useful operational policy is:
1
2
3
4
5
6
7
8
9
10
11
custom/parsers/
|
+-- Private/internal malware
|
+-- Work in progress
|
+-- Proprietary extraction
|
+-- Emergency upstream overrides
|
+-- Research prototypes
If a parser is broadly useful and contains no proprietary information, consider contributing it to:
1
CAPESandbox/CAPE-parsers
CAPE maintainers request a parser, unit test, and appropriate test sample when contributing parser support upstream.
24. Dependencies
Try to keep private parsers lightweight.
Prefer Python standard-library modules where practical:
1
2
3
4
5
6
7
8
9
10
base64
binascii
hashlib
io
json
logging
re
socket
struct
urllib.parse
Before using a third-party dependency:
1
from Crypto.Cipher import AES
verify it exists inside CAPE’s Poetry environment.
Test:
1
2
3
4
5
cd /opt/CAPEv2
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 -c \
'from Crypto.Cipher import AES; print("OK")'
Do not install random packages globally with:
1
sudo pip install ...
CAPE should use its managed Poetry environment.
25. Common Failure Modes
Parser works manually but CAPE never calls it
Most likely:
1
YARA detection/metadata mismatch
Check the CAPE YARA hit and family name.
ModuleNotFoundError
Usually:
- Third-party package missing from Poetry environment.
- Import path incorrect.
- Parser assumes a package from your shell environment rather than CAPE’s environment.
Test with:
1
2
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3
Permission denied
Check:
1
namei -l /opt/CAPEv2/custom/parsers/ExampleRAT.py
and:
1
2
3
4
ls -ld \
/opt/CAPEv2 \
/opt/CAPEv2/custom \
/opt/CAPEv2/custom/parsers
Then ensure the cape user can read the parser.
Parser runs but returns nothing
Test:
1
2
3
result = extract_config(data)
print(repr(result))
Then progressively test:
1
print(_locate_config(data))
and:
1
print(_parse_config(raw_config))
Break the parser into stages rather than debugging everything through CAPE.
Parser imports but breaks cape-processor
Inspect:
1
2
3
sudo journalctl -u cape-processor \
--since "10 minutes ago" \
--no-pager
Typical causes:
1
2
3
4
SyntaxError
ImportError
ModuleNotFoundError
NameError at import time
Avoid performing parsing work globally at module import.
Bad:
1
pe = pefile.PE("/tmp/sample.exe")
at the top level.
Parsing should happen inside:
1
extract_config()
26. Recommended Development Workflow
Use this workflow for every new malware family:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1. Identify malware family
|
v
2. Reverse configuration format
|
v
3. Write standalone Python extractor
|
v
4. Convert extractor to:
extract_config(data)
|
v
5. Test parser directly
|
v
6. Place under custom/parsers/
|
v
7. Create/verify CAPE YARA rule
|
v
8. Restart cape-processor
|
v
9. Submit known sample
|
v
10. Verify config in report
|
v
11. Add regression test
|
v
12. Document sample + parser version
27. Recommended Parser Header
For internally maintained parsers, use a standardized header:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
Malware Family:
ExampleRAT
Purpose:
CAPEv2 configuration extractor.
Maintainer:
Travis Mathison
Created:
2026-08-25
Configuration Versions:
v1
Known Sample SHA256:
<SHA256>
References:
<internal reference>
<public reference if applicable>
Notes:
Parser is intended for CAPEv2 custom/parsers/.
"""
This becomes extremely valuable months later when someone has to determine:
1
2
3
4
Why does this parser exist?
Who wrote it?
What sample was used?
Which version does it support?
28. Suggested Repository Structure for Team Parsers
Rather than allowing /opt/CAPEv2/custom/parsers/ to become the authoritative copy, maintain your parsers in Git.
For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mare-cape/
├── README.md
├── parsers/
│ ├── ExampleRAT.py
│ ├── QV02.py
│ └── PrivateLoader.py
├── yara/
│ ├── ExampleRAT.yar
│ ├── QV02.yar
│ └── PrivateLoader.yar
├── tests/
│ ├── test_ExampleRAT.py
│ ├── test_QV02.py
│ └── test_PrivateLoader.py
└── docs/
├── ExampleRAT.md
├── QV02.md
└── PrivateLoader.md
Deployment could then copy approved parsers into:
1
/opt/CAPEv2/custom/parsers/
This gives the team:
1
2
3
4
5
6
Version control
Code review
History
Rollback
Testing
Author attribution
and keeps locally developed malware intelligence separate from CAPE itself.
29. Quick Parser Creation Checklist
- Determine exact malware family name.
- Determine how CAPE/YARA will identify the family.
- Reverse the configuration location.
- Determine encoding/encryption.
- Write standalone extraction code.
- Expose
extract_config(data). - Return a JSON-compatible dictionary.
- Name the parser consistently with the CAPE family.
- Copy parser to
/opt/CAPEv2/custom/parsers/. - Set ownership to
cape:cape. - Run
py_compile. - Test
extract_config()directly against a known sample. - Verify CAPE YARA detection.
- Restart
cape-processor. - Submit known-positive sample.
- Confirm parser execution in logs.
- Confirm configuration appears in the CAPE report.
- Add regression/unit test.
- Commit parser and YARA changes to internal source control.
30. Quick Command Reference
Create parser:
1
2
3
cd /opt/CAPEv2
sudo -u cape vim custom/parsers/ExampleRAT.py
Set ownership:
1
2
sudo chown cape:cape \
custom/parsers/ExampleRAT.py
Compile test:
1
2
3
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 -m py_compile \
custom/parsers/ExampleRAT.py
Import test:
1
2
3
sudo -u cape env POETRY_CACHE_DIR=/opt/CAPEv2/.cache/pypoetry \
/etc/poetry/bin/poetry run python3 -c \
'from custom.parsers.ExampleRAT import extract_config; print(extract_config)'
Restart processor:
1
sudo systemctl restart cape-processor
Status:
1
sudo systemctl status cape-processor --no-pager
Logs:
1
2
3
sudo journalctl -u cape-processor \
-n 100 \
--no-pager
Live logs:
1
sudo journalctl -u cape-processor -f
31. Key Takeaways
The most important architectural point is that:
1
custom/parsers/
is not a replacement copy of CAPE-parsers.
The modern model is:
1
2
3
4
5
6
7
8
9
Official parsers
|
+-- Installed CAPE-parsers Python package
|
+-- Updated through CAPE/Poetry dependency management
Private parsers
|
+-- /opt/CAPEv2/custom/parsers/
CAPE’s current configuration explicitly references custom/parsers/, and the CAPE maintainers recommend that directory for private pure-Python config parsers.
The second critical point is:
1
Parser != Detection
Your Python extractor can be perfect, but CAPE still needs to identify the malware family before it knows which extractor to execute.
Think of the complete capability as:
1
2
3
4
5
6
7
CAPE YARA Detection
+
Python Config Parser
+
Known-positive Regression Sample
=
Reliable CAPE Family Support
For CAPE-native parsers, the core interface remains deliberately simple:
1
2
3
def extract_config(data):
...
return config
which is why this is the framework CAPE itself recommends for straightforward malware configuration extraction.