detection rules: complete sigma/yara/falco coverage across the corpus

Three parallel research agents drafted 49 detection rules grounded in
each module's source + existing .opsec_notes string + existing .detect_auditd
counterpart. A one-shot tools/inject_rules.py wrote them into the
right files and replaced the .detect_<format> = NULL placeholders.

Coverage matrix (modules with each format / 31 total):
                  before        after
  auditd          30 / 31       30 / 31   (entrybleed skipped by design)
  sigma           19 / 31       31 / 31   (+12 added)
  yara            11 / 31       28 / 31   (+17 added; 3 documented skips)
  falco           11 / 31       30 / 31   (+19 added; entrybleed skipped)

Documented skips (kept as .detect_<format> = NULL with comment):
  - entrybleed: yara + falco + auditd. Pure timing side-channel via
    rdtsc + prefetchnta; no syscalls, no file artifacts, no in-memory
    tags. The source comment already noted this; sigma got a 'unusual
    prefetchnta loop time' rule via perf-counter logic.
  - ptrace_traceme: yara. Pure in-memory race; no on-disk artifacts
    or persistent strings to match. Falco + sigma + auditd cover the
    PTRACE_TRACEME + setuid execve syscall sequence.
  - sudo_samedit: yara. Transient heap race during sudoedit invocation;
    no persistent file artifact. Falco + sigma + auditd cover the
    'sudoedit -s + trailing-backslash argv' pattern.

Rule discipline (post-agent QA):
  - All rules ground claims in actual exploit code paths (the agents
    were instructed to read source + opsec_notes; no fabricated syscalls
    or strings).
  - Two falco rules were narrowed by the agent to fire only when
    proc.pname is skeletonkey itself; rewrote both to fire on any
    non-root caller (otherwise we'd detect only our own binary, not
    real attackers).
  - Sigma rule fields use canonical {type: 'SYSCALL', syscall: 'X'}
    detection blocks consistent with existing rules (nf_tables,
    dirty_pipe, sudo_samedit).
  - YARA rules prefer rare/unique tags (SKELETONKEYU, SKELETONKEY_FWD,
    SKVMWGFX, /tmp/skeletonkey-*.log) over common bytes — minimizes
    false positives.
  - Every rule tagged with attack.privilege_escalation + cve.YYYY.NNNN;
    cgroup_release_agent additionally tagged T1611 (container escape).

skeletonkey.c: --module-info text view now dumps yara + falco rule
bodies too (was auditd + sigma only). All 4 formats visible per module.

Verification:
  - macOS local: clean build, 33 kernel_range tests pass.
  - Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
  - --module-info nf_tables / af_unix_gc / etc.: 'detect rules:'
    summary correctly shows all 4 formats and the bodies print.
This commit is contained in:
2026-05-23 11:10:54 -04:00
parent ee3e7dd9a7
commit 8ab49f36f6
21 changed files with 837 additions and 49 deletions
@@ -407,6 +407,56 @@ static const char overlayfs_setuid_auditd[] =
"-a always,exit -F arch=b64 -S mount -F a2=overlay -k skeletonkey-overlayfs\n"
"-a always,exit -F arch=b64 -S chown,fchown,fchownat -k skeletonkey-overlayfs-chown\n";
static const char overlayfs_setuid_sigma[] =
"title: Possible CVE-2023-0386 overlayfs setuid copy-up\n"
"id: 0891b2f7-skeletonkey-overlayfs-setuid\n"
"status: experimental\n"
"description: |\n"
" Detects the upstream overlayfs setuid copy-up bug: unshare\n"
" (CLONE_NEWUSER|CLONE_NEWNS) + mount('overlay') with a setuid-\n"
" root binary in lower + chown on the merged view to trigger\n"
" copy-up. Setuid bit persists in upper layer despite\n"
" unprivileged ownership.\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
" userns: {type: 'SYSCALL', syscall: 'unshare'}\n"
" overlay: {type: 'SYSCALL', syscall: 'mount'}\n"
" chown_up: {type: 'SYSCALL', syscall: 'chown'}\n"
" condition: userns and overlay and chown_up\n"
"level: critical\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2023.0386]\n";
static const char overlayfs_setuid_yara[] =
"rule overlayfs_setuid_cve_2023_0386 : cve_2023_0386 userns_lpe\n"
"{\n"
" meta:\n"
" cve = \"CVE-2023-0386\"\n"
" description = \"overlayfs setuid copy-up workdir signature\"\n"
" author = \"SKELETONKEY\"\n"
" strings:\n"
" $work = /\\/tmp\\/skeletonkey-ovlsu-[A-Za-z0-9]+/\n"
" condition:\n"
" $work\n"
"}\n";
static const char overlayfs_setuid_falco[] =
"- rule: overlayfs chown on setuid binary in userns (copy-up)\n"
" desc: |\n"
" Non-root chown on a setuid-root binary inside an overlayfs\n"
" mount in a userns. Triggers copy-up that preserves the\n"
" setuid bit despite unprivileged upper-layer ownership.\n"
" CVE-2023-0386.\n"
" condition: >\n"
" evt.type in (chown, fchown, fchownat) and not user.uid = 0\n"
" and (fd.name in (/usr/bin/su, /bin/su, /usr/bin/sudo,\n"
" /usr/bin/passwd, /usr/bin/pkexec)\n"
" or fd.name endswith /su)\n"
" output: >\n"
" chown on setuid binary by non-root\n"
" (user=%user.name pid=%proc.pid file=%fd.name)\n"
" priority: CRITICAL\n"
" tags: [filesystem, mitre_privilege_escalation, T1068, cve.2023.0386]\n";
const struct skeletonkey_module overlayfs_setuid_module = {
.name = "overlayfs_setuid",
.cve = "CVE-2023-0386",
@@ -418,9 +468,9 @@ const struct skeletonkey_module overlayfs_setuid_module = {
.mitigate = NULL,
.cleanup = overlayfs_setuid_cleanup,
.detect_auditd = overlayfs_setuid_auditd,
.detect_sigma = NULL,
.detect_yara = NULL,
.detect_falco = NULL,
.detect_sigma = overlayfs_setuid_sigma,
.detect_yara = overlayfs_setuid_yara,
.detect_falco = overlayfs_setuid_falco,
.opsec_notes = "unshare(CLONE_NEWUSER|CLONE_NEWNS) + overlayfs mount with a setuid-root binary in lower (e.g. /usr/bin/su); chown on the merged view triggers copy-up that preserves the setuid bit in upper - but upper is owned by the unprivileged user. Overwrites upper-layer contents with attacker payload and execve's for root. Artifacts: /tmp/skeletonkey-ovlsu-XXXXXX/ (workdir with payload.c, binary, overlay mounts); cleanup callback removes these. Audit-visible via unshare(CLONE_NEWUSER|CLONE_NEWNS) + mount(overlay) + chown on the merged view. No network. Dmesg silent on success.",
};