new(rules): detect GPU/accelerator cryptojacking and device access (T1496) - #373
new(rules): detect GPU/accelerator cryptojacking and device access (T1496)#373DevamShah wants to merge 2 commits into
Conversation
…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>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: DevamShah The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
Welcome @DevamShah! It looks like this is your first PR to falcosecurity/rules 🎉 |
leogr
left a comment
There was a problem hiding this comment.
Reviewed with https://github.com/leogr/falco-expert
| # 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] |
There was a problem hiding this comment.
| 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- 16nvidia-persistenced- 19nvidia-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.
There was a problem hiding this comment.
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 fa9be60 — rules/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.
| 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 |
There was a problem hiding this comment.
| 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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
| 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.
There was a problem hiding this comment.
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>
|
@leogr thanks — all four were right, and the first one was a real defect, not a style nit. Pushed as fa9be60. What changed
What I verified, and how Built and ran the repo's own checker rather than trusting 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,
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 (
So the check is red on They cannot be brought under 130 without diverging from the file: all 95 If it is useful I am happy to open a separate PR that makes the job green repo-wide — most cleanly by relaxing DCO is signed on both commits. Ready for another look. |
|
One thing I missed when I pushed Only Since the I did not run No code changes since |
/kind feature
/area rules
/area maturity-sandbox
What type of PR is this?
Any specific area of the project related to this PR?
What this PR does / why we need it
Adds three
maturity_sandboxartifacts torules/falco-sandbox_rules.yamlthat 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.yamlis CPU- and network-centric:proc.cmdlineforstratum+tcp/stratum+ssl.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
stratumURI 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-uvmfor CUDA;/dev/kfdand/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— anopen_read/open_writeon agpu_device_filesentry, any/dev/nvidia*per-GPU node, or a/dev/dri/renderD*DRM render node.list: gpu_management_binaries—nvidia-smi,nvidia-debugdump,nvidia-persistenced,nvidia-cuda-mps-control,rocm-smi,rocminfo.macro: user_known_gpu_workloads(defaults tonever_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(priorityCRITICAL, disabled by default) — behavioral signal: a container opens a GPU compute device and is not an allowlisted workload. Shipped disabled because it is noisy untiluser_known_gpu_workloadsis tuned to the images expected to use the GPU.rule: GPU Management Tool Run in Container(priorityCRITICAL) — signature signal: GPU fingerprinting tooling runs in a non-allowlisted container.Conventions mirror the adjacent rules: first tag is the maturity level (
maturity_sandbox);containerscope, killchain phase (mitre_impact) and TTP (T1496) tags match the sibling stratum and miner-binary rules; both rules use anever_true-backeduser_known_*tuning macro and the establishedevt_type / user / process / proc_exepath / parent / command / terminaloutput 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
user_known_gpu_workloadstuning macro. The behavioral rule shipsenabled: falseper repo precedent for noisy-by-default rules.Testing / validation
falco --validate rules/falco-sandbox_rules.yamlinfalcosecurity/falco:0.44.1→Ok.modern_ebpfdriver and the rule temporarily enabled, then opened character-device nodes at the matched paths inside a separate container:Container Accessing GPU Devicefired CRITICAL oncat /dev/nvidia0andhead -c1 /dev/nvidiactlfrom abusyboxcontainer:Critical Container accessing GPU device | device=/dev/nvidia0 image=busybox evt_type=openat ... process=cat ... container_name=gpu-trigger2GPU Management Tool Run in Containerfired CRITICAL on executingnvidia-smiinside a container:Critical GPU management tool run in container | image=busybox evt_type=execve process=nvidia-smi ...open_read/open_writemacros (fd.typechar='f',fd.num>=0) do match successful character-device opens, exactly as the sibling block-device rule relies on.Special notes for your reviewer
Submitted as
maturity_sandboxper 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.