Pocket CTF Reference · Device home

Pocket CTF logo

Pocket CTF Student Guide

This is the practical companion to the game. It shows you where to look, how to use the common tools, and how to get unstuck without printing the answers. Use the glossary for unfamiliar web, data, evidence, and cryptography terms.

Each playable profile has six modules. Open one from the Pocket CTF home page, read its task, and use this guide for tool recipes and module links.

Headless boards start in Documentation. On a board with a display, use its buttons to select and confirm Documentation or your assigned game before joining Wi-Fi. In Documentation on any board, choose Choose a track, select your assigned track, and press Start selected track. The board restarts; stay on the same PocketCTF-XXXX Wi-Fi network and reopen http://192.168.4.1/ if needed.

During play, Reference opens the offline lessons without restarting or clearing progress. Use Device home to return to the game. Tool exercises, fundamentals, and module guidance remain available without internet access.

To reselect, follow the game's Switch track link and confirm End game and return to Documentation. This ends the game for everyone on the board and clears all progress. Opening the confirmation page alone does not end the game. A reset or power cycle returns headless boards to Documentation and display boards to their physical menu. These controls are shared; coordinate with your instructor and teammates before switching.

USB remains an optional alternative when mpremote is installed:

mpremote connect PORT exec "from pocket_ctf.selection import set_active; set_active('hard')" + reset

Replace hard with easy, medium, insane, glass-witness, river-station, lattice, or docs as needed. The docs option returns to the documentation home page. Find PORT with mpremote devs. The request applies to the next boot only; the following reset restores the board's normal startup. Switching restarts the board and clears in-memory progress.

Quick start

  1. Follow the connection guide and open the assigned Pocket CTF profile.
  2. Keep the game open in one normal browser profile. Do not use a private window or delete the pctf_session cookie.
  3. Start with the browser. Use a terminal only when the task mentions headers, methods, hashes, metadata, or an API.
  4. Copy a revealed flag or seal exactly, including POCKET{...} and its final suffix. Each flag in the selected track has its own four-character hexadecimal suffix, regenerated when the game starts. Discover flags again after a restart; do not reuse another challenge's suffix.
  5. If you are stuck, read only the first hint for that module. Try it before reading the second hint or revealing an in-game hint.

What you need

ToolWhat you use it forAlternative
Modern browserPages, source, developer tools, cookies, requestsAny current Firefox-, Chromium-, or WebKit-based browser
Text editorNotes, logs, JSON, copied valuesA browser tab for small text files
curlHeaders, methods, custom headers, formsBrowser Network panel or an instructor-approved HTTP client
Python 3Decoding, small searches, arithmetic, advanced modulesAnother language with the same operations
file, strings, sha256sumFile type, readable strings, hashesFile-properties or checksum tools
exiftool, pdfinfo, pdfdetachImage/PDF metadata and attachmentsThe offline tool linked by the module
jqRead and filter JSONA text editor or browser JSON viewer

You do not need every tool for every profile. Easy and most scenario modules can be completed with a browser and a few local file tools. Hard and Insane expect short scripts and more command-line work.

How to use this reference

Start with the tool exercise linked by your module. Run its synthetic example locally and compare your output with the expected result before substituting challenge evidence. A successful command is only the first checkpoint: name the field or bytes it exposed and explain what they establish.

Keep notes with five entries: original request or filename, observation, question, one controlled change, and result. For a file, include the field name or line number. Keep observations separate from inferences: “the author field contains a name” does not prove that person wrote the document.

If stuck, first decide whether the problem is tool syntax, interpretation, missing evidence, or submission format. Use the expected output and error guidance below to fix tool problems before spending a puzzle hint.

Stay inside the exercise

Use these techniques only on the Pocket CTF board assigned to you. Do not scan, flood, or run general guessing scripts against the board. Quiet Oracle and Final Authority explicitly require bounded, sequential tests; follow their module limits. Send one request at a time; it is a small single-threaded device. A module may explicitly permit a small, bounded search on data copied to your computer. Run that search locally, then send only the result to the board.

The commands below use DEVICE_IP, PATH, NAME, and VALUE as placeholders. Replace them only with information shown by your assigned exercise.

Browser basics

Open your browser's menu and find Developer Tools (Chrome/Edge: More tools; Firefox: More tools → Web Developer Tools). The panel names below refer to that window. Firefox calls the DOM panel Inspector; Chrome and Edge call it Elements. Select Network before making a request so it can record the exchange.

Choose a tool path

You do not need to learn every tool before starting. Use one path, then try the alternative to check your understanding. Both paths should expose the same evidence; record any differences rather than assuming the GUI is incomplete.

Task and example modulesBrowser or graphical pathTerminal path
Source and hidden controls: Source Sleuth, Behind the Button, Covered NameView Source and Elements/InspectorSave HTML and search it as text
Headers and forms: Header Hunter, Profile Overflow, Trusted GatewayNetwork and a request editorcURL
Encodings: Encoded Telemetry, Layered Token, Checksum CrownLocal browser consolePython or Base64/hex CLI
Structured records: Weekend Posts, Echo in the Rain, Consent in the Wrong OrderJSON viewer and evidence tablejq or Python
Image metadata: Byline Photo, Portrait of StaticMetadata-aware image viewerExifTool or supplied PNG script
PDF properties/attachments: Pitch PDF, Lawsuit FilingDesktop PDF properties and attachment panelspdfinfo, ExifTool, pdfdetach
File integrity: Hairline FractureFile-checksum utility with SHA-256sha256sum, Get-FileHash, or Python
Chronology: Four ClocksPaper or spreadsheetPython datetime
Evidence decisions: Safety DecisionBrowser and evidence tableJSON viewer

The sections below explain how to operate each path. Browser-only devices may not have a capable metadata or PDF attachment viewer; ask for the classroom's prepared tool or an instructor-provided text export. A screenshot of a file is not equivalent to that file's metadata or attachments. Insane modules still require local scripting for repeated calculations and controlled requests.

Practice with a local HTML file

Create a folder for practice. In a plain-text editor, save the following as practice.html (not practice.html.txt), then open it in your browser. This exercise only reads and edits your local browser view; it sends no board requests.

<!doctype html>
<title>Tool practice</title>
<p id="message">Ready</p>

<form>
  <input type="hidden" name="layout" value="compact">
</form>

The page should display Ready. Use View Page Source, then search for Review note. You should find the comment even though it is absent from the rendered page. Next, open Inspect, select the paragraph, and change Ready to Changed locally. The visible page changes, but reopening source still shows Ready. Reloading restores the original page because you did not edit the saved file.

Find the hidden input in the inspector. Its name identifies the form field; its value is the data it would submit. Changing type to text makes it visible. Do not submit this practice form: it has no server handler.

Checkpoint: Explain why the comment, hidden input, and local text edit produce three different views of the same document. If the browser displays the markup as text, check the extension and that you used a plain-text editor.

View the original page source

Right-click the page and choose View Page Source, or use the browser's view source shortcut. This opens the HTML the server sent.

Search for:

Important: The Elements/Inspector panel shows the browser's current document. View Source shows the original response. Check both when a task says the browser received something it did not display.

Inspect or edit an element

  1. Open developer tools.
  2. Choose Elements or Inspector.
  3. Use the element-picker tool, then click the form or text you want to inspect.
  4. Double-click an attribute or value to edit it.
  5. Submit the form normally.

Edits affect only your browser's copy of the page. They are useful for testing whether the server trusts hidden or disabled form data.

See a complete request and response

  1. Open developer tools and select Network.
  2. Reload the page or perform one action.
  3. Select the new request.
  4. Check its URL, method, status, headers, query string or form data, and response body.

Enable Preserve log when a button moves you to a new page. Use Copy as cURL when you want a terminal command matching a browser request. Reproduce that baseline before removing headers: cookies, content type, and custom fields may matter. Keep copied session cookies out of shared notes.

Browser path: Clear the Network list, perform one action, then select the row whose URL matches the action. If many rows appear, filter by a distinctive part of the route; do not select an image or stylesheet accidentally. The request Headers section shows method and URL; Payload or Request shows submitted fields; Response Headers and Response show what came back. A form navigation may replace the page, which is why Preserve log matters.

Terminal path: Use curl -i as explained in Run and read a cURL command. A browser and cURL comparison is meaningful only if method, path, fields, and required cookies match. The browser normally sends cookies automatically; cURL does not.

Edit and resend a request in the browser

In Firefox Network, right-click the recorded request and choose Edit and Resend (the label may vary by browser version). Retain a baseline, change one method, header, or body field in the editor, and send it once. Select the new Network row to check what was actually sent and inspect the result. If your browser has no request editor, use Copy as cURL and the terminal path.

For a browser-only alternative, open the Console while on your assigned board's page. This synthetic template sends one GET on that same board; replace PATH only with a permitted route. It may create or update exercise session state. Enter code only after reading what it does. If browser paste protection blocks an example, use the request editor or terminal instead of disabling protection.

fetch('/PATH', {
  headers: {'X-Workshop-Mode': 'compact'}
}).then(async response => {
  console.log('status', response.status);
  console.log('body', await response.text());
}).catch(error => console.error('request failed', error));

fetch uses GET unless given a method, sends same-origin cookies by default, and does not reject its promise just because the server returns 403. Read the printed status and inspect the request in Network. The header here is an unrelated example; it is not a puzzle's required name or value. Do not try to set a Cookie header in browser JavaScript; the browser manages it.

For a permitted form submission, the corresponding structure is:

const fields = new URLSearchParams({label: 'blue & gold'});
fetch('/PATH', {method: 'POST', body: fields})
  .then(async r => console.log(r.status, await r.text()));

This changes server state if the route implements an update. URLSearchParams encodes the ampersand as part of the value and supplies form encoding. Compare this with Submit form data. Browser request editors and fetch cannot reproduce every possible raw HTTP request; use cURL when browser restrictions prevent the operation the module calls for.

A client sends a request with method, target, headers, and optional body. The server returns a response with status, headers, and optional body; headers and bodies answer different questions.

A request includes a method and target, headers, and sometimes a body. The response includes a status, headers, and sometimes a body. Inspect both directions: a browser's rendered page usually shows only the response body. Methods, cookies, authorization, and application state can affect the result; a status code alone is not a complete explanation. The diagram shows a generic exchange, not a station request.

  1. Open developer tools.
  2. Choose Storage or Application, then Cookies.
  3. Select the Pocket CTF address.
  4. Locate the cookie named by the module and inspect its value.
  5. If the task asks whether the browser controls that value, edit only that challenge cookie and reload.

Warning: Do not edit or delete pctf_session. It connects the browser to your progress on the board.

The cookie table may show Name, Value, Domain, Path, and expiry. Select the row with the module's challenge-cookie name; do not replace the whole table. Copy its original value to a private note, edit the value cell, press Enter, and reload the challenge. Then inspect the new request's Cookie header in Network to confirm what was actually sent. A value in storage is not proof that the request used it: domain and path determine where a cookie is sent.

If an edit has no effect, check for a second cookie with the same name but a different path, and inspect the response's Set-Cookie header to see whether the server replaced your value. Restore the original challenge value when done. Set-Cookie travels from server to browser; Cookie travels back in a request.

Change a URL safely

A URL may contain a path and a query string:

http://DEVICE_IP/some/path?name=VALUE&mode=VALUE

Command-line HTTP recipes

These examples use synthetic paths and values. They do not contain a Pocket CTF solution.

Run and read a cURL command

Open a terminal and run curl --version; on Windows PowerShell use curl.exe to select the cURL executable rather than a shell alias. If it is unavailable, use the browser Network panel or the classroom's prepared HTTP client. Arrange tool installation before joining the board's offline network.

The shell examples use Bash-style quoting and \ to continue a line. In PowerShell, enter each command on one line with curl.exe; do not copy the Bash continuation backslashes. Quote a URL containing & so the shell does not treat it as a command separator.

curl -sS -i 'http://DEVICE_IP/PATH?format=text&limit=2'

Replace the placeholders with a permitted route on your assigned board. -sS hides the progress meter but keeps connection errors; -i includes the response headers. A synthetic response might look like:

HTTP/1.0 200 OK
Content-Type: application/json
X-Workshop-Note: draft

{"ready":true}

Read from top to bottom: the status describes the result, headers describe the response, and the blank line ends the headers. Everything after it is the body. X-Workshop-Note is a response header, not a field to send back. In the browser, choose the matching Network row and compare Headers and Response; the Preview tab may transform the presentation.

ObservationMeaning and next check
cURL connection error, no HTTP statusCheck Wi-Fi, address, and port; the application has not answered.
200Request handled; inspect the body to learn what it returned.
204Successful response with no body; empty output can be expected.
400Request format rejected; compare names, encoding, and required fields.
403Operation denied; preserve the reason and your unchanged baseline.
404Resource not found; recheck the route before changing your hypothesis.
405Method not allowed; inspect Allow and the response body.

HTTP errors usually do not make cURL itself report a connection failure. Read the HTTP status. Avoid -I when reproducing a GET: it sends HEAD, which may behave differently. Avoid following redirects automatically until you have recorded the original status and Location.

Read a page and its response headers

This sends one GET request to retrieve a page. Retrieval can still affect internal state: opening the home page can create a session and evict an older one, and some profile pages update hint state.

curl -i http://DEVICE_IP/PATH

Use -sS when you want less progress output but still want errors:

curl -sS -i http://DEVICE_IP/PATH

Ask which HTTP methods are supported

This asks the endpoint about its supported operations. Implementations differ; an absent or incomplete answer is not proof that no other method exists:

curl -i -X OPTIONS http://DEVICE_IP/PATH

Read both the response body and the Allow header. A module may name the next method there.

Send a custom header

This sends a retrieval GET with one client-controlled header. The same session and hint-state caveats apply:

curl -i -H 'HEADER-NAME: VALUE' http://DEVICE_IP/PATH

Use the exact header name and value provided by the module's evidence. Header names are case-insensitive; values may be case-sensitive.

Send a different method

Replace METHOD only with a method named by the exercise:

curl -i -X METHOD http://DEVICE_IP/PATH

Warning: A non-GET method may change exercise state. Run it only against the assigned module and only after the module indicates that method.

Submit form data

This sends a state-changing request:

curl -i -X POST \
  --data-urlencode 'NAME=VALUE' \
  http://DEVICE_IP/PATH

For more than one field, repeat --data-urlencode. Copy field names from the HTML form, Network panel, or module response.

For practice, consider a form field label whose value is blue & gold. The argument --data-urlencode 'label=blue & gold' encodes the space and literal ampersand as value data. Writing label=blue & gold directly into a raw form body would make & a field separator. Supply ordinary text to --data-urlencode; supplying %26 when you mean & encodes the percent sign again. --data-urlencode selects POST by default; -X POST here is explicit.

To add one field, first capture the normal form submission in Network. Note the URL, method, content type, and each name/value pair in Payload or Request. Use Copy as cURL, reproduce that request, then add one --data-urlencode argument. Compare both complete responses. Changing several fields at once prevents you from identifying which change caused the result.

Keep the same Pocket CTF session

curl does not automatically share your browser's cookies. Prefer the browser for stateful scenario tracks. If an exercise specifically requires curl to use the same session, copy only your own pctf_session value:

curl -i --cookie 'pctf_session=YOUR_VALUE' \
  http://DEVICE_IP/PATH

Treat that value as private and delete it from shared notes.

Save a response without damaging the evidence

Download through the browser's link or save a response body to a new local filename. This GET writes local files and can create or update game session state as described above:

curl -sS -D response-headers.txt -o evidence-copy.bin \
  'http://DEVICE_IP/PATH'

-D saves headers separately; -o saves only the body. These options overwrite the named local files, so use a fresh exercise folder or new names. Read the saved status and Content-Type before processing the download. An HTML error page saved with a .png extension is still HTML. Do not redirect curl -i into a file you intend to hash: that would prepend HTTP headers to its bytes.

Checkpoint: You can locate the status, one response header, and body independently, and can explain why only the body belongs in a downloaded artifact.

Data and file recipes

Run a short Python script

Use a plain-text editor to save code as practice.py in your exercise folder. Open a terminal in that folder and run python3 practice.py. On a Windows installation with the Python launcher, use py practice.py. Do not copy the Markdown backticks or an interactive prompt such as >>> into the file.

text = "lamp"
raw = text.encode("utf-8")
print(repr(raw), len(raw), raw.hex())

Expected output: b'lamp' 4 6c616d70. Text is a sequence of characters; bytes is the exact sequence used by encoders, hashes, and network protocols. repr makes invisible characters visible: b'lamp\n' contains a newline and has length 5. .hex() prints two hexadecimal digits per byte.

If Python reports “can't open file,” check the working folder and filename. For SyntaxError, inspect the indicated line for copied smart quotes or missing punctuation. For IndentationError, keep indentation consistent with the example. ModuleNotFoundError names a missing library; these practice examples use Python's standard library and need no extra packages.

Base64

Browser path

Open developer tools → Console on the local practice HTML page. Enter each line separately so you can inspect its result:

atob('bGFtcA==')
btoa('lamp')

The results are lamp and bGFtcA==. atob decodes Base64 to a byte-valued string; btoa encodes that kind of string. These examples are ASCII. They do not make btoa a general Unicode encoder: characters outside its byte range can raise an error. For UTF-8 text use Python's explicit .encode('utf-8') and .decode('utf-8') path below. Keep challenge data local; an online decoder is unnecessary and would disclose what you paste.

Terminal path

Base64 commonly uses letters, digits, +, /, and optional = padding. Decode a value locally:

printf '%s' 'ENCODED_VALUE' | base64 --decode

Encode changed text:

printf '%s' 'TEXT_TO_ENCODE' | base64

Base64 is an encoding, not encryption.

Try this: Run this locally to check the whole conversion chain:

import base64

raw = b"lamp"
encoded = base64.b64encode(raw)
print(encoded.decode("ascii"))
print(base64.b64decode(encoded, validate=True))

Expected output is bGFtcA==, then b'lamp'. The trailing == is encoding padding, not a signature. Copy only a JSON field's string value into a decoder, without its surrounding JSON quotes. Incorrect padding can mean a truncated copy or the wrong Base64 variant; establish which before appending characters. If decoded bytes are not text, print their .hex() instead of forcing UTF-8.

Use printf, not an ordinary echo, when exact bytes matter: echo commonly adds a newline. Preserve the original and compare a decode/re-encode round trip before changing a challenge value.

A comparison distinguishes reversible encoding, accidental-error checksums, cryptographic hashes, shared-secret MACs, and private-key signatures with public-key verification.

Encoding is reversible. Checksums detect accidental errors. Hashes fingerprint bytes but do not authenticate a source. A MAC provides integrity and origin within a group that shares a secret; any member can create tags. A signature binds bytes to a private-key holder when verification and public-key trust are correct. None of these alone encrypts, proves freshness, or grants authorization.

Hexadecimal

Browser path: On the local practice page's Console, the following decodes four byte pairs and then interprets those bytes as UTF-8 text:

const hex = '6c616d70';
const raw = Uint8Array.from(hex.match(/../g), h => parseInt(h, 16));
console.log(raw.length, new TextDecoder().decode(raw));

Expected: 4 lamp. This short demonstration assumes valid even-length hex. For unfamiliar input, validate the characters and length first or use Python's strict decoder below. An arbitrary ciphertext is bytes, not necessarily text.

Terminal path:

Hex text contains pairs of characters from 0-9 and a-f. Decode it:

printf '%s' 'HEX_VALUE' | xxd -r -p

Encode text as hex:

printf '%s' 'TEXT_TO_ENCODE' | xxd -p

Python provides a strict alternative when xxd is unavailable:

raw = bytes.fromhex("6c616d70")
print(raw, len(raw))       # b'lamp' 4
print(raw.hex())          # 6c616d70

The text "6c" has two characters; bytes.fromhex("6c") has one byte. Decode hex before XOR or block arithmetic. An odd number of hex digits cannot describe whole bytes. Python's decoder accepts spaces between byte pairs, but form fields may require uninterrupted hex. Do not include 0x unless the receiving tool explicitly supports it.

JSON and JSON Lines

Browser path: Open the module's JSON link. If the browser presents an expandable tree, expand the top-level object or array, then one record and its nested fields. Use the raw view or copy the text into an editor to preserve original values. Record the parent ID with each nested field. If no tree is available, practice in the Console on the local HTML page:

const data = JSON.parse('{"items":[{"id":"demo-a","zone":"north"}]}');
console.log(data.items[0].id, data.items[0].zone);
console.log(JSON.stringify(data, null, 2));

Expected first line: demo-a north; the second call produces indented JSON. JSON.parse reads data; it does not execute it as code. Do not paste a downloaded file as executable JavaScript. For small JSONL files, inspect one object per line or use the terminal path. JavaScript numbers cannot represent arbitrarily large integers exactly; use Python integers for the cryptographic transcripts.

Terminal path:

Pretty-print JSON:

jq . sample.json

A JSON Lines file contains one complete JSON value per line. Values may be objects, arrays, strings, numbers, booleans, or null; these exercise logs use objects:

jq . telemetry.jsonl

Compare IDs, sequence numbers, timestamps, actors, and repeated payloads. Do not assume file order is time order.

Try this: Save this unrelated data as practice.json:

{"items":[{"id":"demo-a","meta":{"zone":"north"}},
          {"id":"demo-b","meta":{"zone":"south"}}]}

Run these local, read-only commands:

jq . practice.json
jq '.items[0].meta.zone' practice.json
jq -r '.items[] | [.id, .meta.zone] | @tsv' practice.json

The first command indents the structure. The second prints "north": arrays start at index zero, and dots select fields inside objects. The third prints two tab-separated rows, demo-a north and demo-b south. [] visits each array item, | passes each item to the next operation, and -r prints strings without JSON quotes. Keeping the ID beside a nested value prevents you from attributing it to the wrong record.

Without jq, use Python on the same file:

import json
from pathlib import Path

data = json.loads(Path("practice.json").read_text())
for item in data["items"]:
    print(item["id"], item["meta"]["zone"])

For JSON Lines, read each nonempty line with json.loads(line) instead of loading the whole file as one value. A Python “Extra data” error often means you tried to load JSONL as a single JSON document. In jq, null can indicate a missing field; inspect the parent object and spelling before interpreting it as evidence. A parse error can mean you downloaded HTML instead of JSON.

File type and readable strings

file sample.bin
strings sample.bin

file identifies the likely format. strings finds readable text inside a file; search its output for labels such as TOKEN, KEY, USER, or PASSWORD.

Use a text editor's Find function or a local search command to retain context:

rg -n -i -C 2 'token|password|secret' downloaded-notes.txt

-n displays line numbers, -i ignores letter case, and -C 2 includes two surrounding lines. The quoted pattern matches any of the three alternatives. For an exact identifier containing punctuation, use a literal search:

rg -n -F 'demo.device-8' downloaded-notes.txt

No matches is a search result, not proof that the file contains no credential. Try labels named by the artifact. A strings result does not preserve a binary file's full structure and can miss compressed, encoded, or short strings; use a format-aware tool for metadata. In the supplied firmware-text exercise, open the text export directly rather than trying to execute it as firmware.

A file container encloses visible content, metadata, embedded objects, and structural information. Different inspection views expose different layers.

A file container can hold visible text or pixels, metadata, embedded objects, and structural information. A rendered view exposes only part of that content. Compare appropriate views of a preserved copy and record where each observation came from. Metadata may be stale or edited; corroborate it before attributing an action or identity.

Image metadata

Graphical path: Open a downloaded copy in a metadata-aware image viewer. Look for Metadata, EXIF, or Image information, expand GPS or textual metadata groups, and record field/value pairs. A generic file manager may show only filesystem properties. If the viewer displays no embedded metadata, compare with ExifTool or the supplied PNG tool before concluding it is absent.

Terminal path:

exiftool image.jpg

Look for GPS coordinates, software, author, description, comment, and device fields. If the module supplies an offline evidence tool, that tool is the preferred alternative.

Read the output: A metadata tool typically prints Field name : value. Record both. Here is synthetic output for interpreting location:

GPS Latitude  : 12 deg 30' 0.00" S
GPS Longitude : 34 deg 15' 0.00" E
Software      : Example Exporter

Convert degrees/minutes/seconds with degrees + minutes/60 + seconds/3600. South and west are negative, so this pair is -12.5,34.25 in latitude-longitude order. A software field is a claim stored in the file, not proof of ownership.

To reduce a long listing, these read-only ExifTool options show metadata groups, duplicate tag names, or numeric coordinate values:

exiftool -G1 -a image.jpg
exiftool -n -GPSLatitude -GPSLongitude image.jpg

If coordinates are absent, inspect the full listing before concluding there is no location data; fields differ by format. Do not add assignments such as -TAG=VALUE while inspecting evidence: those write metadata.

For a PNG module with the supplied notes tool, download both files and run:

python3 png-notes.py portrait.png

Expected output is one textual key=value entry per supported text chunk. Record the key beside its value. This supplied tool is a narrow PNG text-chunk reader, not a substitute for inspecting every possible PNG metadata format.

PDF metadata and attachments

Graphical path: Open the downloaded PDF in a desktop reader. Use File → Properties or Document Properties for Author/Creator/Producer. For embedded files, open Attachments (often a paperclip panel), record its list, and save the selected attachment into a new exercise folder. Choose Save rather than launching the attachment; open an extracted email in a text editor. Browser PDF viewers vary and may omit attachments or detailed properties. If either panel is unavailable, use the terminal path or a prepared text export.

Terminal path:

pdfinfo document.pdf
exiftool document.pdf
pdfdetach -list document.pdf

To extract listed attachments into the current directory:

pdfdetach -saveall document.pdf

This writes files locally but does not change the PDF. Work in a folder created for the exercise so extracted files do not overwrite other work.

Read the output: pdfinfo may show Author, Creator, and Producer. Author describes attributed authorship; Creator describes the source application; Producer describes the PDF-generating software. A generic office application is weaker organizational evidence than a specialized producer string. These fields can be edited, so retain their attribution limits.

A synthetic pdfdetach -list result might be:

2 embedded files
1: release-note.txt
2: review-message.eml

Extract just the selected file to a new local name:

pdfdetach -save 2 -o extracted-message.eml document.pdf

The number is the PDF attachment index, not a page number. Open the extracted .eml in a text editor. Its header block ends at the first blank line; From and To describe sender and recipient, while Message-ID and In-Reply-To describe message relationships. A continuation line belongs to the preceding header. Distinguish an attachment inside the message from the message's own position in the PDF attachment list. A file merely mentioned in email text is not necessarily embedded; state which evidence supports the count.

If pdfinfo works but the browser's attachment panel is missing, use pdfdetach; the tool operations answer different questions. “0 embedded files” does not rule out sensitive text or metadata elsewhere in the PDF.

SHA-256

Graphical path: In a checksum utility, select the downloaded file, choose SHA-256, and calculate. Record the full digest and filename; if it offers a comparison box, paste the expected manifest digest there. A progress bar finishing only proves the calculation finished. A match requires comparison with the correct manifest row. If your graphical tool cannot choose SHA-256, use one of the terminal options below.

Terminal path:

sha256sum FILE

Compare the entire digest with the matching line in the manifest. A matching hash supports byte-for-byte integrity. It does not prove who published or authorized the file.

Try this: Create two local practice files and compare exact bytes:

import hashlib
from pathlib import Path

for name, raw in [("sample-a.txt", b"lamp"),
                  ("sample-b.txt", b"lamp\n")]:
    Path(name).write_bytes(raw)
    print(name, len(raw), hashlib.sha256(raw).hexdigest())

This writes two practice files; run it in a fresh folder. Each digest has 64 hex digits, but the digests differ because the second file has a newline. Run sha256sum sample-a.txt sample-b.txt and compare it with Python's results. On macOS, shasum -a 256 is an alternative; on PowerShell use Get-FileHash -Algorithm SHA256 sample-a.txt.

For a download, Python's Path("filename").read_bytes() gives the bytes to hash; do not use read_text().strip() because that changes the evidence. Pair each filename with its expected digest and calculated digest. If every row mismatches, first check that you saved file bodies, not HTML pages or copied text. Differences in uppercase versus lowercase hex do not change a digest; differences in the underlying file do.

Small local searches with Python

Some modules give you a small, explicitly bounded keyspace. Search it locally instead of sending hundreds of requests to the board:

import hashlib

target = "COPY_THE_MODULE_HASH"
for number in range(1000):
    candidate = f"{number:03d}"
    digest = hashlib.sha256(candidate.encode()).hexdigest()
    if digest == target:
        print(candidate)
        break

This script performs local calculations only. Send the recovered candidate to the board once.

Time zones and offsets

Worksheet path: Make columns for event ID, local date/time, signed offset, and UTC date/time. Convert each row before sorting. A spreadsheet can help, but keep an untouched original column and sort the whole table so the IDs do not detach from their events. Check one row manually: spreadsheet time-zone parsing depends on the program and input format.

Python path: Use the explicit-offset example below to verify the same conversion locally.

A timestamp ending in Z is already UTC. For an offset timestamp:

Write every event in UTC, then sort by the converted time.

Try this: 2031-05-07T00:20:00+02:00 becomes 2031-05-06T22:20:00+00:00: subtract two hours and retain the previous date. 2031-05-06T19:50:00-03:00 becomes 2031-05-06T22:50:00+00:00. The first event happened 30 minutes earlier, despite its later local date. Check the arithmetic locally:

from datetime import datetime, timezone

samples = ["2031-05-07T00:20:00+02:00",
           "2031-05-06T19:50:00-03:00"]
for value in samples:
    utc = datetime.fromisoformat(value).astimezone(timezone.utc)
    print(value, "->", utc.isoformat())

Keep the event ID beside both timestamps in your real table. This conversion handles explicit offsets; it does not discover a missing time zone or correct an inaccurate clock.

Sensor time, receipt time, and a local clock are preserved separately and converted to UTC. A later receipt of the same event may indicate replay or delay, not a fresh event.

Keep event time, receipt time, source identifier, time-zone offset, and any known clock error in separate columns. In this synthetic example, the sensor's 10:00Z event is received at 10:04Z; a local log at 12:02+02:00 corresponds to 10:02Z. A second receipt of the same event at 10:09Z is not proof of a new event. Time-zone conversion does not correct clock drift; preserve uncertainty and corroborate replay before concluding it.

Module map

Each module page contains its own investigation prompts and progressive hints. Start with the task, reveal one hint only when needed, and record what the response proves before moving on.

Module guides

Choose your track: Core, Lattice Trail, River Station, or The Glass Witness.

Scenario-track tool guide

Scenario modules ask for evidence-backed findings before revealing a seal. Use the module guide for the artifact set and submission format.

Lattice Trail

The Byline Photo

Open the module guide for image metadata.

The Pitch PDF

Open the module guide for PDF provenance.

Working Notes

Open the module guide for combined identifiers.

The Lawsuit Filing

Open the module guide for redaction layers.

The Portfolio Page

Open the module guide for third-party requests.

The Weekend Posts

Open the module guide for cross-artifact correlation.

River Station 14

Maintenance Window

Open the module guide for log and policy comparison.

Calibration Package

Open the module guide for integrity and publisher trust.

Service Credential

Open the module guide for embedded shared secrets.

Echo in the Rain

Open the module guide for event and receipt times.

Trusted Gateway

Open the module guide for proxy boundaries.

Safety Decision

Open the module guide for the incident-response capstone.

Read the operations note, then assess three claims against the linked records. For each claim, select a verdict and a specific evidence observation. Choose one control addressing acceptance of repeated telemetry as fresh. All seven choices are required; no event-ID sequence or exact prose answer is requested.

Use an evidence table: claim, observed fields, conclusion, and remaining uncertainty. Explain why the selected control addresses the stated failure and what it leaves unresolved. The board checks structured decisions; discuss your reasoning with the instructor. Rejected forms retain recognized selections and give targeted feedback. An accepted finding reveals a seal to file on the incident board; repeating accepted decisions retrieves it within that session.

The Glass Witness

The Covered Name

Open the module guide for incomplete redaction.

Portrait of Static

Open the module guide for PNG metadata.

Hairline Fracture

Open the module guide for manifest comparison.

Four Rooms, Four Clocks

Open the module guide for time normalization.

The Memory Next Door

Open the module guide for bounded object access.

Open the module guide for sequence integrity.

Core — Insane

These modules expect short local scripts. Run requests sequentially and keep intermediate artifacts when a module says the next stage needs them.

1. Split Decision

Open the module guide for parser disagreement.

2. False Identity

Open the module guide for token verifier policy.

3. Quiet Oracle

Open the module guide for CBC error feedback.

4. Final Authority

Open the module guide for unsafe prefix hashes.

5. Nonce Encore

Open the module guide for repeated signature nonces.

6. Chained Forgery

Open the module guide for variable-length CBC-MAC.

Flags, hints, and progress

When a page reveals a flag or seal:

  1. copy the complete POCKET{...} value;
  2. return to the profile or track home page;
  3. paste it into the flag/seal form; and
  4. keep the success page or progress count visible before continuing.

In The Glass Witness, record each pane's key finding before filing its witness seal in the same browser session. If you lose the seal response, revisit the pane or repeat the accepted finding to retrieve it; this does not add progress twice. Six key findings and six filed seals unlock all three ending choices. Supporting findings are optional and strengthen the final account.

In Lattice Trail, reopen the station or repeat an accepted finding to retrieve its earned seal in the same browser session. Already recorded means the finding is saved and no extra progress was added. File the seal on the track index before closing the review; closure prevents further findings and filing.

Only incorrect flags consume attempts. An unearned seal or a closed review does not. After 25 incorrect flag submissions, further flag submissions are blocked for the current session; other pages remain available. Check braces, capitalization, and the per-boot suffix instead of repeatedly guessing. Ask the instructor before a board restart, which clears everyone's progress.

In-game progressive hints are session-specific. Reveal one, try it, then return for the next only if needed. Restarting the board clears all sessions, hints, progress, and per-boot completion tokens.

Quick troubleshooting

For basic connection recovery, use the connection guide.