Skip to main content
Sigvex

Red Team / Blue Team EVM · Part 5: Blue Team: From Bytecode to Triaged Findings

How an attacker's checklist becomes an automated detector — and why the hard part is not finding suspicious patterns but deciding which findings deserve a human's next hour.

Red Team / Blue Team EVM · Part 5: Blue Team: From Bytecode to Triaged Findings

EVM Red Team / Blue Team — a six-part path from reading a stranger’s deployed bytecode to shipping a contract that survives contact with an attacker.

  1. Read a Deployed Contract Like an Attacker
  2. Mapping the Attack Surface
  3. The Bugs That Move Money
  4. Owning the Contract: Access Control and Upgrades
  5. Blue Team: From Bytecode to Triaged Findings
  6. Blue Team: Hardening and Holding the Line

Four posts ago you started reading deployed bytecode the way an attacker does; by Part 3 and Part 4 you had a checklist of shapes worth hunting for. Here is the pivot to defense, and it starts with a simple observation: a checklist is a specification. Everything the attacker looks for by hand, a detector can look for automatically — across every function, every path, every contract you care about. The hard part is not finding suspicious patterns. It is deciding which of the thousand matches deserve a human’s next hour.

The attacker’s map, automated

Take the shape from Part 3: a function checks a stored balance, makes an external call, and only then writes the balance back. Reading it off the control-flow graph by hand, you looked for a CALL followed by an SSTORE to a slot that was read before the call. A detector does exactly that, exactly once per code path, without getting bored:

  • Control-flow analysis rebuilds the graph of basic blocks and asks ordering questions. Is there a path where an external call happens before the state write that should have preceded it? Is there an entry point with no ownership check on the way to SELFDESTRUCT or DELEGATECALL?
  • Data-flow analysis and taint tracking follow values instead of paths. Mark everything derived from calldata as untrusted, propagate that mark through arithmetic, memory, and storage, and flag when a tainted value reaches a sensitive sink: the target of a delegatecall, the recipient of a value transfer, the slot that holds the admin address.
  • Symbolic reasoning goes one step further and asks whether the flagged path is actually reachable — whether some input satisfies every branch condition between the entry point and the sink. That machinery has its own trade-offs, covered in symbolic execution for smart contracts.

None of this needs source. The signals from Parts 1–4 — dispatch tables, storage-slot access patterns, call/write ordering — are all bytecode-visible, which is the whole reason they generalize to the unverified majority of the chain.

The false-positive problem

Here is where most scanners die, and it is the core of this post: on real contracts, the naive version of every check above fires constantly, and almost every firing is wrong.

The “state write after external call” pattern is the canonical example. Match it literally and you will flag:

  • Reentrancy guards themselves. A mutex-style guard sets a flag, makes the external call, then clears the flag — a state write after an external call, by design. The pattern that prevents reentrancy looks exactly like the bug when you only look at ordering.
  • Token wrapper libraries. Widely used safe-transfer helpers wrap every token interaction in a low-level call and then check the result. Contracts inlined with them are full of external calls followed by bookkeeping writes that no attacker-controlled contract will ever intercept, because the call target is a known token, not arbitrary.
  • Proxies. A proxy contract’s entire job is to load an address from storage and delegatecall into it. “Tainted storage value flows into delegatecall target” is a critical finding on a vault and the design spec on a proxy. A detector that cannot tell the difference is useless on roughly every upgradeable contract in existence.
  • Compiler idioms. Solidity emits recognizable boilerplate — value cleanup masks, dispatcher comparisons, the overflow checks that 0.8+ inserts around arithmetic — that shallow pattern matchers misread as suspicious masking or redundant checks.

The lesson: a good detector is defined at least as much by what it deliberately stays silent about as by what it flags. Suppressing the reentrancy-guard shape, recognizing inlined library code, and classifying proxy dispatch before flagging delegatecall are not compromises bolted on after the fact. They are the detector. A scanner without them produces a report nobody reads, and a report nobody reads protects nothing.

Severity and confidence are different axes

The second discipline is refusing to compress two questions into one number.

Severity asks: if this finding is real, how bad is it? An unprotected upgrade function is critical — whole-contract takeover. A missing zero-address check on a setter is low — annoying, recoverable.

Confidence asks: how sure are we that it is real? A delegatecall whose target provably comes from unchecked calldata is near-certain. A state-write-after-call where the call target might be a fixed, trusted token is a genuine maybe.

Conflate them and you get noise in both directions. Rank purely by severity and your queue fills with low-confidence criticals that are mostly proxy idioms — cry wolf for a week and the auditor stops reading. Rank purely by confidence and a shaky-but-plausible takeover path sinks below a rock-solid style nit. The uncomfortable rule that falls out: a low-confidence critical still deserves a human look, precisely because the cost of it being real is unbounded and the cost of checking is minutes. You can deprioritize a low-confidence low. You cannot silently drop a low-confidence critical.

Grounding: a finding you can’t trace is a guess

Every finding should carry its evidence: the specific entry point, the call site, the storage slot involved, the path from untrusted input to sink. Not “possible reentrancy in this contract” but “this externally callable function reads this slot, calls out at this site, and writes the same slot afterward — here is the path.”

The test is practical. Handed the finding, can a reviewer confirm or dismiss it in minutes by looking at the cited bytecode locations? If yes, the finding is grounded. If the reviewer has to redo the analysis from scratch to figure out what the tool even meant, the tool has produced a hypothesis, not a finding — and hypotheses do not scale past the first page of the report.

Confidence has to be earned, not asserted

One more honesty requirement. When a tool attaches a confidence score to a finding, that number is a claim about the future: “findings like this one turn out to be real this often.” The only legitimate source for such a claim is measured history — run the detector across cases where the truth is known, count true and false positives, and let the observed accuracy set the tier. A detector that has been right on every known example of its pattern has earned “high.” A new detector with no track record has earned nothing, however clever its logic, and should say so.

A precise-looking score that was never measured is worse than no score, because it borrows the authority of measurement without doing the work. This is the discipline Sigvex builds its findings around: confidence tiers backed by recorded accuracy on known-outcome cases, adjusted as evidence accumulates — never a number typed in because it felt right.

A triage workflow you can adopt

Given a report with grounded findings, severity, and honest confidence, triage becomes mechanical:

  1. Sort by severity × confidence. High-confidence criticals first; the low-confidence criticals go next, not last.
  2. Verify grounding before reasoning. Jump to the cited path. If the evidence doesn’t show what the finding claims, dismiss it and note why — that note is calibration data.
  3. Dismiss the idioms explicitly. Guard patterns, inlined library calls, proxy dispatch. Record the dismissal reason rather than deleting silently, so the same idiom stops resurfacing.
  4. Escalate what survives. Anything still standing gets a human deep-dive: reconstruct the attack the way Parts 3–4 taught you, and decide whether it moves money.

What that deep-dive should produce — the concrete fixes, the guard patterns done right, and how to keep a hardened contract hardened after the audit ends — is Part 6: Hardening and Holding the Line.