Modern internet-services emulation suite for malware analysis labs
Project description
Lyrebird
Modern internet-services emulation suite for malware analysis labs.
Lyrebird stands up fake-but-believable network services — HTTP/HTTPS, DNS (UDP + TCP), SMTP, POP3/IMAP, FTP, TFTP, SSH, Telnet, IRC, NTP, TLS, and a generic TCP sink — so that a malware sample running in an isolated sandbox behaves as if it were online. Every interaction is recorded as a structured JSON event, and every payload the sample sends is captured to disk. It's a spiritual successor to INetSim (last release 1.3.2, 2020), rebuilt async-first in Python with detection telemetry as a first-class output.
⚠️ Authorized, isolated lab use only. Lyrebird emulates benign services to observe malware in containment. It is a defensive/research tool. It contains no implant, no command-and-control capability, and no evasion tooling. Run it on a segmented, non-routable network. Do not expose it to the internet.
Demo
A lab boots, a stand-in sample talks to the emulated services, and every interaction lands as structured JSONL with detections firing as tags:
The "sample" is a benign
curl/dig— Lyrebird only observes. The recording is generated fromdemo/lyrebird.tapewith VHS; seedemo/to reproduce it.
With the AI model layer
Optionally, Lyrebird can call a model to (1) improvise a believable, inert reply for an endpoint no static rule anticipated — so an unfamiliar sample keeps talking — and (2) triage the captured session into a verdict and candidate Sigma detections:
Off by default. The responder is constrained to generic placeholder content — never payloads, scripts, or tasking (see
SCOPE.mdandsrc/lyrebird/models/responder.py).
Why
INetSim is still the reference tool, but it's Perl-based, synchronous, config is a custom format, and its logging was designed for human reports rather than SIEM ingestion. Lyrebird keeps the proven model and modernizes it:
- Async core (asyncio / FastAPI / aiosmtpd / dnslib)
- YAML config with sane defaults — an empty file still gives a working lab
- Structured JSONL events — one normalized object per interaction, tailable straight into a SIEM (this is the backbone everything keys off)
- Artifact capture — uploads, mail bodies, and raw socket data stored + hashed
- Auto lab CA — TLS handshakes just work; certs are minted on first run
- Container-native —
docker compose upon an internal (no-egress) network - Paired Sigma detections — every service ships with detection content
Quick start
pip install -r requirements.txt
python -m lyrebird --config config/lyrebird.yaml
Or containerized (recommended — the compose network is internal: true, so the
lab has no outbound route by default):
cd docker && docker compose up --build
Point your malware analysis VM's default gateway / DNS at the Lyrebird host, then detonate the sample and watch the events stream in.
What gets emitted
Each interaction is one line of JSON:
{"schema":"1.0","ts":"2026-06-29T12:00:00.000+00:00","session":"...",
"service":"dns","transport":"udp","src_ip":"10.13.37.66","src_port":51000,
"dst_port":53,"event_type":"request","summary":"A evil.example",
"request":{"qname":"evil.example.","qtype":"A"},
"response":{"rcode":0,"answer":"10.13.37.1"},"artifacts":[],"tags":[]}
Events land in labdata/events/<session>.jsonl; captured payloads in
labdata/artifacts/<service>/.
The full field-by-field event schema and the complete detection catalog are in
REFERENCE.md — generated from events.py and the Sigma rules,
so it never drifts. Regenerate with python scripts/gen_reference.py.
Services
| Service | Transport | Status | Notes |
|---|---|---|---|
| HTTP / HTTPS | TCP | ✅ implemented | catch-all any method/path; auto-TLS; body capture |
| DNS | UDP | ✅ implemented | sinkhole responder; logs every lookup; optional realistic NXDOMAIN mode (off by default) |
| SMTP | TCP | ✅ implemented | accepts + captures mail; logs envelope |
| POP3 | TCP | ✅ implemented | fake mailbox; logs credentials/commands |
| FTP | TCP | ✅ implemented | passive + active (PORT) mode; captures STOR uploads |
| TFTP | UDP | ✅ implemented | captures WRQ uploads; per-transfer TID |
| IRC | TCP | ✅ implemented | observes bot C2 — nick, channels, PRIVMSG tasking |
| NTP | UDP | ✅ implemented | answers time; configurable faketime delta |
| TCP sink | TCP | ✅ implemented | logs all data to extra ports (INetSim "Dummy") |
| IMAP | TCP | ✅ implemented | fake mailbox; logs LOGIN credentials; IDLE push (mailbox-C2 long-poll) → imap-idle |
| DNS over TCP | TCP | ✅ implemented | sinkhole over TCP transport |
| TLS (fingerprint + serve) | TCP | ✅ implemented | JA3/JA4 + SNI, terminates & serves, same-connection SNI-vs-Host (off by default) |
| TLS fingerprint tap | TCP | ✅ implemented | JA3/JA4 + SNI capture then close (off by default) |
| SSH | TCP | ✅ implemented | asyncssh honeypot; captures brute-force credentials, then a fake shell logs commands (ssh-bruteforce, ssh-payload-pull) |
| Telnet | TCP | ✅ implemented | plaintext IoT/Mirai honeypot; brute-force creds → fake shell logs commands (telnet-bruteforce, telnet-payload-pull) |
| QUIC / HTTP-3 | UDP | ✅ implemented | aioquic h3 server; captures each HTTP/3 request and answers benignly (http3-transport) |
The SSH honeypot uses the
asyncsshlibrary (a required dependency). Ifasyncsshis unavailable the SSH service is skipped and the rest of the emulator keeps running.
Customizing responses
Tailor what any service returns without touching code, via a responses block
per service (the modern take on INetSim's fakefiles). Resolution order is
operator rule → fakefile → model responder (if enabled) → built-in default,
and the chosen source is recorded on every event (response.source).
services:
http:
responses:
http:
- path: "/gate.php" # glob over the URL path
method: "POST"
status: 200
content_type: "application/json"
body: '{"status":"ok","task":"none"}'
- path: "/*.exe"
body_file: "fakefiles/stub.bin" # relative to data_dir
fakefiles_dir: "fakefiles" # also serve real files by URL path
dns:
responses:
dns:
- qname: "*.evil-c2.com" # point a family at a specific sink host
qtype: "A"
answer: "10.13.37.66"
Fakefile serving is path-traversal protected — paths are confined to the configured directory.
Enabling / disabling services
Per-service enabled: true|false in config, or override at launch:
python -m lyrebird --disable smtp,ntp
python -m lyrebird --enable http,dns --no-banner
Model layer (frontier + local)
Lyrebird talks to models through one vendor-agnostic interface, so you can use a
frontier API or a fully local model interchangeably. Selecting local keeps all
data on-host for air-gapped analysis.
| provider | backend |
|---|---|
anthropic |
Claude (ANTHROPIC_API_KEY) |
openai |
OpenAI (OPENAI_API_KEY) |
gemini |
Google Gemini (GEMINI_API_KEY) |
local |
any OpenAI-compatible local server — Ollama, LM Studio, llama.cpp, vLLM |
mock |
offline deterministic stub (tests / dry-runs) |
Primary use — session triage into detections. Point it at a captured session and get a structured verdict + candidate Sigma ideas:
python -m lyrebird.analyze --session labdata/events/<id>.jsonl --provider local
python -m lyrebird.analyze --session <file> --provider anthropic --model claude-sonnet-4-6
Optional use — response generation. When models.respond.enabled: true, an
HTTP request with no matching rule gets a model-generated benign placeholder
body so unfamiliar samples keep talking. It's off by default (static templates
are preferred), and it's deliberately constrained: captured input is sanitized
first, the model is restricted to generic inert content (never payloads, scripts,
or commands), and output is canary-checked and length-capped — any failure falls
back to the static default.
Untrusted input is treated as untrusted
Captured traffic is adversary-controlled, so anything that reaches a model is a
prompt-injection surface. models/sanitize.py defangs injection markers, frames
captured data as inert observations behind a canary, and validates model output
against a schema. Nothing captured is ever executed — services only serve or log
bytes.
Adding a service
Subclass BaseService, implement start() / stop(), emit events with
self.emit(...), and register the class in orchestrator.REGISTRY. That's the
whole contract — see services/dns.py for a compact example.
Detections
detections/sigma/ holds Sigma rules that key off the JSONL schema
(logsource.product: lyrebird). Shipped so far: DNS long-label/DGA, HTTP
missing-User-Agent beacon, SMTP bulk recipients. The principle is that every
emulated technique ships with its paired detection.
Detection analytics
Beyond single-event Sigma rules, three analytics run over a captured session for the statistical / behavioural cases:
python -m lyrebird.beacons --session <jsonl>— beaconing, jitter (via inter-arrival CV), and channel rotation. The defensive pair to Phase 2.python -m lyrebird.mimicry --session <jsonl> [--data-dir labdata]— traffic-mimicry and encryption tells: protocol-on-unexpected-port, domain- fronting heuristics, browser-UA-but-bot, and high-entropy (encrypted) bodies. The defensive pair to Phase 3.python -m lyrebird.dns_tunnel --session <jsonl>— DNS tunneling / data-exfil channels: high-entropy, near-all-unique subdomains streamed under one parent domain, distinct from the single-query long-label DGA rule.
Layout
src/lyrebird/
events.py # structured event model + JSONL sink (the backbone)
config.py # YAML loading + defaults
base.py # BaseService plugin contract
certs.py # lab CA / leaf certs
tls.py # ClientHello parsing + JA3 / JA4
profiles.py # operator response templates
orchestrator.py # loads config, runs enabled services
cli.py # `python -m lyrebird`
analyze.py # model-assisted session triage
beacons.py # beacon / jitter / channel-rotation analytic
mimicry.py # traffic-mimicry / encryption-tell analytic
dns_tunnel.py # DNS tunneling / exfil analytic
services/ # http, dns, dns_tcp, smtp, pop3, imap, ftp, tftp,
# irc, ntp, tls_capture, tcp_sink
models/ # anthropic, openai, gemini, local, mock + sanitize
config/lyrebird.yaml
detections/sigma/
scripts/lint_sigma.py
docker/
tests/
Status
All sixteen services and all three session analytics are implemented, tested
(113 tests, green on Python 3.10–3.12), and runnable; the suite boots end to end
and every service emits telemetry. Recent additions — SSH and Telnet
credential-capture honeypots, IMAP IDLE emulation, FTP active-mode (FTP-bounce)
hardening, TLS JA3/JA4 fingerprinting, NTP mode-6/7 MONLIST detection, and a DNS
tunneling/exfil analytic — each ship with their paired detection. The plugin
contract is stable; further work is additive. See SCOPE.md for positioning and
the roadmap.
Contributing
PRs welcome — see CONTRIBUTING.md for setup, the bar for changes, and a
step-by-step walkthrough for adding a new service. CI (GitHub Actions) runs the
test suite on Python 3.10–3.12, lints the Sigma rules, and smoke-tests the CLI on
every push and PR.
PYTHONPATH=src python -m pytest tests/ -q
PYTHONPATH=src python scripts/lint_sigma.py
License
GPL-3.0-or-later (see LICENSE). Chosen to keep Lyrebird a copyleft, fork-friendly community tool in the same spirit as INetSim (GPLv2). Every source file carries an SPDX header.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lyrebird_emulator-0.2.0.tar.gz.
File metadata
- Download URL: lyrebird_emulator-0.2.0.tar.gz
- Upload date:
- Size: 95.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d070bb3fc9413e0620a0a62178fe0b47b44e61f74f201f311185b75a1d6ace65
|
|
| MD5 |
3c20fab182309afaa59465c44fd58e9d
|
|
| BLAKE2b-256 |
7755f0724875d85f4279a2cdf405eeabeff04838fb46a89d206075e88edfd470
|
Provenance
The following attestation bundles were made for lyrebird_emulator-0.2.0.tar.gz:
Publisher:
publish.yml on cac0ns3c/Lyrebird
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lyrebird_emulator-0.2.0.tar.gz -
Subject digest:
d070bb3fc9413e0620a0a62178fe0b47b44e61f74f201f311185b75a1d6ace65 - Sigstore transparency entry: 2034559043
- Sigstore integration time:
-
Permalink:
cac0ns3c/Lyrebird@99c84edf8dbbb28a8aa17ae5674a6e3975e0efbe -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/cac0ns3c
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99c84edf8dbbb28a8aa17ae5674a6e3975e0efbe -
Trigger Event:
release
-
Statement type:
File details
Details for the file lyrebird_emulator-0.2.0-py3-none-any.whl.
File metadata
- Download URL: lyrebird_emulator-0.2.0-py3-none-any.whl
- Upload date:
- Size: 90.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c0ace679023e7d54231be336fc5074128ada8cfb2b38255511ed43a9e876798
|
|
| MD5 |
4a8e0e8feb4c2bb3718ede00e408a547
|
|
| BLAKE2b-256 |
75179d1a5da0f36cd7a629ebc9f63ccb531da68742c988b50f159a5b7f3cb200
|
Provenance
The following attestation bundles were made for lyrebird_emulator-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on cac0ns3c/Lyrebird
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lyrebird_emulator-0.2.0-py3-none-any.whl -
Subject digest:
3c0ace679023e7d54231be336fc5074128ada8cfb2b38255511ed43a9e876798 - Sigstore transparency entry: 2034559694
- Sigstore integration time:
-
Permalink:
cac0ns3c/Lyrebird@99c84edf8dbbb28a8aa17ae5674a6e3975e0efbe -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/cac0ns3c
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99c84edf8dbbb28a8aa17ae5674a6e3975e0efbe -
Trigger Event:
release
-
Statement type: