Analysing a suspicious email: verifiable facts only

Faced with a suspicious email, most tools look at the words: «urgent», «account suspended», «click here». Probatio does not, and that is a product decision. An indicator is a fact anyone can check again in the file or in the DNS — a header, a hash, a published record — or it is not an indicator.

Why the wording does not count

The rule was born from a false alarm: a legitimate customer-service email, flagged as suspicious because it carried a «click here» leading to the privacy notice, a plain http link to the shop's home page, the hidden preview text typical of newsletters, and two DKIM signatures, the sender's and the mailing service's. None of these things means anything on its own; added up as clues, they produced a wrong verdict that sounded like a right one.

The bank's genuine notice uses the same words as the fake one: judging by vocabulary is an interpretation, and an interpretation dressed up as a check does not survive cross-examination. For the same reason Probatio keeps no list of brands, does not measure how much a domain «resembles» a famous name, assigns no reputation to top-level domains or hosting providers, and draws no conclusion from a server's reverse name: a dyn or a pool in the name proves nothing. That message now comes out with no findings, and it is one of the module's tests.

Three sources, three weights

The report keeps apart three kinds of information, worth different things.

SourceExamplesWhen it was produced
The messageheaders, server route, links, attachments, DKIM body hashit is in the file and does not change
The recipient's serverAuthentication-Results, Received-SPFat delivery time
Today's checks (optional)DKIM key and SPF/DMARC records in the DNS, reverse name and owner of the IPsnow: the DNS may have changed

For the sender's authenticity the second row is the reference, the verdict reached on arrival; today's checks confirm it or put it in context, they do not replace it.

The route: read Received headers from the bottom up

Every server that takes charge of a message adds a Received line, one of the trace fields of the RFC 5322 format, on top of the headers. The result is a stack with the latest step at the top and the first at the bottom: to rebuild the journey you read from the bottom. Probatio does it for you, numbers the hops in chronological order and, for each one, reports the sending host and IP, the receiving server, protocol, TLS and time.

The headers as they sit in the file How to read them Authentication-Results: mx.example.com; spf=pass; dkim=pass; dmarc=pass Verdict at delivery written by the recipient's server: the reference result Received: from mx.example.com [10.1.0.5] by store.example.com · 10:00:06 Hop 4 · +1 s inside the recipient's network, private IP Received: from out.example.net [203.0.113.40] by mx.example.com · 10:00:05 Hop 3 · +2 s · the IP that delivered SPF is evaluated against this IP (client-ip) DKIM-Signature: v=1; a=rsa-sha256; d=example.net; s=sel2026; bh=…; b=… The sending server's signature the key lives at sel2026._domainkey.example.net Received: from mail.example.net [198.51.100.25] by out.example.net · 10:00:03 Hop 2 · +1 s · first public IP the origin IP: reverse name and owner via RDAP Received: from [192.168.1.20] by mail.example.net · 10:00:02 Hop 1 private IP: the network of whoever wrote the message From: · Return-Path: notices@example.net · <bounce@example.net> The From the reader sees DMARC compares it with SPF and with DKIM's d= bottom up Each server adds its Received line on top: chronological order runs from the bottom up. The lowest lines are written by the sender: they are claims. Addresses and domains are examples.
The first public IP from the bottom is the origin; the last one is the IP that delivered, against which SPF is evaluated. The delivery verdict sits on top.

The timestamp checks are arithmetic: a hop that precedes the one before it by more than five minutes (wrong clocks or hand-inserted lines), a delivery that took more than a day, a Date more than an hour later than arrival on the last server or more than seven days before departure.

Each IP is classified: private, loopback, link-local, CGNAT, reserved, public. The first public IP in chronological order is the origin IP. With network checks on, for the first eight public IPs Probatio reads the reverse name (PTR), checks that it points back to the same IP (forward-confirmed reverse DNS) and asks via RDAP who the block is assigned to: network, organisation, country, abuse contact. Data to report, not to interpret.

One caution that applies to any tool: the lower Received lines were written by the sender or by servers under the sender's control. They are claims; only those added by the recipient's own servers can be relied on.

SPF: at delivery and today

SPF (RFC 7208) answers a precise question: was the IP that delivered the message authorised to send for the domain of the envelope sender, the one in Return-Path? Not the From the user reads: the technical sender.

The delivery verdict sits in the Authentication-Results or Received-SPF headers written by the receiving server. Probatio reads the topmost block — the last one added — and warns when there is more than one, because the ones below could have been written by anybody. An spf=fail counts as a medium finding, a softfail as a low one.

With network checks on, Probatio evaluates SPF again against the record published today. It takes the IP from the client-ip= of Received-SPF or, failing that, from the last hop with a public IP; it takes the domain from Return-Path or, failing that, from the From. It follows include, redirect, a, mx, ip4, ip6 and exists, honours the standard's limit of 10 DNS lookups (beyond it, permerror) and shows the trace of every step, so the result can be redone by hand.

The two results may differ, and the report flags it. The reasons are usually mundane: the domain changed mail provider, the provider changed its addresses, someone fixed a record. That is why today's result is a confirmation, never the primary datum: an SPF that fails today does not prove it failed on the day of delivery.

DKIM: two checks not to confuse

A DKIM signature (RFC 6376) is a DKIM-Signature header added by the sending server. Among other things it carries the signing domain (d=), the selector (s=), the body hash (bh=) and the signature itself (b=). Two different checks follow from it.

  • The body hash is recomputed on the file, with the declared canonicalization (simple or relaxed) and the optional length limit l=. No network is needed and time does not matter: if it matches, the body is the one that was signed; if not, text or attachments changed after signing (innocently too, when a mailing list appends a footer: for list messages the finding drops from medium to low).
  • The signature needs the public key, which lives in the DNS at <selector>._domainkey.<domain>: with s=sel2026 and d=example.net the lookup is sel2026._domainkey.example.net. Probatio verifies rsa-sha256, rsa-sha1 and ed25519-sha256 (RFC 8463) and flags RSA keys below 1024 bits.

The second check is a check of today. Keys rotate, and a key removed from the DNS does not falsify a signature that was valid on the day of delivery: the report writes «the key may have been rotated after sending», not «invalid signature». A record with an empty p= is a key the domain has revoked, and is reported as such.

DMARC: the domain the reader sees

SPF and DKIM authenticate domains the user never sees: the envelope sender and the d= signer. DMARC (RFC 7489) ties them to the From domain, the one the mail client displays, by requiring at least one of them to be aligned with it.

Probatio reports the receiving server's dmarc= result — a fail is the only authentication result that counts as a high finding — and computes itself the alignment of the From with the Return-Path domain and with the DKIM domains, the latter only when the body hash matches. Without the network the comparison is relaxed: the same organisational domain is enough, derived from the Public Suffix List built into the program. So mail.city.milano.it belongs to city.milano.it, and city.milano.it is not the same domain as scam.milano.it, because milano.it is a public suffix.

With the network Probatio looks up the record as the standard prescribes: first at _dmarc. plus the exact From domain, then at _dmarc. plus the organisational domain, and it says where it found it. It shows the p= policy and reads the alignment modes aspf= and adkim=: with no tag, or with r, the relaxed comparison applies; with s (strict) the exact domain is required, and the result is narrowed accordingly. A domain without DMARC is a low finding, because anyone can use it as a sender without recipients noticing.

The report repeats it every time: a DMARC pass says the displayed domain is authentic, not that whoever uses it acts in good faith. A domain registered by the attacker passes perfectly well.

Links: the text and the destination

No link is ever opened: Probatio reads them from the message's code (<a href>, forms, clickable areas, addresses in the text). The central rule concerns a link whose text is itself an address: if the text shows one host and the href goes to another, that is a high finding. The comparison is between hosts, after dropping a leading www.: path, port and parameters do not count, and a subdomain of the same domain, in either direction, is consistent. It is an exact string comparison, with no approximation of the «registrable domain».

Displayed text Real destination (href) Outcome www.bank.example/login host: www.bank.example → https://www.bank.example/area?id=7 host: www.bank.example ✓ consistent same host; path and parameters ignored bank.example host: bank.example → https://secure.bank.example/ host: secure.bank.example ✓ consistent subdomain of the same domain www.bank.example host: www.bank.example → https://evil.test/?u=www.bank.example host: evil.test ✗ flagged: high finding the query string does not count Sign in to your account not an address → https://evil.test/login host: evil.test — no comparison the rule does not apply Only hosts are compared, after dropping www.: equal, or one a subdomain of the other. The link is never opened: the destination is read from the message's code.
Path, port and parameters play no part: in the third row the bank's domain appears in the query string, but the host is evil.test.

The third row is a widespread trick: the destination names the bank's domain in the query string, so that at a glance the address looks right; comparing hosts defeats it. The fourth shows the rule's honest limit: if the text is not an address, there is nothing to compare.

The other facts that can be read from a link: it points to an IP address instead of a name; it contains an @ before the host, so that in https://www.bank.example@evil.test/ the destination is evil.test; it uses the javascript:, data:, vbscript: or file: schemes, or it is the target of a form (all high findings). Counting as medium: a punycode domain (xn--) or one with non-ASCII characters, a link shortener — the fact being that the real destination does not appear in the message — and a link that downloads an executable, a ZIP or an ISO image.

An unencrypted http link is a note, not a clue: plenty of legitimate sites still have them. Remote images are counted and not loaded, because opening them would tell the sender the message was read, when, and from which address.

Attachments

Attachments are examined across the whole tree, including messages forwarded as attachments and Outlook winmail.dat containers. Here too, only facts: a double extension (invoice.pdf.exe), spaces or text-direction characters hiding the real extension, a real type, recognised from the bytes, different from the declared one, extensions that run code, attached web pages, macro-enabled documents, PDFs with JavaScript or /Launch, encrypted ZIP archives no antivirus can inspect, documents that load a template from the Internet when opened.

YARA: «could not look» is not «clean»

With YARA rules on, Probatio downloads them from the configured endpoint and applies them to the whole message and to each attachment (the first 64 MiB of each). A match is a high finding. If the endpoint does not answer, the report states exactly that: a scan that did not run is not a negative scan, and writing them the same way would be a mistake.

The score, and what it does not mean

Findings carry declared weights: high 25, medium 10, low 3; informational notes and positive signals count zero. The sum, capped at 100, gives the verdict: high from 50 up or with at least two high findings, medium from 20 or with one high finding, low above zero. Every finding carries the fact it comes from, so the examiner can check it again.

«No indicators» means the automated checks found no signals, not that the message is safe. The report says so in those words.

Privacy: what leaves the computer

The message is read in an isolated process with no network. Network checks — optional, switched off with a checkbox before the analysis — work on the extracted data alone: domains and IP addresses. DNS queries go through DNS-over-HTTPS, Cloudflare first and Google as fallback; IP ownership lookups through the RDAP service rdap.org. No server named by the message is contacted directly: a query on an attacker's domain still reaches the attacker's DNS server, but through the public resolver, without your IP address.

Without the network you keep the route, receiving-server results, DKIM body hash, links and attachments; you lose the DKIM signature, today's SPF, the DMARC record, reverse names and IP owners.

Limits

  • SPF macros (%{…}) are not evaluated: the record is declared «not evaluable», not guessed. The deprecated ptr mechanism is counted but not evaluated.
  • Today's SPF and DKIM key are not those of delivery; without the network, only the DKIM body hash is verified.
  • Without the network, DMARC alignment is computed in relaxed mode: the aspf/adkim modes are known only by reading the record. The Public Suffix List is the one built into the version in use.
  • ARC headers are not taken into account.
  • Among encrypted archives only ZIPs are recognised: a password-protected 7z or RAR is not flagged as such.
  • An MSG file without the original transport headers does not allow route and authentication to be analysed: you need the .eml exported from the mailbox.
  • The meaning of the text is not analysed. That is a choice, not a gap.

The module is described on the Phishing analysis page; for the structure of .eml, .emlx and .msg files see the guide to email file formats.

A report built this way says fewer things than one that judges the wording. But each of them can be redone: with the headers in front of you, with a DNS query, with a hash calculation. That is what makes it usable in an expert report.