---
title: 'Packet Capture and Analysis'
source: 'https://academia.sh/en/courses/penetration-testing/packet-capture-and-analysis'
course: 'Security Tooling and Penetration Testing'
language: en
updated: '2026-09-26T03:02:55+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Derives flows from a fictional packet set and counts the visible fields per flow'
    - 'Classifies fields by their producer as direct evidence, inference, or a spoofable claim'
    - "Measures the capture point's placement in terms of claim count, evidence ratio, and the trace it leaves"
---

# Packet Capture and Analysis

Measuring the evidentiary value of captured traffic: 10 flows from 24 fictional packets, 18, 44, and 85 claim candidates at three capture points, 50 of the 85 candidates at the core mirror turning out to be evidence and 7 spoofable, low-value claims being proven at a rate of 20/20 while high-value claims are proven at only 10/25, and passive capture leaving zero records on the target while requiring one configuration change.

The previous topic produced the externally visible inventory and counted candidates; the question
left open at its close was this — no reconnaissance step can say whether a candidate is truly open,
and answering that requires looking at the traffic and the request itself. This topic covers the
tools that look there, and it tests all of them against a single measure: the gap between the
**candidate** they produce and the **evidenced finding** that remains.

The first tool class is the **packet capturer**: it records the frames passing through the network
at one point. How protocols work was already established in the Network Models course in the
Computer Networks curriculum, and it is not repeated here. The only question asked here is this:
which field in this recorded pile counts as **direct evidence**, which is only an **inference**, and
which is a claim the other side can write freely. Even in an authorized assessment, recording
traffic belonging to another organization without written authorization and a defined scope is both
illegal and unprofessional.

## A Field's Class Depends on Its Producer

Reading every field in a packet with the same eye is analysis's most common mistake. **The capture
point itself** produces one portion of the fields: the time of the record, which leg the frame
entered on, direction, size, count. These are not the sender's claim; they are the measuring
instrument's observation, and the sender cannot change them. **Headers** carry another portion:
addresses, the port number, the server name field. These have to stay consistent along the network
path, so they are not entirely free either, but they are not an observation — they are
**inference**. The remaining portion is carried by the **payload**: username, meter ID, client
clock. The party writing these is the sender, and unless they are cryptographically bound, the
sender can write whatever it wants.

There is a fourth class: the **signature**. If a field inside the payload is cryptographically
bound, that field stops being the sender's claim and turns into evidence that can be tested — but
if the flow is encrypted, the capture point cannot see the signature either.

- **AT1.** The packet set, service table, capture points, and claim list are a **model**; no real
  capture was performed. No real address, hostname, or organization is written.
- **AT2.** A flow is the set of packets carrying the same source, destination, and service triple.
- **AT3.** A field's class depends on its producer: a point observation and a signature are
  **evidence**, a header field is **inference**, a payload field is **spoofable**.
- **AT4.** In an encrypted flow, payload fields and the signature are not visible; the server name
  field is counted as visible.
- **AT5.** A **claim** is a proposition derived from captured traffic. If at least one of its bases
  is visible, the claim is a **candidate**; its class is the class of the strongest visible basis.
- **AT6.** Every claim is given a **value**: how much weight it carries in the report. The value was
  assigned by hand.
- **AT7.** Capture is passive; it produces no record on the target systems.

```js
// pkt/flow.mjs — fictional packet set, flow extraction, and field classification are a MODEL.
// There is no real capture; no real address, hostname, or organization is written.
// A field's class depends on who produced it.
export const FIELD = {
  'capture-time': 'point', 'ingress-leg': 'point', 'direction': 'point',
  'frame-size': 'point', 'packet-count': 'point', 'plaintext-presence': 'point',
  'source-address': 'header', 'destination-address': 'header', 'port-number': 'header',
  'server-name-field': 'header', 'signature-verification': 'signature',
  'username': 'payload', 'password-field': 'payload', 'meter-id': 'payload',
  'reading-value': 'payload', 'subscriber-no': 'payload', 'session-token': 'payload',
  'client-clock': 'payload', 'device-id': 'payload', 'transaction-id': 'payload',
};
export const CLASS = { point: 'evidence', signature: 'evidence', header: 'inference', payload: 'spoofable' };
export const STRENGTH = { evidence: 3, inference: 2, spoofable: 1 };

// Service: whether it is encrypted, whether the payload is signed, which fields the payload carries.
export const SERVICE = {
  'meter-collection':  { encrypted: false, signed: true,  payload: ['meter-id', 'reading-value', 'client-clock'] },
  'validation-call':   { encrypted: true,  signed: true,  payload: ['transaction-id', 'subscriber-no'] },
  'portal-session':    { encrypted: true,  signed: false, payload: ['session-token', 'username'] },
  'field-sync':        { encrypted: false, signed: false, payload: ['username', 'device-id'] },
  'admin-access':      { encrypted: false, signed: false, payload: ['username', 'password-field'] },
  'external-notify':   { encrypted: true,  signed: false, payload: ['subscriber-no'] },
};

// Capture point: which legs' packets it sees.
export const POINT = {
  'branch-leg':         ['branch-1'],
  'core-mirror':        ['branch-1', 'branch-2', 'core', 'external'],
  'collector-endpoint': ['core'],
};

// row: time(s) | leg | source | destination | service | size(bytes)
export const PACKET = `
  0 branch-1 s1-end collector meter-collection 420
  1 branch-1 s1-end collector meter-collection 418
  2 branch-1 s1-end collector meter-collection 421
  3 branch-2 s2-end collector meter-collection 419
  4 branch-2 s2-end collector meter-collection 420
  5 core collector validator validation-call 1180
  6 core collector validator validation-call 1174
  7 core collector validator validation-call 1190
  8 external sub-end portal portal-session 2240
  9 external sub-end portal portal-session 2180
 10 external sub-end portal portal-session 2260
 11 branch-1 field-end field-svc field-sync 960
 12 branch-1 field-end field-svc field-sync 955
 13 branch-2 field-end field-svc field-sync 962
 14 core admin collector admin-access 310
 15 core admin collector admin-access 305
 16 core admin validator admin-access 308
 17 external billing ext-end external-notify 1420
 18 external billing ext-end external-notify 1415
 19 branch-1 s1-end collector meter-collection 417
 20 core collector billing validation-call 1160
 21 core collector billing validation-call 1168
 22 branch-2 s2-end collector meter-collection 422
 23 core admin field-svc admin-access 312
`.trim().split('\n').map((r) => {
  const [t, leg, source, destination, service, size] = r.trim().split(/\s+/);
  return { t: +t, leg, source, destination, service, size: +size };
});

// Flow: packets carrying the same (source, destination, service) triple.
export function flows(packets) {
  const m = new Map();
  for (const p of packets) {
    const k = `${p.source}>${p.destination}/${p.service}`;
    if (!m.has(k)) m.set(k, { name: k, service: p.service, leg: new Set(), packets: 0, bytes: 0 });
    const a = m.get(k);
    a.leg.add(p.leg); a.packets += 1; a.bytes += p.size;
  }
  return [...m.values()];
}

// Fields visible in a flow: point and header fields always; payload fields and the
// signature only in an unencrypted flow.
export function visibleFields(a) {
  const h = SERVICE[a.service];
  const g = ['capture-time', 'ingress-leg', 'direction', 'frame-size', 'packet-count',
    'source-address', 'destination-address', 'port-number'];
  if (h.encrypted) g.push('server-name-field');
  else {
    g.push('plaintext-presence', ...h.payload.filter((f) => FIELD[f]));
    if (h.signed) g.push('signature-verification');
  }
  return g;
}

// Claim: a proposition derived from captured traffic. If at least one of its basis fields is
// visible, the claim becomes a CANDIDATE; the claim's class is the class of the strongest visible basis.
export const CLAIM = [
  { name: 'flow-exists',               value: 'low',    basis: ['direction', 'frame-size'] },
  { name: 'volume-and-timing',         value: 'low',    basis: ['packet-count', 'capture-time'] },
  { name: 'transaction-time',          value: 'medium', basis: ['client-clock', 'capture-time'] },
  { name: 'traffic-from-this-leg',     value: 'medium', basis: ['ingress-leg'] },
  { name: 'service-spoken',            value: 'medium', basis: ['port-number'] },
  { name: 'counterparty-identity',     value: 'medium', basis: ['destination-address', 'server-name-field'] },
  { name: 'fields-in-plaintext',       value: 'high',   basis: ['plaintext-presence'] },
  { name: 'sender-identity',           value: 'high',   basis: ['source-address', 'signature-verification'] },
  { name: 'meter-reading-value',       value: 'high',   basis: ['reading-value', 'meter-id', 'signature-verification'] },
  { name: 'credential-field-contents', value: 'high',   basis: ['password-field', 'session-token'] },
  { name: 'person-performing-action',  value: 'high',   basis: ['username', 'device-id'] },
];

export function claimClass(claim, visible) {
  const v = claim.basis.filter((f) => visible.includes(f));
  if (v.length === 0) return null;
  return v.map((f) => CLASS[FIELD[f]]).sort((x, y) => STRENGTH[y] - STRENGTH[x])[0];
}

export function pointMeasure(point) {
  const legs = POINT[point];
  const p = PACKET.filter((x) => legs.includes(x.leg));
  const s = { packets: p.length, bytes: p.reduce((t, x) => t + x.size, 0), flows: 0,
    candidates: 0, evidence: 0, inference: 0, spoofable: 0 };
  for (const a of flows(p)) {
    s.flows += 1;
    const g = visibleFields(a);
    for (const i of CLAIM) {
      const c = claimClass(i, g);
      if (c) { s.candidates += 1; s[c] += 1; }
    }
  }
  return s;
}
```

## Three Capture Points

```js
// pkt/measure.mjs — the same packet set is measured from three capture points.
import { PACKET, POINT, CLAIM, SERVICE, flows, visibleFields, claimClass, pointMeasure } from './flow.mjs';

const y = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('').trimEnd());

console.log(`${PACKET.length} packets -> ${flows(PACKET).length} flows, ` +
  `${Object.keys(SERVICE).length} services, ${CLAIM.length} claim types`);

const A = [-22, -18, 9, 7, 11, 8, 8];
console.log('\n1. visible fields per flow');
y(A, 'flow', 'service', 'packets', 'bytes', 'encrypted', 'signed', 'fields');
for (const a of flows(PACKET)) {
  const h = SERVICE[a.service];
  y(A, a.name.split('/')[0], a.service, a.packets, a.bytes, h.encrypted ? 'yes' : 'no',
    h.signed ? 'yes' : 'no', visibleFields(a).length);
}

const B = [-20, 9, 7, 12, 10, 11, 11];
console.log('\n2. claims per capture point');
y(B, 'point', 'packets', 'flows', 'candidates', 'evidence', 'inference', 'spoofable');
for (const n of Object.keys(POINT)) {
  const s = pointMeasure(n);
  y(B, n, s.packets, s.flows, s.candidates, s.evidence, s.inference, s.spoofable);
}

const C = [-27, 8, 10, 11, 11];
console.log('\n3. class distribution per claim type at the core mirror');
y(C, 'claim', 'value', 'evidence', 'inference', 'spoofable');
const ag = flows(PACKET).map(visibleFields);
const box = { low: [0, 0], medium: [0, 0], high: [0, 0] };
for (const i of CLAIM) {
  const c = { evidence: 0, inference: 0, spoofable: 0 };
  for (const g of ag) { const s = claimClass(i, g); if (s) c[s] += 1; }
  y(C, i.name, i.value, c.evidence, c.inference, c.spoofable);
  box[i.value][0] += c.evidence;
  box[i.value][1] += c.evidence + c.inference + c.spoofable;
}

console.log('\n4. evidence ratio by claim value');
for (const d of ['low', 'medium', 'high'])
  console.log(`  ${d.padEnd(8)} evidence ${String(box[d][0]).padStart(3)} / ${box[d][1]}`);

const t = pointMeasure('core-mirror');
console.log(`\nrecords produced on target systems            0`);
console.log(`configuration change needed to enable capture 1 (mirror leg, written approval)`);
console.log(`volume examined                               ${t.bytes} bytes`);
console.log(`flows not visible from the branch leg         ${t.flows - pointMeasure('branch-leg').flows} / ${t.flows}`);
```

```
24 packets -> 10 flows, 6 services, 11 claim types

1. visible fields per flow
flow                  service             packets  bytes  encrypted  signed  fields
s1-end>collector      meter-collection          4   1676         no     yes      13
s2-end>collector      meter-collection          3   1261         no     yes      13
collector>validator   validation-call           3   3544        yes     yes       9
sub-end>portal        portal-session            3   6680        yes      no       9
field-end>field-svc   field-sync                3   2877         no      no      11
admin>collector       admin-access              2    615         no      no      11
admin>validator       admin-access              1    308         no      no      11
billing>ext-end       external-notify           2   2835        yes      no       9
collector>billing     validation-call           2   2328        yes     yes       9
admin>field-svc       admin-access              1    312         no      no      11

2. claims per capture point
point                 packets  flows  candidates  evidence  inference  spoofable
branch-leg                  6      2          18        12          5          1
core-mirror                24     10          85        50         28          7
collector-endpoint          9      5          44        23         15          6

3. class distribution per claim type at the core mirror
claim                         value  evidence  inference  spoofable
flow-exists                     low        10          0          0
volume-and-timing               low        10          0          0
transaction-time             medium        10          0          0
traffic-from-this-leg        medium        10          0          0
service-spoken               medium         0         10          0
counterparty-identity        medium         0         10          0
fields-in-plaintext            high         6          0          0
sender-identity                high         2          8          0
meter-reading-value            high         2          0          0
credential-field-contents      high         0          0          3
person-performing-action       high         0          0          4

4. evidence ratio by claim value
  low      evidence  20 / 20
  medium   evidence  20 / 40
  high     evidence  10 / 25

records produced on target systems            0
configuration change needed to enable capture 1 (mirror leg, written approval)
volume examined                               22436 bytes
flows not visible from the branch leg         8 / 10
```

## Evidence Is Cheap, a Valuable Claim Is Expensive

The fourth table is this lesson's result. **20 of the 20** low-value claims fall in the evidence
class. "There is a flow between these two endpoints," "this many packets passed at this time" — all
of it is true, none of it is disputable, and none of it is worth a line in the report. For
medium-value claims the ratio drops by half: the leg and the time are evidence, while the spoken
service and the counterparty's identity are read from the header and are inference. For high-value
claims, only **10 of 25** fall into the evidence class.

This inverse relationship is not a flaw; it is the structure of the method. The more a capture point
observes, the less that observation says; the fields that actually say something are produced by
the sender, and the sender is always suspect. On the measurement network this has three concrete
counterparts.

**Credential field contents** and **person performing the action** are never evidence in any flow;
they are produced as candidates seven times, and all seven rest solely on a payload field. Seeing a
username in a capture does not show that person performed the action — it only shows that name was
written into that field. A report line that reads "user X performed this action" is an unproven
candidate, and it spends the remediation budget in the wrong place.

**Sender identity** is evidence in two of ten candidates, inference in eight. The two pieces of
evidence come from the meter-collection flows, whose payload is signed and unencrypted: the
signature is visible and can be verified. In the remaining eight, all that is on hand is the source
address. In encrypted flows the signature also stays inside the payload and so is not visible —
encryption is the right call, but it also hides from the analyst a field that could have been
evidence.

**Fields in plaintext** are in the evidence class in six flows, and this is the most solid finding
on the fictional network. The three admin-access flows, the field-sync flow, and two
meter-collection flows carry their fields unencrypted. The distinction matters here: that the fields
**are carried in plaintext** is the capture point's observation and is evidence; **what those fields
say** is the sender's claim and is not.

## The Point's Placement and the Trace It Leaves

The second table is the measure of where the capturer is placed. The branch leg has 6 packets, 2
flows, and 18 candidates; the core mirror has 24 packets, 10 flows, and 85 candidates. An analyst
looking from the branch leg **never sees eight of the ten flows** and can say nothing about a flow
it does not see. The collector endpoint sits in the middle: it sees the core's traffic, not the
branch legs, but because three of the five flows it sees are unencrypted, it is the point with the
highest spoofable-candidate ratio — 6 of 44 candidates.

The cost column consists of two items, and both are small. The volume examined is 22,436 bytes; in a
real assessment this number is far larger, and it is the actual item that determines analysis time.
The trace left on the defense side, though, is **zero records**: capture is passive, no request goes
to the target systems, no log line is produced. This is the lowest-trace step we will see in this
course.

The one item that is not zero is what **makes the capture possible**. Opening a mirror leg is a
configuration change; it produces a change record and requires the network team's approval and an
explicit line in the scope document. Because subscriber data passes through the traffic, where the
record will be stored and when it will be deleted are also written into the same document. It is the
capture itself that is passive; the permission to access it is not.

## Summary

- Twenty-four fictional packets produce 10 flows; the number of visible fields per flow is 9 in
  encrypted flows and between 11 and 13 in unencrypted flows.
- A field's evidentiary value depends on its producer: the capture point's observation and a
  cryptographic signature are evidence, a header field is inference, a payload field is a spoofable
  claim.
- At the core mirror, 50 of 85 claim candidates are evidence, 28 are inference, and 7 are spoofable;
  a claim's class is the class of its strongest visible basis.
- The evidence ratio runs inverse to the claim's value: 20/20 of low-value claims are proven, but
  only 10/25 of high-value ones. None of the seven candidates that tie a user to an action are
  evidence.
- The capture point's placement determines which flow is seen: the branch leg never sees 8 of the 10
  flows.
- Passive capture leaves zero records on the target; the only trace is the configuration change the
  mirror leg requires and the written approval tied to it.

## Next Step

In this lesson, fields were classified one by one, but encrypted flows were passed over in a single
line: the payload is not visible, the server name field is. Four of the ten flows on the fictional
network are encrypted, and those four are exactly the flows carrying the most weight — the
subscriber portal session, the validation call, the external notification. It is not true that
analysis comes up empty-handed on these flows; its hands are **full in a different way**.

The next lesson measures application-layer inspection in terms of visibility. The same set of flows
is run through a protocol analyzer, and two columns are counted separately: how many fields can be
read in an unencrypted flow, and how many fields remain only **metadata** in an encrypted flow. It
measures how much of a claim metadata carries, which scope and privacy item decrypting requires, and
when the analyzer's **own interpretation** amounts to an inference.
