4. Time-travelling the Runtime
Making old source code compile is not the same as reconstructing the system it belonged to. RCS depended on a particular Ruby generation, native extensions, old library behavior, a sharded MongoDB deployment, generated certificates, licence state, and interfaces shared across several repositories. Replacing each obsolete part with a current equivalent would have produced a different application—if it produced a working application at all.
I treated the job as conserving a machine, not porting a program. The lab recreates the narrow historical environment the backend expects, applies only the compatibility changes needed to bridge the surviving source snapshots, and puts modern isolation around the result. The application inside the container remains old. The safety boundary around it is new.
This chapter describes that reconstruction as systems archaeology. It does not build or run an implant, delivery vector, or exploit. The active services are the database backend, its evidence worker, MongoDB, and the defensive web console. Synthetic state replaces target devices and operational collection.
Reconstruct the assumptions before the program
My first task was not choosing a Docker base image. It was identifying the runtime contract hidden across the archive. Several kinds of evidence define that contract:
| Evidence | What it establishes | What it cannot establish alone |
|---|---|---|
.ruby-version | The database repository named Ruby 2.0.0-p451 | The exact Ruby used for every shipped installation |
Gemfile.lock | One resolved 2014-era dependency graph | That all surviving repository snapshots used that exact graph |
| Source APIs | Language and library behavior the code actually calls | Whether an unused branch ever worked in production |
| Installer and configuration scripts | Expected ports, paths, certificates, and database roles | The topology of every customer deployment |
| Lab failures and successes | Which combinations work in the reconstruction | Historical prevalence or customer exposure |
The database repository’s .ruby-version contains 2.0.0-p451. Its lockfile
records EventMachine 1.0.3, Mongoid 3.1.6, Moped 1.5.2, ActiveSupport 3.2.17,
and other versions contemporary with the snapshot. The code reinforces the
age signal: it opens ciphers through OpenSSL::Cipher::Cipher, calls the old
Digest::HMAC interface, and distinguishes Fixnum. Those are not cosmetic
anachronisms. They are executable dependencies on a Ruby and OpenSSL API era.
The reconstructed image uses Debian Jessie’s packaged Ruby 2.1.5 rather than claiming an exact resurrection of Ruby 2.0.0-p451. That is a deliberate fidelity boundary. Ruby 2.1.5 is close enough to preserve the required language and extension interfaces while remaining obtainable inside the Jessie package set. That makes Ruby 2.1.5 a compatibility choice, not evidence that HackingTeam shipped that interpreter. The lab is not a bit-for-bit historical image.
This distinction prevents two common errors. The first is treating a version file as a complete deployment specification. The second is treating any runtime that happens to start as historically faithful. The archive supports a compatible reconstruction, not a recovered customer appliance.
Why modernization was the wrong first move
The tempting path was to begin with a supported Linux distribution and a
current Ruby, then fix exceptions one by one. That path quickly changes the
subject of study. Modern Ruby collapsed Fixnum into Integer; OpenSSL APIs
changed; old native extensions stopped compiling; contemporary Mongo drivers
expect different servers and wire behavior; and permissive library calls can
become errors. A long chain of “small” source edits would eventually reveal
more about the port than about RCS.
There is also a security-research reason to resist modernization. Chapter 10 assesses the original backend’s trust boundaries and authorization behavior. If request parsing, session handling, database semantics, or event dispatch were silently rewritten for a new framework, later observations could no longer be attributed to the frozen source. Reproduction fidelity is part of the evidentiary method.
The lab therefore follows four rules:
- preserve the application’s language and dependency generation;
- reproduce the database roles the source expects;
- patch only demonstrated compatibility gaps between surviving snapshots;
- add containment at the deployment boundary rather than “hardening” the code under examination.
This produces an intentionally unusual architecture: an obsolete application stack runs inside a current container workflow, while the host publishes only the interfaces required for research. The age inside the boundary is the object of study. The boundary itself is the safety control.
Debian Jessie as a compatibility capsule
The backend image begins with debian:jessie (docker-lab/Dockerfile:1).
Jessie is no longer served through Debian’s ordinary package mirrors, so the
Dockerfile points APT at archive.debian.org, disables repository-validity
date checking, and allows unauthenticated installation from the frozen archive
(Dockerfile:3-10). These choices would be unacceptable defaults for a new
production service. Here they are explicit costs of reconstructing an
obsolete dependency set inside an isolated lab.
The image installs Ruby, the Ruby development headers, a compiler toolchain,
OpenSSL, libssl-dev, Git, and the media libraries required by backend
dependencies. It then installs Bundler 1.17.3 and resolves the application
gems (Dockerfile:7-20). Nothing in this arrangement should be mistaken for
a supported base image. The container is a disposable compatibility capsule,
not a security boundary strong enough to justify Internet exposure.
An older official ruby:1.9.3 image was not a dependable escape hatch. Its
obsolete image format is no longer normally retrievable from the public
registry. Even if it were available, choosing 1.9.3 merely because it is old
would not satisfy the database repository’s explicit 2.0.0 signal. The useful
question is not “what is the oldest image we can find?” but “what smallest
environment preserves the interfaces this source demonstrably uses?”
The choice of Jessie also contains a reproducibility warning. Archived package repositories are historical infrastructure, not immutable research bundles. A future rebuild still depends on the availability of the Jessie base image, archive metadata, Git-hosted source, and RubyGems packages. A publication-grade reproduction therefore needs more than a Dockerfile: it needs recorded image digests, fetched package hashes or an approved mirror, and a bill of materials. The present reconstruction proves the build path; it does not yet guarantee that every upstream byte will remain downloadable.
Rebuilding the 2014 gem graph
The original lockfile is unusually valuable because it turns “old Ruby app” into a concrete dependency graph. Among its resolved versions are:
- EventMachine 1.0.3;
em-websocket0.3.8 andem-http-server0.1.8;- Mongoid 3.1.6 and Moped 1.5.2;
- ActiveSupport 3.2.17;
rest-client1.6.7;rubyzip1.0.0; and- a Git revision of a HackingTeam developer’s
minitarfork.
The lab Gemfile exactly pins several versions that materially affect
compatibility and constrains others (docker-lab/rcs-db/Gemfile:1-23). Its
tested generated lock is therefore a distinct reconstructed graph, including
Mongoid 3.1.7/Moped 1.5.3 rather than the historical 3.1.6/1.5.2 pair. It also
changes dead or unsafe fetch
mechanisms where doing so does not alter application semantics: RubyGems uses
HTTPS rather than HTTP, and the minitar repository uses an HTTPS Git URL
rather than the retired unauthenticated Git protocol.
minitar illustrates why a name and version are sometimes insufficient. The
database task code calls Minitar.pack_stream
(lib/rcs-db/tasks.rb:17-26). The original lockfile does not select a generic
0.5.5 release from RubyGems; it records revision
56af58400e2d8171906dee4bf5c5a1d930e5b7b6 from the
danielemilan/minitar fork (rcs-db/Gemfile.lock:1-6). When an upstream version has
been yanked or lacks a project-specific method, replacing it with “the nearest
available gem” is not dependency recovery. It is an unreviewed code change.
Not every dependency in the historical lockfile is needed for the defensive backend path. The lab Gemfile excludes development tooling and optional packages that would expand the build surface without helping the database and worker run. This is one place where reconstruction and replication differ: the goal is to execute the in-scope backend paths with their original semantics, not to recreate every developer workstation feature.
The resulting dependency policy is conservative but not perfectly frozen. Several requirements remain compatible ranges, and the Git dependency names a branch in the lab Gemfile even though the historical lockfile records a commit. A disposable empty-volume commissioning replay now records the resolved image identities, the image-created lockfile hash, fresh generated-state hashes, and service readiness. A later separate-tag, no-cache build fetched the dependency graph again and produced the same package, gem, generated-lockfile, and application-tree inventories as the working image. The host lab still lacks a frozen lockfile, however, and the fetched upstream artifacts have not been retained as an authorized immutable bundle. Until that policy is decided, claims in this book refer to the examined images and frozen source, not to every future rebuild of the same text files.
The EventMachine OpenSSL trap
The most instructive runtime failure did not occur at process start. The server could launch, initialize enough state to look healthy, and then fail on the first TLS connection. The cause was EventMachine compiled without OpenSSL support.
RCS uses EventMachine as the reactor for both its REST service and WebSocket
service. The event setup starts an HTTPS handler on the configured port and a
secure WebSocket listener on the next port. It supplies the generated private
key and certificate to EM::WebSocket, then schedules heartbeat, session,
and backup work inside the same reactor (lib/rcs-db/events.rb:222-268). A
working Ruby process is therefore a weak health test. The native EventMachine
extension must also contain the encryption support required by the first real
connection.
Build order was the critical detail. libssl-dev has to be present before
Bundler compiles EventMachine. Installing OpenSSL after the gem already exists
does not retroactively add TLS support to the native extension. In the failed
configuration, the service reached startup and then crashed when TLS was
exercised. In the working Dockerfile, OpenSSL headers are installed with the
compiler toolchain before bundle install (Dockerfile:7-20).
This failure teaches three broader lessons.
First, native-extension capability is part of the artifact. A Gemfile records a Ruby package version, not the configure-time features compiled into its shared object. Second, startup logs are not an end-to-end test. A TLS service must complete a TLS request. Third, cleaning up a Dockerfile can change behavior even when every named dependency remains: moving a system package below the bundle layer can rebuild the same gem without a required feature.
The negative result is retained as lab history, but deliberately reproducing the crash on every build would add little evidence. The publication gate should instead assert the positive property: a newly built image negotiates HTTPS and WSS using its generated certificates, and the build record shows the OpenSSL headers present before EventMachine compilation.
MongoDB was a topology, not a socket
Pointing Mongoid at a standalone modern MongoDB instance does not reproduce
the backend. The source expects the old Mongoid/Moped stack and actively
manages sharding. During startup, the database establishes a connection,
calls enable_sharding, creates indexes, and shards the audit collection
before it enters the EventMachine loop (lib/rcs-db/db.rb:51-110). If no
shard record exists, enable_sharding creates the first one using
MONGOID_HOST, then enables sharding on the RCS database
(lib/rcs-db/db_layer.rb:240-248).
The lab consequently runs MongoDB 2.6 in three roles:
| Role | Internal port | Purpose in the reconstruction |
|---|---|---|
mongos router | 27017 | Default application session and sharded routing |
| shard server | 27018 | Data shard and the worker database session |
| configuration server | 27019 | Cluster metadata |
All three processes currently live in one Mongo container, which is an
economy for a single-host lab rather than a claim about production topology.
Compose starts the configuration server, then the journalled shard server,
then mongos pointed at the configuration server
(docker-compose.yml:2-15). Named volumes separately preserve the
configuration and shard data.
The application’s Mongoid configuration mirrors the split. Its default
session uses the router named by MONGOID_HOST:MONGOID_PORT; a second
worker session uses the shard-side host and port
(rcs-db/config/mongoid.yaml:1-14). Compose supplies 27017 for the main
session and 27018 for the worker session. This distinction later matters for
evidence processing and for forensic acquisition: “copy the Mongo database”
is not a sufficient instruction when cluster metadata and target-scoped
sharded collections are part of the evidence.
MongoDB is not published to the host in the lab. The application services can reach it on their private compose network, while an external client cannot connect through a declared host port. That containment compensates for a historical stack whose bundled startup mode does not enable MongoDB authorization. It does not fix the application; it narrows the reachable surface for this reconstruction.
The use of MongoDB 2.6 is another hard boundary. Moving to a current server would introduce years of wire-protocol, command, authentication, index, and sharding changes. If the objective were a maintained derivative product, a database migration would be necessary. For studying how this snapshot behaves, migration would be a confounder.
Containers express dependencies, not readiness
Compose makes the reconstructed topology legible, but it does not by itself prove that the services are ready in the order the application needs them. The database service depends on the Mongo container, and the worker depends on both, yet process creation is not database readiness. Inside the Mongo container, the three roles are also launched in sequence with short delays. This is adequate for a small laboratory; it is not orchestration with health-aware failover.
The application entrypoint adds a second guard. It tries a TCP connection to
the mongos address every two seconds for up to 60 iterations
(entrypoint.sh:12-20). This reduces ordinary boot races, but a listening
socket is only the first checkpoint. Shard membership, writable cluster
metadata, and RCS’s ability to enable sharding are tested later by the
application’s own initialization.
The guard also has a subtle limitation: exhausting the loop does not
explicitly abort the shell script. The backend subsequently attempts its
normal database connection with wait_until_connected: true. A missing
MongoDB service can therefore look like a long application hang rather than a
clean container health failure. The reconstruction preserves that behavior
for fidelity, while the reproduction procedure treats the later sharding and
index messages—not the initial TCP success—as the meaningful database gate.
Stateful dependencies make those checkpoints easy to confuse. There are at least four different milestones:
- the operating-system process exists;
- its TCP port accepts a connection;
- the expected role and protocol operations work; and
- the application has completed its own schema and state initialization.
Only the fourth means RCS is ready. The same logic applies to EventMachine: opening a port is weaker than completing TLS, and completing TLS is weaker than authenticating and exercising an authorized endpoint.
First boot is a state transition
The container entrypoint is small, but it captures a critical property of the original system: starting a process and commissioning a server are different events.
On each launch, the entrypoint waits for the Mongo router to accept TCP
connections. It then checks for config/config.yaml. If that file is absent,
the launch is treated as first boot. The configuration tool writes defaults,
sets the certificate common name and listening port, generates a certificate
authority and service certificates, and enables logging. A separate lab
helper creates a synthetic licence acceptable to the original verifier
(docker-lab/entrypoint.sh:4-30). Chapter 5 examines the licence and trust
model; here the important fact is that configuration, identity, and licence
are durable commissioning state.
Compose mounts named volumes for config, application data, and logs, in
addition to the two Mongo volumes (docker-compose.yml:33-36,75-80). A normal
container replacement therefore reuses the generated identity and database.
Deleting the containers is not the same as resetting the experiment. Only an
explicit volume wipe returns the lab to first boot.
That persistence is useful and dangerous. It makes iterative reconstruction possible, but it can hide initialization bugs and contaminate repeatability. A service that starts against yesterday’s valid certificates and database has not proven that today’s image can commission itself. Conversely, wiping the volumes destroys research state. The clean-boot reproduction gate was therefore run in a separate disposable Compose namespace while the working lab’s named volumes were stopped but preserved. The disposable volumes were removed only after readiness and sanitized hashes were captured, and the original namespace was then restored. This is the required pattern: never wipe a working lab casually.
Once commissioned, the original application performs its own second-stage initialization. Its startup order is roughly:
- clear the temporary directory and run the source-integrity check;
- load the licence, configuration, and certificates;
- connect to MongoDB and enable sharding;
- create indexes and shard the audit collection;
- ensure an administrator and signature records exist;
- create default evidence filters, queues, and metadata backup state;
- recover journalled offload work and clear stale server sessions;
- establish firewall state; and
- start the HTTPS and secure WebSocket listeners.
This sequence comes from lib/rcs-db/db.rb:27-110. One line also asks the
backend to load operational core packages from a cores directory. The
defensive lab includes no built implant payloads and does not exercise build
or delivery paths. An empty operational directory is a scope control, not
evidence that those original product features were unimportant.
The startup sequence explains why a port-open check is incomplete. A useful
health gate must demonstrate that durable state loaded, database preparation
completed, and the event listeners came up. The original trace messages
“Listening for https” and “Listening for wss” occur only after the reactor
starts those two services (rcs-db/lib/rcs-db/events.rb:238-255). They are valuable terminal
markers, but an authenticated application request and WebSocket exchange are
stronger checks.
Bridging source snapshots without inventing a release
Chapter 3 established that the archive does not contain one internally
coherent release. The database lockfile expects rcs-common 9.2.3, while the
checked-out common library declares 9.6.0. The version number alone does not
make the later tree a drop-in replacement. The database calls heartbeat hooks
and status accessors that are absent from that checked-out implementation.
The lab adds two narrow compatibility shims to its copied rcs-common tree.
The first adds before_heartbeat and after_heartbeat registration to the
heartbeat base class and invokes those callbacks around its normal status
update (rcs-common/heartbeat.rb:18-39). The database, worker, connector, and
other components declare those callbacks. For example, the database’s
pre-heartbeat work checks the licence, firewall, shards, and component status
(rcs-db/heartbeat.rb:12-32). Without the registration methods, class loading
fails before the intended heartbeat behavior can occur.
The second shim adds my_status, my_status=, and my_error_msg= accessors
to RCS::SystemStatus (rcs-common/systemstatus.rb:53-64). Those names are
called by the database event setup and shard health checks. The shim maps them
onto the common library’s existing current-status structure rather than
rewriting the database callers.
Repository history supplies unusually direct evidence for the interfaces:
common-library revision 34da36c873873a50a5ef053aa8f055207c1f1f2f, declaring
9.2.3, contains the expected heartbeat hooks and status accessors. A 13 May
2014 heartbeat refactor at 261e08dddf2f581e96a8798e041c182551dfd536 removes or
reshapes them while the version file still says 9.2.3, before the later 9.3.0
version bump. The lab patches are still adaptations, because they restore the
older interface onto the selected 9.6 implementation rather than running a
pinned historical dependency. Every behavioral claim about callback ordering
or state mapping through a shim must retain that limitation.
The patch discipline is therefore as important as the code:
- original leak material remains read-only;
- patched copies live only under
docker-lab; - each change bridges a demonstrated interface or deployment problem;
- modern refactors are excluded from the research image; and
- the web console is identified as a reimplementation, not silently mixed into the original source.
This makes disagreement inspectable. A future researcher can compare the original tree, the copied lab tree, and the patch rationale rather than being asked to trust a binary image with undocumented repairs.
The reconstructed service boundary
The compose stack has four services. Mongo provides the internal 2.6 cluster
roles. rcs-db runs the REST and secure WebSocket backend. rcs-worker uses
the same image with a worker entrypoint and receives evidence only on the
internal network. console-web serves the reconstructed operator interface
and reverse-proxies its same-origin API and WebSocket paths.
The host mappings reflect different research needs:
- database HTTPS is published as
127.0.0.1:44443to container 443; - database WSS is published as
127.0.0.1:44444to container 444; - the web console is bound only to
127.0.0.1:8080; and - worker and MongoDB ports are not published.
The adjacent backend ports begin with one original configuration value. The database defaults to 443, while the old firewall helper derives Worker and WebSocket listeners by subtracting or adding one:
# rcs-db/lib/rcs-db/config.rb:28-39
DEFAULT_CONFIG = {'CN' => '127.0.0.1',
'CA_PEM' => 'rcs.pem',
'DB_CERT' => 'rcs-db.crt',
'DB_KEY' => 'rcs-db.key',
'CERT_PASSWORD' => 'password',
'LISTENING_PORT' => 443,
'HB_INTERVAL' => 15,
'BACKUP_DIR' => 'backup',
'POSITION' => true,
'PERF' => false,
'SLOW' => 0,
'SHARD' => 'shard0000'}
The lab changes host exposure, not that internal port relationship. Its loopback mappings make the original convention observable without publishing the services to the surrounding network.
The database mappings originally lacked an explicit host address during the research and could therefore bind more broadly than the console. The reproduction-guide review corrected them to loopback. That containment change does not make the services safe: the lab still needs an isolated host and firewall, and nothing should forward the ports to another network.
The worker service definition corrects an early simplification in the research
notes. Compose now declares the 2015 evidence processor on an internal-only
service, and no implant is needed to test its decode-and-store contract. The
stored publication-pass container, however, predates the worker-aware
entrypoint by minutes: although its configured command is worker, its logs
show the old image launching rcs-db. Project notes record an earlier manual
synthetic exercise, but the current topology claim requires a clean rebuild,
named worker test, and sanitized trace before publication.
Observing an application that predates container logging
RCS was not written for a container runtime. Its trace system writes important
events to files under the application log directory, and those files can be
more complete than the container’s standard output. Compose therefore mounts
the log directory as a named volume shared by the database and worker. A
researcher checking only docker compose logs can miss the explanation for a
failure or incorrectly conclude that a code path was never reached.
The reconstruction uses three complementary observation layers:
| Layer | Useful evidence | Principal limitation |
|---|---|---|
| Container output | entrypoint progress, process exits, selected application traces | not every application trace is emitted there |
| RCS log volume | detailed database and worker traces, including asynchronous work | mutable application output, not an independent audit record |
| External probe | TLS negotiation, HTTP response, WSS behavior, console flow | a probe can change sessions and application state |
This arrangement also affects how failures are dated. Container timestamps, application timestamps, Mongo document dates, evidence acquisition dates, and host time are separate clocks. For routine debugging they may appear close enough. For publication evidence or incident response they must be preserved and correlated rather than collapsed into one narrative timestamp.
The most trustworthy startup record combines the build metadata, container
output, file traces, and an external request. A log statement can show that
the code reached EM.start_server; only the client demonstrates that the
native TLS path actually works. Conversely, a successful HTTP reply does not
show which dependency versions were loaded. Reproduction is strongest when
the independent layers agree.
External probing needs one special precaution. A login is not a passive version query in RCS. It creates and invalidates session state and can notify a same-named user’s active console. For that reason, status collection should prefer non-mutating process and certificate inspection first, then use the live-test harness with a unique account for application-level checks. The default administrator is not a general-purpose health probe.
Reproducibility needs an evidence bundle
A Dockerfile is a recipe whose ingredients can move. A convincing archival rebuild should emit a compact evidence bundle beside the book rather than rely on a future reader to trust “it worked here.” The planned bundle has five parts.
Inputs. Record the frozen source hashes, Dockerfile and compose hashes, base-image digest, package sources, and any copied compatibility files. Record the provenance of the synthetic licence helper without publishing embedded secret material more widely than the leak already did.
Resolved environment. Capture the operating-system release, ruby -v,
RubyGems and Bundler versions, the fully resolved gem graph, loaded native
extension locations, OpenSSL version, MongoDB binaries, and web-server image.
This turns compatible version ranges and mutable Git branches into an observed
artifact.
Commissioning trace. From fresh disposable volumes, preserve sanitized logs showing the Mongo roles, first-boot configuration, certificate generation, licence acceptance, sharding, index creation, administrative bootstrap, and both EventMachine listeners. Certificate fingerprints and public fields can be retained; private keys and session cookies cannot.
Behavioral gates. Complete HTTPS and WSS checks, a normal authorized API request using a disposable test identity, a named synthetic worker ingestion test, and the console mock and live suites. The result should name the exact tests and counts instead of saying only that the interface “looked right.”
Safety record. Show that worker and Mongo ports are not published, the web console is loopback-bound, evidence is synthetic, and the build did not compile or execute any implant or exploit repository. The safety statement belongs in the evidence bundle because scope is a property to verify, not merely an editorial promise.
A separate bounded reliability run adds one scale point without pretending to
be a capacity study. Eight distinct analyst loops completed 640 authenticated
status, search, count, and paginated evidence reads over 5,000 synthetic
metadata rows. The row count and expected {type, da, aid} target shard key
survived database, Worker, and Mongo container restarts; the Mongo restart
recovered without a coordinated application restart. Host-local p95 latency
was 49.6 ms before and 53.7 ms after those restarts. These figures describe one
single-host run, not a service-level objective. Large GridFS bodies, sustained
writes, multiple shards, network partitions, resource exhaustion, and
long-duration behavior remain untested (RELIABILITY-VALIDATION.md).
Fresh volumes are essential to this gate, but the existing research volumes must not be sacrificed to obtain it. Compose project names or a copied lab can provide a separate volume namespace. After the run, the bundle can retain configuration metadata and sanitized traces while the disposable databases are removed through an explicitly approved cleanup. This design separates repeatability from destruction.
What counts as “back to life”
A process list is not the finish line. For this project, the backend is alive when the following claims hold together:
| Gate | Evidence expected |
|---|---|
| Image construction | Jessie packages resolve; Bundler installs the intended era graph; EventMachine is built with TLS support |
| Commissioning | Empty state produces configuration, CA, service certificates, and a valid synthetic lab licence |
| Database | Router, shard, and configuration roles start; RCS enables sharding and creates required indexes |
| Backend | Licence, config, and cert checks pass; HTTPS and WSS listeners start |
| Application | A dedicated test identity can authenticate and make an authorized request without disturbing a human admin session |
| Worker | A synthetic backend fixture can traverse the internal worker path and be stored in the expected schema |
| Console | Static UI, same-origin REST proxy, and WSS proxy connect to the reconstructed backend |
| Safety | No implant or exploit is built or run; worker and MongoDB remain unexposed; synthetic data is identifiable |
Most of these properties have been demonstrated during the lab and console milestones. The remaining publication gate is a recorded rebuild from empty, disposable volumes with exact component versions, image identifiers, commands, sanitized logs, and test results. That destructive reset was not performed while drafting this chapter because the existing named volumes may contain valuable research state. Restraint here is part of reproducibility: an experiment is not repeatable if its procedure casually destroys the evidence needed to describe it.
Authentication checks also require care. The original login design permits only one active session per user and can push a forced logout to another session of the same account. Reusing the default administrator for automated health checks can therefore eject a researcher from the console. The full behavior belongs in Chapter 5; the runtime lesson is simple: health probes are state-changing application actions, and publication testing should use unique temporary users through the established live-test harness.
Failure modes as architectural evidence
Not every failed attempt deserves space in a reconstruction narrative. These ones do because each exposed a hidden contract:
| Failure | Hidden contract revealed |
|---|---|
| Current Ruby raises compatibility errors | Source depends on pre-modern Ruby classes and crypto APIs |
| Old official image cannot be pulled reliably | Historical container tags are not archival guarantees |
| Dependencies drift or disappear | The lockfile and project-specific forks are part of the source record |
| Server starts, then dies on TLS | Native extension build features matter beyond package versions |
| Standalone Mongo is insufficient | RCS expects router, shard, and configuration roles and manages sharding itself |
| Selected database and 9.6 common snapshots fail at class load or heartbeat | Repository history can recover an expected interface even when adjacent tips are incompatible |
| Restart appears healthy using old volumes | Persistent commissioning state can mask first-boot failures |
Together they change the story from “we found the right Docker incantation” to a defensible systems model. RCS was tied to a runtime ecosystem, and parts of that ecosystem existed outside its repository: Linux archive services, native compilation, Git forks, Mongo cluster roles, generated identity, and state accumulated after installation.
The limits of this reconstruction
The lab is intentionally faithful in some dimensions and intentionally unlike a deployment in others.
It is faithful to the database and worker source paths under examination, the era of their Ruby dependencies, their EventMachine networking model, their MongoDB generation and sharding expectations, and their first-boot configuration flow. The compatibility shims are narrow and documented. The backend has been exercised through real REST, WSS, storage, and synthetic worker flows.
It differs by using Linux containers, a one-container Mongo cluster, a later but compatible Ruby 2.1.5 package, a synthetic licence, a newly written web console, synthetic investigations, and modern host-level isolation. It does not recreate a Windows production installation, execute the Adobe AIR binary, deploy operational collectors or anonymizers, or run any implant or exploit.
Those differences are not embarrassment to be smoothed away. They define which observations can answer which questions. In this lab I can show that the backend source initializes, stores data, serves its API, pushes console events, and exhibits particular authorization and trust behavior. The evidence stops before any named customer’s network, any historical infection, or the exact binaries that ran on a seized appliance.
The strongest result of the runtime work is consequently methodological. I did not need to revive the offensive edge to study the platform. I needed to preserve the backend’s assumptions closely enough that its data model, operator contracts, and security boundaries became observable. That is the foundation for the chapters that follow: licence and identity, the console reconstruction, evidence processing, and the question of whether the system guarding the collected material could guard itself.
Sources and evidence
- SOURCE:
rcs-db/.ruby-version,Gemfile.lock,lib/rcs-db/db.rb,db_layer.rb,events.rb, configuration tools, and the original MongoDB launchers at commit6cff59d28634d718cac9fdd17cb629fd59a3cf3f. - SOURCE:
rcs-commoncompatibility contracts at commit38290d4eab2b2c295bea021429848a3666647827, subject to the version boundary in Chapter 3. - HISTORICAL SOURCE:
rcs-commonrevision34da36c873873a50a5ef053aa8f055207c1f1f2fcontains the 9.2.3 heartbeat and status interfaces;261e08dddf2f581e96a8798e041c182551dfd536refactors them, before9a770f8455d9c250b62ae1a0034c1d45cdc3fbbcbumps the visible version to 9.3.0. The database lockfile does not select a commit. - LAB:
docker-lab/Dockerfile,entrypoint.sh,docker-compose.yml,README.md, the copiedrcs-db/rcs-commontrees, and the sanitizedresearch/hackingteam-rcs/COMMISSIONING-VALIDATION.mdreplay record. - PATCH INVENTORY: Appendix C records every carried source difference and grouped omission; Appendix B records the safe reproduction procedure.
- LAB: fresh empty-volume commissioning completed in a disposable Compose namespace on 2026-09-12 and the preserved research namespace was restored.
- LAB BUILD: a separate-tag no-cache build resolved the same 44-gem graph
and identical package, gem, lockfile, and application-tree inventories as
the working image; see
DEPENDENCY-REBUILD-VALIDATION.md. - LAB: a disposable bounded run served 640 authenticated reads from eight
analyst loops over 5,000 synthetic rows and exercised component restarts;
see
RELIABILITY-VALIDATION.mdandvalidate-bounded-reliability.js. - LIMITATION: authorized immutable upstream artifact retention and a deliberate lab lockfile policy remain deferred.
↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life