Neven Marić
Hidden Investigations CTF 2026

MiscHidden Investigations CTF 2026

The Foxglove Trace

Reconstruct a scoring rule from a debug log — movement cutoff, signal-strength weighting, and trust weights — then apply it to a CSV export to find the highest-scoring access point.

  • misc
  • data-analysis
  • log-analysis
  • scoring-rule
  • wifi
  • csv

What this one teaches

  • Deriving a threshold from boundary cases in a debug log — step_delta 40 is dropped, 41 is kept, so the cutoff is > 40.
  • Verifying the scoring formula against known subtotals before applying it to the full dataset.

Task description

A hobbyist had a quiet obsession: figuring out, without asking anyone, which unknown place they kept drifting back to on autopilot. Their idea was to mine two signals their phone already recorded — step-count spikes and the strength of nearby Wi-Fi access points — on the theory that a place that truly matters shows up as you moving around near the same access point, over and over.

They never finished it. All that’s left is a handful of files pulled off the old project folder: some scratch notes, a tiny config, one exported scan, and the log of the single run that actually worked.

No server, no service, nothing to connect to. This is pure offline forensics. Reconstruct the person’s scoring rule from what the files imply — the movement cutoff that separates “walking” from “sitting still”, and how a per-access-point score is built from signal strength and the trust weights — then apply it to the full export and name the place they kept returning to. The loudest access point is a trap; so is the one that appears most often. Get the rule right and exactly one place rises to the top.

The derived place name is the key to the flag. Submit it as hiCTF{PlaceName}, with the name spelled as the access point appears in the files.

Approach

The four files

  • notes.txt: explains the scoring logic — only trusted APs from aps.conf are considered, stationary readings (low step_delta) are discarded, and each kept reading contributes signal strength scaled by the AP’s trust weight.
  • aps.conf: lists the 4 trusted access points and their weights:
    FoxgloveCoffee_Guest=3
    HomePlex_5G=2
    CafeDelMundo=2
    NETGEAR-8841=1
  • run.log: a debug run (tracker.py --verbose --only CafeDelMundo,HomePlex_5G) with kept/dropped annotations and subtotals:
    -- CafeDelMundo (w=2) --
       signal=62  step_delta= 40  -> stationary, dropped
       signal=58  step_delta= 41  -> moving, kept
       signal=60  step_delta= 58  -> moving, kept
       signal=64  step_delta= 77  -> moving, kept
       subtotal = 364
    -- HomePlex_5G (w=2) --
       signal=92  step_delta=  5  -> stationary, dropped
       signal=90  step_delta= 11  -> stationary, dropped
       signal=87  step_delta= 44  -> moving, kept
       (7 more rows dropped)
       subtotal = 174
  • scan_export.csv: 33 rows of timestamp,ap_name,signal,step_delta.

Reconstructing the scoring rule

Movement cutoff: step_delta = 40 is marked stationary, dropped, while step_delta = 41 is moving, kept. The condition is step_delta > 40.

Subtotal formula: For CafeDelMundo (w=2), kept signals are 58 + 60 + 64 = 182, times weight 2 = 364 — matches. For HomePlex_5G (w=2), only kept signal is 87, times 2 = 174 — matches. So: score(AP) = weight * sum(signal for rows where step_delta > 40).

Solution

import csv

weights = {
    "FoxgloveCoffee_Guest": 3,
    "HomePlex_5G": 2,
    "CafeDelMundo": 2,
    "NETGEAR-8841": 1,
}

scores = {ap: 0 for ap in weights}

with open("scan_export.csv", newline="") as f:
    for row in csv.DictReader(f):
        ap = row["ap_name"]
        signal = int(row["signal"])
        step_delta = int(row["step_delta"])
        if ap in weights and step_delta >= 41:
            scores[ap] += signal * weights[ap]

for ap, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
    print(f"{ap}: {score}")

Results:

Access point Calculation Score
FoxgloveCoffee_Guest (68 + 71 + 66 + 73 + 70) x 3 1044
CafeDelMundo (60 + 64 + 58) x 2 364
NETGEAR-8841 (80 + 78 + 82) x 1 240
HomePlex_5G 87 x 2 174

FoxgloveCoffee_Guest has the highest score, giving the flag.

The flagRevealhiCTF{FoxgloveCoffee_Guest}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.