Blog · Run notes

Bogus Bill: the key to the laptop was on the phone

BelkaCTF 6, worked by the swarm across an iPhone and a Windows laptop: a vault opened with a key found in a note on the phone, a cropped bank screenshot rebuilt from what the crop left behind, a phone Shortcut's cipher re-implemented to read a secret channel, and the questions it did not answer.

By Halil Öztürkci ·

The run in figuresBelkaCTF 6, Bogus Bill, on the basic flow
Case
BelkaCTF 6, Bogus Bill, on the basic flow
Run
s306463, 27 September 2026
Evidence
14.5 GB, shared read-only into every agent's microVM and hashed at kickoff: a full file system acquisition of an iPhone as one tar archive, and a six-segment EWF image of a Windows laptop.
Team
4 on gpt-daybreak-blue, 3 on gpt-6-sol and 3 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, mobile or full image
Agents
10
On the clock
44m 42s
Spend
No metered cost: a subscription (142M tokens)
Caps
400M tokens, $250 and 180 minutes for the swarm; 60M tokens per agent
Finish-line checks
11/11
Answers
Scored against the published answers: 10 correct, 2 partial, 2 wrong, 4 unanswered.
In the case ledger →

BelkaCTF 6, "Bogus Bill", is the largest case we run. A cashier at a corner shop took a counterfeit bill and reported it. The suspect's iPhone and his Windows laptop were imaged, and the published challenge asks a long list of questions, each with a single answer somewhere in the images: who owns the phone, who he talked to, where the gang celebrated, where the encrypted container is, what he bought with the money, who designed the print template, where they printed, when the largest batch finished, which printer, which ATM, who leaked the bill validator's data, which offshore bank, and his bank statement. On top of that, our goal asked the swarm to work out which questions depend on which, because we did not know, and to keep a dependency map as it went.

We have run this case several times on earlier versions of the harness. This post follows the run on the current platform. It names the methods the agents used and none of the answers. Where a question's answer would slip into a sentence, the sentence describes the kind of artefact and the technique instead.

The setup

Every agent reasoned in a microVM of its own, booted from the base image, with no forensic programs installed. The tools lived in job images: one for disk work, one for mobile work, and a full image for anything that named no profile. Every extraction and parse was a job in a throwaway worker VM, sealed into the store and cited by job id. The team mixed gpt-daybreak-blue, gpt-6-sol and gpt-6-luna through a subscription. The evidence was shared read-only into every VM. Nobody was assigned anything. Quotes from the board carry each agent's final name.

The first minutes: everyone wants the phone

The first questions on the list are about the phone's owner, and the board shows most of the team going straight for them. Claims on the same handful of iPhone questions landed within seconds of each other, and the hub refused one agent's name because another already had it. The first to step aside said so in plain terms:

Timeline and dependency QA (s30646307), #13

I see Q1–4 are already covered by s30646300 and s30646303. To avoid overlap, I will investigate laptop questions not yet claimed

A little later another agent said what everyone could see:

Offshore finance (s30646305), #23

iPhone Q1–7 are very heavily duplicated among peers; please reassign yourselves to remaining Q9–12, 15–16, or integrator tasks.

Nobody proposed a plan. The split formed from pivots, one agent at a time, over the first minute or so. One took the offshore bank and the statement. One took the printer and the batch timings. One took the luxury purchase and the concert, one the template and the print lab, one the ATM and the leak, and one the laptop's username, which it answered from the catalogue's file list with a single search and then, on reflection, lowered its own confidence. One became the timeline keeper and one the dependency mapper. The agent that took the encrypted container later became the report integrator. By the time the split settled, the names on the board read like a case file's index: Mobile Messages, Template Trail, Offshore finance, Luxury & Concert, ATM & Validator Leads, Printer Evidence Analyst.

The console's story tab: claims on the same iPhone questions from several agents, and the intros and renames as they spread out across the list
The first negotiation: several claims on the same iPhone questions, then the agents spreading out over the rest of the list.
The agents tab: each agent's chosen name and the scope it described, its calls, tokens and failures, and its context with the hand-offs it made
The agents with the names they chose and the scope each described, with their calls and the hand-offs they made as their context filled.

Reading a phone out of a tar file

The phone arrived as a full file system acquisition in one large tar archive. The harness had catalogued its member list at the kickoff, before any agent asked, and that list turned out to be the most searched index of the run. It also recorded where each member's data starts inside the archive, and the agents used that in a way I had not planned for.

The Template Trail agent first tried the obvious thing: tar -xO over the whole archive to pull out the Notes databases. The job ran for minutes and was cancelled. Its next job read the catalogue's offsets for the same files and seeked straight to them, and it finished in seconds. The printer analyst had already pulled the Telegram database out of the archive that way, and the Shortcuts database was carved out the same way later.

Job j000053: a tar extraction over the whole archive for the Notes databases, cancelled after running for minutes
The first attempt: a tar -xO over the whole phone archive, cancelled after minutes of reading.
Job j000073: a short Python script that seeks to the catalogued offsets of the same files and writes them out, finished in seconds
The second attempt: the same files read from the offsets the catalogue recorded, in seconds.

Everywhere a SQLite database mattered, the agents copied it with its write-ahead log and shared memory files before querying, so the most recent writes were not lost. For Telegram's binary message blobs, Mobile Messages wrote its own reader and mapped the chat peers from it.

The vault, and the key on the other device

The laptop held an encrypted container, and finding it was the first real piece of work on that side. The agent that took it listed the file system in a job, looked for container-like names, and extracted the hit with istat and icat. It did not need to run a BitLocker tool to know what it had: when the container was sealed into the store, the harness's derived catalogue ran the disk recipe over it on its own and reported it partial, with encryption detected.

The key was the interesting part. On the laptop, a Recent shortcut pointed to a recovery key file on a network share outside the image, which no examiner can follow. On the phone, the Template Trail agent found the recovery password in the body of a note. It posted the find with a rule for how to use it:

Template Trail (s30646304), #90

No password in this post: read row yourself into a local key FILE and pass via cryptsetup --key-file, never argv.

Almost at once, separate agents had unlocked the vault independently, in jobs, with pybde reading the recovery password inside the worker and pytsk3 on top, and neither printed the key. Shortly after, the derived catalogue had listed the decrypted volume, linked it to the earlier partial generations as their readable form, and deduplicated the integrator's own request to catalogue it. Several answers came from inside the vault: a spreadsheet of purchases the offshore finance agent decoded by reading the workbook's XML directly, and a Photoshop print template whose XMP editing history named its designer.

Job j000092: a Python script that reads the recovery password from the sealed notes database inside the worker and unlocks the volume without printing it
The unlock, as a job: the recovery password is read from the sealed notes database inside the worker, and the output says the volume opened without ever showing the key.

A screenshot rebuilt from what the crop left behind

Early in the run, the Printer Evidence Analyst noticed a cropped screenshot of a bank page in the laptop's Pictures folder. Later, the offshore finance agent looked at the file again and saw that there were a great many bytes after the image's end marker. Its first reading was steganography. Then it corrected itself:

Offshore finance (s30646305), #144

Reconstructing uncropped bank statement from stale deflate data; do not treat trailer as encrypted blob.

It was the Snipping Tool flaw known as aCropalypse: when the tool saved a cropped image over the original, it wrote the new, smaller image at the start of the file and left the rest of the original behind it. The agent brute-forced the points where a raw DEFLATE stream could restart, with an empty preset dictionary, worked out the image's row stride from pixel statistics, and rebuilt the bottom of the original screenshot. The report integrator rebuilt a full-width version independently, with its method written down beside it. Then the offshore finance agent checked where the Snipping Tool keeps its temporary files, and found the uncropped original there, in more than one copy. It called that a superior independent source, and it was: several peers checked the transcription against it.

That did not settle the question it was aimed at. The screenshot shows the last few transactions, and the question asks for the whole statement. The printer analyst said so on the board: everything the panel displays is still not the account's whole history.

A phone Shortcut's cipher, re-implemented

The offshore bank question sat in hex strings in an encrypted Telegram channel. The report integrator carved the Shortcuts database out of the archive, dumped the actions of the Shortcut that did the decryption as property lists, and re-implemented its byte cipher in a few lines of Python. It derived the cipher's parameters from a test exchange in the chat whose plaintext was known, applied the key rotations the chat itself announced, and decoded the channel. The method is written out beside the job's output, and the uncropped screenshot corroborated the result.

Finding the answer to someone else's question

One small moment shows the board doing its job. The Luxury & Concert agent was searching the laptop's Windows push-notification database for the concert, and found instead a chat app's notification that answered a different question, about the suspect's first take in April. It sent the find to the agent that held that question. That agent re-queried the row itself before recording it.

Another: part way through, the model provider failed a turn of the offshore finance agent, and the harness freed its work. The Printer Evidence Analyst claimed the bank questions, saw the owner come back within a minute, and gave them back:

Printer Evidence Analyst (s30646306), #150

I will immediately yield Q17–18 back to you; no duplicate reconstruction.

Holds, and what "done" means

The critic in this run was an agent that had tried to call done early and been refused, because one agent's slice being finished does not end the swarm. It became the critic when the integrator asked for one, and it held the report back more than once: for pending sections, for a stale revision, and for privacy. Others held it too. The Template Trail agent objected that a panel of the last few transactions is not an entire statement. The offshore finance agent held the report for printing payee identifiers, and for a sentence that overstated how completely a search had been done:

Offshore finance (s30646305), #219

entire statement was not present overstates search (unallocated/encrypted artifacts not comprehensively examined).

Late in the run the Printer Evidence Analyst read the definition of done literally: every section had to rest on a finding or on a search that found nothing, and a limitation on its own did not qualify, even if the automated checker accepted it. The integrator agreed:

Report integrator (s30646300), #232

Do not overstate completion: name exactly the allocated table/file set fully searched.

The owners of the open questions recorded scoped searches that had found nothing, and the last report revision added only those citations. On the questions they could not answer, the agents chose not to guess:

Luxury & Concert (s30646302), #179

Q10 remains unresolved after direct artifact review; I recorded limitation ledger #53 rather than guess.

The critic signed off with a sentence I would put in front of anyone who wants to use a finish line as a score:

April proceeds investigator (s30646309), #231

This sign-off verifies accuracy and transparent limitations; it does NOT assert the strict goal of all … exact answers has been met

The ledger tab: counts of events, indicators and findings, nothing uncited, the examiner review not yet signed, and the filters by kind
The ledger as the examiner receives it: every entry cites what it rests on, and searches that found nothing, the hypothesis and the limitations sit beside the findings.

What the harness did, and what it got wrong

The harness built the catalogue at the kickoff, the phone's member list and the laptop's volumes, and kept cataloguing as jobs sealed new objects: the container, the decrypted vault, a downloaded archive it listed without being asked. It nudged agents that went quiet, suggested writing a tool to agents that kept typing Python, ran the agents' self-compactions, redirected direct writes to shared files to the publish step, and refused early done calls. No cap fired, nothing was reaped, no lease was breached, and nothing was installed in an agent's VM.

It also got several things wrong in this run, and they are on the list:

  • The iOS file system recipe never ran. Its detection step failed at the kickoff with a usage error, so the phone was catalogued as a member list only.
  • Every icat_extract call failed. The tool was rerun as a job, as designed, and each job then failed writing to a read-only path. The agents fell back to icat in their own jobs.
  • The goal said geolocation lookups were allowed, and the network allowlist did not include any. The agents' geocoder jobs failed at DNS, and the goal was wrong to promise it.
  • One agent searched its peers' session transcripts for coordinates. The files were readable to it; whether they should be is a question for the harness, and it is open.

At the stop the host took custody, re-hashed the evidence, sealed the sessions and the chains, and checked every sealed job output against its manifest. The verdict was clean. The ledger entries that touch the vault's recovery password are marked sensitive, which keeps them blurred in the console and lets a package for handing over leave them out.

The custody tab: the verdict, and every check with its status
Custody at the stop: every check with its status, and the ones that did not apply named as such.

What it found and what it missed

Graded against the published answers, with the same standard we used for the earlier runs of this case:

Question What it asks Result Why, in method terms
Q1 The Apple ID Correct The phone's accounts database.
Q2 The owner's name Correct The same database.
Q3 The Telegram accounts Partial The answer left out an account the agents had seen and judged out of scope.
Q4 Where William lives Wrong The wrong location artefact was chosen. The right one was in output the swarm had produced for another question, and nobody went back to this one.
Q5 The laptop username Correct The catalogue's file list.
Q6 The first take in April Correct Found by another agent's search and passed over the board.
Q7 Where the gang celebrated Correct The venue from a record on the phone. The address was the model's own knowledge, labelled as a low-confidence inference; we score it as the earlier runs were scored.
Q8 The file holding the container Correct A file system listing and istat.
Q9 The luxury purchase Correct A spreadsheet inside the vault.
Q10 The concert Unanswered Searched widely on the phone and the laptop; nothing found named the event.
Q11 The template's designer Correct The template's XMP editing history, inside the vault.
Q12 The print lab's address Unanswered Searched widely. An artefact extracted for another question was never considered for this one.
Q13 When the largest batch finished Correct The print bot's order and completion chronology, re-derived by the critic.
Q14 The printer model Wrong Kept as a low-confidence hypothesis from a manual found in the vault; the registry checks did not settle it.
Q15 The ATM Unanswered Many location sources were chased. No run of ours has answered it.
Q16 Who leaked the validator data Unanswered The document was recovered and examined several ways, and none of them settled who produced it.
Q17 The offshore bank Correct The secret channel decoded through the phone Shortcut's cipher.
Q18 The bank statement Partial The rebuilt and recovered screenshots give part of the account's history; the whole statement was not recovered from the evidence.

The finish line certified every section's structure and citations, the flags table, the dependency map, the timeline, the sign-off and that the evidence was unchanged. It certifies form and provenance, and the critic's sign-off above says so better than I can.

What I take from this run

What I will remember from this run is about crossing boundaries. The laptop's vault opened with a key the phone was keeping, found by an agent that was looking for a print template. And a bank screenshot that had been cropped on purpose gave back what the crop was meant to hide, because an agent looked at the bytes after the end of the image and asked why they were there.

The misses have a pattern too. In several of them, the artefact that held the answer had already been extracted, for another question, and was sitting in a sealed job output. The board is good at passing a find to its owner when the finder recognises it, as it did for the April question. It has no way yet to say "this output might matter to a question somebody else holds". That is the next thing to build.

The case ledger has this run beside the earlier runs of the same case.