---
title: 'Disk Imaging'
source: 'https://academia.sh/en/courses/security-operations/disk-imaging'
course: 'Security Operations and Incident Response'
language: en
updated: '2026-09-26T03:02:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows what proves a copy is identical to its source through hash comparison'
    - 'Compares copying scope in terms of slices taken, duration, and evidence items missed'
    - 'Counts the slices that change and the items that cannot be verified in a copy taken from a running system'
---

# Disk Imaging

Measuring the verifiability of write-blocked copying: on a fictional 4,096-slice source, a selective directory copy takes 4 of 15 evidence items, a logical copy takes 8, a partition block takes 13, and a full block copy takes all 15. Because the source hash can be recomputed in an offline copy, 15 of 15 items are verified; in a copy taken from a running system, 303 of 4,096 slices change, all three hashes come out different, and the number of verified items drops to 9.

The previous lesson built the chain around the item and noted that three of the 28 items are disk images, each carrying a 5,400-second collection duration in the model. The chain's first link, the **collection** step, passed there as a single box. This lesson opens that box and asks a single question: what proves the copy is identical to the source.

The question is not as narrow as it looks. A hash compares two values; for the comparison to mean anything, **the second value has to be computable**. If the source does not change during copying, its hash can be recomputed afterward and compared with the image's. If the source changes, there is no fixed value to compare against, and the hash only binds the image to itself. This lesson works out three numbers separately: how many items are bound to the source by a hash, how many items a partial copy misses, and how many items change during copying.

- **FE7.** The source is modeled as a 512 GiB storage volume divided into 4,096 **slices**. A slice here is a unit of measurement, not a file-system block; no real storage volume is read.
- **FE8.** A **write blocker** does not pass through write requests directed at the source. The source does not change during or after copying.
- **FE9.** A **forensic image** is a sequential copy of every slice within scope. The copying speed is 97 MiB/s, and it is fictional.
- **FE10.** A copy is **verifiable** only if the source's hash can be recomputed after copying and compared with the image's hash.
- **FE11.** On a running system, each zone changes at its own rate; the rate is the number of slices that change per 1,000 seconds, and it is fictional.
- **FE12.** An evidence item occupies five consecutive slices; the item's hash is computed from the digests of those five slices.

## Source, Zone, and Scope

```js
// forensics/disk.mjs — slice model of a 512 GiB source, its zones, and its evidence items.
// A MODEL: no real disk is read; slice content comes from the generator.
import { createHash } from "node:crypto";

export const SEED = 20260803, SLICE = 4096, MIB = 128, SPEED = 97;   // slice 128 MiB, 97 MiB/s
export const DURATION = Math.round(SLICE * MIB / SPEED);             // full copy duration (s)
const generator = (t) => { let s = t >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; };

// zone -> [slice count, slices changed per 1,000 seconds (on a running system)]
export const ZONE = {
  "partition-table": [8, 0], "filesystem-meta": [240, 9], "active-files": [2400, 26],
  "filesystem-journal": [128, 14], "swap-area": [512, 11],
  "unallocated-space": [760, 2], "off-partition-area": [48, 0],
};

// Copying scope -> which zones it takes.
export const SCOPE = {
  "selective directory": ["active-files"],                    // only the selected directories
  "logical copy": ["active-files", "filesystem-meta"],
  "partition block": ["active-files", "filesystem-meta", "filesystem-journal",
    "swap-area", "unallocated-space"],
  "full block": Object.keys(ZONE),
};

// item -> [zone, whether inside the selected directory]
export const ITEM = {
  "configuration-file": ["active-files", true], "service-definition": ["active-files", true],
  "application-log": ["active-files", true], "scheduled-task": ["active-files", true],
  "user-document": ["active-files", false], "cache-file": ["active-files", false],
  "deleted-installer-remnant": ["unallocated-space", false],
  "deleted-log-fragment": ["unallocated-space", false],
  "temp-file-residue": ["unallocated-space", false],
  "deleted-file-name": ["filesystem-meta", false],
  "timestamp-record": ["filesystem-meta", false],
  "log-entry": ["filesystem-journal", false],
  "swap-space-residue": ["swap-area", false],
  "off-partition-configuration": ["off-partition-area", false],
  "partition-table-record": ["partition-table", false],
};

// Slices are laid out zone by zone; each item occupies five consecutive slices.
export function source() {
  const r = generator(SEED), d = [];
  for (const [b, [n]] of Object.entries(ZONE))
    for (let i = 0; i < n; i += 1)
      d.push({ no: d.length, zone: b, content: `${b}:${i}:${Math.floor(r() * 1e9)}` });
  const item = {};
  for (const [name, [b, selected]] of Object.entries(ITEM)) {
    const zoneSlices = d.filter((x) => x.zone === b).map((x) => x.no);
    const start = Math.floor(r() * (zoneSlices.length - 5));
    item[name] = { zone: b, selected, slice: zoneSlices.slice(start, start + 5) };
  }
  return { slice: d, item };
}

export const digest = (s) => createHash("sha256").update(s).digest("hex").slice(0, 16);
export const wholeHash = (d) => digest(d.map((x) => digest(x.content)).join("|"));
export const itemHash = (d, k) => digest(k.slice.map((n) => digest(d[n].content)).join("|"));
export const covered = (scope, name) => {
  const [b, selected] = ITEM[name];
  return SCOPE[scope].includes(b) && (scope !== "selective directory" || selected);
};
```

The zone list is this lesson's real framework. A storage volume is not just files: file-system metadata carries the names and timestamps of deleted files, unallocated space carries the not-yet-overwritten remnants of deleted content, swap space carries fragments dropped out of running processes' memory, and the area outside the partition table is never seen by the file system at all. Most evidence items live in these zones.

## How Many Items Scope Misses

```js
// forensics/scope.mjs — slices, duration, and items missed by copying scope.
import { source, ZONE, SCOPE, ITEM, SLICE, MIB, SPEED, DURATION, covered } from "./disk.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 { slice } = source();
const selectedShare = 0.35;                         // selected directories are 35% of active files

console.log(`model (seed 20260803): ${SLICE} slices x ${MIB} MiB = ` +
  `${SLICE * MIB / 1024} GiB, ${SPEED} MiB/s, full copy ${DURATION} s`);
console.log(`${Object.keys(ZONE).length} zones, ${Object.keys(ITEM).length} evidence items`);

const A = [-24, 8, 10, 12, 9, 9];
console.log("");
y(A, "scope", "slices", "GiB", "duration s", "items", "missed");
for (const name of Object.keys(SCOPE)) {
  let n = SCOPE[name].reduce((a, b) => a + ZONE[b][0], 0);
  if (name === "selective directory") n = Math.round(n * selectedShare);
  const taken = Object.keys(ITEM).filter((k) => covered(name, k)).length;
  y(A, name, n, (n * MIB / 1024).toFixed(1), Math.round(n * MIB / SPEED), taken,
    Object.keys(ITEM).length - taken);
}

const B = [-32, -22, 22, 17, 17, 12];
console.log("");
y(B, "item", "zone", "selective directory", "logical copy", "partition block", "full block");
for (const name of Object.keys(ITEM))
  y(B, name, ITEM[name][0], ...Object.keys(SCOPE).map((k) => (covered(k, name) ? "yes" : "-")));

const missed = Object.keys(ITEM).filter((k) => !covered("partition block", k));
console.log(`\nitems missed by partition block: ${missed.join(", ")}`);
console.log(`logical copy takes ${Math.round((ZONE["active-files"][0] +
  ZONE["filesystem-meta"][0]) * 100 / slice.length)}% of slices, takes ` +
  `${Object.keys(ITEM).filter((k) => covered("logical copy", k)).length} / ` +
  `${Object.keys(ITEM).length} items`);
```

```
model (seed 20260803): 4096 slices x 128 MiB = 512 GiB, 97 MiB/s, full copy 5405 s
7 zones, 15 evidence items

scope                     slices       GiB  duration s    items   missed
selective directory          840     105.0        1108        4       11
logical copy                2640     330.0        3484        8        7
partition block             4040     505.0        5331       13        2
full block                  4096     512.0        5405       15        0

item                            zone                     selective directory     logical copy  partition block  full block
configuration-file              active-files                             yes              yes              yes         yes
service-definition              active-files                             yes              yes              yes         yes
application-log                 active-files                             yes              yes              yes         yes
scheduled-task                  active-files                             yes              yes              yes         yes
user-document                   active-files                               -              yes              yes         yes
cache-file                      active-files                               -              yes              yes         yes
deleted-installer-remnant       unallocated-space                          -                -              yes         yes
deleted-log-fragment            unallocated-space                          -                -              yes         yes
temp-file-residue               unallocated-space                          -                -              yes         yes
deleted-file-name               filesystem-meta                            -              yes              yes         yes
timestamp-record                filesystem-meta                            -              yes              yes         yes
log-entry                       filesystem-journal                         -                -              yes         yes
swap-space-residue              swap-area                                  -                -              yes         yes
off-partition-configuration     off-partition-area                         -                -                -         yes
partition-table-record          partition-table                            -                -                -         yes

items missed by partition block: off-partition-configuration, partition-table-record
logical copy takes 64% of slices, takes 8 / 15 items
```

The scope table traces a curve, and the curve is not straight. The selective directory copy takes twenty percent of the source and brings back **four** of 15 items. The logical copy triples the area, and the item count doubles. The partition block copy adds fifty percent more area on top of the logical copy and raises the item count from 8 to **13**. The last step is the cheapest: 56 more slices, 74 more seconds, two more items.

This last step is the most instructive row in the table. The partition block copy has taken 98.6 percent of the source and looks "nearly complete"; the two items it misses are the partition table record and the configuration in the off-partition area. A decision that measures scope **by ratio** treats the remaining 1.4 percent as negligible. A decision that measures scope **by item** sees that the same 1.4 percent carries two items. Evidence is counted by item, not by ratio.

The second table shows the same result item by item. The deleted file name and the timestamp record are in metadata; a file copier sees these but does not read the deleted record. The deleted installer remnant, the deleted log fragment, and the temp-file residue are in unallocated space; no file copy can reach them, because the file system no longer binds these slices to a file. Counted by item, a file copy leaves **at best half, at worst three-quarters** of the evidence set outside its scope.

## Copying from a Running System

```js
// forensics/live.mjs — verifiability of an offline copy versus a copy from a running system.
import { source, ZONE, ITEM, SLICE, DURATION, wholeHash, itemHash } from "./disk.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 { slice, item } = source();
const start = slice.map((d) => ({ ...d }));               // source state at the start of copying
const r = (() => { let s = 20260804 >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; })();

// Offline copy: the write blocker does not change the source.
const offline = start.map((d) => ({ ...d }));
console.log(`offline copy: source hash ${wholeHash(start)}, image hash ` +
  `${wholeHash(offline)}, equal ${wholeHash(start) === wholeHash(offline) ? "yes" : "no"}`);

// Running system: each zone changes at its own rate, copying advances slice by slice.
const end = start.map((d) => ({ ...d }));
const image = start.map((d) => ({ ...d }));
const copyMoment = (no) => (no + 1) * DURATION / SLICE;
const before = new Set(), after = new Set();
for (const [b, [n, rate]] of Object.entries(ZONE)) {
  const pool = start.filter((d) => d.zone === b).map((d) => d.no);
  for (let e = 0; e < Math.round(rate * DURATION / 1000); e += 1) {
    const no = pool[Math.floor(r() * n)], t = r() * DURATION;
    end[no].content += `|write${e}`;
    if (t < copyMoment(no)) { image[no].content = end[no].content; before.add(no); }
    else after.add(no);
  }
}
for (const no of before) after.delete(no);

const A = [-22, 9, 10, 16, 17];
console.log("");
y(A, "zone", "slices", "changed", "before copy", "after copy");
for (const b of Object.keys(ZONE)) {
  const h = start.filter((d) => d.zone === b).map((d) => d.no);
  y(A, b, h.length, h.filter((n) => end[n].content !== start[n].content).length,
    h.filter((n) => before.has(n)).length, h.filter((n) => after.has(n)).length);
}
const changed = start.filter((d, i) => end[i].content !== d.content).length;
y(A, "total", SLICE, changed, before.size, after.size);

console.log(`\nrunning system: source at copy start ${wholeHash(start)}, source at copy ` +
  `end ${wholeHash(end)}, image ${wholeHash(image)}`);
console.log(`all three differ; there is no single source state the image can be equated with`);

const B = [-30, 16, 16, 12];
console.log("");
y(B, "item", "image=copy start", "image=copy end", "verified");
let clean = 0, neither = 0;
for (const [name, k] of Object.entries(item)) {
  const a = itemHash(image, k) === itemHash(start, k);
  const b = itemHash(image, k) === itemHash(end, k);
  if (a && b) clean += 1;
  else y(B, name, a ? "yes" : "no", b ? "yes" : "no", "no");
  if (!a && !b) neither += 1;
}
const N = Object.keys(ITEM).length;
console.log(`${clean} of ${N} items are the same under both source states; ` +
  `${N - clean} items changed during copying, and ${neither} of those cannot be equated ` +
  `with either source state`);
console.log(`items verified in the offline copy ${N}/${N}, in the running-system copy ${clean}/${N}`);
```

```
offline copy: source hash 8db5b10c2d423fe9, image hash 8db5b10c2d423fe9, equal yes

zone                     slices   changed     before copy       after copy
partition-table               8         0               0                0
filesystem-meta             240        43               4               39
active-files               2400       139              43               96
filesystem-journal          128        56              37               19
swap-area                   512        54              43               11
unallocated-space           760        11               8                3
off-partition-area           48         0               0                0
total                      4096       303             135              168

running system: source at copy start 8db5b10c2d423fe9, source at copy end 429ed9b9ddd32a58, image 8128d3063118f7f3
all three differ; there is no single source state the image can be equated with

item                          image=copy start  image=copy end    verified
scheduled-task                             yes              no          no
cache-file                                  no             yes          no
deleted-file-name                          yes              no          no
timestamp-record                           yes              no          no
log-entry                                   no              no          no
swap-space-residue                          no             yes          no
9 of 15 items are the same under both source states; 6 items changed during copying, and 1 of those cannot be equated with either source state
items verified in the offline copy 15/15, in the running-system copy 9/15
```

The first line carries copying's evidentiary side by itself. Behind a write blocker the source does not change; after copying finishes, the source's hash is recomputed and compared with the image's. The two values are equal. What is proven is **that the image is a copy of the source**, and that is the strongest statement a hash can make.

The same computation cannot be done on a running system. Copying takes 5,405 seconds, and in that time **303** of 4,096 slices change. Where the change concentrates directly reflects the zone rates: 56 of the file-system journal's 128 slices, 54 of swap space's 512 slices, only 11 of unallocated space's 760 slices. Unallocated space, the most valuable zone for evidence, is also the most stable among the data-bearing zones; the most active zones are the ones that turn over from the system's own operation.

All three hashes come out different, and this result has only one reading: **there is no source state the image can be equated with.** The image is neither the source at the start of copying nor the source at the end; it carries the new content in 135 slices and the old content in 168. What results is a mixture that never existed at any single moment.

The item table counts the cost of this: six of 15 items change during copying, and verified items drop from 15 to **9**. A warning is needed here. The model's "image = copy start" column **cannot be observed** in a real examination, because the source's state at the start of copying no longer exists. All that remains is the image's own hash, and that hash shows only that the image did not change afterward — not that it is identical to the source. In a live copy, the number of verified items is counted as zero, because it cannot be measured.

This does not make a live copy the wrong move. On a system that cannot be shut down, it is the only option, and this lesson has already counted how many items scope brings back. What must be done is to write the condition under which the image was taken into the chain-of-custody record: the source was running, copying took this long, verification is limited to the image itself. A condition left unwritten later looks like **a gap that was never even noticed**.

## Summary

- A copy's verifiability depends on whether the source's hash can be recomputed; behind a write blocker, the two hashes come out equal and 15 of 15 items are verified.
- The scope curve is not straight: a selective directory brings back 4 items, a logical copy 8, a partition block 13, and a full block copy 15; the last step adds two items for 56 slices and 74 seconds.
- A partition block copy that takes 98.6 percent of the source still misses two items; scope is measured by item, not by ratio.
- Most evidence items are in zones that are not files: 3 in unallocated space, 2 in metadata, 1 in the file-system journal, 1 in swap space.
- In a copy taken from a running system, 303 of 4,096 slices change, all three hashes come out different, and no source state remains that the image can be equated with; verified items drop from 15 to 9.

## Next Step

One item never appeared in this lesson. The model's zone list has no memory, and rightly so: memory is not on disk, and no copying scope takes it. Swap-space residue carries fragments dropped out of memory, but that is memory's remnant, not memory itself — and that remnant is among the zones that change during copying.

The next lesson works out this topic's most important number. Evidence items are arranged in an **order of volatility** by their lifetime, and it calculates how many items are lost when the collection order does not follow that order. The same 28 items, the same durations, three different orders: by volatility, host by host, and largest item first. The difference between them is not a matter of preference — it is directly the number of pieces of evidence lost.
