Skip to content
academia.sh

Lesson 08 / 22

Phishing Types

Counting ten indicator types in a fictional set of twenty-six messages: the gap in average indicator count across bulk, spear, and whaling messages, the opposite directions of what a threshold scan misses versus what it falsely flags, legitimate messages flagged independent of any threshold, and the early-break gain of a five-step chain.

Published Updated

In this lesson

  • Counts distinguishing indicator types across a message set and derives the per-class average
  • Runs a threshold scan and reads the missed count together with the false-positive count
  • Shows that indicator count falls as the target narrows, and where the threshold collapses
Contents

The previous lesson assumed the request had already reached the decision-maker and passed over the chain’s third link — making contact — in a single line. Yet the message that contact carries is itself a measurable object: it carries countable indicators, and those indicators can be summed into a single number. This lesson produces that number and records, together, what the chosen threshold misses and what it falsely flags.

What is measured is not the message’s text. Text varies endlessly; indicator type does not. A sender coming from a domain never seen before, a sender verification record that fails, a link target that diverges from the displayed text — each of these is an item a control can see. How sender verification records are set up was measured in a different course; here, that record’s result is only one of ten indicators. The real question is this: why does phishing carry fewer indicators as its target narrows, and what does that do to the threshold?

  • SE7. The message set belongs to the field-crew mailbox of the fictional regional measurement network. No real message exists; the model carries only indicator type and count — no message text, subject line, or link ever appears.
  • SE8. Every message’s true class is known in the model. In reality, class is known only after review; it is assumed known here so that missed and falsely flagged messages can be counted separately.
  • SE9. Indicator weights are fixed, and the score is the sum of the weights. In real systems weight is learned and drifts; a fixed weight simplifies the curve’s shape and does not change the direction of either error.
  • SE10. The set is not realistic in class ratio: there are sixteen fake and ten legitimate messages. In a real inbox the number of legitimate messages is far larger, and the number of false positives grows in the same proportion. The model therefore understates the false-positive rate.
  • SE11. A message is either flagged or it passes. Intermediate states such as quarantine, a warning banner, or asking the user are not modeled.
  • SE12. The chain was reduced to a single pattern: delivery, interaction, credential entry, use. Variants that proceed with an attachment or that directly request payment were not counted as separate chains.

Indicator Types and Classes

// messages.mjs — indicator types and each message's indicator set; model (no message content)
export const INDICATOR = { // indicator type: distinguishing weight
  'sender domain seen for the first time': 3, 'sender verification record fails': 3,
  'sender domain resembles a protected name': 3, 'link target diverges from displayed text': 3,
  'reply address differs from sender': 2, 'unexpected attachment type': 2,
  'wide recipient set': 1, 'unusual send time': 1,
  'first contact outside any reply thread': 1, 'time-pressure phrasing': 1,
};
const [F, V, R, L, P, A, W, U, C, T] = Object.keys(INDICATOR);
export const CLASS = ['bulk', 'spear', 'whaling', 'legitimate'];
// [message id, class, indicators it carries]
export const MESSAGE = [
  ['B1', 'bulk', [F, V, L, P, W, U, T]], ['B2', 'bulk', [F, V, L, A, W, T]],
  ['B3', 'bulk', [F, V, P, W, C, T]], ['B4', 'bulk', [F, R, L, W, U, T]],
  ['B5', 'bulk', [F, V, L, W, T]], ['B6', 'bulk', [V, L, P, W, U]],
  ['B7', 'bulk', [F, V, A, W, C, T]], ['B8', 'bulk', [F, L, W, U, C, T]],
  ['S1', 'spear', [R, L, C, T]], ['S2', 'spear', [F, L, C]],
  ['S3', 'spear', [R, P, T]], ['S4', 'spear', [V, L]], ['S5', 'spear', [R, A, C]],
  ['W1', 'whaling', [R, T]], ['W2', 'whaling', [L]], ['W3', 'whaling', [R, C]],
  ['L1', 'legitimate', [C]], ['L2', 'legitimate', [W, U]], ['L3', 'legitimate', []],
  ['L4', 'legitimate', [F, U, C]], ['L5', 'legitimate', [A]], ['L6', 'legitimate', [U, T]],
  ['L7', 'legitimate', [P]], ['L8', 'legitimate', [W, U, C, T]],
  ['L9', 'legitimate', [F, V, L, P, W]], ['L10', 'legitimate', [F, V, P, C]],
];
export const score = (m) => m[2].reduce((a, g) => a + INDICATOR[g], 0);
export const isFake = (m) => m[1] !== 'legitimate';
export function atThreshold(e) {
  const flagged = MESSAGE.filter((m) => score(m) >= e);
  return { missed: MESSAGE.filter((m) => isFake(m) && score(m) < e), falsePositives: flagged.filter((m) => !isFake(m)),
    caught: flagged.filter(isFake) };
}
// [step, precondition, control that sees the step, item dropped once the step completes]
export const CHAIN = [
  ['preparing the message', 'target address list', null, 0],
  ['delivery', 'a valid mail path', 'indicator threshold', 1],
  ['opening and interaction', "the recipient's interest", 'user reporting', 1],
  ['credential entry', 'a convincing collection point', 'second factor', 3],
  ['use of the collected information', 'a valid session', 'anomalous session detection', 6],
];
// run1.mjs — per-indicator class distribution and the indicator-count gap across three attack types
import { INDICATOR, MESSAGE, CLASS, score, isFake } from './messages.mjs';

console.log(`${MESSAGE.length} messages, ${MESSAGE.filter(isFake).length} fake; ${Object.keys(INDICATOR).length} indicator types, ` +
  `highest possible score ${Object.values(INDICATOR).reduce((a, b) => a + b, 0)}\n`);
console.log(`${'indicator'.padEnd(40)} ${'wt.'.padStart(3)} ${'bulk'.padStart(5)} ${'spear'.padStart(7)} ` +
  `${'whaling'.padStart(8)} ${'legitimate'.padStart(10)}`);
for (const [g, a] of Object.entries(INDICATOR)) {
  const c = CLASS.map((s) => MESSAGE.filter((m) => m[1] === s && m[2].includes(g)).length);
  console.log(`${g.padEnd(40)} ${String(a).padStart(3)} ${String(c[0]).padStart(5)} ${String(c[1]).padStart(7)} ` +
    `${String(c[2]).padStart(8)} ${String(c[3]).padStart(10)}`);
}
console.log(`\n${'class'.padEnd(10)} ${'messages'.padStart(8)} ${'avg. indicators'.padStart(15)} ${'avg. score'.padStart(10)} ` +
  `${'min score'.padStart(13)} ${'max score'.padStart(14)}`);
for (const s of CLASS) {
  const k = MESSAGE.filter((m) => m[1] === s), p = k.map(score);
  console.log(`${s.padEnd(10)} ${String(k.length).padStart(8)} ` +
    `${(k.reduce((a, m) => a + m[2].length, 0) / k.length).toFixed(2).padStart(15)} ` +
    `${(p.reduce((a, b) => a + b, 0) / k.length).toFixed(2).padStart(10)} ${String(Math.min(...p)).padStart(13)} ` +
    `${String(Math.max(...p)).padStart(14)}`);
}
const whl = MESSAGE.filter((m) => m[1] === 'whaling'), leg = MESSAGE.filter((m) => m[1] === 'legitimate');
console.log(`\naverage indicator count of whaling messages (${(whl.reduce((a, m) => a + m[2].length, 0) / whl.length).toFixed(2)}), ` +
  `lower than that of legitimate messages (${(leg.reduce((a, m) => a + m[2].length, 0) / leg.length).toFixed(2)})`);
console.log(`4 highest-scoring messages: ${[...MESSAGE].sort((a, b) => score(b) - score(a)).slice(0, 4).map((m) => `${m[0]}/${m[1]} (${score(m)})`).join(', ')}`);
26 messages, 16 fake; 10 indicator types, highest possible score 20

indicator                                wt.  bulk   spear  whaling legitimate
sender domain seen for the first time      3     7       1        0          3
sender verification record fails           3     6       1        0          2
sender domain resembles a protected name   3     1       3        2          0
link target diverges from displayed text   3     6       3        1          1
reply address differs from sender          2     3       1        0          3
unexpected attachment type                 2     2       1        0          1
wide recipient set                         1     8       0        0          3
unusual send time                          1     4       0        0          4
first contact outside any reply thread     1     3       3        1          4
time-pressure phrasing                     1     7       2        1          2

class      messages avg. indicators avg. score     min score      max score
bulk              8            5.88      11.50            10             14
spear             5            3.00       6.60             6              8
whaling           3            1.67       3.67             3              4
legitimate       10            2.30       3.90             0             12

average indicator count of whaling messages (1.67), lower than that of legitimate messages (2.30)
4 highest-scoring messages: B1/bulk (14), B2/bulk (13), B4/bulk (12), L9/legitimate (12)

Read row by row, the indicator table shows which item belongs to which class. A wide recipient set appears in eight of eight bulk messages and in none of the spear or whaling messages; a sender domain that resembles a protected name runs the other way — it appears in one bulk message and in five narrowly targeted ones. Cheap indicators are a scale signal; expensive indicators are a preparation signal.

The class table gives the lesson’s central number. Bulk phishing carries 5.88 indicators per message, spear phishing 3.00, whaling 1.67. The drop is not chance; it is the definition itself: as the target narrows, cheap indicators such as a wide recipient set, an unusual hour, and generic sending infrastructure disappear. What remains is only the expensive indicators, and there are few of those too.

The last line shows the measure’s limit. The average indicator count of whaling messages is 1.67, that of legitimate messages 2.30. A fake message carries fewer indicators than a legitimate one. No threshold that looks only at indicator count can separate these two sets in the right direction; a threshold that tries flags legitimate messages before it flags fake ones.

The top of the score ranking shows the same problem on the weight side. Fourth place is a legitimate message, sitting at twelve points in the same band as three bulk phishing messages. This message is a subscriber notice sent through an external delivery service: it carries a domain seen for the first time, a verification record that fails, a reply address that diverges from the sender, and a link target that diverges from the displayed text. Every one of these indicators is counted correctly; the message not being fake does not change that.

Where the Threshold Collapses

// run2.mjs — threshold scan, structural false positives, and the chain's early-break gain
import { MESSAGE, CHAIN, score, isFake, atThreshold } from './messages.mjs';

const FAKE = MESSAGE.filter(isFake).length;
console.log(`${'threshold'.padStart(9)} ${'caught'.padStart(9)} ${'missed'.padStart(6)} ${'false pos.'.padStart(10)} ` +
  `${'total wrong'.padStart(11)} ${'class distribution of missed'.padStart(30)}`);
const result = [];
for (let e = 13; e >= 2; e--) {
  const r = atThreshold(e); result.push({ e, r });
  const d = ['bulk', 'spear', 'whaling'].map((s) => `${s[0].toUpperCase()}${r.missed.filter((m) => m[1] === s).length}`).join(' ');
  console.log(`${String(e).padStart(9)} ${`${r.caught.length}/${FAKE}`.padStart(9)} ${String(r.missed.length).padStart(6)} ` +
    `${String(r.falsePositives.length).padStart(10)} ${String(r.missed.length + r.falsePositives.length).padStart(11)} ${d.padStart(30)}`);
}
const least = result.reduce((a, s) => (s.r.missed.length + s.r.falsePositives.length < a.r.missed.length + a.r.falsePositives.length ? s : a));
const clean = result.filter((s) => s.r.falsePositives.length === 0);
const full = result.filter((s) => s.r.missed.length === 0);
console.log(`\nthreshold minimizing total wrong ${least.e}: missed ${least.r.missed.length}, falsely flagged ${least.r.falsePositives.length}`);
console.log(`lowest threshold with zero false positives ${Math.min(...clean.map((s) => s.e))}: missed there ${clean.reduce((a, s) => (s.e < a.e ? s : a)).r.missed.length}`);
console.log(`highest threshold that misses nothing ${Math.max(...full.map((s) => s.e))}: falsely flagged there ${full.reduce((a, s) => (s.e > a.e ? s : a)).r.falsePositives.length}`);
const always = MESSAGE.filter((m) => !isFake(m) && score(m) >= 9);
console.log(`legitimate messages flagged independent of threshold ${always.length}: ${always.map((m) => `${m[0]} (${score(m)} points, ${m[2].length} indicators)`).join('; ')}`);

let cum = 0; const cumul = CHAIN.map((a) => (cum += a[3]));
const breakable = CHAIN.map((a, i) => (a[2] ? i : -1)).filter((i) => i >= 0);
console.log(`\nchain has ${CHAIN.length} steps, with a control ${breakable.length}, with no control ${CHAIN.length - breakable.length}`);
console.log(`${'#'.padStart(2)} ${'step'.padEnd(34)} ${'precondition'.padEnd(31)} ${'control'.padEnd(29)} ${'cum.'}`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(34)} ${a[1].padEnd(31)} ${(a[2] ?? '-').padEnd(29)} ${cumul[i]}`));
console.log(`earliest breakable link ${breakable[0] + 1} (items dropped ${breakable[0] ? cumul[breakable[0] - 1] : 0}), ` +
  `latest ${breakable[breakable.length - 1] + 1} (items dropped ${cumul[breakable[breakable.length - 1] - 1]})`);
console.log(`early-break gain ${cumul[breakable[breakable.length - 1] - 1] - (breakable[0] ? cumul[breakable[0] - 1] : 0)} items; if never broken ${cumul[CHAIN.length - 1]}`);
threshold    caught missed false pos. total wrong   class distribution of missed
       13      2/16     14          0          14                       B6 S5 W3
       12      3/16     13          1          14                       B5 S5 W3
       11      6/16     10          1          11                       B2 S5 W3
       10      8/16      8          1           9                       B0 S5 W3
        9      8/16      8          2          10                       B0 S5 W3
        8      9/16      7          2           9                       B0 S4 W3
        7     10/16      6          2           8                       B0 S3 W3
        6     13/16      3          2           5                       B0 S0 W3
        5     13/16      3          3           6                       B0 S0 W3
        4     15/16      1          4           5                       B0 S0 W1
        3     16/16      0          4           4                       B0 S0 W0
        2     16/16      0          8           8                       B0 S0 W0

threshold minimizing total wrong 3: missed 0, falsely flagged 4
lowest threshold with zero false positives 13: missed there 14
highest threshold that misses nothing 3: falsely flagged there 4
legitimate messages flagged independent of threshold 2: L9 (12 points, 5 indicators); L10 (9 points, 4 indicators)

chain has 5 steps, with a control 4, with no control 1
 # step                               precondition                    control                       cum.
 1 preparing the message              target address list             -                             0
 2 delivery                           a valid mail path               indicator threshold           1
 3 opening and interaction            the recipient's interest        user reporting                2
 4 credential entry                   a convincing collection point   second factor                 5
 5 use of the collected information   a valid session                 anomalous session detection   11
earliest breakable link 2 (items dropped 0), latest 5 (items dropped 5)
early-break gain 5 items; if never broken 11

The class-distribution-of-missed column repeats the same thing throughout the scan. As the threshold falls from ten to six, bulk phishing is already at zero and spear phishing begins to erode, but all three whaling messages still evade the threshold entirely at six. The W column only moves once the threshold reaches four, and it hits zero at three. A threshold that drops from thirteen to three catches every fake message; in exchange, four legitimate messages get flagged — two-fifths of the ten legitimate messages.

Two of the false positives are not a threshold problem. The messages scoring twelve and nine points are flagged at every threshold that catches whaling messages — even at most thresholds that fail to catch them. The two sets are interleaved in the same score band. This is not fixed by adjusting weights; fixing it requires legitimate bulk mail to lose its own indicators: leaving its own domain, having its verification record pass, having its link target match the text. The thing to fix is not the threshold but the legitimate flow itself.

The chain table closes three columns. The first of the five steps is a preparation step no control sees. The earliest of the four breakable links is delivery, where the dropped-item count is zero; the latest is the fifth step, by which point five items have already dropped. The early-break gain is five items; if the chain never breaks, the dropped-item count reaches eleven. Raising the threshold to loosen the second link is therefore not just a matter of one number changing: it shifts the load onto the third and fourth links — user reporting and the second factor.

Summary

  • 10 indicator types were counted across a set of 26 messages. Bulk phishing carries 5.88 indicators per message, spear phishing 3.00, whaling 1.67; cheap indicators disappear as the target narrows.
  • The average indicator count of whaling messages (1.67) is lower than that of legitimate messages (2.30). A measure that looks only at indicator count separates these two sets in the wrong direction.
  • At threshold 13, false positives are zero and 14 of 16 fake messages evade it. At threshold 3, none evade it and 4 of 10 legitimate messages are flagged. The threshold that minimizes total error is 3 (0 + 4).
  • The class distribution of missed messages evades all 3 whaling messages entirely up to threshold 6; this column only moves at threshold 4.
  • 2 legitimate messages are flagged independent of threshold (12 and 9 points). This is not a threshold-tuning problem; it is legitimate bulk mail carrying the same indicators.
  • The chain has 5 steps, 1 without a control. The earliest breakable link is step 2 (0 items), the latest is step 5 (5 items); the early-break gain is 5 items, and 11 items drop if the chain is never broken.

Next Step

Every indicator in this lesson belonged to a single channel, and that channel had one thing in common: the message is stored. Headers can be read, the domain can be queried, the link target can be compared with the text, and all of this can be done before the message is opened. In text messages and voice calls, most of these items do not exist; in a voice call, the one verifiable item that remains is usually the caller’s number, and even that may not be what it appears to be. That is the next lesson’s question: how many verification items exist per channel, at which link the chain can be broken in each channel, and how many chains a callback verification procedure breaks, channel by channel.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close