Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close