Enodia Sentinel v0.1 — bash host-IDS prototype
Poll-based host intrusion-detection daemon modeled on freeze-watcher's sample→trigger→snapshot→classify loop, with security signatures instead of performance ones: - reverse_shell (interpreter with a network socket on stdio) - ld_preload (/etc/ld.so.preload or LD_PRELOAD in writable dirs) - deleted_exe (process running from a deleted/memfd binary) - new_listener (listening port absent from startup baseline) - new_suid (new SUID/SGID binary; critical in writable dirs) - persistence (cron/systemd/authorized_keys/rc-file changes) - suspicious_egress (interpreter holding an outbound public connection) Each capture writes a forensic snapshot with per-signature incident- response guidance. Includes a safe, self-cleaning red-team harness (sentinel-redteam), hardened systemd unit, config, Makefile, and Arch packaging. Tested on Arch: detectors fire on drills, no false positives on a clean sweep, sweep cost optimized from ~15s to ~1s via batched syscalls and a gated filesystem-wide SUID scan. This bash implementation is retained as the regression oracle for the forthcoming Python rewrite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
45f8acb24a
10 changed files with 1026 additions and 0 deletions
175
README.md
Normal file
175
README.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Enodia Sentinel
|
||||
|
||||
A host intrusion-detection daemon for Linux. It continuously runs a set of
|
||||
detectors over live system state — processes, sockets, file descriptors, the
|
||||
SUID inventory, and sensitive files — and writes a detailed forensic snapshot
|
||||
with incident-response guidance the moment a known attack signature appears.
|
||||
|
||||
Think of it as the security counterpart to a performance watchdog: instead of
|
||||
"I/O pressure spiked, here's the kernel state," it's *"a shell just wired itself
|
||||
to a socket — here's the process tree, the peer, and what to do about it."*
|
||||
|
||||
> **Design lineage.** Sentinel reuses the proven control loop of a performance
|
||||
> anomaly-capture daemon (sample → threshold/trigger → rich snapshot → classify
|
||||
> → notify/retain) and swaps the inputs and signatures from *performance* to
|
||||
> *security*. The forensic-snapshot-on-trigger pattern is the same; the
|
||||
> detectors and the response guidance are new.
|
||||
|
||||
## Why these detectors
|
||||
|
||||
Every detector keys on a behavior that is **cheap to observe** and **expensive
|
||||
for an attacker to avoid** — the high-signal, low-false-positive heuristics that
|
||||
real EDRs are built on:
|
||||
|
||||
| Signature | What it catches | Why it's hard to evade |
|
||||
|---|---|---|
|
||||
| `reverse_shell` | A shell/interpreter with a **socket on fd 0/1/2** | Interactive shells get a pty, not a socket — a socket on stdio is almost always `nc -e` / `bash -i >& /dev/tcp/...` |
|
||||
| `ld_preload_global` | A non-empty `/etc/ld.so.preload` | Injecting into *every* process is the whole point of a userland rootkit; the file has to exist |
|
||||
| `ld_preload_proc` | `LD_PRELOAD` pointing into `/tmp`, `/dev/shm`, … | Hooking libc to hide files/creds needs the library somewhere writable |
|
||||
| `deleted_exe` | A process running from a **deleted / `memfd:`** binary | Fileless malware deletes its dropper but the kernel still names the inode `(deleted)` |
|
||||
| `new_listener` | A listening port absent from the startup baseline | Bind shells and backdoors have to listen somewhere |
|
||||
| `new_suid` | A new SUID/SGID binary (critical in writable dirs) | A SUID `/tmp` binary is a textbook privesc persistence trick |
|
||||
| `persistence` | Changes to cron, systemd units, `authorized_keys`, rc files | Persistence has to write *somewhere* that survives reboot |
|
||||
| `suspicious_egress` | An interpreter holding an outbound conn to a public IP | C2 beacons and exfil need to phone home |
|
||||
|
||||
## The control loop
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ every SAMPLE_INTERVAL seconds │
|
||||
│ │
|
||||
│ run_detectors() ──► alert lines │
|
||||
│ │ SEV signature detail │
|
||||
│ ▼ │
|
||||
│ cooldown dedup (per signature+key) │
|
||||
│ │ │
|
||||
│ ▼ (fresh alerts only) │
|
||||
│ capture_snapshot() │
|
||||
│ • flagged-pid deep dive (/proc) │
|
||||
│ • full process tree + sockets │
|
||||
│ • ld.so.preload, recent file mods │
|
||||
│ • logins, failed auth, kernel modules │
|
||||
│ • per-signature response guidance │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ events.log + alert-*.log + desktop notify │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Output lives in `/var/log/enodia-sentinel/`:
|
||||
- `events.log` — one line per alert (timestamp, severity, signatures, file)
|
||||
- `alert-YYYYMMDD-HHMMSS.log` — the full forensic snapshot
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
sudo make install
|
||||
sudo make enable # start + enable the systemd service
|
||||
|
||||
# prove it works — in one terminal:
|
||||
sudo tail -f /var/log/enodia-sentinel/events.log
|
||||
# in another:
|
||||
sentinel-redteam # safe, self-cleaning attack simulations
|
||||
```
|
||||
|
||||
You'll watch the drills trip `reverse_shell`, `ld_preload_proc`, `deleted_exe`,
|
||||
`new_listener`, and `new_suid` in real time, each producing a snapshot with
|
||||
response guidance.
|
||||
|
||||
### Without installing (try it in place)
|
||||
|
||||
```bash
|
||||
sudo ./src/sentinel.sh --baseline # establish listener/SUID baselines
|
||||
sudo ./src/sentinel.sh --check # run every detector once, print findings
|
||||
```
|
||||
|
||||
## The red-team harness
|
||||
|
||||
`sentinel-redteam` is the demo and the regression test in one. It simulates each
|
||||
threat with **safe, clearly-labeled stand-ins** (everything tagged
|
||||
`enodia-drill`, auto-cleaned on exit):
|
||||
|
||||
- **reverse_shell** — a `bash` whose stdio is a TCP socket to a *local* listener
|
||||
(no traffic leaves the box)
|
||||
- **ld_preload** — a process with `LD_PRELOAD=/tmp/...` (an empty file; never
|
||||
actually loaded, and it does **not** touch the system-wide `/etc/ld.so.preload`)
|
||||
- **deleted_exe** — a copy of `/bin/sleep` run from `/tmp`, then deleted
|
||||
- **new_listener** — an ephemeral `nc -l` on loopback
|
||||
- **new_suid** — a `chmod 4755` copy of `/bin/true` in `/tmp`
|
||||
- **persistence** — writes to a sandbox `authorized_keys` (only fires if you opt
|
||||
the sandbox into `WATCH_PERSISTENCE`; it refuses to touch your real dotfiles)
|
||||
|
||||
```bash
|
||||
sentinel-redteam --list # list drills
|
||||
sentinel-redteam reverse_shell new_suid # run specific ones
|
||||
HOLD=30 sentinel-redteam # keep artifacts alive 30s
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `/etc/enodia-sentinel.conf`, then `sudo systemctl restart
|
||||
enodia-sentinel.service`. Key options:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `SAMPLE_INTERVAL` | 4 | seconds between detector sweeps |
|
||||
| `COOLDOWN` | 60 | min seconds before re-alerting the same signature |
|
||||
| `DET_*` | 1 | per-detector on/off switches |
|
||||
| `INTERPRETERS` | bash sh … | process names treated as shells for rsh/egress |
|
||||
| `EGRESS_ALLOW_CIDRS` | "" | trusted public ranges that won't trip egress |
|
||||
| `LISTENER_ALLOW_PORTS` | "" | ports that never trip `new_listener` |
|
||||
| `SUID_HOT_DIRS` | /tmp /dev/shm … | dirs where SUID = CRITICAL |
|
||||
| `WATCH_PERSISTENCE` | cron/systemd/… | files watched for tampering |
|
||||
| `CAPTURE_EXECVE_BPFTRACE` | 0 | add a bpftrace execve trace to snapshots |
|
||||
| `MAX_SNAPSHOTS` / `MAX_SNAPSHOT_AGE_DAYS` | 300 / 60 | retention |
|
||||
| `NOTIFY_USERS` | "" | desktop notify-send targets |
|
||||
|
||||
## Tuning out false positives
|
||||
|
||||
- A long-running daemon legitimately running a deleted exe after a package
|
||||
upgrade won't alert — `deleted_exe` only fires for `/tmp`, `/dev/shm`,
|
||||
`/run`, `/var/tmp`, or `memfd:` paths.
|
||||
- Your own admin shells over SSH won't trip `reverse_shell` — they're on a pty.
|
||||
- Browsers and chat apps that interpreters open to public IPs *can* trip
|
||||
`suspicious_egress` if you add them to `INTERPRETERS`; the default list is
|
||||
deliberately scripting-focused. Use `EGRESS_ALLOW_CIDRS` for known-good ranges.
|
||||
|
||||
## Security model
|
||||
|
||||
Sentinel runs as root because it must read every process's `/proc`, the full
|
||||
socket table, and root-owned files like `authorized_keys`. The systemd unit
|
||||
constrains that power hard: `ProtectSystem=strict` with the log dir as the only
|
||||
writable path, `ProtectHome=read-only`, `NoNewPrivileges`,
|
||||
`MemoryDenyWriteExecute`, and a minimal capability set
|
||||
(`CAP_SYS_PTRACE`, `CAP_DAC_READ_SEARCH`). It only ever **reads** the system and
|
||||
**writes** to its own log directory.
|
||||
|
||||
## Roadmap — from polling to eBPF
|
||||
|
||||
This v0 is **poll-based**: it sweeps `/proc` and `ss` every few seconds. That's
|
||||
robust, dependency-light, and catches anything that lingers — but it can miss
|
||||
sub-second processes, and a sweep is observable. The planned evolution:
|
||||
|
||||
1. **bpftrace tracepoints (now, optional)** — `CAPTURE_EXECVE_BPFTRACE=1` adds a
|
||||
live `execve` trace to each snapshot. The gentle on-ramp to kernel tracing.
|
||||
2. **Event-driven execve/connect detection** — replace the polling gap with
|
||||
`bpftrace` probes on `sys_enter_execve`, `security_bprm_check`, and
|
||||
`tcp_connect`, streamed to the daemon so short-lived reverse shells can't
|
||||
slip between sweeps.
|
||||
3. **A libbpf + CO-RE agent (Go or Rust userland)** — the real EDR core: LSM
|
||||
hooks, ring-buffer event streaming, per-process lineage tracking, and
|
||||
tamper-resistance. This is the production-grade rewrite the polling daemon
|
||||
prototypes the detection logic for.
|
||||
|
||||
The polling daemon isn't throwaway — it's the **oracle**: every signature here
|
||||
is a test case the eBPF agent must reproduce, and `sentinel-redteam` is the
|
||||
shared regression suite for both.
|
||||
|
||||
## Project status
|
||||
|
||||
v0.1 — working poll-based detection across 7 signatures, forensic snapshots,
|
||||
red-team harness, systemd packaging. Built and tested on Arch Linux.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Loading…
Add table
Add a link
Reference in a new issue