# Meeting Location: the case was in the free space

> Ali Hadi's Meeting Location challenge, worked by the swarm with every agent in a tool-less microVM and every heavy step a sealed job: a sweep of the whole free space, a clock that ran backwards, an argument about who did what, and a password nobody found.

By Halil Öztürkci, 27 September 2026. https://dfirswarm.ai/blog/meeting-location-the-case-was-in-the-free-space

| The run | |
| --- | --- |
| Case | Meeting Location, on the basic flow |
| Run | s6895a8, 27 September 2026 |
| Evidence | 16.3 GB: one E01 image of a Windows machine, shared read-only into every agent's microVM and hashed at kickoff. |
| Team | 3 on gpt-daybreak-blue, 3 on gpt-6-sol and 2 on gpt-6-luna, all through an openai-codex subscription; every agent in a microVM of the base image, every heavy step a job in the disk or full image |
| Agents | 8 |
| On the clock | 39m 03s |
| Spend | No metered cost: a subscription (108M tokens) |
| Caps | 400M tokens, $250 and 180 minutes for the swarm; 60M tokens per agent |
| Finish-line checks | 8/8 |
| Answers | Scored against the published answers: 3 correct, 3 partial, 0 wrong, 0 unanswered. The password of the encrypted object was not found. |

Challenge #10 of Ali Hadi's [Digital Forensic Challenge Images](https://www.ashemery.com/dfir.html) is among the harder ones in the set. A man is suspected of belonging to a foreign intelligence group and has agreed to meet someone. His only computer was imaged after he left, and the first investigation found nothing. The brief asks how he hid his activity, which tools he used and how to restore them, what he searched for, which encrypted file holds the meeting place and how it was decrypted, where he got the location from, and a timeline.

This post follows one run of the swarm on that image, from the kickoff to the sentinel. It names every method the agents used and none of the answers. Where a sentence would need an answer to make sense, I use a plain placeholder: "the concealment" for the way he hid his work, "its disk file" for the file that held it, and "cleaning utility A" and "B" for the programs he used to clean up after himself. Naming any of them would hand over part of the answer to Q1.

## The setup

The run used what we call the basic flow. Every agent works in a microVM of its own, booted from a small base image that has no forensic programs in it at all: no Sleuth Kit, no libewf, no ESE or registry parsers. When an agent needs one, it asks for a job, and the job runs in a throwaway worker VM of the image that holds the program, here the disk image or the full one. What the job writes is sealed into the store, hashed file by file, and a finding cites it by job id. The agents think in one place and the tools run in another, and the record keeps the two apart.

The team was mixed: some agents on `gpt-daybreak-blue`, some on `gpt-6-sol` and some on `gpt-6-luna`, all through a subscription. Nobody was assigned anything. The goal document carried the questions, the finish line and a warning that the answers had to rest on the ledger. The figures for the run are in the box beside this text; the prose keeps to what happened.

## The first minute: everyone wants the encryption question

The first thing the board shows is a collision. Within the same second several agents claimed the encryption question, others announced the same browser track, and some tried to take names already in use. The hub refused the duplicate names, and the agents sorted the rest out themselves, on the board, almost at once.

![The console's story tab: the agents' first claims and intros, several of them on the same question, and the renames that followed](https://dfirswarm.ai/assets/img/blog-c10-negotiate.webp)
*The first negotiation, as the console reads it back from the board: claims on the same question from several agents, and the renames that untangled them.*

The yield that settled it was short:

> **JournalChronicle (s6895a800), #15**
>
> I saw s6895a803 already took encrypted-location recovery, so I yield that collision.

The agent that wrote this had called itself CipherTrail seconds earlier. It became JournalChronicle and took the file system journals, which turned out to matter more than anyone knew yet. Another agent went through Lockpick and Chronicle before settling on UserTrace, the user-interaction artefacts. One became Timeline Keeper. One took the report and, before writing a line of it, asked for someone else to check it:

> **Report integrator (s6895a805), #26**
>
> I need an independent critic for sign-off (cannot sign my own report).

By the end of the first minute the split was: encryption and the meeting place, browser and search history, journals and shadow copies, user activity, the timeline, anti-forensics, deleted material in unallocated space, and the report.

![The agents tab: each agent's chosen name, what it said it was doing, its calls, tokens and failures, and its context with the hand-offs it made](https://dfirswarm.ai/assets/img/blog-c10-agents.webp)
*Each agent with the name it chose and what it said it was doing, with its calls, its context and the hand-offs it made when its context filled up.*

## Thinking without tools

The agents' own VMs are deliberately bare, and the first minutes show what that means in practice. Several agents called the `image_layout` tool inside their own VMs and got nothing back, because the programs it needs are not there. They switched to jobs. Some of those jobs repeated work the harness was already doing: the kickoff's evidence catalogue arrived with the partition table, the file list, the body file and a full MAC timeline while the first layout jobs were running, and was announced on the board as the first catalogue revision. One agent that relisted the file system anyway shared its output so that peers could "grep instead of rerunning".

![The VM panel: one image digest for every agent's VM, the network each VM may reach, and each VM's life from creation to being put away](https://dfirswarm.ai/assets/img/blog-c10-vms.webp)
*Every agent's VM from the same base image, the network each may reach and the placeholders where the keys would be, and each VM's life from creation to being put away with its checks held.*

## Nothing where it should be

The first real lead came early. The encryption examiner found the concealment's disk file in the file system listing, and `istat` showed that its data was gone: the entry was there and its content was not. The paging file was all zeroes. A first attempt to dump the whole of unallocated space to a file was cancelled partway, and the agent that started it said so plainly:

> **Deleted artifacts analyst (s6895a807), #67**
>
> do not interpret this as a completed unallocated-space sweep.

What followed is the part of the run I like most. The report integrator and the encryption examiner argued that a streaming scan was better than no scan, and the deleted-artifacts analyst built one: `blkls` streamed the whole free space of the volume through `strings` and `grep`, inside a job, without storing any of it. It found remnants of the concealment's own records sitting in NTFS free space, and asked the others to hold their conclusions open. Almost at once, agents working on different questions each found what they needed in the same hit file.

## From strings to bytes

A string filter drops the position of each hit, and position is what proves where something came from. So the next step was to go back to the bytes. The encryption examiner wrote a small scanner in Python, run as a job, that walked the free space in fixed-size chunks with an overlapping tail, recorded the absolute offset of every hit and kept a hex window around it. That recovered the whole message the location was in, sector-aligned and padded, which a string search had shown only in part.

It then did the thing that makes the finding defensible. `blkls -l` gives the physical cluster behind each position in the free-space stream, and `blkcat` reads that cluster straight from the E01. The key bytes now rest on separate reads of the evidence, and the finding cites both jobs. UserTrace also warned the others against borrowing a neighbouring cache entry's timestamp for the message.

## Proving a program ran, and not who ran it

Q2 asks for the tools and what each did. Both cleaning utilities had left their archives and executables behind in the temp directory, and JournalChronicle carved them out with a hash list. Prefetch showed a run. That is not the same as showing what the run did.

The journal would say, and here the library let the swarm down. The pack's `usn_journal` tool and MFTECmd, run as a job, both reported no records at all in a journal that plainly had them. JournalChronicle wrote its own parser for the journal's record format in a job, decoding the records, their reason flags and their FILETIMEs, and skipping the sparse zeroes at the head of the stream. It read the journal cleanly, and with it the agent could show utility A at work on the volume; strings in the recovered binary tied what the journal showed to one of its modes of operation. Then it put a hold on its own finding:

> **JournalChronicle (s6895a800), hold, #155**
>
> Do not label #68 'Max wiped' without independent linkage.

The operation was proven, and the report says just as plainly that who started it remains open.

## The clock that ran backwards

The longest argument of the run was about attribution. Early on, the timeline keeper recorded the concealment as the subject's own work, with high confidence. The report integrator challenged the order of events: the host's records of how the concealment arrived seemed to postdate its first use, and pointed at an outside path, which read like preparation of the image rather than the subject's activity. Entries were downgraded, then the free-space sweep found case material inside the concealment, and the argument swung back.

JournalChronicle settled it with a question of its own: does the clock ever go backwards? It wrote a detector that flagged adjacent journal records stepping back in time, found a single step, and pulled the System event log to explain it: an orderly shutdown, and a boot whose clock read earlier than the time the machine went down. Events that looked early on the wall clock were late in journal order. That removed the premise of every "preparation" finding. The report integrator vetoed the stale ones, JournalChronicle put a hold on the rest, and the entries were rewritten neutrally. The ledger kept every superseded version beside the one that replaced it.

![The ledger tab: counts of events, indicators and findings, nothing uncited, the examiner review not yet signed, and the filters by kind](https://dfirswarm.ai/assets/img/blog-c10-ledger.webp)
*The ledger as the examiner receives it: every entry cites what it rests on, the review waits for an examiner's signature, and searches that found nothing, hypotheses and limitations sit beside the findings.*

## Peers catching each other's bugs

The agents checked each other's work, and this run has clean examples of it. The deleted-artifacts analyst wrote a byte-signature scanner to find file headers in free space. Its only SQLite hit had an impossible page size, and the browser examiner worked out why from the code: the patterns had been escaped twice, so the scanner was searching for the literal text of a backslash sequence instead of the bytes.

> **Browser Search (s6895a804), veto, #181**
>
> j000062 signature scanner has CRITICAL Python escape bug

The encryption examiner re-read another cluster the scanner had flagged, with `blkcat`, and found plain text; the analyst retracted every count the scanner had produced. Later the browser examiner diagnosed another agent's failed job from its standard error alone: a `$OUT` inside a single-quoted Python script, which the shell never expanded.

![Job j000062 in the console: the command with its signature table and chunked reader, who asked for it and what it was doing, and its record in the store](https://dfirswarm.ai/assets/img/blog-c10-scanner.webp)
*The job the browser examiner vetoed: its command is sealed with its output, so the escaping mistake can be read in the record by anyone after the fact.*

## The encrypted object and the message

Q4 asks which encrypted file holds the location and how it was decrypted. The UserTrace agent had taken over the provenance question from the encryption examiner mid-run, and it found the cached copy of what had delivered the encrypted object, and from what came with it worked out how long the plaintext had to be, without running anything. The carved message was slightly longer.

The report integrator suggested, as a hypothesis, that the difference was whitespace. JournalChronicle counted the leading spaces. UserTrace then decoded the browser's compressed session-restore snapshots, which had been deleted and survived only in free space, with a decompressor it wrote by hand, and found the plaintext in a form field of the tab that had shown the object. After one declared normalization, stated in the ledger with its rule, the lengths and the hashes, the snapshot's plaintext and the carved message were identical byte for byte.

So the plaintext had been on the machine, in a place the swarm could prove. The swarm decrypted nothing itself, found no password in any artefact, and chose not to guess one:

> **JournalChronicle (s6895a800), #241**
>
> equally, it cannot prove identity without password/decryption.

None of our runs of this case has recovered that password. The location and the source are proven from the cached copies and the snapshots; the key is recorded as a limitation.

## What the harness did

Most of what the harness did in this run was quiet. It built the catalogue at the kickoff and catalogued the archives the agents carved as they appeared. It nudged agents that had gone silent, suggested writing a tool to agents that kept typing Python, and refused early `done` calls because posts had landed after the last report write. Nobody hit a cap and nothing was reaped. Every VM's isolation checks held, and the evidence was unchanged at the end.

Some things it did not handle well, and they are worth saying. The encryption examiner started compacting its context in the second half of the run and the compaction never finished. The trace shows it starting, the idle nudges that followed, and then nothing; the console lists that agent as unfinished. And the anti-forensics examiner spent most of the run calling `wait`, which the harness counts as activity, so it was never nudged. Both are harness problems, and both are on the list.

![The raw trace filtered to one agent and the word compact: compaction configured, notices and warnings, then compaction starts that are not followed by a finish](https://dfirswarm.ai/assets/img/blog-c10-compaction.webp)
*The encryption examiner's compaction, as the trace records it: the notices, the start, and no finish after it.*

At the stop the host took custody: it re-hashed the evidence, sealed the sessions, walked every chain it holds and checked each sealed job output against its manifest. The verdict was clean. It also named the ledger entries that rest on the output of a job that had been cancelled, because the swarm had cut windows out of that job's kept prefix instead of streaming the free space again.

![The custody tab: the verdict, and every check with its status: evidence, sessions, kept outputs, trace, ledger, VMs, work files and the store](https://dfirswarm.ai/assets/img/blog-c10-custody.webp)
*Custody at the stop: every check with its status, and the ones marked not applicable because no acquisition hashes were given and the model calls did not go through a gateway on the host.*

## What it found and what it missed

Graded against the published answers, by question:

| Question | What it asks | Result | Why, in method terms |
| --- | --- | --- | --- |
| Q1 | How he hid his activity | Partial | The concealment, its disk file, both cleaning utilities and the command history are named with their traces. Another utility appears in outputs the agents read, and nobody recorded it. A further cleaning trace was examined and recorded only as a search that found nothing. |
| Q2 | Restore the tools, with hashes | Partial | Both utilities' archives and executables are hashed and explained. The other utility is missing, `$LogFile` was screened for strings but not decoded transaction by transaction, and the concealment's disk file was read from free space in pieces rather than restored. |
| Q3 | What he searched for | Correct | The searches, recovered from free space and confirmed as successive entries of one tab, with scoped negatives for the browser stores the first examination had covered. |
| Q4 | The encrypted file, the decryption, the place | Partial | The object, how it was encrypted and the place are right. The password was not found. |
| Q5 | Where he got the location | Correct | The source, from history strings, the cached copies and both snapshots. |
| Q6 | Reflection and a timeline | Correct | A timeline built from the ledger that accounts for the clock going backwards. |

The finish line certifies structure, citation and integrity: every answer rests on a ledger entry with its references, the timeline is long enough, a sign-off is on the board and the evidence is unchanged. It does not certify correctness, which is why the grades above are separate from it.

## What I take from this run

The decisive work in this run was done with short, single-purpose programs the agents wrote in jobs: a streaming sweep, an offset-keeping scanner, a journal parser, a clock-step detector, a hand-written decompressor. The library's big tools played a small part. Every one of them is sealed with its output, so anyone can read exactly what was run. None was turned into a library tool, although the harness suggested it more than once, and that is a gap between what the swarm did and what the next swarm will have.

Some of it goes back into the platform. The pack's journal tool read nothing where a short parser read everything, and that has to be fixed. A compaction that starts and never finishes has to be detected and recovered. And `wait` should not count as work. The run also shows the thing the harness is for: a long argument about attribution was held in the open, every claim was downgraded when its premise fell, and the report says what was proven and what was not.

The case ledger has this run's figures beside every earlier run of the same case.
