Skip to content

False positive: exploit_framework's $rop_chain matches every Rust impl Drop (fn drop(&mut self)) #605

Closed

Description

@dalekurt

Summary

exploit_framework's $rop_chain string matches Rust's destructor signature, so every impl Drop in every Rust codebase is reported as an exploit framework at HIGH severity.

src/skillspector/yara_rules/hacktools.yar:83:

$rop_chain     = /ROP\s*\(.*elf\)/ nocase

The string is clearly meant for pwntools' ROP(elf). But nocase with no word boundary before ROP lets it match the interior of fn drop(&mut self):

pattern consumes
ROP\s*\( the tail of drop(
.* &mut s
elf\) self)

Since the rule's condition is any of them, this one string alone fires the whole HIGH/0.8-confidence rule.

Reproduction

A nine-line Rust file containing nothing but a textbook RAII destructor:

pub struct TempDir {
    path: std::path::PathBuf,
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.path);
    }
}
import yara
rules = yara.compile(source=r'''
rule exploit_framework {
    strings:
        $rop_chain = /ROP\s*\(.*elf\)/ nocase
    condition:
        any of them
}
''')
for m in rules.match('drop.rs'):
    for s in m.strings:
        for inst in s.instances:
            print(m.rule, s.identifier, inst.matched_data)
exploit_framework $rop_chain b'rop(&mut self)'

Impact

We run SkillSpector as a blocking security gate in CI, pinned at 939da7d41eed4282e4d8217fe2254c69f690027e. Across two Rust marketplace plugins, 14 of 14 exploit_framework hits were this, and not one was a real finding. Every one was an ordinary destructor: impl Drop for TempDir removing its own temp dir, impl Drop for Browser reaping a child process it spawned, impl Drop for KeypressSession restoring termios.

Because the finding is per-occurrence, it had already cost us twelve hand-written waivers, and four of those had gone stale as the destructors moved. That is what prompted digging into the rule itself rather than waiving occurrence number thirteen.

This is the same shape as #487 (php_webshell_known firing on the German word "behindert"): a nocase substring with no word boundary matching inside an unrelated word.

Suggested fix

A word boundary is enough, and keeps nocase:

$rop_chain     = /\bROP\s*\(.*elf\)/ nocase

\b fails inside drop( because the preceding d is a word character. Dropping nocase for this string would also work, since pwntools is always written ROP(...), and would be the more faithful reading of the intent.

Worth noting separately that .* is unanchored here, so $rop_chain can also span a long stretch of unrelated text between a ROP( and a later elf). Tightening it to something like [^\n)]* would narrow that, though the word boundary alone resolves the case reported here.

Happy to send a PR with the fix and a regression fixture if that is useful.

Activity

  1. added a commit that references this issue on Sep 23, 2026
    eca8df3
  2. dalekurt commented on Sep 23, 2026

    @dalekurt
    Author

    Confirmed fixed, thanks. Verified independently against yara-python: the 9-line Rust impl Drop repro no longer matches under /\bROP\s*\(.*elf\)/, and ROP(elf) still does, so the precision gain costs no detection.

    For context on the impact, this rule accounted for 14 of 14 exploit_framework hits in our scan of two Rust workspaces, none of them real, and had cost us twelve hand-written waivers before we traced it to the rule itself.

    Appreciate the quick turnaround and the regression tests.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions