Lesson 18 / 20
Rogue Access Points
Six of the eight signals a client looks at when choosing a network are seen before the connection is made, and none of them carry proof. Across eight registered profiles, four paths to the evil twin are open and three are partially open; trust-root pinning, deleting unused profiles, and turning off automatic connection bring the open path to zero, while the two partial paths close only with a generation mandate that leaves four thousand two hundred ninety-two devices out.
Published Updated
In this lesson
- Separates the signals a client uses when choosing a network by source and counts how many carry proof
- Classifies the paths registered client profiles open to an evil twin as open, partial, or closed
- Measures cumulatively how many paths mutual authentication and profile cleanup close
- Calculates the cost of each defense in terms of devices touched, manual approvals, and devices left out
Contents
The previous lesson found the claim enterprise mode adds and left one row open in the table: whether the client verifies who the other party is. The same row had a personal-mode counterpart as well, and in the first lesson’s table the network-name claim was unproven across all four generations.
This gap is where the evil twin lives. Setting up a rogue access point means running a device that announces the name the organization uses and appears with a stronger signal. The lesson does not measure how this technique is set up, but what the client looks at to decide and how many paths mutual authentication closes.
- WS13. A signal is data the client uses when choosing a network. It can have three sources: the other party’s own announcement, the client’s own measurement, or signed proof that can be tied to a trust root.
- WS14. A path is a registered client profile matching an evil twin. It has three outcomes: open (the client connects or presents its credentials), partial (the connection does not complete but handshake messages leak out), closed (the client rejects the other party).
- WS15. The evil twin announces the network’s name, channel, and security indicator as they are, and appears with a stronger signal. It does not know the shared secret or the identity of the organization’s authentication server.
- WS16. The profile list is fictional; how many devices each profile is registered on is a model value. The same device can carry more than one profile, so the total of profile-carrying devices exceeds the device count.
- WS17. Manual configuration per device is four minutes, and manual approval on an open network is twice daily; both are model constants.
What the Client Looks At
When a device chooses a network, what it has at hand can be counted item by item.
// twin.mjs — the signals a client looks at when choosing a network, and its registered profiles (model). // Fictional regional measurement network; real network name, real location, and real hardware address are not written. // [signal, source, can the client verify it] export const SIGNAL = [ ['network name', 'the other party\'s own announcement', false], ['signal strength', 'the client\'s own measurement', false], ['channel', 'the other party\'s own announcement', false], ['hardware address', 'unencrypted header field', false], ['security indicator', 'the other party\'s own announcement', false], ['capability announcements', 'the other party\'s own announcement', false], ['identity presented by the authentication server', 'signed, can be tied to a trust root', true], ['verification derived from the shared secret', 'a secret value both parties know', true], ]; // Registered client profiles: [profile, mode, is server identity verified, automatic // connection, device count carrying this profile] export const PROFILE = [ ['branch-network', 'enterprise', true, true, 210], ['field-network', 'enterprise', false, true, 183], ['aggregator-network', 'personal', false, true, 4100], ['maintenance-network', 'personal', false, true, 9], ['guest-network', 'open', false, true, 222], ['old-field-network', 'personal', false, true, 183], ['coworking-space', 'open', false, true, 219], ['conference-hall', 'open', false, true, 47], ]; // The evil twin announces the same name, the same channel, and a stronger signal. Outcome: // 'open' = the client connects or presents its credentials, 'partial' = the connection does not // complete but handshake messages leak out, 'closed' = the client rejects the other party. export function outcome([name, mode, verifies], d) { if (d.autoOff.has(name)) return 'closed'; // does not connect without user approval if (mode === 'open') return 'open'; if (mode === 'enterprise') return (verifies || d.fixed) ? 'closed' : 'open'; return d.gen4 ? 'closed' : 'partial'; // personal mode: shared secret verifies mutually }
// run1.mjs — what does the client look at to accept a network's identity import { SIGNAL, PROFILE } from './twin.mjs'; const E = [52, 40, 12]; const row = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](E[i])).join(''); console.log(row(['signal', 'source', 'is proof'])); for (const [name, source, k] of SIGNAL) console.log(row([name, source, k ? 'yes' : 'no'])); const proof = SIGNAL.filter(([, , k]) => k).length; console.log(`\nsignal seen before connecting ${SIGNAL.length - proof}, with proof 0`); console.log(`signal carrying proof ${proof}; both appear only once the connection begins`); const A = [22, 12, 22, 14, 10]; const row2 = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](A[i])).join(''); console.log('\n' + row2(['profile', 'mode', 'server verified', 'automatic', 'device'])); for (const [name, mode, d, o, n] of PROFILE) console.log(row2([name, mode, d ? 'yes' : 'no', o ? 'yes' : 'no', n])); console.log(`\nprofile ${PROFILE.length}, device carrying a profile ` + `${PROFILE.reduce((s, p) => s + p[4], 0)}`);
signal source is proof network name the other party's own announcement no signal strength the client's own measurement no channel the other party's own announcement no hardware address unencrypted header field no security indicator the other party's own announcement no capability announcements the other party's own announcement no identity presented by the authentication server signed, can be tied to a trust root yes verification derived from the shared secret a secret value both parties know yes signal seen before connecting 6, with proof 0 signal carrying proof 2; both appear only once the connection begins profile mode server verified automatic device branch-network enterprise yes yes 210 field-network enterprise no yes 183 aggregator-network personal no yes 4100 maintenance-network personal no yes 9 guest-network open no yes 222 old-field-network personal no yes 183 coworking-space open no yes 219 conference-hall open no yes 47 profile 8, device carrying a profile 5173
The second column of the first table is the whole point of this lesson. Of the six signals the client sees before connecting, four are the other party’s own announcement about itself, one is the client’s own measurement, and one is an unencrypted header field. None of them carry proof.
Signal strength occupies a special place, because it is the only “own measurement” row, and that does not make it trustworthy. The client generally chooses the strongest signal among candidates announcing the same name. This is not a flaw but a design decision — it is required so that a moving device can hand off between access points. But as a selection criterion, signal strength is a value the other party can raise with a single hardware setting; being its own measurement does not make it proof.
There are two signals that carry proof, and both appear only after the connection is made. Verification derived from the shared secret comes into play in personal mode, and signed identity in enterprise mode. There is no proof available at the moment of selection; the selection is always made with unproven signals, and proof is only requested after the client starts talking with the other party.
The second table shows this gap’s counterpart inside the organization. There are eight registered profiles, and automatic connection is on for all of them. Two are not the organization’s networks: the coworking-space and conference-hall profiles are open networks employees connected to outside, and they have stayed in the device’s profile list. One more is an organization network that is no longer in use.
The figure of five thousand one hundred seventy-three in the last line is not a device count but a profile-carrying device count: a branch laptop keeps the branch network, the guest network, and two external networks registered at the same time. The unit of measure is therefore not the device but the profile. How many paths a device is open to is determined not by how many networks it is connected to, but by how many networks it has ever connected to; the profile list is the one item that keeps growing until it is deleted.
The Path Mutual Authentication Closes
Each of the eight profiles is a path. The run below stacks defenses on top of each other and prints the count of open, partial, and closed paths at every step.
// run2.mjs — how many paths does mutual authentication close, how many stay open, at what cost import { PROFILE, outcome } from './twin.mjs'; const EMPTY = new Set(); const UNUSED = new Set(['old-field-network', 'conference-hall']); const OPEN_NETWORKS = new Set(PROFILE.filter((p) => p[1] === 'open').map((p) => p[0])); const ALL = new Set([...UNUSED, ...OPEN_NETWORKS]); const DEFENSE = [ ['baseline (none)', { fixed: false, gen4: false, autoOff: EMPTY }], ['trust root is pinned', { fixed: true, gen4: false, autoOff: EMPTY }], ['unused profile is deleted', { fixed: true, gen4: false, autoOff: UNUSED }], ['automatic connection off on open networks', { fixed: true, gen4: false, autoOff: ALL }], ['simultaneous authentication generation', { fixed: true, gen4: true, autoOff: ALL }], ]; const E = [45, 7, 9, 9, 13, 16]; const row = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](E[i])).join(''); console.log(row(['defense (stacked)', 'open', 'partial', 'closed', 'open device', 'partial device'])); const results = DEFENSE.map(([name, d]) => { const r = PROFILE.map((p) => [p, outcome(p, d)]); const n = (t) => r.filter(([, s]) => s === t).length; const a = (t) => r.filter(([, s]) => s === t).reduce((s, [p]) => s + p[4], 0); console.log(row([name, n('open'), n('partial'), n('closed'), a('open'), a('partial')])); return r; }); const P = [22, 10, 10, 10]; const row2 = (h) => h.map((d, i) => String(d)[i ? 'padStart' : 'padEnd'](P[i])).join(''); console.log('\n' + row2(['profile', 'baseline', 'pinning', 'all'])); PROFILE.forEach((p, i) => console.log(row2([p[0], results[0][i][1], results[1][i][1], results.at(-1)[i][1]]))); // Cost: the devices each defense touches and the legitimate work it interrupts (model constants). const MANUAL = 4, APPROVAL = 2; // minutes per device, daily manual approvals const enterprise = PROFILE.filter((p) => p[1] === 'enterprise').reduce((s, p) => s + p[4], 0); const deleted = PROFILE.filter((p) => UNUSED.has(p[0])).reduce((s, p) => s + p[4], 0); const openDevice = PROFILE.filter((p) => OPEN_NETWORKS.has(p[0])).reduce((s, p) => s + p[4], 0); const personal = PROFILE.filter((p) => p[1] === 'personal').reduce((s, p) => s + p[4], 0); console.log(`\ncost:`); console.log(` trust root pinning ${enterprise} devices with profiles, ${enterprise * MANUAL} min`); console.log(` profile deletion ${deleted} devices touched, ${deleted * MANUAL} min`); console.log(` automatic connection ${openDevice} devices, ${openDevice * APPROVAL} manual approvals per day`); console.log(` top-generation mandate ${personal} devices left out`);
defense (stacked) open partial closed open device partial device baseline (none) 4 3 1 671 4292 trust root is pinned 3 3 2 488 4292 unused profile is deleted 2 2 4 441 4109 automatic connection off on open networks 0 2 6 0 4109 simultaneous authentication generation 0 0 8 0 0 profile baseline pinning all branch-network closed closed closed field-network open closed closed aggregator-network partial partial closed maintenance-network partial partial closed guest-network open open closed old-field-network partial partial closed coworking-space open open closed conference-hall open open closed cost: trust root pinning 393 devices with profiles, 1572 min profile deletion 230 devices touched, 920 min automatic connection 488 devices, 976 manual approvals per day top-generation mandate 4292 devices left out
At baseline, only one of the eight paths is closed: the enterprise profile with a pinned trust root. Four paths are open, and three are partially open. This is the opposite of the answer one would give without reading the table — most of the organization’s networks are encrypted, but encryption does not close the path to an evil twin.
The partial result shows exactly what personal mode does. Because the shared secret is known by both parties, an access point that does not know the secret cannot complete the connection; the client rejects it. But the client only realizes this after it has started the handshake, and those messages have already gone out. For the three generations that lesson one’s table found no proof of offline attempts against, this means a recorded handshake exists even if the connection is never established. Personal mode closes the path, not the trace.
The pinning row closes one path: the enterprise profile that does not verify. The affected device count is one hundred eighty-three, and this is the counterpart of the one row the second lesson left open. A profile that has moved to enterprise mode but does not verify server identity is worse off than personal mode: where personal mode’s result is partial, here it is open, because the client presents its identity to the other party.
Deleting unused profiles closes two more paths, and this has nothing to do with defense technology. The old-field-network and conference-hall profiles kept the names of networks that no longer exist in the client; the name of a nonexistent network means the network belongs to whoever announces that name. These two rows are the cheapest defense in the whole table: nine hundred twenty minutes of cleanup, no new infrastructure item, and no legitimate connection is cut, because there is no legitimate connection left to cut. The path it closes being cheap makes it the most frequently skipped defense — an item invisible in inventory does not make it onto the maintenance list either.
The Path That Drops to Zero, and the Cost Paid
The fourth row drops the open path to zero. Turning off automatic connection on open networks affects four hundred eighty-eight devices and adds nine hundred seventy-six manual approvals per day. What closes the path is not verification but shifting the decision to the user; this is the row where the defense’s cost is most visible, because the cost is paid every day.
The two partial paths close only on the last row, at a cost of four thousand two hundred ninety-two devices. The first lesson’s inventory table says where this number comes from: nearly all of the devices remaining in personal mode do not support the top generation. The last row is therefore measurable but not selectable; the two partial paths stay open in practice.
The total cost adds up across four items: a one-time configuration of two thousand four hundred ninety-two minutes, nine hundred seventy-six manual approvals per day, and a renewal affecting four thousand two hundred ninety-two devices. The first three defenses are one-time and cheap; the fourth is continuous; the fifth is a budget decision.
There is something the model does not see. The table counts the client matching a registered profile; it does not count connecting to an unregistered name at all. When a user manually selects a network, the pinned trust root still works, but when they select an open network, no defense is active. The eight paths dropping to zero means the registered profiles on this network drop to zero; it does not mean the paths the client could choose drop to zero.
Summary
- None of the six signals the client sees before connecting carry proof: four are the other party’s announcement, one is its own measurement, one is an unencrypted header field. The two signals that do carry proof appear only once the connection is being made.
- Of the eight registered profiles, only one is closed at baseline; four are open, three are partially open. The encryption generation alone does not close the path to an evil twin.
- Personal mode does not let the connection complete, but handshake messages leak out; a profile that has moved to enterprise mode but does not verify server identity is worse off than personal mode, because it presents its identity to the other party.
- Trust-root pinning, deleting unused profiles, and turning off automatic connection on open networks bring the open path from 4 to 0; the cost is 2,492 minutes of configuration and 976 manual approvals per day.
- The two partial paths close only when the top generation is made mandatory, at a cost of 4,292 devices; it is therefore measurable but not a practical option.
Next Step
This lesson’s whole measurement rested on one assumption: that the client establishes its connection on its own. Once six of the eight profiles are closed, the client stays on the network it is already connected to and never sees the rogue candidate. But a client staying connected also requires the messages that keep that connection alive to be verified. Data frames are encrypted and carry integrity proof; the frames that establish, sustain, and terminate the connection are a separate class. This is the next lesson’s question: which frame type in a wireless connection carries integrity proof, which does not, how many frame types gain proof once protection is turned on, and in which defense counter the deviation shows up.
To keep your progress and take notes, Log in
My notes
Log in to take notes.