Antivirus/crates/houndd/rules/hound-builtin.yar
Hound be5396821d gate: stop blocking reads, and stop calling documents malware
Armed the gate on / on the live server. Aborted after about twenty
seconds. The box was never at risk — Caddy stayed sub-millisecond and
load never rose — but the gate blocked reads of an AI agent's session
transcript, reporting it as Linux.Coinminer.XMRig.

It was not wrong about the bytes. That transcript contains
"stratum+tcp://", "donate-level" and "xmrig" because the miner rule was
being written in that session. The rule matched a document ABOUT
malware.

Three bugs, none of which the tmpfs stage could have shown:

1. The miner rule had no file-type condition, so any text mentioning
   mining tripped it: threat-intelligence reports, security blog posts,
   support tickets, an antivirus's own logs. It now requires ELF magic,
   as the rootkit rule always did. Two regression tests: a transcript
   discussing the rule is clean, and an ELF carrying the same strings
   still matches — the fix must not cost the detection it exists for.

2. The gate requested FAN_OPEN_PERM, so it held every OPEN, not every
   execve. A matching file could not be read by anything. That is a
   different product from the one advertised, and on a multi-tenant box
   it is a denial of service against the operator rather than a defence.

   Read events are no longer requested at all. FAN_OPEN_EXEC_PERM and
   FAN_CLOSE_WRITE cover the threat: execution is refused before it
   happens, and anything malicious written to disk is quarantined when
   the write completes. An interpreted script is caught as it lands
   rather than as it is read — the same protection, one step earlier.
   `serve` also guards deny-on-exec explicitly, so re-requesting read
   events later cannot silently restore the old behaviour.

3. Hound did not exclude its own state. /var/lib/hound and /run/hound
   are now always excluded; the vault holds live malware by definition.

Henry asked whether the single watchdog rescue was queue pressure or
scan time. It was scan time: the gate inherited the on-demand 100 MB
limit and tried to read and match a multi-megabyte transcript inline
while holding a process. A gate's budget is a deadline, not a size, so
it now caps at 32 MB — anything larger is allowed through unread rather
than turned into a rescue, which is a process released unscanned and
worse than never having looked.

Dropping read events made everything faster, because most opens on a
running machine are reads:

  latency     +1.38 -> +0.79 ms per exec
  throughput  2,680 -> 4,178 execs/sec (58% of ungated, was 36%)
  events      1,179 in five seconds on an idle tmpfs -> 1

Re-verified on the tmpfs: an ELF miner is quarantined before it can even
be made executable, a document naming every one of its strings is
readable, and a clean binary runs.

297 tests pass. The gate stays off; stage 3 gets attempted again with
these fixes and fresh numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:13:02 -05:00

105 lines
3.6 KiB
Text

/*
* Hound built-in starter pack.
*
* Deliberately tiny and deliberately tight. Every rule here requires
* several independent strings before it fires, because a false positive
* in an antivirus is worse than a miss — one rule that quarantines a
* system binary ends the product.
*
* The real corpus lands in Phase 3 (the signed Hound Linux pack, gated
* behind the goodware CI regression suite). This pack exists so a fresh
* install detects *something* before it has ever contacted the network.
*/
rule EICAR_Test_File
{
meta:
name = "EICAR-Test-Signature"
severity = "info"
desc = "Industry-standard antivirus test file. Harmless."
strings:
$eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
condition:
$eicar
}
rule Linux_Coinminer_XMRig
{
meta:
name = "Linux.Coinminer.XMRig"
severity = "critical"
desc = "XMRig cryptocurrency miner. Requires pool protocol plus two config keys."
strings:
$pool1 = "stratum+tcp://" ascii
$pool2 = "stratum+ssl://" ascii
$cfg1 = "donate-level" ascii
$cfg2 = "rig-id" ascii
$cfg3 = "randomx" ascii nocase
$name = "xmrig" ascii nocase
condition:
// ELF magic is not optional here.
//
// Without it this rule matches any TEXT that mentions mining:
// a blog post, a support ticket, a threat-intelligence report,
// or — as happened on a live server — an AI session transcript
// in which somebody was writing this very rule. Malware is a
// program; a document about malware is not.
uint32(0) == 0x464c457f
and ($pool1 or $pool2) and 2 of ($cfg*) and $name
}
rule Linux_Webshell_PHP_Eval
{
meta:
name = "Linux.Webshell.PHP-Eval"
severity = "critical"
desc = "PHP webshell: request-driven eval of decoded input."
strings:
$php = "<?php"
$eval1 = /eval\s*\(\s*(base64_decode|gzinflate|str_rot13|gzuncompress)\s*\(/
$src1 = "$_POST"
$src2 = "$_GET"
$src3 = "$_REQUEST"
$src4 = "$_COOKIE"
condition:
$php and $eval1 and 1 of ($src*)
}
rule Linux_Rootkit_Preload
{
meta:
name = "Linux.Rootkit.Preload"
severity = "critical"
desc = "LD_PRELOAD userland rootkit: hooks libc lookup calls and hides itself."
strings:
$dlsym = "dlsym" ascii
$libc = "RTLD_NEXT" ascii
$hook1 = "readdir64" ascii
$hook2 = "readdir" ascii
$hook3 = "lxstat" ascii
$hook4 = "fopen" ascii
$hide1 = "ld.so.preload" ascii
$hide2 = "/proc/net/tcp" ascii
condition:
uint32(0) == 0x464c457f // ELF magic
and $dlsym and $libc
and 2 of ($hook*)
and 1 of ($hide*)
}
/*
* REMOVED: Linux_Backdoor_ReverseShell_ELF
*
* It required an ELF containing "/bin/sh" plus four of
* {dup2, socket, connect, inet_addr, execve}. That is a perfect
* description of a reverse shell and also a perfect description of
* /usr/bin/sudo, which the goodware test caught immediately. Any
* dynamically linked network-capable binary imports those symbols
* legitimately, so no threshold tweak saves this rule — it would only
* move the false positive to a different binary on a different distro.
*
* Catching reverse shells properly needs either ELF structure (statically
* linked, tiny, no libc) or the behaviour itself, which is Phase 7's job.
* Left out rather than shipped loose: a rule that quarantines sudo is
* worse than no rule at all.
*/