Skip to content

fix(yara): make reverse-shell socket strings span newlines - #593

Open
udsy19 wants to merge 3 commits into
NVIDIA:mainfrom
udsy19:fix/yara-reverse-shell-multiline
Open

udsy19 wants to merge 3 commits into
NVIDIA:mainfrom
udsy19:fix/yara-reverse-shell-multiline

Conversation

@udsy19

@udsy19 udsy19 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #592

What was wrong

The built-in reverse_shell rule's $python_socket and $perl_socket strings (src/skillspector/yara_rules/malware.yar.b64, base64-packaged) use .* between the socket-family constant and the connect call with no dotall modifier:

$python_socket    = /socket\.socket\(.*SOCK_STREAM.*\.connect\(/
$perl_socket      = /use\s+Socket;.*socket\s*\(\s*SOCK/

YARA's . does not match \n by default, so both only match when the whole snippet is on one physical line — the python3 -c '...' / perl -e '...' one-liner form. A reverse shell bundled as an actual script file writes socket.socket(...) and .connect(...) as separate statements, and neither string — nor any other string in the rule — matches it. static_yara reports completed, the scan stays SAFE, --fail-on-incomplete exits 0.

Who reaches this / entry point: every skillspector scan invocation goes through the built-in reverse_shell YARA rule on every scanned artifact — this is the default, always-on detection path, not an opt-in flag. Triggered by: a skill bundle containing a Python (or Perl) source file with a raw socket-based reverse shell written as normal multi-line code, which is how this payload looks when it ships as a file rather than a one-liner.

Fix

Add s (dotall) to both strings, bounded with .{0,200} instead of unbounded .*, so the match still requires the socket call and the connect call to sit within roughly a couple of screens of each other rather than opening an any-file-wide match. Verified this bound does not introduce a false positive: an unrelated socket.socket(...)/.connect(...) pair 200 lines apart in the same file does not match.

Confirmed the regex content is unchanged since the initial release (7ced4fb) and untouched by the base64-repackaging commit (90a9181, #236) — that commit changed only the on-disk encoding, not any rule text.

Testing

$ PYTHONPATH=src .venv/bin/python -m pytest tests/nodes/analyzers/test_static_yara.py -q
87 passed

New test test_reverse_shell_rule_matches_multiline_python_socket builds a realistic multi-line Python reverse shell (base64-encoded fixture, matching this test file's existing convention of not embedding raw malware-signature strings in the source, per the module docstring) and asserts reverse_shell/YR1 fires.

Negative control, reverting only malware.yar.b64 and keeping the new test:

$ PYTHONPATH=src .venv/bin/python -m pytest tests/nodes/analyzers/test_static_yara.py::TestBuiltInMalwarePackaging::test_reverse_shell_rule_matches_multiline_python_socket -q
1 failed

restoring the fix:

$ PYTHONPATH=src .venv/bin/python -m pytest tests/nodes/analyzers/test_static_yara.py::TestBuiltInMalwarePackaging::test_reverse_shell_rule_matches_multiline_python_socket -q
1 passed

ruff check src/ tests/nodes/analyzers/test_static_yara.py and ruff format --check tests/nodes/analyzers/test_static_yara.py: all clean. (malware.yar.b64 is not Python; ruff check src/ tests/ in directory mode correctly skips it, matching how the repo's own make lint runs.)

Note on the diff: because the rule file is base64-packaged, this two-line source change renders as a full-file diff in the .b64 blob. The decoded before/after above is the actual change; nothing else in the rule file moved.

Impact: silent-wrong-result

Signed-off-by: Udaya Tejas udayatejas2004@gmail.com

$python_socket and $perl_socket in the built-in reverse_shell rule
(src/skillspector/yara_rules/malware.yar.b64) use `.*` between the
socket-family constant and the connect call with no dotall modifier.
YARA's `.` does not match `\n` by default, so the pattern only matches
when the whole snippet is on one physical line. A reverse shell
bundled as a script file (rather than passed to `python3 -c '...'` /
`perl -e '...'`) writes socket.socket(...) and .connect(...) as
separate statements and the rule never fires — reverse_shell falls
back to any of the other, unrelated strings in the same rule, so a
plain multi-line Python or Perl socket reverse shell with no other
telltale string is scored SAFE.

Add `s` (dotall) with a bounded {0,200} gap on both strings so the
match still requires the socket call and the connect call to be
close together, rather than opening an unbounded any-file-wide match
that would trade the false negative for a false positive.

Signed-off-by: Udaya Tejas <udayatejas2004@gmail.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Reviewed exact head 6a3cd51f18b09ac0b351c26135ac419ff869efeb. The encoded Python fixture is a real reverse shell and the bounded dot-all spans fix the multiline miss, but the actual YARA match stops at socket creation and connect; none of the fixture's later descriptor redirection or shell execution is required. Consequently an ordinary multiline Python TCP client is now classified as CRITICAL reverse_shell/YR1. The widened Perl expression is even broader—use Socket plus socket creation—and its changed behavior has no Perl regression in this PR.

Require bounded reverse-shell-specific evidence, add ordinary-client negative regressions and real-shell positives for both Python and Perl, and preserve the current encoded-fixture safety convention. PRs #593, #594, and #599 alter the same packaged signatures, so only one corrected implementation should land.

All six hosted checks pass and GitHub reports mergeable/clean, but the detector false positives and missing Perl coverage require changes before approval.

is unambiguously a working reverse shell.
"""
findings = _run_builtin(
_multiline_python_socket_reverse_shell_fixture(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P0] This positive fixture contains shell behavior, but the revised regex succeeds before reaching any of it—an ordinary client ending after connect() would also pass. Add that benign client as a negative regression and make the packaged rule require bounded descriptor-redirection/shell evidence. Add equivalent positive and negative Perl coverage for the simultaneously changed Perl signature.

$python_socket and $perl_socket matched any multiline TCP client that
reached connect() (or, for Perl, just imported Socket and called
socket()), since neither required any evidence the code goes on to
spawn a shell. Bound both strings to require descriptor-redirection or
shell-exec evidence (dup2/subprocess/os.system/pty.spawn/execve for
Python; open(STDIN,...)/exec( for Perl) within 200 chars after
connect(), so an ordinary client no longer classifies as CRITICAL
reverse_shell/YR1.

Adds a benign-client negative test for each language plus a real
multiline Perl reverse-shell positive, since the prior Perl string had
no test coverage at all.

Signed-off-by: Udaya Tejas <udayatejas2004@gmail.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[SkillSpector Review]

Re-reviewed exact head 095f3e51c6b1b2ec949ffda8c58495a16af3798e. The update resolves part of the prior request: plain send/receive clients are now negative controls, and Perl has both a real reverse-shell positive and an ordinary-client negative.

The required false-positive issue is not fully resolved. $python_socket accepts the bare token subprocess (or any os.system / execve) within 200 characters after any .connect(; it does not require descriptor redirection to that socket or a shell payload. An ordinary TCP client that invokes a benign helper subprocess immediately after connecting is therefore still classified as CRITICAL reverse_shell/YR1. $perl_socket has the same structural issue: either any open(STDIN or any exec( after connect is sufficient, rather than requiring redirection to the socket and/or an explicit shell command.

Please bind the corroborating evidence to reverse-shell behavior—for example, socket-backed standard-stream redirection plus a shell execution marker—and add negative regressions for clients that run an unrelated helper or reopen standard input after connecting. Keep the real Python and Perl positives and the encoded-fixture convention. PRs #593, #594, and #599 overlap, so only one corrected implementation should land.

Five hosted checks pass; test-unit is currently pending. The pending check is a merge gate, not the reason for this decision. Static review only; contributor code/tests were not executed locally.

… a bare token

$python_socket accepted any of dup2/subprocess/os.system/pty.spawn/execve as
alternatives, so a client that ran an unrelated helper subprocess right after
connect() (no descriptor redirection to the socket, no shell) still matched.
$perl_socket had the same shape: open(STDIN,...) or exec( alone was enough,
so reopening stdin without ever exec'ing, or exec'ing a non-shell helper,
also matched.

Require both pieces of evidence together instead of either alone: dup2
redirecting a descriptor onto the socket followed by a shell-exec marker
(subprocess.call/Popen/run, os.system, pty.spawn, or execve invoking
something ending in sh) for Python; open(STDIN|STDOUT|STDERR,...) followed
by exec(...) of a shell for Perl.

Adds four negative regressions matching the false positives named in review:
a Python client running an unrelated helper subprocess, a Python client that
reopens stdin without a shell, and the same two shapes in Perl. The existing
positive/negative fixtures for both languages still pass unchanged.

Signed-off-by: Udaya Tejas <udayatejas2004@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in reverse_shell YARA rule misses multi-line Python/Perl socket reverse shells

2 participants