Ptrace attach or cross-process memory write on Linux
Matches the two syscalls that carry ptrace-based process injection's actual memory-write primitives: `ptrace`, which covers both the `PTRACE_ATTACH`/ `PTRACE_SEIZE` step and the `PTRACE_POKETEXT`/`PTRACE_POKEDATA`/`PTRACE_SETREGS` calls AN0579 names, and `process_vm_writev`, the newer cross-process memory API that debuggers and injection tooling increasingly use instead of poke-based ptrace because it moves more data per call. `mmap`, the third syscall the brief's logsource note lists, is deliberately left out: every process calls it continually as part of ordinary memory allocation and dynamic linking, so gating on it would make the selection effectively always-true rather than a signal. What this cannot express: AN0579's `TargetProcessNameFilter`, `UserContextMismatch` and `ProcessRelationshipConstraint` knobs all require knowing which process was ptraced and comparing its identity, UID or parent relationship to the tracer's — that identity lives in the syscall's `a1` argument (the target PID) and would need a join against a process table to resolve, which a single auditd SYSCALL record cannot supply. The specific ptrace request (`PTRACE_POKEDATA` vs. read-only `PTRACE_PEEKTEXT` vs. plain `PTRACE_ATTACH`) sits in `a0` as a raw integer that this rule does not decode, so a benign `strace -p` attach and an actual poke-based injection produce the same match. MITRE's `TimeWindowBetweenPtraceAndMemoryWrite` knob is a two-event correlation lib/sigma has no timeframe construct for. The brief's other two logsources are not attempted: `linux:osquery` has no standardised Sigma field vocabulary in this corpus (the same reason `macos:unifiedlog` was skipped for T1027.015's AN0784 — inventing field names would render in four backends and match nothing), and the `process_creation` execve logsource would only see the debugger binary being launched, which is a strictly weaker signal than the syscalls it goes on to make. Prerequisite: neither syscall is audited by default on any major distribution. This rule returns zero rows — meaning blind, not quiet — until a rule such as `-a always,exit -F arch=b64 -S ptrace -S process_vm_writev -k ptrace_inject` is loaded. Second prerequisite specific to the `syscall` field: raw, uninterpreted auditd output records `syscall=` as the architecture-dependent kernel syscall number (`ptrace` is 101 and `process_vm_writev` is 311 on x86_64; both differ on arm64), not the name string this selection matches against. Name resolution is not universal — Elastic's Auditbeat resolves it by default, but a plain-text `audit.log` tail, `ausearch` without `-i`, and Wazuh's default auditd field extraction all pass the number through unresolved, so on those collectors this selection never fires until an interpreting layer (`ausearch -i`, `laurel`, or an equivalent enrichment pipeline) sits in front of it. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0203, not hand-written and not tested by its author. Any lab result is on this rule's own page.Full descriptionShow less
The detection
The 3 SIEM queries below are previews produced by Siemphony’s own translator, not pySigma, and none has been executed against a real backend — review before you deploy.
detection.yml
title: Ptrace attach or cross-process memory write on Linuxid: a0f849a7-c77f-4da5-a26b-0e7e3a06afa3status: experimentaldescription: | Matches the two syscalls that carry ptrace-based process injection's actual memory-write primitives: `ptrace`, which covers both the `PTRACE_ATTACH`/ `PTRACE_SEIZE` step and the `PTRACE_POKETEXT`/`PTRACE_POKEDATA`/`PTRACE_SETREGS` calls AN0579 names, and `process_vm_writev`, the newer cross-process memory API that debuggers and injection tooling increasingly use instead of poke-based ptrace because it moves more data per call. `mmap`, the third syscall the brief's logsource note lists, is deliberately left out: every process calls it continually as part of ordinary memory allocation and dynamic linking, so gating on it would make the selection effectively always-true rather than a signal. What this cannot express: AN0579's `TargetProcessNameFilter`, `UserContextMismatch` and `ProcessRelationshipConstraint` knobs all require knowing which process was ptraced and comparing its identity, UID or parent relationship to the tracer's — that identity lives in the syscall's `a1` argument (the target PID) and would need a join against a process table to resolve, which a single auditd SYSCALL record cannot supply. The specific ptrace request (`PTRACE_POKEDATA` vs. read-only `PTRACE_PEEKTEXT` vs. plain `PTRACE_ATTACH`) sits in `a0` as a raw integer that this rule does not decode, so a benign `strace -p` attach and an actual poke-based injection produce the same match. MITRE's `TimeWindowBetweenPtraceAndMemoryWrite` knob is a two-event correlation lib/sigma has no timeframe construct for. The brief's other two logsources are not attempted: `linux:osquery` has no standardised Sigma field vocabulary in this corpus (the same reason `macos:unifiedlog` was skipped for T1027.015's AN0784 — inventing field names would render in four backends and match nothing), and the `process_creation` execve logsource would only see the debugger binary being launched, which is a strictly weaker signal than the syscalls it goes on to make. Prerequisite: neither syscall is audited by default on any major distribution. This rule returns zero rows — meaning blind, not quiet — until a rule such as `-a always,exit -F arch=b64 -S ptrace -S process_vm_writev -k ptrace_inject` is loaded. Second prerequisite specific to the `syscall` field: raw, uninterpreted auditd output records `syscall=` as the architecture-dependent kernel syscall number (`ptrace` is 101 and `process_vm_writev` is 311 on x86_64; both differ on arm64), not the name string this selection matches against. Name resolution is not universal — Elastic's Auditbeat resolves it by default, but a plain-text `audit.log` tail, `ausearch` without `-i`, and Wazuh's default auditd field extraction all pass the number through unresolved, so on those collectors this selection never fires until an interpreting layer (`ausearch -i`, `laurel`, or an equivalent enrichment pipeline) sits in front of it. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0203, not hand-written and not tested by its author. Any lab result is on this rule's own page.references: - https://attack.mitre.org/techniques/T1055/008 - https://attack.mitre.org/detectionstrategies/DET0203author: Siemphony pipeline (machine-derived from MITRE ATT&CK v19.2)date: 2026-08-18tags: - attack.defense-evasion - attack.privilege-escalation - attack.t1055.008logsource: product: linux service: auditddetection: selection_ptrace: type: 'SYSCALL' syscall: 'ptrace' selection_memwrite: type: 'SYSCALL' syscall: 'process_vm_writev' condition: 1 of selection*falsepositives: - "Interactive debugging and profiling on development or build hosts: gdb, strace, ltrace and valgrind all attach via ptrace, and modern gdb uses process_vm_writev to poke memory, so a developer debugging a crash produces the same syscalls as the technique." - "Sandboxed and gVisor-style container runtimes (runsc) that intercept every syscall of every process they run via ptrace, meaning one or more matches per contained process on any host using that runtime, not just at injection time." - "Crash and coredump handling: systemd-coredump and similar collectors attach via ptrace to a crashing process to capture its state before it exits, which fires on every application crash on the host."level: lowSplunk · SPL
Run this as a search.
index=* ((type="SYSCALL" AND syscall="ptrace") OR (type="SYSCALL" AND syscall="process_vm_writev"))Elastic · ES|QL
Run this as a search.
FROM logs-*| WHERE ((TO_LOWER(type) == "syscall" AND TO_LOWER(syscall) == "ptrace") OR (TO_LOWER(type) == "syscall" AND TO_LOWER(syscall) == "process_vm_writev"))
Wazuh · XML rule
Deploy to your manager — this is a rule, not a search.
<group name="sigma,linux,"> <!-- Rule ids must be unique on your manager; 100000+ is the user range. --> <!-- 2 rules: the Sigma condition ORs across different fields, which one rule cannot express. Any one matching is a hit. --> <rule id="100000" level="5"> <!-- Set <if_sid> to the decoder/base rule for linux so this only evaluates relevant events. --> <field name="type" type="pcre2">(?i)^SYSCALL$</field> <field name="syscall" type="pcre2">(?i)^ptrace$</field> <description>Ptrace attach or cross-process memory write on Linux (1/2)</description> <mitre> <id>T1055.008</id> </mitre> </rule> <rule id="100001" level="5"> <!-- Set <if_sid> to the decoder/base rule for linux so this only evaluates relevant events. --> <field name="type" type="pcre2">(?i)^SYSCALL$</field> <field name="syscall" type="pcre2">(?i)^process_vm_writev$</field> <description>Ptrace attach or cross-process memory write on Linux (2/2)</description> <mitre> <id>T1055.008</id> </mitre> </rule></group>
Sentinel · KQL
Run this as a search.
Sentinel cannot express this construct. No Sentinel table is modelled for this logsource ({"product":"linux","service":"auditd"}), so there is nothing honest to query. Pick a logsource this translator maps, or write the KQL by hand. The Sigma source is on the first tab, unchanged.
Verdicts · reactions · comments
Community
Verdicts from engineers who actually deployed it, and the conversation around it.
Nobody has run this in a real environment and said what happened.
Verdicts from engineers who deployed it
0 castNo verdicts reported yet. The first one is worth more than the tenth — say what you ran it against.
Log in to report a verdict.
Log in to join the discussion.
No comments yet. Someone who deploys this will have something to say about it.