Skip to content

new(rules): detect GPU/accelerator cryptojacking and device access (T1496) - #373

Open
DevamShah wants to merge 2 commits into
falcosecurity:mainfrom
DevamShah:rules/gpu-cryptojacking-t1496
Open

new(rules): detect GPU/accelerator cryptojacking and device access (T1496)#373
DevamShah wants to merge 2 commits into
falcosecurity:mainfrom
DevamShah:rules/gpu-cryptojacking-t1496

Conversation

@DevamShah

Copy link
Copy Markdown

/kind feature

/area rules

/area maturity-sandbox

What type of PR is this?

/kind feature

Any specific area of the project related to this PR?

/area rules
/area maturity-sandbox

What this PR does / why we need it

Adds three maturity_sandbox artifacts to rules/falco-sandbox_rules.yaml that detect GPU/accelerator cryptojacking — a container opening an NVIDIA/AMD compute device, and GPU management tooling (nvidia-smi, rocm-smi) running inside a container — closing a coverage gap the existing CPU/network-centric cryptominer rules do not address.

Problem / motivation

Falco's current cryptominer coverage in falco-sandbox_rules.yaml is CPU- and network-centric:

  • Detect crypto miners using the Stratum protocol matches proc.cmdline for stratum+tcp / stratum+ssl.
  • Detect outbound connections to common miner pool ports matches known pool domains/ports.
  • Known Cryptominer Process Executed matches a fixed list of miner binary names (xmrig, ethminer, ...).

All three are trivially evaded by a GPU miner. An attacker who has compromised a GPU-equipped node (common in ML/AI and rendering fleets, where idle accelerator capacity is a high-value cryptojacking target) can use a renamed or custom miner binary, connect over an encrypted/proxied channel to an unknown pool, and avoid the stratum URI scheme entirely.

What the miner cannot avoid is opening a GPU character device to submit work to the hardware (/dev/nvidia*, /dev/nvidiactl, /dev/nvidia-uvm for CUDA; /dev/kfd and /dev/dri/renderD* for AMD ROCm). That device open is the chokepoint this PR instruments. This is GPU compute, distinct from the existing Privileged Container Device Access rule, which targets raw block/storage devices (/dev/sd, /dev/nvme, /dev/mem) for container escape (T1611) — no overlap with GPU compute devices.

Change

  • list: gpu_device_files/dev/nvidiactl, /dev/nvidia-uvm, /dev/nvidia-uvm-tools, /dev/kfd.
  • macro: open_gpu_device — an open_read/open_write on a gpu_device_files entry, any /dev/nvidia* per-GPU node, or a /dev/dri/renderD* DRM render node.
  • list: gpu_management_binariesnvidia-smi, nvidia-debugdump, nvidia-persistenced, nvidia-cuda-mps-control, rocm-smi, rocminfo.
  • macro: user_known_gpu_workloads (defaults to never_true) — the documented tuning hook for clusters with legitimate ML/HPC/rendering workloads, overridable by image (e.g. container.image.repository in (my_ml_images)).
  • rule: Container Accessing GPU Device (priority CRITICAL, disabled by default) — behavioral signal: a container opens a GPU compute device and is not an allowlisted workload. Shipped disabled because it is noisy until user_known_gpu_workloads is tuned to the images expected to use the GPU.
  • rule: GPU Management Tool Run in Container (priority CRITICAL) — signature signal: GPU fingerprinting tooling runs in a non-allowlisted container.

Conventions mirror the adjacent rules: first tag is the maturity level (maturity_sandbox); container scope, killchain phase (mitre_impact) and TTP (T1496) tags match the sibling stratum and miner-binary rules; both rules use a never_true-backed user_known_* tuning macro and the established evt_type / user / process / proc_exepath / parent / command / terminal output template. The device-open rule deliberately reuses the exact (open_read or open_write) and container and fd.name startswith /dev/... pattern already used by the existing Privileged Container Device Access rule in this same file.

Security rationale

  • MITRE ATT&CK T1496 — Resource Hijacking (Impact tactic). GPU cryptojacking is the same technique as CPU cryptojacking on a far higher-value target. Tagged identically to the existing T1496 rules.
  • Defense-in-depth against evasion. Pairs a behavioral detection (device open — hard to avoid) with a signature detection (management tooling — high precision), consistent with the project's preference for behavioral robustness over command-line string matching.
  • Bounded false-positive surface. Both rules are container-scoped and gated by a single documented user_known_gpu_workloads tuning macro. The behavioral rule ships enabled: false per repo precedent for noisy-by-default rules.

Testing / validation

  • Engine validation against the real Falco engine (the authoritative CI path):
    falco --validate rules/falco-sandbox_rules.yaml in falcosecurity/falco:0.44.1Ok.
  • Runtime firing proof (modern eBPF, capture-on-host). Ran Falco live with the modern_ebpf driver and the rule temporarily enabled, then opened character-device nodes at the matched paths inside a separate container:
    • Container Accessing GPU Device fired CRITICAL on cat /dev/nvidia0 and head -c1 /dev/nvidiactl from a busybox container:
      Critical Container accessing GPU device | device=/dev/nvidia0 image=busybox evt_type=openat ... process=cat ... container_name=gpu-trigger2
    • GPU Management Tool Run in Container fired CRITICAL on executing nvidia-smi inside a container:
      Critical GPU management tool run in container | image=busybox evt_type=execve process=nvidia-smi ...
    • This also confirms the open_read/open_write macros (fd.typechar='f', fd.num>=0) do match successful character-device opens, exactly as the sibling block-device rule relies on.
  • Diff is additive only: 1 file, +81 lines.

Special notes for your reviewer

Submitted as maturity_sandbox per the maturity framework — an experimental detection that pairs a behavioral and a signature signal. Happy to split the two rules, adjust the device/management-binary lists, or change the default-enabled state based on maintainer preference.

…1496)

Add three maturity_sandbox artifacts to falco-sandbox_rules.yaml that detect
GPU/accelerator cryptojacking, closing a gap the existing CPU/network-centric
cryptominer rules do not cover:

- list gpu_device_files + macro open_gpu_device + rule "Container Accessing
  GPU Device" (disabled by default): flags a container opening an NVIDIA/AMD
  compute device (/dev/nvidia*, /dev/nvidiactl, /dev/nvidia-uvm, /dev/kfd,
  /dev/dri/renderD*). The device open is the chokepoint a GPU miner cannot
  avoid even when it renames its binary and hides its pool traffic.
- list gpu_management_binaries + rule "GPU Management Tool Run in Container":
  flags nvidia-smi/rocm-smi and friends running in a non-allowlisted
  container, a common pre-mining reconnaissance step.
- macro user_known_gpu_workloads (never_true) as the documented tuning hook.

Conventions mirror the sibling T1496 rules and the existing "Privileged
Container Device Access" rule, which uses the same (open_read or open_write)
+ fd.name device-prefix pattern. Priority CRITICAL to match sibling T1496
rules; output uses the established bare key=%val template.

Signed-off-by: Devam Shah <devamshah91@gmail.com>
@poiana poiana added the kind/feature New feature or request label Jun 23, 2026
@poiana
poiana requested a review from darryk10 June 23, 2026 07:41
@poiana

poiana commented Jun 23, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: DevamShah
Once this PR has been reviewed and has the lgtm label, please assign darryk10 for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@poiana

poiana commented Jun 23, 2026

Copy link
Copy Markdown

Welcome @DevamShah! It looks like this is your first PR to falcosecurity/rules 🎉

@poiana
poiana requested a review from Kaizhe June 23, 2026 07:41
@poiana poiana added the size/M label Jun 23, 2026

@leogr leogr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread rules/falco-sandbox_rules.yaml Outdated
# by an ML/HPC workload; suspicious when spawned inside a workload that has no
# business touching the accelerator (e.g. a web frontend or a sidecar).
- list: gpu_management_binaries
items: [nvidia-smi, nvidia-debugdump, nvidia-persistenced, nvidia-cuda-mps-control, rocm-smi, rocminfo]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
items: [nvidia-smi, nvidia-debugdump, nvidia-persistenced, nvidia-cuda-mps-control, rocm-smi, rocminfo]
items: [nvidia-smi, nvidia-debugdum, nvidia-persiste, nvidia-cuda-mps, rocm-smi, rocminfo]

proc.name is the kernel task->comm, which is a char comm[16], so 15 usable chars plus the NUL. Three of the six entries are longer than that and can never match:

  • nvidia-debugdump - 16
  • nvidia-persistenced - 19
  • nvidia-cuda-mps-control - 23

nvidia-smi, rocm-smi and rocminfo are fine. So as written the rule silently covers half of the list. Since it is a false negative rather than a false positive, it is the kind of thing that goes unnoticed for a long time.

The suggestion truncates them to their comm form, which is what we already do elsewhere: falco_rules.yaml:585 lists nvidia-installe (i.e., the 16-char nvidia-installer), and :588 has unicorn_launche.

If you prefer to keep the names readable, the alternative is matching on proc.exepath endswith /nvidia-persistenced and friends, since proc.exepath is not truncated and is already in the output template. Either way is fine by me.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and this was the worst kind of bug to ship: three of six entries were dead and the rule would have looked like it was working.

Applied your suggestion verbatim in fa9be60rules/falco-sandbox_rules.yaml:2015 is now:

  items: [nvidia-smi, nvidia-debugdum, nvidia-persiste, nvidia-cuda-mps, rocm-smi, rocminfo]

I took the comm truncation rather than proc.exepath endswith, for two reasons. It matches the in-tree convention you pointed at (nvidia-installe, unicorn_launche), and proc.name on spawned_process is the cheaper field. The readability cost is real, so I added a comment above the list explaining that the names are the char[16] comm form and listing the full binary each one truncates from, specifically so nobody "fixes" them back later.

One thing I could not verify: I do not have NVIDIA hardware, so I have not observed the actual comm value of a running nvidia-persistenced. I am relying on the kernel truncating task->comm at 15 chars, which is what your character counts show. If any of these daemons calls prctl(PR_SET_NAME, ...) and reports something other than the truncated argv[0], the truncated entry would still miss — worth someone with a GPU node confirming before this leaves sandbox maturity.

Comment thread rules/falco-sandbox_rules.yaml Outdated
and container
and not user_known_gpu_workloads
enabled: false
output: Container accessing GPU device | device=%fd.name image=%container.image.repository evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
output: Container accessing GPU device | device=%fd.name image=%container.image.repository evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty
output: Container accessing GPU device | device=%fd.name evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty

Dropping image=%container.image.repository for consistency.

No rule in the tree carries container identity fields in its output. Across the three rule files the only %container.* uses are %container.mounts in Container with sensitive mount started and %container.start_ts in two falco_rules.yaml rules, and in both cases the field is what the rule is actually about.

The style guide asks to keep upstream outputs minimal and to leave ID-style fields to the customization phase 👉 https://falco.org/docs/rules/style-guide/#output-fields

Operators get container metadata downstream anyway, either through append_output (suggested_output: true) or the -pc option.

Also worth noting that the sibling Privileged Container Device Access rule, which this one mirrors, uses exactly device=%fd.name evt_type=... with no image field.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted, applied verbatim in fa9be60 (rules/falco-sandbox_rules.yaml:2042).

The Privileged Container Device Access point is the one that settles it for me — I mirrored that rule's condition but then diverged from its output, which is the inconsistency I should have caught. And the append_output / -pc path means the field was not even buying operators anything they could not already get.

I have left device=%fd.name in, since that is what the rule is actually about, matching the %container.mounts reasoning you describe.

priority: CRITICAL
tags: [maturity_sandbox, container, filesystem, mitre_impact, T1496]

- rule: GPU Management Tool Run in Container

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both rules gate on user_known_gpu_workloads, which ships as (never_true), but only the device-access rule ships enabled: false. Was that deliberate? 🤔

I do not have a good feeling for how often nvidia-smi runs legitimately inside containers in GPU fleets, so I may well be wrong here. However, if the reason for disabling the first rule is "noisy until the macro is tuned", the same seems to apply to this one, since it depends on the very same untuned macro.

wdyt?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate, but on reasoning that does not survive your question. Agreed — both now ship enabled: false as of fa9be60 (rules/falco-sandbox_rules.yaml:2064).

What I was actually thinking: I gated on expected event volume, not on precision. The device-open rule sits on a hot path — every CUDA process opens /dev/nvidiactl and /dev/nvidia-uvm at init, so on any node running real GPU work it produces a continuous stream. The exec of a small named binary set is discrete and comparatively rare, so it felt survivable at default-on.

That distinction is the wrong one. With the macro at never_true neither rule can separate legitimate from illegitimate at all, so both are at 0% precision; volume only changes how fast an operator notices. A low-rate CRITICAL that is always wrong is arguably worse than a high-rate one, because it lingers instead of forcing the tuning. Shipping a rule at CRITICAL that cannot currently be right is not something I should have done on either rule.

On your actual uncertainty — I do not have hard fleet data either, and I would not want the default to rest on my guess. The one concrete thing I can point to is that nvidia-smi inside a container is the documented smoke test for the NVIDIA Container Toolkit (docker run --rm --gpus all ... nvidia-smi), and it shows up in GPU image entrypoints and readiness checks. That alone suggests "rare in containers" was an assumption I had no basis for.

So the honest state is: both rules are sandbox maturity, both disabled, both waiting on user_known_gpu_workloads. If someone running a GPU fleet later reports that nvidia-smi execs are in fact rare in workload containers, flipping the second one back to default-on is a one-line follow-up with evidence behind it, which is the order those two things should have happened in.

Comment thread rules/falco-sandbox_rules.yaml Outdated
and container
and proc.name in (gpu_management_binaries)
and not user_known_gpu_workloads
output: GPU management tool run in container | image=%container.image.repository evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty exe_flags=%evt.arg.flags

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
output: GPU management tool run in container | image=%container.image.repository evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty exe_flags=%evt.arg.flags
output: GPU management tool run in container | evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty exe_flags=%evt.arg.flags

Same here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, applied verbatim in fa9be60 (rules/falco-sandbox_rules.yaml:2065). exe_flags=%evt.arg.flags kept at the end as in your suggestion.

- gpu_management_binaries: truncate entries to their kernel comm form.
  proc.name is task->comm (char[16], 15 usable chars), so nvidia-debugdump
  (16), nvidia-persistenced (19) and nvidia-cuda-mps-control (23) could
  never match and silently made half the list dead. Now nvidia-debugdum,
  nvidia-persiste and nvidia-cuda-mps, matching the existing
  nvidia-installe / unicorn_launche convention in falco_rules.yaml. Added
  a comment so the names are not "corrected" back.

- Both rules: drop image=%container.image.repository from the output. No
  rule in the tree carries container identity fields; the style guide asks
  to keep upstream outputs minimal and leave ID-style fields to the
  customization phase. Operators still get container metadata via
  append_output or -pc. The sibling "Privileged Container Device Access"
  rule this one mirrors uses device=%fd.name evt_type=... with no image.

- GPU Management Tool Run in Container: ship enabled: false. It gates on
  the same never_true user_known_gpu_workloads macro as the device-access
  rule, so the "noisy until the macro is tuned" reasoning applies equally.
  The previous asymmetry was not justified.

Signed-off-by: Devam Shah <devamshah91@gmail.com>
Signed-off-by: devamshah <devamshah91@gmail.com>
@poiana poiana added size/L and removed size/M labels Aug 28, 2026
@DevamShah

Copy link
Copy Markdown
Author

@leogr thanks — all four were right, and the first one was a real defect, not a style nit. Pushed as fa9be60.

What changed

  1. rules/falco-sandbox_rules.yaml:2015gpu_management_binaries truncated to the kernel comm form: [nvidia-smi, nvidia-debugdum, nvidia-persiste, nvidia-cuda-mps, rocm-smi, rocminfo]. Took the truncation over proc.exepath endswith to match the nvidia-installe / unicorn_launche precedent you cited. Added a comment above the list recording which full binary each entry truncates from, so they do not get "corrected" back into dead entries.
  2. :2042 — dropped image=%container.image.repository from Container Accessing GPU Device. Your suggestion applied verbatim.
  3. :2065 — same for GPU Management Tool Run in Container. Applied verbatim.
  4. :2064GPU Management Tool Run in Container now also ships enabled: false. My reasoning for the asymmetry was event volume, not precision, which does not hold up; full answer in that thread.

What I verified, and how

Built and ran the repo's own checker rather than trusting falco --validate alone:

cd build/checker && go build -o rules-check && go test ./... -cover
  -> ok  checker/cmd  0.712s  coverage: 60.9% of statements

build/checker/rules-check validate --falco-image=falcosecurity/falco:$V -r rules/falco-sandbox_rules.yaml
  -> exit 0 for $V in 0.44.1, 0.44.0, 0.43.1, 0.43.0, 0.42.0

As a control that the exit-0 above means something, I pointed the second rule at a nonexistent list name in a scratch copy and re-ran it: exit 1, LOAD_UNUSED_LIST on gpu_management_binaries. So the validator does catch a dangling reference in this file.

./.github/compare-rule-files.sh against falco-sandbox-rules-6.1.0 reports Minor changes — 2 rules, 2 macros, 2 lists added, nothing modified or removed.

On the failing Yamllint check — I could not fix it, and I do not think this PR can

Running the job's exact config locally (yamllint 1.38.0 -c .github/workflows/.yamllint rules/*.yaml):

  • on origin/main: exit 1, 300 errors
  • on this branch: exit 1, 302 errors

So the check is red on main independently of this PR, and it is red on every other open PR I looked at (#363, #364, #367, #374, #379). My contribution to it is exactly 2 errors, both line-length on the two new output: lines. Your suggestions 2 and 3 shortened them from 281/296 to 247/262 characters, but the limit is 130, so they still trip it.

They cannot be brought under 130 without diverging from the file: all 95 output: lines across the three rule files exceed it (min 226, median 286, max 545), and none uses a folded scalar. Making mine the only two folded outputs seemed worse than matching the tree, so I left them single-line.

If it is useful I am happy to open a separate PR that makes the job green repo-wide — most cleanly by relaxing line-length in .github/workflows/.yamllint for these files, since the current 130 is not a limit the rule files have ever observed. Entirely your call — I just did not want to leave a red check looking like it belonged to this PR.

DCO is signed on both commits. Ready for another look.

@DevamShah

Copy link
Copy Markdown
Author

One thing I missed when I pushed fa9be60: none of the CI on it has actually run. All three workflows (Rules, Yamllint, Registry) are sitting at action_required, i.e. held pending maintainer approval for a first-time contributor:

gh api /repos/falcosecurity/rules/commits/fa9be60a/check-runs -q .total_count
  -> 0

Only dco (pass) and tide (pending lgtm/approved) are reporting on the PR. So the red Yamllint result still visible here is the one from d53e0b50, the pre-review commit — nothing has been evaluated against the current head. Flagging it in case you were waiting on checks that will not arrive on their own.

Since the validate job could not run, I ran its work locally against real Falco images rather than leave the current head unverified:

cd build/checker && go build -o rules-check && go test ./... -cover
  -> ok  checker/cmd  0.442s  coverage: 60.9% of statements

./build/checker/rules-check validate --falco-image=falcosecurity/falco:$V -r rules/falco-sandbox_rules.yaml
  -> exit 0 for $V in 0.44.1, 0.43.1, 0.42.0

I did not run master, 0.44.0 or 0.43.0 from .github/FALCO_VERSIONS, so those three are still unverified on my side.

No code changes since fa9be60 — the four review items are applied there and I have nothing further queued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/maturity-sandbox See the Rules Maturity Framework area/rules dco-signoff: yes kind/feature New feature or request size/L

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants