Martin's Blog

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:

EvidenceWhat it establishesWhat it cannot establish alone
.ruby-versionThe database repository named Ruby 2.0.0-p451The exact Ruby used for every shipped installation
Gemfile.lockOne resolved 2014-era dependency graphThat all surviving repository snapshots used that exact graph
Source APIsLanguage and library behavior the code actually callsWhether an unused branch ever worked in production
Installer and configuration scriptsExpected ports, paths, certificates, and database rolesThe topology of every customer deployment
Lab failures and successesWhich combinations work in the reconstructionHistorical 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:

  1. preserve the application’s language and dependency generation;
  2. reproduce the database roles the source expects;
  3. patch only demonstrated compatibility gaps between surviving snapshots;
  4. 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:

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:

RoleInternal portPurpose in the reconstruction
mongos router27017Default application session and sharded routing
shard server27018Data shard and the worker database session
configuration server27019Cluster 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:

  1. the operating-system process exists;
  2. its TCP port accepts a connection;
  3. the expected role and protocol operations work; and
  4. 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:

  1. clear the temporary directory and run the source-integrity check;
  2. load the licence, configuration, and certificates;
  3. connect to MongoDB and enable sharding;
  4. create indexes and shard the audit collection;
  5. ensure an administrator and signature records exist;
  6. create default evidence filters, queues, and metadata backup state;
  7. recover journalled offload work and clear stale server sessions;
  8. establish firewall state; and
  9. 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:

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.

Diagram of the isolated RCS reconstruction with browser, web console, database, worker, and MongoDB roles
LAB ARCHITECTUREThe active reconstruction is loopback-bound at its browser and backend entry points. Only the control plane and synthetic evidence path run; implant and exploit execution remain outside the lab boundary.

The host mappings reflect different research needs:

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:

LayerUseful evidencePrincipal limitation
Container outputentrypoint progress, process exits, selected application tracesnot every application trace is emitted there
RCS log volumedetailed database and worker traces, including asynchronous workmutable application output, not an independent audit record
External probeTLS negotiation, HTTP response, WSS behavior, console flowa 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:

GateEvidence expected
Image constructionJessie packages resolve; Bundler installs the intended era graph; EventMachine is built with TLS support
CommissioningEmpty state produces configuration, CA, service certificates, and a valid synthetic lab licence
DatabaseRouter, shard, and configuration roles start; RCS enables sharding and creates required indexes
BackendLicence, config, and cert checks pass; HTTPS and WSS listeners start
ApplicationA dedicated test identity can authenticate and make an authorized request without disturbing a human admin session
WorkerA synthetic backend fixture can traverse the internal worker path and be stored in the expected schema
ConsoleStatic UI, same-origin REST proxy, and WSS proxy connect to the reconstructed backend
SafetyNo 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:

FailureHidden contract revealed
Current Ruby raises compatibility errorsSource depends on pre-modern Ruby classes and crypto APIs
Old official image cannot be pulled reliablyHistorical container tags are not archival guarantees
Dependencies drift or disappearThe lockfile and project-specific forks are part of the source record
Server starts, then dies on TLSNative extension build features matter beyond package versions
Standalone Mongo is insufficientRCS expects router, shard, and configuration roles and manages sharding itself
Selected database and 9.6 common snapshots fail at class load or heartbeatRepository history can recover an expected interface even when adjacent tips are incompatible
Restart appears healthy using old volumesPersistent 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

↑ HackingTeam's RCS: Bringing a Commercial Spyware Platform Back to Life