Skip to content
academia.sh

Lesson 17 / 22

Forensic Examination Principles

Measuring evidence integrity and chain of custody in items: 28 evidence items, 78 transfers, and five record fields per transfer; 10 of 390 fields left blank puts 9 items in question, and hash mismatches put 3 more in question, leaving 12 of 28 items indefensible. If a single point in the field chain loses its record entirely, 13 items open up at once; the way a 2.6 percent record gap turns into a 43 percent item loss comes from the chain's structure.

Published Updated

In this lesson

  • Defines the evidence item, the chain of custody, and the record fields required per transfer
  • Runs hash-based integrity verification and counts how many items a mismatch affects
  • Measures, on the chain, how many items a single missing record puts in question
Contents

The previous topic closed the incident: containment was applied, the affected machines were pulled off the network, service came back. That closure left one question hanging — most of the items that would prove what the incident was had already been erased by the time containment happened. Memory was gone once the machine was powered off, the connection list was gone once the session dropped, temp files were gone once the system was reinstalled. A narrative can be built from what is left; whether that narrative is testable is a separate question.

This topic measures that question, and the unit of measure is the evidence item. Three columns get counted: how many items are volatile and vanish if not collected, how many items the collection order costs, and how many items the chain of custody breaks at. This lesson builds the third column. In a fictional regional measurement network, unauthorized access was seen at a branch endpoint, and the examination covers six hosts. Every number comes from a model. Collecting evidence from a system that belongs to someone else — imaging it, dumping it, copying its records — is both illegal and unprofessional without written authorization and a defined scope.

  • FE1. An evidence item is a piece of data taken from a single source and bindable to a single integrity value: a disk image, a memory dump, a log slice, a configuration copy.
  • FE2. Every item has a lifetime: the time that passes before it is lost if not collected. An item whose lifetime does not exceed one hour is volatile.
  • FE3. The chain of custody is the record of transfers an item passes through from the moment it is collected to the moment it is analyzed. The chain consists of nodes; a transfer is the handover between two nodes.
  • FE4. Every transfer requires five record fields: from whom, to whom, when, in what medium, and which hash. The fields are filled in by hand and can be left blank.
  • FE5. An item is defensible only if the hash at the time of collection matches the hash at the time of analysis and no transfer in its chain has a missing field. Otherwise the item is contestable — it has not been shown to be wrong, but it cannot be shown to be right.
  • FE6. Item counts, durations, and the error rate are fictional; the random seed is visible.

Item, Hash, and Transfer

// forensics/item.mjs — evidence item set, hash-based integrity, and chain of custody. A MODEL.
// The regional measurement network is fictional; no real organization, incident, or machine name is used.
import { createHash } from "node:crypto";

export const SEED = 20260731;
const generator = (t) => { let s = t >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };

// class -> [lifetime(s): time before it is lost if not collected, collection duration(s)]
export const CLASS = {
  "network-state": [120, 45], "process-list": [240, 60], "open-handle": [300, 55],
  "session-state": [600, 70], "memory-content": [900, 420], "temp-file": [3600, 180],
  "flow-record": [86400, 240], "disk-image": [604800, 5400], "configuration": [604800, 90],
  "remote-log": [2592000, 300],
};

// host -> [chain of custody nodes, item classes it carries]
export const HOST = {
  "branch-endpoint-3": [["collection", "packaging", "transit", "lab", "analysis"],
    ["network-state", "process-list", "open-handle", "session-state", "memory-content",
      "temp-file", "disk-image", "configuration"]],
  "field-laptop-14": [["collection", "packaging", "transit", "lab", "analysis"],
    ["network-state", "process-list", "session-state", "memory-content", "disk-image"]],
  "collector-2": [["collection", "transit", "analysis"],
    ["network-state", "process-list", "open-handle", "session-state", "memory-content",
      "disk-image", "configuration"]],
  "portal-frontend": [["collection", "transit", "analysis"],
    ["network-state", "process-list", "memory-content", "configuration"]],
  "central-log": [["intake", "analysis"], ["remote-log", "configuration"]],
  "flow-collector": [["intake", "analysis"], ["flow-record", "configuration"]],
};

// The five record fields required at each transfer.
export const FIELD = ["from", "to", "time", "medium", "hash"];
// Items opened without a write blocker: content unchanged, but the access timestamp changes.
export const UNPROTECTED = ["branch-endpoint-3/temp-file", "collector-2/disk-image",
  "portal-frontend/configuration"];

export const digest = (s) => createHash("sha256").update(s).digest("hex").slice(0, 16);

export function items() {
  const r = generator(SEED), list = [];
  for (const [host, [nodes, classes]] of Object.entries(HOST))
    for (const c of classes) {
      const name = `${host}/${c}`;
      const content = `${name}|${Math.floor(r() * 1e9)}`;
      list.push({ name, host, class: c, nodes, lifetime: CLASS[c][0], duration: CLASS[c][1],
        collectionHash: digest(content),
        analysisHash: digest(content + (UNPROTECTED.includes(name) ? "|access-timestamp" : "")),
        transfer: nodes.slice(1).map((d, i) => {
          const record = Object.fromEntries(FIELD.map((a) => [a, true]));
          if (r() < 0.11) record[FIELD[Math.floor(r() * 5)]] = false;   // field left blank
          return { point: `${nodes[i]}>${d}`, record };
        }) });
    }
  return list;
}

export const missingFields = (k) =>
  k.transfer.reduce((a, t) => a + FIELD.filter((x) => !t.record[x]).length, 0);
export const intact = (k) => k.collectionHash === k.analysisHash;
export const defensible = (k) => intact(k) && missingFields(k) === 0;

The hash does exactly one job here: it binds the item to itself. The digest computed at collection time is recorded; it is recomputed at analysis time. If the two values match, the item is shown not to have changed between those two moments. If they do not match, the item is shown to have changed — not how or by whom, only that it changed. That is what the hash proves, and no more.

Three items were opened without write protection. Nothing was written to their content; only the access timestamp was refreshed. The hash does not see this distinction, because it should not: an integrity value measures difference, not intent. Whether an item changed by accident or on purpose is a question for the chain, not for the hash.

The Chain’s Numbers

// forensics/measure.mjs — item, transfer, and defensibility count per host.
import { items, FIELD, CLASS, missingFields, intact, defensible, SEED } from "./item.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());

const K = items();
const volatile = K.filter((k) => k.lifetime <= 3600).length;
console.log(`model (seed ${SEED}): ${K.length} evidence items, ${Object.keys(CLASS).length} ` +
  `classes, ${FIELD.length} record fields per transfer`);
console.log(`${volatile} items volatile (lifetime under one hour), ${K.length - volatile} items persistent`);

const A = [-19, 6, 7, 11, 8, 16, 12];
console.log("");
y(A, "host", "item", "nodes", "transfers", "fields", "missing fields", "defensible");
for (const host of [...new Set(K.map((k) => k.host))]) {
  const g = K.filter((k) => k.host === host);
  const transfers = g.reduce((a, k) => a + k.transfer.length, 0);
  y(A, host, g.length, g[0].nodes.length, transfers, transfers * FIELD.length,
    g.reduce((a, k) => a + missingFields(k), 0), `${g.filter(defensible).length}/${g.length}`);
}
const transfers = K.reduce((a, k) => a + k.transfer.length, 0);
y(A, "total", K.length, "-", transfers, transfers * FIELD.length,
  K.reduce((a, k) => a + missingFields(k), 0), `${K.filter(defensible).length}/${K.length}`);

console.log(`\nhash mismatch in ${K.filter((k) => !intact(k)).length} items: ` +
  K.filter((k) => !intact(k)).map((k) => k.name).join(", "));
model (seed 20260731): 28 evidence items, 10 classes, 5 record fields per transfer
18 items volatile (lifetime under one hour), 10 items persistent

host                 item  nodes  transfers  fields  missing fields  defensible
branch-endpoint-3       8      5         32     160               4         3/8
field-laptop-14         5      5         20     100               4         2/5
collector-2             7      3         14      70               1         5/7
portal-frontend         4      3          8      40               0         3/4
central-log             2      2          2      10               1         1/2
flow-collector          2      2          2      10               0         2/2
total                  28      -         78     390              10       16/28

hash mismatch in 3 items: branch-endpoint-3/temp-file, collector-2/disk-image, portal-frontend/configuration

Two columns in the table must be read together. The field hosts follow a five-node chain: the item is collected, packaged, moved to the lab, received, and analyzed. Four transfers means twenty fields to fill in per item. At the central hosts the chain is three nodes, and for items pulled in remotely it is two nodes — there is no physical transport, so there are fewer fields to fill in.

The result column reflects this structure directly. At the eight-item branch endpoint, the number of defensible items is three; at the two-item flow collector, two out of two are defensible. As the chain gets longer, the record load per item grows, and every extra field is one more chance to be left blank. A longer chain does not protect the item more; it makes it breakable in more places.

A Single Missing Record

// forensics/gap.mjs — how many items a single missing record puts in question.
import { items, FIELD, missingFields, intact } from "./item.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());
const K = items();

// For each transfer point: how many items pass through it; if its record drops entirely, all open up.
const point = new Map();
for (const k of K)
  k.transfer.forEach((t, i) => {
    if (!point.has(t.point)) point.set(t.point, { passing: 0, next: 0 });
    const n = point.get(t.point);
    n.passing += 1; n.next = k.transfer.length - i - 1;
  });

const A = [-24, 14, 14, 26];
y(A, "transfer point", "items passing", "next step", "opens up if record drops");
for (const [name, n] of [...point].sort((a, b) => b[1].passing - a[1].passing))
  y(A, name, n.passing, n.next, `${n.passing} items (${Math.round(n.passing * 100 / K.length)}%)`);

// Observed state: which field is missing, which question goes unanswered.
const QUESTION = { from: "who had the item", to: "who received it",
  time: "can the order be proven", medium: "under what condition was it stored",
  hash: "can it be shown unchanged" };
console.log("");
const B = [-15, -37, 9, 7];
y(B, "missing field", "question left unanswered", "transfers", "items");
for (const a of FIELD) {
  const t = K.reduce((s, k) => s + k.transfer.filter((x) => !x.record[a]).length, 0);
  y(B, a, QUESTION[a], t, K.filter((k) => k.transfer.some((x) => !x.record[a])).length);
}

const fields = K.reduce((a, k) => a + k.transfer.length, 0) * FIELD.length;
const empty = K.reduce((a, k) => a + missingFields(k), 0);
const open = K.filter((k) => missingFields(k) > 0 || !intact(k)).length;
console.log(`\n${empty} of ${fields} record fields are empty (${(empty * 100 / fields).toFixed(1)}%); ` +
  `this puts ${K.filter((k) => missingFields(k) > 0).length} items in question, and hash mismatches ` +
  `put ${K.filter((k) => !intact(k)).length} more in question`);
console.log(`total ${open} / ${K.length} items contestable ` +
  `(${Math.round(open * 100 / K.length)}%), ${K.length - open} items defensible`);
const worst = Math.max(...[...point.values()].map((n) => n.passing));
console.log(`if a single point's record drops entirely, the worst case is ${worst} items ` +
  `(${Math.round(worst * 100 / K.length)}%), added on top of the ${open} items already observed`);
transfer point           items passing     next step  opens up if record drops
collection>packaging                13             3            13 items (46%)
packaging>transit                   13             2            13 items (46%)
transit>lab                         13             1            13 items (46%)
lab>analysis                        13             0            13 items (46%)
collection>transit                  11             1            11 items (39%)
transit>analysis                    11             0            11 items (39%)
intake>analysis                      4             0             4 items (14%)

missing field  question left unanswered             transfers  items
from           who had the item                             3      3
to             who received it                              1      1
time           can the order be proven                      1      1
medium         under what condition was it stored           2      2
hash           can it be shown unchanged                    3      3

10 of 390 record fields are empty (2.6%); this puts 9 items in question, and hash mismatches put 3 more in question
total 12 / 28 items contestable (43%), 16 items defensible
if a single point's record drops entirely, the worst case is 13 items (46%), added on top of the 12 items already observed

The last three lines are this lesson’s result. 2.6 percent of record fields are empty, and that puts 43 percent of items in question. The gap between the two rates is not an arithmetic error; it is the chain’s structure: a field is filled in one at a time, but an item is defended as a whole. If one of an item’s twenty fields is empty, the verdict for the entire item is “cannot be shown unchanged.” Integrity is not a divisible property.

The first table shows where this fragility concentrates. Each of the four points in the field chain carries thirteen items; if any one of them loses its record entirely, thirteen items open up at once. This is not an item-to-record relationship; it is a bottleneck relationship: the same courier, the same intake desk, the same form. For the four items taken in remotely, the same risk measures four.

The second table separates what goes unanswered by the type of missing field, and the columns are not equal. If the hash field is empty, the item cannot be shown to be unchanged; that makes the item directly unusable. If the medium field is empty, only the storage condition is unknown; the item is still bound by its hash, and that gap can later be closed by testimony. If a chain audit records these two cases in the same column, it confuses a closable gap with one that cannot be closed, and spends the examination’s budget in the wrong place.

The Cost of Shortening the Chain

// forensics/shorten.mjs — the link between chain length and expected open items.
import { items, FIELD, missingFields } from "./item.mjs";
const K = items(), P = 0.11;                    // probability a field is left blank, per transfer
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());
const expect = (a) => 1 - (1 - P) ** a;         // probability at least one field is left blank

const A = [-20, 7, 11, 8, 10, 21];
y(A, "chain", "item", "transfers", "fields", "observed", "expected open items");
let tk = 0, ta = 0, tb = 0, tg = 0;
for (const [name, n] of [["field (5 nodes)", 4], ["central (3 nodes)", 2], ["remote (2 nodes)", 1]]) {
  const g = K.filter((k) => k.transfer.length === n);
  const observed = g.filter((k) => missingFields(k) > 0).length;
  y(A, name, g.length, g.length * n, g.length * n * FIELD.length, observed,
    (g.length * expect(n)).toFixed(2));
  tk += g.length; ta += g.length * n; tg += observed; tb += g.length * expect(n);
}
y(A, "total", tk, ta, ta * FIELD.length, tg, tb.toFixed(2));

// On-site analysis: field items are not transported, the chain drops to a single transfer.
const s = K.filter((k) => k.transfer.length === 4).length;
const a2 = s + K.filter((k) => k.transfer.length === 2).length * 2 +
  K.filter((k) => k.transfer.length === 1).length;
const b2 = s * expect(1) + K.filter((k) => k.transfer.length === 2).length * expect(2) +
  K.filter((k) => k.transfer.length === 1).length * expect(1);
y(A, "on-site analysis", tk, a2, a2 * FIELD.length, "-", b2.toFixed(2));
console.log(`transfers ${ta} -> ${a2} (${Math.round((1 - a2 / ta) * 100)}% fewer), ` +
  `expected open items ${tb.toFixed(2)} -> ${b2.toFixed(2)}`);
chain                  item  transfers  fields  observed  expected open items
field (5 nodes)          13         52     260         7                 4.84
central (3 nodes)        11         22     110         1                 2.29
remote (2 nodes)          4          4      20         1                 0.44
total                    28         78     390         9                 7.57
on-site analysis         28         39     195         -                 4.16
transfers 78 -> 39 (50% fewer), expected open items 7.57 -> 4.16

The expected column tests the model against itself: with an 11 percent probability of a blank field per transfer, 4.84 of the field items are expected to open up; the observed number is 7. The deviation is normal for a small set, and that is exactly why chain audits are done item by item, not by average.

The last row measures an option. If field items are not transported to the lab and analysis is done on-site, the chain drops to a single transfer: 78 transfers fall to 39, and expected open items fall from 7.57 to 4.16. The decision cannot be made without writing down what this gain costs. On-site analysis requires someone with that skill set to travel to the field; the time they spend traveling is added to every item’s clock, and in the table, items whose lifetime is under two hundred seconds will not wait that long. Shortening the chain protects the item; delay destroys the item — the two are opposite ends of the same measure, and which one dominates can only be worked out with actual durations.

Summary

  • An evidence item is a piece of data bindable to a single integrity value; in the fictional incident, 18 of 28 items are volatile, with a lifetime under one hour.
  • The hash binds the item to itself and shows only that it changed: three items have a mismatch, all three were opened without write protection, and only their access timestamp was refreshed.
  • The chain of custody is four transfers at field hosts, two at central hosts, and a single transfer for remote intake; five fields per transfer means 78 transfers and 390 fields in total.
  • 2.6 percent of fields being left blank puts 43 percent of items in question: a field is filled in one at a time, but an item is defended as a whole.
  • Each of the four points in the field chain carries 13 items; a single point losing its record opens up 46 percent of items at once.

Next Step

This lesson built the chain around the item: who carried it, when it was handed over, which hash was recorded. How the item itself was collected was never asked. In the table, three of the 28 items are disk images, and each carries a 5,400-second collection duration in the model — these are the most expensive items in the topic.

The next lesson measures that copying in terms of verifiability. What proves the copy is identical to the source, which part of the source never makes it into the copy, how many items a partial copy misses, and how many items change during copying when the copy is taken from a running system. Three numbers come out separately; the first is established with a hash, the second with scope, and the third can only be seen by comparing two hashes.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close