3. Archaeology of an Inconsistent Leak
There is no single version of RCS in the archive examined for this book. There are several repositories that happened to be present when the material was copied, and their branch tips do not describe one product release. Once I compared the embedded repository histories, the archive stopped looking like a release and started looking like a developer’s workspace. Treating the directory tree as a coherent source distribution would make the reconstruction look simpler than it was—and the resulting claims less reliable.
The archive is a workspace, not a release image
The top-level hackedteam directory contains dozens of embedded Git
repositories: backend services, console projects, platform-specific cores,
delivery vectors, test utilities, drivers, and supporting libraries. Each has
its own refs, commits, branches, and working-tree state. Their coexistence in
one leaked directory says that they were available in the same development
environment; it does not say that their checked-out tips formed a supported
bill of materials.
That difference is easy to lose when browsing a leak as files. A normal release archive is intentionally assembled: versions are selected, generated assets are included, development files are removed, and dependencies are locked. A development workspace accumulates branch tips, obsolete siblings, build outputs, local configuration, subprojects, and histories from different dates. RCS has the latter shape.
I therefore kept four separate inventories:
- Repository identity: which embedded repository owns a file, and which commit its working tree represents.
- Declared product identity: version constants, build numbers, changelogs, protocol fields, and package manifests inside that repository.
- Dependency identity: lockfiles, path dependencies, bundled runtimes, and caller/callee interfaces that constrain what could work together.
- Lab identity: the exact copied subset, adaptations, generated state, and newly written components used during reconstruction.
These inventories answer different questions. A Git commit dates a source state, while an internal build identifier describes the developer’s product line. A lockfile records one dependency resolution, while a path dependency can silently substitute whatever tree happens to be adjacent. A running lab proves that selected interfaces can be bridged, not that the resulting combination was a vendor release.
The examined source roots preserve Git metadata. No Mercurial .hg metadata
was found in the corresponding leak tree, so this book makes claims from the
surviving Git histories only. Absence is recorded rather than filled with an
assumption.
The book narrows this large workspace aggressively. Backend reconstruction uses
copied rcs-db and rcs-common trees. The AIR console and its library provide
read-only behavioral contracts for the new web port. Collector and anonymizer
repositories are read statically. The exploit repository is inventoried through
Git history without opening an execution path. Implant, installer, and delivery
trees remain outside the active build even when they help explain vocabulary or
architecture.
The mismatch is visible before any code runs. The database repository’s
config/VERSION contains 9.2.3, its CHANGELOG begins with “Changelog for
9.2.3,” and its build identifier is 2014051201. That repository’s final
examined commit is dated 12 May 2014 and titled Merge branch '9.2.x'. The
common library beside it comes from a commit dated 31 March 2015, titled Merge branch '9.6.x', and declares VERSION = "9.6.0". The console snapshot from
the same day sends rcsVersion = '9.6.0' during login.
The old database lockfile records rcs-common (9.2.3). The directory actually
checked out as rcs-common, however, is the later 9.6 branch tip. Its history
does retain an earlier 9.2.3 revision with the heartbeat and status interfaces
the database expects. What the path dependency does not record is a commit
hash, so that compatible historical revision cannot be identified as the exact
build input. The archive preserves more of the relationship than the adjacent
tips initially suggest, but not a fully pinned dependency set for the database
release.
Six primary source snapshots, not one release
The reconstruction freezes these repository commits:
| Component | Commit | Commit date | Repository clue |
|---|---|---|---|
rcs-db | 6cff59d28634d718cac9fdd17cb629fd59a3cf3f | 2014-05-12 | Merge branch '9.2.x'; version 9.2.3 |
rcs-common | 38290d4eab2b2c295bea021429848a3666647827 | 2015-03-31 | Merge branch '9.6.x'; version 9.6.0 |
rcs-collector | 42cf61af9ec3e7b886e87eac20e762c3ce51c78c | 2015-03-31 | Merge branch '9.6.x' |
rcs-console | a90041fcd32bf1a82276ac20c2cd5ecb7d108db0 | 2015-03-31 | Merge branch '9.6.x'; protocol version 9.6.0 |
rcs-console-library | 97531201600a8a223392e955a35b2b2605c1c1d4 | 2015-03-31 | Merge branch '9.6.x'; REST/service layer |
vector-exploit | 2cf574d3bf2ea44f67433e32ccc9906ded6218e0 | 2014-12-12 | catalogue build 2014093001 |
The mismatch is visible in code, not inferred only from directory names. The checked-out common library and AIR console each declare the later protocol line:
# rcs-common/lib/rcs-common/version.rb:3
VERSION = "9.6.0"
// rcs-console/src/Console.mxml:31-37
[Bindable]
public static var rcsVersion:String='9.6.0';
Those declarations sit beside the database’s config/VERSION value 9.2.3.
The path annotations above are editorial; the declarations themselves are
unchanged source excerpts.
Those hashes describe the evidence base, not a claim that the six components were ever shipped together. The original repositories remain read-only. The lab contains separate copies of the server and common library, and every runtime change is made to those copies.
One supporting current-tree revision was added when the deployment-edge analysis was
consolidated: rcs-anonymizer at
0c4189b1e5d7e23ad0bf85d0f184d9d8e46decbe, committed 3 April 2015. It is a
static source for the forwarding model, not an active lab service. Appendix I
records all seven current-tree revisions, plus the historical common-library
revision used to resolve the compatibility question, and their timestamps.
The dates form a useful, deliberately incomplete timeline:
2014-05-12 rcs-db 9.2.x tip; build 2014051201
│
2014-09-30 version named by the later exploit manifests
│
2014-12-12 examined vector-exploit repository tip
│
2015-03-31 common, collector, console, and console-library 9.6.x tips
│
2015-04-03 examined anonymizer tip
This is a source-state timeline, not a shipping calendar. Commit timestamps can be rewritten, merge commits can contain older work, and the date encoded in a build number may describe a catalogue contract rather than its final commit. The safe conclusion is relative: the database is from an earlier product line than the examined common and console trees, while the exploit catalogue sits between their visible dates and expects a different loader contract.
Branch subjects provide supporting context but not release proof. A merge of a
9.6.x branch tells us what line developers were integrating; it does not show
that a signed installer, customer bundle, or server fleet used the exact tip.
For that, the archive would need release manifests or artifact hashes that tie
all components together. They have not been established for this reconstruction.
That cut goes both ways. A defect caused by mixing the snapshots is not automatically a historical RCS vulnerability. A quirk that survives without a compatibility patch is stronger evidence of the original component’s behavior.
Where the skew becomes executable
The first incompatibilities appear at startup when the selected database is
paired with the checked-out 9.6 common-library tree. The database calls
before_heartbeat and after_heartbeat hooks and assigns
SystemStatus.my_status and SystemStatus.my_error_msg; those interfaces are
absent from that checkout. The common repository’s history makes the seam more
precise. Revision 34da36c873873a50a5ef053aa8f055207c1f1f2f, dated 28 April
2014 and declaring 9.2.3, contains the expected hooks and accessors. Revision
261e08dddf2f581e96a8798e041c182551dfd536, dated 13 May and still declaring
9.2.3, refactors the heartbeat and removes or reshapes those legacy APIs; the
visible version bump to 9.3.0 follows at 9a770f8455d9c250b62ae1a0034c1d45cdc3fbbc.
This is stronger evidence than a differing version string because the mismatch crosses a live interface and the expected interface is historically attested. The path lockfile still does not identify which common-library commit built the database release, however. The lab therefore restores those interfaces onto the selected 9.6 body, rather than substituting the older tree wholesale. Callback ordering and status-state mapping remain lab adaptations and behavior through them stays patch-dependent.
The operator API exposes a second form of skew. The 9.6 console library calls
/evidence/sync_history, but the 9.2.3 REST controller has no such action. The
request falls through to the item handler and returns 404, leaving Sync History
empty. Reimplementing the screen does not justify inventing an endpoint: the
web port preserves the empty result against the live backend. Here the mismatch
itself becomes an observable property of this particular component pairing.
Other seams appear as quirks rather than startup failures. The console library can name a route that the older dispatcher resolves differently; a view may expect fields computed only by a later response; a client can preserve local state because no matching server push exists. Each case must be resolved by following both ends of the contract. “The UI calls it” proves client intent. “The controller defines it” proves a server path. Only a matching request, parameter shape, response, and state transition establish compatibility.
The login version field illustrates a softer boundary. The console sends
9.6.0, while the server identifies itself as 9.2.3. That pairing can still
authenticate in the lab; the version declaration is not a universal package
manager. It remains useful provenance because it identifies the client
snapshot, but it cannot harmonize the rest of the API.
The exploit catalogue offers an even cleaner date boundary. Packages present
at its repository head declare manifest version 20140930, and the repository
VERSION is 2014093001. The database’s exploit loader accepts a package only
when info['version'] == 20140512. Dropping the later packages into this server
would not produce a working catalogue. The contents are historically useful,
but they target a server line newer than the database snapshot reconstructed
here.
The project does not “fix” that mismatch. Doing so would require changing an exploit ingestion path and would also erase evidence of the incompatible release contract. The catalogue is instead analyzed as static metadata in Chapter 11 and Appendix A. Its Git history can date the appearance, change, or removal of a manifest. It tells us nothing by itself about whether this backend accepted the package, a customer received it, or its contents worked.
Original code, runtime adaptations, and the web port
The lab has three different kinds of change, and they should not be collapsed into one “patched RCS” label.
Runtime adaptations recreate an environment the source expected: Debian Jessie, Ruby 2.1.5, MongoDB 2.6, pinned gems, archived package repositories, and Linux library names for the worker’s audio dependencies. They aim to make old behavior reachable, not redesign it.
Compatibility shims restore APIs that are missing between the selected repository snapshots but attested in the common library’s 9.2.3 history. The heartbeat and status setters fall into this category. Claims that pass through the adapted implementations need to say so, especially the long-run heartbeat observation discussed later.
The console web port is new code. Its authority comes from parallel MXML structure, original service calls, mock and live tests, and a manual-based audit—not from being an original executable. The port can demonstrate that the backend supports a workflow. It can also reproduce a workflow specified by the AIR source. It cannot independently prove pixel-for-pixel or runtime-specific behavior of Adobe AIR 15.
The lab also excludes large parts of the source distribution from execution.
No core-*, vector-*, scout, soldier, melter, or installer code is built or
run. Collector and injector behavior is read statically. Synthetic agent
records are created through the server’s first-sync route, while most of the
demonstration evidence is inserted through the database driver and clearly
identified as such. A separate named synthetic Worker replay confirms backend
evidence decryption, storage, alert logging, and console push. Its sanitized
trace is preserved, but it begins at Worker ingress and was not an
implant-to-collector session.
The patch inventory is part of the result
Reconstruction changes are not housekeeping to be hidden after the service
starts. They define the boundary of every lab claim. A refreshed comparison of
the read-only references and mutable copies finds exactly ten carried source
files that differ: five in rcs-db and five in rcs-common. The copied backend
also has one lab-only source file, the synthetic licence generator.
The ten differences fall into four classes:
| Class | Files | Interpretive consequence |
|---|---|---|
| Dependency and packaging capsule | 2 | The lab resolves an era-compatible runtime, not the vendor’s exact locked build |
| Cross-snapshot API shims | 2 | The interfaces are historically attested; their adaptation onto the 9.6 body remains lab behavior |
| Linux native-library/loadability changes | 6 | The base Worker can load on Linux, but affected parsers and codecs need separate tests |
| Lab-only licence generation | 1 new file | A synthetic entitlement enables the isolated backend; it is not original product code |
The backend copy intentionally omits 601 original files after runtime
placeholders are excluded. The groups include the original lockfile, bundled
Windows binaries, HASP materials, a Windows MongoDB distribution, NSIS
installation files, OCR packages and sources, and the original spec tree. They
remain available for read-only analysis under data/; they do not enter the
container image merely because they were adjacent to the server source.
That omission is a safety and fidelity decision. Removing an unneeded bundled
executable reduces accidental execution surface. At the same time, replacing a
vendor-provided Windows MongoDB build with the mongo:2.6 container means the
lab cannot make byte-level claims about the vendor binary or installer. A
transparent reconstruction states both effects.
The Linux loading edits also require restraint. Adding library names for AMR, LAME, Speex, and sample-rate conversion helps old FFI wrappers locate shared objects. Allowing an optional library failure to remain nonfatal lets unrelated evidence paths start. Neither outcome proves the audio transformations are correct. Similarly, guarding an unavailable Berkeley DB dependency lets the common library load but does not validate the money-evidence decoder.
Appendix C records every changed path, its line-count delta, its purpose, and the original and lab SHA-256 values. It separately inventories orchestration, nginx, the web port, research documentation, test fixtures, and generated volume state. This separation prevents a new JavaScript console or synthetic certificate from being mistaken for a modification hidden in original source.
Fidelity matrix
The resulting system is best understood as a matrix rather than a binary “original” or “recreated” label:
| Surface | Treatment | Evidence available | Principal limit |
|---|---|---|---|
rcs-db REST/WSS application logic | Original copied source, run with the dependency capsule | SOURCE + LAB | Older database paired with later common code; ten-file patch boundary applies |
| Evidence Worker base path | Original copied source with Linux loading adaptations | SOURCE + LAB + LAB PATCH | Synthetic non-media path does not validate optional codecs or edge transport |
| MongoDB roles | Era-compatible MongoDB 2.6 in one container | SOURCE + LAB | Logical router/shard/config roles are preserved, not production host separation |
| First boot and certificates | Original configuration and certificate tools under container orchestration | SOURCE + LAB | Generated keys, licence, and database state belong to the lab |
| Licence acceptance | Original verifier with a new synthetic licence generator | SOURCE + LAB PATCH | Demonstrates verifier/key-disclosure consequences, not historical entitlement |
| Original AIR Console | Read statically; not executed | SOURCE + MANUAL | AIR runtime-specific and pixel-level behavior is not directly observed |
| Browser Console | New implementation mapped to MXML/services/manuals | WEB PORT + SOURCE + MANUAL + LAB | Behavioral reimplementation, never an original HackingTeam binary |
| Collector and anonymizer | Static source and manual analysis | SOURCE + MANUAL | No edge listener, chain, buffering, or live transport is exercised |
| Network Injector | Static source/manual concepts and render-only administration | SOURCE + MANUAL + WEB PORT | No interception, content replacement, or delivery is performed |
| Implant and installer families | Excluded from active build and execution | SOURCE/MANUAL only where cited | Collection and deployment behavior is not lab-validated |
| Exploit catalogue | Manifest and Git-history normalization only | SOURCE | Metadata presence does not prove reliability, deployment, or authorship |
| Demonstration investigations | Synthetic API and database fixtures | LAB + WEB PORT | UI/workflow evidence, not victim data or end-to-end collection |
This matrix resolves an apparent contradiction. The book can make strong claims about a live REST authorization check and, in the next paragraph, make only a static claim about a collector branch. Confidence follows the surface and method, not the fact that both files originated in the same leak.
Generated state is a fourth evidence layer
Original source, patched copies, and new lab code are only three layers. A running instance adds a fourth: generated state. First boot produces local configuration, a certificate authority and service certificates, a synthetic licence, default accounts, MongoDB collections, logs, backups, sessions, and task state. Demo seeding adds fictional operations, targets, agents, evidence, entities, alerts, and topology objects.
Generated state can make a source diff look dirty even though no code changed, and it can make a successful second boot conceal a first-boot problem. The lab therefore places it in named volumes and excludes runtime directories from the source-patch count. Publication-grade reproduction must hash state separately, record the generation procedure, and distinguish a clean commissioning run from reuse of an already initialized volume.
The book’s 2026-09-12 commissioning replay applies that rule in a disposable Compose namespace: it records source, image, resolved-service, and generated- artifact hashes, then deletes the fresh volumes and restores the preserved lab. It does not erase the separate limitation that dependencies were not rebuilt from a network-empty cache and are not yet archived as immutable inputs.
Synthetic fixtures also have different provenance within that layer. A factory cloned through the authentic status route tests a backend transition. Evidence inserted directly through the database driver tests storage and UI behavior but not the broken or missing REST path. An encrypted blob accepted by the Worker tests decoding and alert flow after the safe ingestion point. The fixture method travels with the claim.
What cannot be reconstructed away
Some uncertainty survives any amount of compatibility work. A compatible
9.2.3 rcs-common revision is present in repository history, but the database’s
path lockfile does not pin the exact commit used for its build. The original AIR binary is not executed, so runtime behaviors
not expressed in source, manuals, or protocol observations remain unknown. The
later server expected by the 20140930 exploit manifests has not been identified
as the reconstructed backend. Customer-specific topology, configuration,
patches, and operating procedures cannot be inferred from repository defaults.
Nor can successful startup answer historical security questions by itself. A modern container wraps the old process in different kernel, filesystem, network, and orchestration behavior. Jessie packages and period gems recover many application assumptions, but the image is not a forensic copy of a customer server. Vulnerability claims must attach to the source path or dependency actually reviewed and state whether the lab exercised it.
These are not defects in the narrative to be smoothed over. They are findings about the archive. The missing release manifest explains why cross-repository claims need version labels. The absent edge execution explains why Chapters 2 and 9 distinguish diagrams and static paths from lab packets. The synthetic data boundary explains why the reconstructed Console demonstrates workflow but reveals nothing about a real victim. An honest fidelity limit often carries more investigative value than a falsely complete recreation.
A fidelity rule for the rest of the book
My rule for the rest of the book is simple: describe the narrowest thing the evidence proves.
- A successful REST request proves behavior of the examined database snapshot under the documented lab adaptations.
- A Playwright result proves behavior of the web port and, when it reaches the live stack, the corresponding backend contract.
- An MXML branch proves the original console source contained that logic, not that every distributed binary did.
- A collector branch proves a code path existed, not that historical traffic took it.
- A package manifest proves what a package declared, not that a customer received or successfully used it.
- Reporting about a client or victim remains reporting unless separate primary evidence supports it.
That discipline is occasionally less satisfying than a single clean version number. It is also what makes the reconstruction useful: the gaps are visible, the adaptations are reproducible, and readers can decide how much weight each observation deserves.
Sources and evidence
- SOURCE:
rcs-db/config/VERSION,config/VERSION_BUILD,CHANGELOG,Gemfile.lock, andlib/rcs-db/build/exploit.rb:238-254at commit6cff59d28634d718cac9fdd17cb629fd59a3cf3f. - SOURCE:
rcs-common/lib/rcs-common/version.rbat commit38290d4eab2b2c295bea021429848a3666647827. - SOURCE:
rcs-console/src/Console.mxml:31-41at commita90041fcd32bf1a82276ac20c2cd5ecb7d108db0, with the service layer atrcs-console-librarycommit97531201600a8a223392e955a35b2b2605c1c1d4. - SOURCE:
vector-exploit/VERSIONand packageinfo.yamlfiles at commit2cf574d3bf2ea44f67433e32ccc9906ded6218e0. - SOURCE:
rcs-anonymizerat supporting commit0c4189b1e5d7e23ad0bf85d0f184d9d8e46decbe(static analysis only). - LAB/WEB PORT:
docker-lab/README.md,docker-lab/IOCs.md, anddocker-lab/console-web/MILESTONES.md. - LAB INVENTORY: Appendix C, “Original-to-Lab Patch Inventory,” including the ten-file hash manifest, grouped omissions, orchestration, and generated state boundaries.
↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life